├── .github
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── 🚀-feature-request.md
│ └── 🦟-bug-report.md
└── workflows
│ ├── publish-to-cocoapods.yml
│ └── run-tests.yml
├── .gitignore
├── LICENSE
├── MijickCamera.podspec
├── Package.swift
├── README.md
├── Sources
├── Internal
│ ├── Assets
│ │ ├── Colors.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── mijick-background-inverted.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── mijick-background-primary-50.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── mijick-background-primary-80.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── mijick-background-primary.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── mijick-background-red.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── mijick-background-secondary.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── mijick-background-yellow.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── mijick-text-brand.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── mijick-text-primary.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── mijick-text-secondary.colorset
│ │ │ │ └── Contents.json
│ │ │ └── mijick-text-tertiary.colorset
│ │ │ │ └── Contents.json
│ │ └── Icons.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── mijick-icon-cancel.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-cancel.png
│ │ │ ├── mijick-icon-change-camera.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-change-camera.png
│ │ │ ├── mijick-icon-check.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-check.png
│ │ │ ├── mijick-icon-crosshair.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-crosshair.png
│ │ │ ├── mijick-icon-flash-auto.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-flash-auto.png
│ │ │ ├── mijick-icon-flash-off.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-flash-off.png
│ │ │ ├── mijick-icon-flash-on.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-flash-on.png
│ │ │ ├── mijick-icon-flip-off.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-flip-off.png
│ │ │ ├── mijick-icon-flip-on.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-flip-on.png
│ │ │ ├── mijick-icon-grid-off.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-grid-off.png
│ │ │ ├── mijick-icon-grid-on.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-grid-on.png
│ │ │ ├── mijick-icon-light.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-light.png
│ │ │ ├── mijick-icon-photo.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-photo.png
│ │ │ └── mijick-icon-video.imageset
│ │ │ ├── Contents.json
│ │ │ └── mijick-icon-video.png
│ ├── Extensions
│ │ ├── AVCaptureVideoOrientation++.swift
│ │ ├── AVVideoComposition++.swift
│ │ ├── Animation++.swift
│ │ ├── CIFilter++.swift
│ │ ├── CIImage++.swift
│ │ ├── CameraUtilities++.swift
│ │ ├── CaseIterable++.swift
│ │ ├── FileManager++.swift
│ │ ├── Task++.swift
│ │ ├── UIImage.Orientation++.swift
│ │ ├── UIView++.swift
│ │ └── View++.swift
│ ├── Manager
│ │ ├── CameraManager+Attributes.swift
│ │ ├── CameraManager+MotionManager.swift
│ │ ├── CameraManager+NotificationCenter.swift
│ │ ├── CameraManager+PermissionsManager.swift
│ │ ├── CameraManager+PhotoOutput.swift
│ │ ├── CameraManager+VideoOutput.swift
│ │ ├── CameraManager.swift
│ │ └── Helpers
│ │ │ ├── Capture Device Input
│ │ │ ├── CaptureDeviceInput+AVCaptureDeviceInput.swift
│ │ │ ├── CaptureDeviceInput+MockDeviceInput.swift
│ │ │ └── CaptureDeviceInput.swift
│ │ │ ├── Capture Device
│ │ │ ├── CaptureDevice+AVCaptureDevice.swift
│ │ │ ├── CaptureDevice+MockCaptureDevice.swift
│ │ │ └── CaptureDevice.swift
│ │ │ └── Capture Session
│ │ │ ├── CaptureSession+AVCaptureSession.swift
│ │ │ ├── CaptureSession+MockCaptureSession.swift
│ │ │ └── CaptureSession.swift
│ ├── Miscellaneous
│ │ └── Typealiases.swift
│ ├── Models
│ │ ├── CameraExposure.swift
│ │ └── MCameraMedia.swift
│ └── UI
│ │ ├── Camera View
│ │ ├── CameraView+Bridge.swift
│ │ ├── CameraView+FocusIndicator.swift
│ │ ├── CameraView+Grid.swift
│ │ └── CameraView+Metal.swift
│ │ ├── Default Screens
│ │ ├── Camera
│ │ │ ├── DefaultCameraScreen+BottomBar.swift
│ │ │ ├── DefaultCameraScreen+ButtonScaleStyle.swift
│ │ │ ├── DefaultCameraScreen+CameraOutputSwitch.swift
│ │ │ ├── DefaultCameraScreen+CaptureButton.swift
│ │ │ ├── DefaultCameraScreen+Config.swift
│ │ │ ├── DefaultCameraScreen+TopBar.swift
│ │ │ ├── DefaultCameraScreen+TopButton.swift
│ │ │ └── DefaultCameraScreen.swift
│ │ ├── Captured Media
│ │ │ └── DefaultCapturedMediaScreen.swift
│ │ ├── Common Components
│ │ │ ├── DefaultScreen+BottomButton.swift
│ │ │ └── DefaultScreen+CloseButton.swift
│ │ └── Error
│ │ │ └── DefaultCameraErrorScreen.swift
│ │ └── MCamera
│ │ ├── MCamera+Config.swift
│ │ ├── MCamera+Controller.swift
│ │ └── MCamera.swift
└── Public
│ ├── Camera Settings
│ ├── Public+CameraSettings+MApplicationDelegate.swift
│ ├── Public+CameraSettings+MCamera.swift
│ └── Public+CameraSettings+MCameraController.swift
│ ├── Models
│ ├── Public+Model+CameraUtilities.swift
│ ├── Public+Model+MCameraError.swift
│ └── Public+Model+MCameraMedia.swift
│ └── UI
│ ├── Public+UI+DefaultCameraScreen.swift
│ ├── Public+UI+MCameraErrorScreen.swift
│ ├── Public+UI+MCameraScreen.swift
│ └── Public+UI+MCapturedMediaScreen.swift
└── Tests
└── Tests+CameraManager.swift
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @FulcrumOne
2 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | team@mijick.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Coming soon...
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: mijick
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: mijick
14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: ❓ Help and Support Discord Channel
4 | url: https://discord.com/invite/dT5V7nm5SC
5 | about: Please ask and answer questions here
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/🚀-feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F680 Feature Request"
3 | about: If you have a feature request
4 | title: "[FREQ]"
5 | labels: 'feature'
6 | projects: "Mijick/15"
7 | assignees: FulcrumOne
8 |
9 | ---
10 |
11 | ## Context
12 | What are you trying to do and how would you want to do it differently? Is it something you currently you cannot do? Is this related to an issue/problem?
13 |
14 | ## Alternatives
15 | Can you achieve the same result doing it in an alternative way? Is the alternative considerable?
16 |
17 | ## If the feature request is approved, would you be willing to submit a PR?
18 | Yes / No _(Help can be provided if you need assistance submitting a PR)_
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/🦟-bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F99F Bug Report"
3 | about: If something isn't working
4 | title: "[BUG]"
5 | labels: 'bug'
6 | projects: "Mijick/15"
7 | assignees: FulcrumOne, jay-jay-lama
8 |
9 | ---
10 |
11 | ## Prerequisites
12 | - [ ] I checked the [documentation](https://github.com/Mijick/Camera/wiki) and found no answer
13 | - [ ] I checked to make sure that this issue has not already been filed
14 |
15 | ## Expected Behavior
16 | Please describe the behavior you are expecting
17 |
18 | ## Current Behavior
19 | What is the current behavior?
20 |
21 | ## Steps to Reproduce
22 | Please provide detailed steps for reproducing the issue.
23 | 1. Go to '...'
24 | 2. Click on '....'
25 | 3. Scroll down to '....'
26 | 4. See error
27 |
28 | ## Code Sample
29 | If you can, please include a code sample that we can use to debug the bug.
30 |
31 | ## Screenshots
32 | If applicable, add screenshots to help explain your problem.
33 |
34 | ## Context
35 | Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions.
36 |
37 | | Name | Version |
38 | | ------| ---------|
39 | | SDK | e.g. 3.0.0 |
40 | | Xcode | e.g. 14.0 |
41 | | Operating System | e.g. iOS 18.0 |
42 | | Device | e.g. iPhone 14 Pro |
43 |
--------------------------------------------------------------------------------
/.github/workflows/publish-to-cocoapods.yml:
--------------------------------------------------------------------------------
1 | name: Publish to Cocoapods
2 | on:
3 | deployment:
4 | workflow_dispatch:
5 | jobs:
6 | build:
7 | runs-on: macos-15
8 | steps:
9 | - uses: actions/checkout@v4
10 | - name: Publish to Cocoapods
11 | env:
12 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
13 | run: |
14 | pod trunk push MijickCamera.podspec
15 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 | on:
3 | push:
4 | branches: [ "main" ]
5 | pull_request:
6 | branches: [ "main" ]
7 | workflow_dispatch:
8 | jobs:
9 | build:
10 | runs-on: macos-15
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Run tests
14 | run: xcodebuild test -scheme MijickCamera -destination 'platform=iOS Simulator,OS=18.0,name=iPhone 16 Pro'
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 | *.xcscheme
10 | *.plist
11 | .swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright ©2024 Mijick
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/MijickCamera.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'MijickCamera'
3 | s.summary = 'Significantly reduces implementation time and effort. Keeps your code clean.'
4 | s.description = <<-DESC
5 | Camera made simple. The ultimate camera library that significantly reduces implementation time and effort. Written with and for SwiftUI.
6 | DESC
7 |
8 | s.version = '3.0.1'
9 | s.ios.deployment_target = '14.0'
10 | s.swift_version = '6.0'
11 |
12 | s.source_files = 'Sources/**/*.{swift}'
13 | s.resources = 'Sources/Internal/Assets/*.{xcassets, json}'
14 | s.dependency 'MijickTimer'
15 | s.frameworks = 'SwiftUI', 'Foundation', 'AVKit', 'AVFoundation', 'MijickTimer'
16 |
17 | s.homepage = 'https://github.com/Mijick/Camera.git'
18 | s.license = { :type => 'Apache License 2.0', :file => 'LICENSE' }
19 | s.author = { 'Tomasz Kurylik from Mijick' => 'tomasz.kurylik@mijick.com' }
20 | s.source = { :git => 'https://github.com/Mijick/Camera.git', :tag => s.version.to_s }
21 | end
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "MijickCamera",
8 | platforms: [
9 | .iOS(.v14)
10 | ],
11 | products: [
12 | .library(name: "MijickCamera", targets: ["MijickCamera"]),
13 | ],
14 | dependencies: [
15 | .package(url: "https://github.com/Mijick/Timer", exact: "2.0.0")
16 | ],
17 | targets: [
18 | .target(name: "MijickCamera", dependencies: [.product(name: "MijickTimer", package: "Timer")], path: "Sources", resources: [.process("Internal/Assets")]),
19 | .testTarget(name: "MijickCameraTests", dependencies: ["MijickCamera"], path: "Tests")
20 | ],
21 | swiftLanguageModes: [.v6]
22 | )
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
Camera made simple
11 | Significantly reduces implementation time and effort. Keeps your code clean.
12 |
13 |
14 |
15 |
16 | Try demo we prepared
17 | |
18 | Framework documentation
19 | |
20 | Roadmap
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Camera Position |
37 | Media Capturing |
38 | Gestures |
39 | Filters |
40 |
41 |
42 |
43 |
44 |
45 |
46 | |
47 |
48 |
49 | |
50 |
51 |
52 | |
53 |
54 |
55 | |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | # ✨ Features
92 |
93 |
94 | 🙏🏻 |
95 | Automatically handles permissions |
96 |
97 |
98 | 🖼️ |
99 | Image capture |
100 |
101 |
102 | 🎬️ |
103 | Video capture (with or without sound) |
104 |
105 |
106 | 📸 |
107 | Camera position changes |
108 |
109 |
110 | 🔍️ |
111 | Supports manual zoom |
112 |
113 |
114 | 👁️ |
115 | Supports manual focus |
116 |
117 |
118 | 🎞️ |
119 | Changeable frame rate |
120 |
121 |
122 | 📺️ |
123 | Changeable camera resolution |
124 |
125 |
126 | 🙈 |
127 | Camera filters |
128 |
129 |
130 | 🔦 |
131 | Torch |
132 |
133 |
134 | 📸 |
135 | Flash |
136 |
137 |
138 | ⏱️ |
139 | Other camera settings (exposure duration, target bias, ISO, HDR mode and more) |
140 |
141 |
142 | ☢️ |
143 | Displays error screen if permissions are not granted |
144 |
145 |
146 | 🖼️ |
147 | Displays captured media screen |
148 |
149 |
150 | 📱 |
151 | Modern and minimalistic UI |
152 |
153 |
154 | 🕺 |
155 | Beautiful animations |
156 |
157 |
158 | 🚧 |
159 | Fully customizable screens |
160 |
161 |
162 | 🤏🏼 |
163 | Gestures support |
164 |
165 |
166 | 📲 |
167 | Blocks screen orientation change |
168 |
169 |
170 | ⚡️ |
171 | Supports Swift 6 |
172 |
173 |
174 | 🚀 |
175 | ... and others |
176 |
177 |
178 |
179 |
180 | # ☀️ Why MijickCamera?
181 | The main problem we wanted to solve was the complexity of implementing camera into Swift projects; to get a camera view, you either have to accept a number of trade-offs or spend hours wrestling with the complexity of the AVKit framework. Here is why we think we have successfully solved the problem:
182 |
183 |
184 |
The power of simplicity
185 | Thanks to a modern and minimalistic UI and a thoughtfully designed public API, the most common use cases can be solved with just a few lines of code.
186 |
187 |
188 |
189 |
190 |
Three in one
191 | MCamera contains three screens - Error Screen, Captured Media Screen and Camera Screen - making the process of handling camera states super easy. Moreover, MijickCamera automatically manages the entire workflow, from requesting camera permissions to displaying the results of camera captures!
192 |
193 |
194 |
195 |
196 |
Engineered for limitless creativity
197 | Every application is a special one, and we at Mijick know this very well, thus we have given you the possibility to customize each of the three screens that constitute MCamera.
198 |
199 |
200 |
201 |
202 | ### There is much more besides:
203 | - Advanced camera controls.
204 | - Gesture support.
205 | - Thoroughly designed animations.
206 | - Supports Swift 6.0.
207 | - ... and much more.
208 |
209 |
210 |
211 | # 🚀 How to use it?
212 | Visit the framework's [documentation page](https://link.mijick.com/camera-wiki) to learn how to integrate your project with **MijickCamera**.
213 |
214 |
215 | # 🍀 Community
216 | Join the welcoming community of developers on [Discord](https://link.mijick.com/discord).
217 |
218 |
219 | # 🌼 Contribute
220 | To contribute a feature or idea to **MijickCamera**, create an [issue](https://github.com/Mijick/Camera/issues/new?assignees=FulcrumOne&labels=state%3A+inactive%2C+type%3A+feature&projects=&template=🚀-feature-request.md&title=%5BFREQ%5D) explaining your idea or bring it up on [Discord](https://discord.com/invite/dT5V7nm5SC).
221 | If you find a bug, please create an [issue](https://github.com/Mijick/Camera/issues/new?assignees=FulcrumOne%2C+jay-jay-lama&labels=state%3A+inactive%2C+type%3A+bug&projects=&template=🦟-bug-report.md&title=%5BBUG%5D).
222 | If you would like to contribute, please refer to the [Contribution Guidelines](https://github.com/Mijick/Camera/blob/main/.github/CONTRIBUTING.md).
223 |
224 |
225 | # 💜 Sponsor our work
226 | Support our work by [becoming a backer](https://link.mijick.com/buymeacoffee).
227 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/mijick-background-inverted.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xEF",
9 | "green" : "0xEF",
10 | "red" : "0xF1"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/mijick-background-primary-50.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "0.500",
8 | "blue" : "0x05",
9 | "green" : "0x05",
10 | "red" : "0x05"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/mijick-background-primary-80.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "0.800",
8 | "blue" : "0x05",
9 | "green" : "0x05",
10 | "red" : "0x05"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/mijick-background-primary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x05",
9 | "green" : "0x05",
10 | "red" : "0x05"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/mijick-background-red.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x3A",
9 | "green" : "0x36",
10 | "red" : "0xBC"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/mijick-background-secondary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x20",
9 | "green" : "0x20",
10 | "red" : "0x20"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/mijick-background-yellow.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x40",
9 | "green" : "0xCA",
10 | "red" : "0xF3"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/mijick-text-brand.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xDF",
9 | "green" : "0x90",
10 | "red" : "0x8A"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/mijick-text-primary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xEF",
9 | "green" : "0xEF",
10 | "red" : "0xF1"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/mijick-text-secondary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xC8",
9 | "green" : "0xC6",
10 | "red" : "0xC6"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Colors.xcassets/mijick-text-tertiary.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xAB",
9 | "green" : "0xA8",
10 | "red" : "0xA8"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-cancel.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-cancel.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-cancel.imageset/mijick-icon-cancel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-cancel.imageset/mijick-icon-cancel.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-change-camera.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-change-camera.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-change-camera.imageset/mijick-icon-change-camera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-change-camera.imageset/mijick-icon-change-camera.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-check.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-check.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-check.imageset/mijick-icon-check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-check.imageset/mijick-icon-check.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-crosshair.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-crosshair.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-crosshair.imageset/mijick-icon-crosshair.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-crosshair.imageset/mijick-icon-crosshair.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flash-auto.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-flash-auto.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flash-auto.imageset/mijick-icon-flash-auto.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flash-auto.imageset/mijick-icon-flash-auto.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flash-off.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-flash-off.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flash-off.imageset/mijick-icon-flash-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flash-off.imageset/mijick-icon-flash-off.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flash-on.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-flash-on.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flash-on.imageset/mijick-icon-flash-on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flash-on.imageset/mijick-icon-flash-on.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flip-off.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-flip-off.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flip-off.imageset/mijick-icon-flip-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flip-off.imageset/mijick-icon-flip-off.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flip-on.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-flip-on.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flip-on.imageset/mijick-icon-flip-on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-flip-on.imageset/mijick-icon-flip-on.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-grid-off.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-grid-off.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-grid-off.imageset/mijick-icon-grid-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-grid-off.imageset/mijick-icon-grid-off.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-grid-on.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-grid-on.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-grid-on.imageset/mijick-icon-grid-on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-grid-on.imageset/mijick-icon-grid-on.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-light.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-light.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-light.imageset/mijick-icon-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-light.imageset/mijick-icon-light.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-photo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-photo.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-photo.imageset/mijick-icon-photo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-photo.imageset/mijick-icon-photo.png
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-video.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mijick-icon-video.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Assets/Icons.xcassets/mijick-icon-video.imageset/mijick-icon-video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mijick/Camera/10cde2e73d9579dc69a825b42db2366312c5e4d1/Sources/Internal/Assets/Icons.xcassets/mijick-icon-video.imageset/mijick-icon-video.png
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/AVCaptureVideoOrientation++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AVCaptureVideoOrientation++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 | import AVKit
14 |
15 | // MARK: To Angle
16 | extension AVCaptureVideoOrientation {
17 | func getAngle() -> Angle { switch self {
18 | case .portrait: .degrees(0)
19 | case .landscapeLeft: .degrees(-90)
20 | case .landscapeRight: .degrees(90)
21 | case .portraitUpsideDown: .degrees(180)
22 | default: .degrees(0)
23 | }}
24 | }
25 |
26 | // MARK: To UIImageOrientation
27 | extension AVCaptureVideoOrientation {
28 | func toImageOrientation() -> UIImage.Orientation { switch self {
29 | case .portrait: .downMirrored
30 | case .landscapeLeft: .leftMirrored
31 | case .landscapeRight: .rightMirrored
32 | case .portraitUpsideDown: .upMirrored
33 | default: .up
34 | }}
35 | }
36 |
37 | // MARK: To UIDeviceOrientation
38 | extension AVCaptureVideoOrientation {
39 | func toDeviceOrientation() -> UIDeviceOrientation { switch self {
40 | case .portrait: .portrait
41 | case .portraitUpsideDown: .portraitUpsideDown
42 | case .landscapeLeft: .landscapeLeft
43 | case .landscapeRight: .landscapeRight
44 | default: .portrait
45 | }}
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/AVVideoComposition++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AVVideoComposition++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | // MARK: Apply Filters
15 | extension AVVideoComposition {
16 | static func applyFilters(to asset: AVAsset, applyFiltersAction: @Sendable @escaping (AVAsynchronousCIImageFilteringRequest) -> ()) async throws -> AVVideoComposition {
17 | if #available(iOS 16.0, *) { return try await AVVideoComposition.videoComposition(with: asset, applyingCIFiltersWithHandler: applyFiltersAction) }
18 | return AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: applyFiltersAction)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/Animation++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Animation++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: Custom Animation
15 | extension Animation {
16 | static var mSpring: Animation { .spring(duration: duration, bounce: 0, blendDuration: 0) }
17 | }
18 | extension Animation {
19 | static var duration: CGFloat { 0.3 }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/CIFilter++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CIFilter++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | extension CIFilter: @unchecked @retroactive Sendable {}
15 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/CIImage++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CIImage++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: Applying Filters
15 | extension CIImage {
16 | func applyingFilters(_ filters: [CIFilter]) -> CIImage {
17 | var ciImage = self
18 | filters.forEach {
19 | $0.setValue(ciImage, forKey: kCIInputImageKey)
20 | ciImage = $0.outputImage ?? ciImage
21 | }
22 | return ciImage
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/CameraUtilities++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraUtilities++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | // MARK: To Device Flash Mode
15 | extension CameraFlashMode {
16 | func toDeviceFlashMode() -> AVCaptureDevice.FlashMode { switch self {
17 | case .off: .off
18 | case .on: .on
19 | case .auto: .auto
20 | }}
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/CaseIterable++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaseIterable++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import Foundation
13 |
14 | // MARK: Next
15 | extension CaseIterable where Self: Equatable {
16 | func next() -> Self {
17 | guard let index = Self.allCases.firstIndex(of: self) else { return self }
18 |
19 | let nextIndex = Self.allCases.index(after: index)
20 | return Self.allCases[nextIndex == Self.allCases.endIndex ? Self.allCases.startIndex : nextIndex]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/FileManager++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileManager++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: Prepare Place for Video Output
15 | extension FileManager {
16 | static func prepareURLForVideoOutput() -> URL? {
17 | guard let fileUrl = getFileUrl() else { return nil }
18 |
19 | clearPlaceIfTaken(fileUrl)
20 | return fileUrl
21 | }
22 | }
23 | private extension FileManager {
24 | static func getFileUrl() -> URL? {
25 | FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
26 | .first?
27 | .appendingPathComponent(videoPath)
28 | }
29 | static func clearPlaceIfTaken(_ url: URL) {
30 | try? FileManager.default.removeItem(at: url)
31 | }
32 | }
33 | private extension FileManager {
34 | static var videoPath: String { "mijick-camera-video-output.mp4" }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/Task++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Task++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import Foundation
13 |
14 | // MARK: Sleep
15 | extension Task where Success == Never, Failure == Never {
16 | static func sleep(seconds: CGFloat) async {
17 | try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/UIImage.Orientation++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage.Orientation++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: From CGImagePropertyOrientation
15 | extension UIImage.Orientation {
16 | init(_ orientation: CGImagePropertyOrientation) { switch orientation {
17 | case .down: self = .down
18 | case .downMirrored: self = .downMirrored
19 | case .left: self = .left
20 | case .leftMirrored: self = .leftMirrored
21 | case .right: self = .right
22 | case .rightMirrored: self = .rightMirrored
23 | case .up: self = .up
24 | case .upMirrored: self = .upMirrored
25 | }}
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/UIView++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: Add to Parent
15 | extension UIView {
16 | func addToParent(_ view: UIView) {
17 | view.addSubview(self)
18 |
19 | translatesAutoresizingMaskIntoConstraints = false
20 | leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
21 | rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
22 | topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
23 | bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
24 | }
25 | }
26 |
27 | // MARK: Apply Blur Effect
28 | extension UIView {
29 | func applyBlurEffect(style: UIBlurEffect.Style) {
30 | let blurEffectView = UIVisualEffectView()
31 | blurEffectView.frame = bounds
32 | blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
33 | blurEffectView.effect = UIBlurEffect(style: style)
34 |
35 | addSubview(blurEffectView)
36 | }
37 | }
38 |
39 | // MARK: Tags
40 | extension Int {
41 | static var blurViewTag: Int { 2137 }
42 | static var focusIndicatorTag: Int { 29 }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/Internal/Extensions/View++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View++.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: Erased
15 | extension View {
16 | func erased() -> AnyView { .init(self) }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/CameraManager+Attributes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraManager+Attributes.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | struct CameraManagerAttributes {
15 | var capturedMedia: MCameraMedia? = nil
16 | var error: MCameraError? = nil
17 |
18 | var outputType: CameraOutputType = .photo
19 | var cameraPosition: CameraPosition = .back
20 | var isAudioSourceAvailable: Bool = true
21 | var zoomFactor: CGFloat = 1.0
22 | var flashMode: CameraFlashMode = .off
23 | var lightMode: CameraLightMode = .off
24 | var resolution: AVCaptureSession.Preset = .hd1920x1080
25 | var frameRate: Int32 = 30
26 | var cameraExposure: CameraExposure = .init()
27 | var hdrMode: CameraHDRMode = .auto
28 | var cameraFilters: [CIFilter] = []
29 | var mirrorOutput: Bool = false
30 | var isGridVisible: Bool = true
31 |
32 | var deviceOrientation: AVCaptureVideoOrientation = .portrait
33 | var frameOrientation: CGImagePropertyOrientation = .right
34 | var orientationLocked: Bool = false
35 | var userBlockedScreenRotation: Bool = false
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/CameraManager+MotionManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraManager+MotionManager.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import CoreMotion
13 | import AVKit
14 |
15 | @MainActor class CameraManagerMotionManager {
16 | private(set) var parent: CameraManager!
17 | private(set) var manager: CMMotionManager = .init()
18 | }
19 |
20 | // MARK: Setup
21 | extension CameraManagerMotionManager {
22 | func setup(parent: CameraManager) {
23 | self.parent = parent
24 | manager.accelerometerUpdateInterval = 0.05
25 | manager.startAccelerometerUpdates(to: .current ?? .init(), withHandler: handleAccelerometerUpdates)
26 | }
27 | }
28 | private extension CameraManagerMotionManager {
29 | func handleAccelerometerUpdates(_ data: CMAccelerometerData?, _ error: Error?) {
30 | guard let data, error == nil else { return }
31 |
32 | let newDeviceOrientation = getDeviceOrientation(data.acceleration)
33 | updateDeviceOrientation(newDeviceOrientation)
34 | updateUserBlockedScreenRotation()
35 | updateFrameOrientation()
36 | redrawGrid()
37 | }
38 | }
39 | private extension CameraManagerMotionManager {
40 | func getDeviceOrientation(_ acceleration: CMAcceleration) -> AVCaptureVideoOrientation { switch acceleration {
41 | case let acceleration where acceleration.x >= 0.75: .landscapeLeft
42 | case let acceleration where acceleration.x <= -0.75: .landscapeRight
43 | case let acceleration where acceleration.y <= -0.75: .portrait
44 | case let acceleration where acceleration.y >= 0.75: .portraitUpsideDown
45 | default: parent.attributes.deviceOrientation
46 | }}
47 | func updateDeviceOrientation(_ newDeviceOrientation: AVCaptureVideoOrientation) { if newDeviceOrientation != parent.attributes.deviceOrientation {
48 | parent.attributes.deviceOrientation = newDeviceOrientation
49 | }}
50 | func updateUserBlockedScreenRotation() {
51 | let newUserBlockedScreenRotation = getNewUserBlockedScreenRotation()
52 | if newUserBlockedScreenRotation != parent.attributes.userBlockedScreenRotation { parent.attributes.userBlockedScreenRotation = newUserBlockedScreenRotation }
53 | }
54 | func updateFrameOrientation() { if UIDevice.current.orientation != .portraitUpsideDown {
55 | let newFrameOrientation = getNewFrameOrientation(parent.attributes.orientationLocked ? .portrait : UIDevice.current.orientation)
56 | updateFrameOrientation(newFrameOrientation)
57 | }}
58 | func redrawGrid() { if !parent.attributes.orientationLocked {
59 | parent.cameraGridView.draw(.zero)
60 | }}
61 | }
62 | private extension CameraManagerMotionManager {
63 | func getNewUserBlockedScreenRotation() -> Bool { switch parent.attributes.deviceOrientation.rawValue == UIDevice.current.orientation.rawValue {
64 | case true: false
65 | case false: !parent.attributes.orientationLocked
66 | }}
67 | func getNewFrameOrientation(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { switch parent.attributes.cameraPosition {
68 | case .back: getNewFrameOrientationForBackCamera(orientation)
69 | case .front: getNewFrameOrientationForFrontCamera(orientation)
70 | }}
71 | func updateFrameOrientation(_ newFrameOrientation: CGImagePropertyOrientation) { if newFrameOrientation != parent.attributes.frameOrientation {
72 | let shouldAnimate = shouldAnimateFrameOrientationChange(newFrameOrientation)
73 | updateFrameOrientation(withAnimation: shouldAnimate, newFrameOrientation: newFrameOrientation)
74 | }}
75 | }
76 | private extension CameraManagerMotionManager {
77 | func getNewFrameOrientationForBackCamera(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { switch orientation {
78 | case .portrait: parent.attributes.mirrorOutput ? .leftMirrored : .right
79 | case .landscapeLeft: parent.attributes.mirrorOutput ? .upMirrored : .up
80 | case .landscapeRight: parent.attributes.mirrorOutput ? .downMirrored : .down
81 | default: parent.attributes.mirrorOutput ? .leftMirrored : .right
82 | }}
83 | func getNewFrameOrientationForFrontCamera(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { switch orientation {
84 | case .portrait: parent.attributes.mirrorOutput ? .right : .leftMirrored
85 | case .landscapeLeft: parent.attributes.mirrorOutput ? .down : .downMirrored
86 | case .landscapeRight: parent.attributes.mirrorOutput ? .up : .upMirrored
87 | default: parent.attributes.mirrorOutput ? .right : .leftMirrored
88 | }}
89 | func shouldAnimateFrameOrientationChange(_ newFrameOrientation: CGImagePropertyOrientation) -> Bool {
90 | let backCameraOrientations: [CGImagePropertyOrientation] = [.left, .right, .up, .down],
91 | frontCameraOrientations: [CGImagePropertyOrientation] = [.leftMirrored, .rightMirrored, .upMirrored, .downMirrored]
92 |
93 | return (backCameraOrientations.contains(newFrameOrientation) && backCameraOrientations.contains(parent.attributes.frameOrientation)) ||
94 | (frontCameraOrientations.contains(parent.attributes.frameOrientation) && frontCameraOrientations.contains(newFrameOrientation))
95 | }
96 | func updateFrameOrientation(withAnimation shouldAnimate: Bool, newFrameOrientation: CGImagePropertyOrientation) { Task {
97 | await parent.cameraMetalView.beginCameraOrientationAnimation(if: shouldAnimate)
98 | parent.attributes.frameOrientation = newFrameOrientation
99 | parent.cameraMetalView.finishCameraOrientationAnimation(if: shouldAnimate)
100 | }}
101 | }
102 |
103 | // MARK: Reset
104 | extension CameraManagerMotionManager {
105 | func reset() {
106 | manager.stopAccelerometerUpdates()
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/CameraManager+NotificationCenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraManager+NotificationCenter.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import Foundation
13 |
14 | @MainActor class CameraManagerNotificationCenter {
15 | private(set) var parent: CameraManager!
16 | }
17 |
18 | // MARK: Setup
19 | extension CameraManagerNotificationCenter {
20 | func setup(parent: CameraManager) {
21 | self.parent = parent
22 | NotificationCenter.default.addObserver(self, selector: #selector(handleSessionWasInterrupted), name: .AVCaptureSessionWasInterrupted, object: parent.captureSession)
23 | }
24 | }
25 | private extension CameraManagerNotificationCenter {
26 | @objc func handleSessionWasInterrupted() {
27 | parent.attributes.lightMode = .off
28 | parent.videoOutput.reset()
29 | }
30 | }
31 |
32 | // MARK: Reset
33 | extension CameraManagerNotificationCenter {
34 | func reset() {
35 | NotificationCenter.default.removeObserver(self, name: .AVCaptureSessionWasInterrupted, object: parent?.captureSession)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/CameraManager+PermissionsManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraManager+PermissionsManager.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | @MainActor class CameraManagerPermissionsManager {}
15 |
16 | // MARK: Request Access
17 | extension CameraManagerPermissionsManager {
18 | func requestAccess(parent: CameraManager) async throws(MCameraError) {
19 | do {
20 | try await getAuthorizationStatus(for: .video)
21 | if parent.attributes.isAudioSourceAvailable { try await getAuthorizationStatus(for: .audio) }
22 | }
23 | catch {
24 | parent.attributes.error = error
25 | throw error
26 | }
27 | }
28 | }
29 | private extension CameraManagerPermissionsManager {
30 | func getAuthorizationStatus(for mediaType: AVMediaType) async throws(MCameraError) { switch AVCaptureDevice.authorizationStatus(for: mediaType) {
31 | case .denied, .restricted: throw getPermissionsError(mediaType)
32 | case .notDetermined: try await requestAccess(for: mediaType)
33 | default: return
34 | }}
35 | }
36 | private extension CameraManagerPermissionsManager {
37 | func requestAccess(for mediaType: AVMediaType) async throws(MCameraError) {
38 | let isGranted = await AVCaptureDevice.requestAccess(for: mediaType)
39 | if !isGranted { throw getPermissionsError(mediaType) }
40 | }
41 | func getPermissionsError(_ mediaType: AVMediaType) -> MCameraError { switch mediaType {
42 | case .audio: .microphonePermissionsNotGranted
43 | case .video: .cameraPermissionsNotGranted
44 | default: fatalError()
45 | }}
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/CameraManager+PhotoOutput.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraManager+PhotoOutput.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | @MainActor class CameraManagerPhotoOutput: NSObject {
15 | private(set) var parent: CameraManager!
16 | private(set) var output: AVCapturePhotoOutput = .init()
17 | }
18 |
19 | // MARK: Setup
20 | extension CameraManagerPhotoOutput {
21 | func setup(parent: CameraManager) throws(MCameraError) {
22 | self.parent = parent
23 | try self.parent.captureSession.add(output: output)
24 | }
25 | }
26 |
27 |
28 | // MARK: - CAPTURE PHOTO
29 |
30 |
31 |
32 | // MARK: Capture
33 | extension CameraManagerPhotoOutput {
34 | func capture() {
35 | let settings = getPhotoOutputSettings()
36 |
37 | configureOutput()
38 | output.capturePhoto(with: settings, delegate: self)
39 | parent.cameraMetalView.performImageCaptureAnimation()
40 | }
41 | }
42 | private extension CameraManagerPhotoOutput {
43 | func getPhotoOutputSettings() -> AVCapturePhotoSettings {
44 | let settings = AVCapturePhotoSettings()
45 | settings.flashMode = parent.attributes.flashMode.toDeviceFlashMode()
46 | return settings
47 | }
48 | func configureOutput() {
49 | guard let connection = output.connection(with: .video), connection.isVideoMirroringSupported else { return }
50 |
51 | connection.isVideoMirrored = parent.attributes.mirrorOutput ? parent.attributes.cameraPosition != .front : parent.attributes.cameraPosition == .front
52 | connection.videoOrientation = parent.attributes.deviceOrientation
53 | }
54 | }
55 |
56 | // MARK: Receive Data
57 | extension CameraManagerPhotoOutput: @preconcurrency AVCapturePhotoCaptureDelegate {
58 | func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: (any Error)?) {
59 | guard let imageData = photo.fileDataRepresentation(),
60 | let ciImage = CIImage(data: imageData)
61 | else { return }
62 |
63 | let capturedCIImage = prepareCIImage(ciImage, parent.attributes.cameraFilters)
64 | let capturedCGImage = prepareCGImage(capturedCIImage)
65 | let capturedUIImage = prepareUIImage(capturedCGImage)
66 | let capturedMedia = MCameraMedia(data: capturedUIImage)
67 |
68 | parent.setCapturedMedia(capturedMedia)
69 | }
70 | }
71 | private extension CameraManagerPhotoOutput {
72 | func prepareCIImage(_ ciImage: CIImage, _ filters: [CIFilter]) -> CIImage {
73 | ciImage.applyingFilters(filters)
74 | }
75 | func prepareCGImage(_ ciImage: CIImage) -> CGImage? {
76 | CIContext().createCGImage(ciImage, from: ciImage.extent)
77 | }
78 | func prepareUIImage(_ cgImage: CGImage?) -> UIImage? {
79 | guard let cgImage else { return nil }
80 |
81 | let frameOrientation = getFixedFrameOrientation()
82 | let orientation = UIImage.Orientation(frameOrientation)
83 | let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: orientation)
84 | return uiImage
85 | }
86 | }
87 | private extension CameraManagerPhotoOutput {
88 | func getFixedFrameOrientation() -> CGImagePropertyOrientation {
89 | guard UIDevice.current.orientation != parent.attributes.deviceOrientation.toDeviceOrientation() else { return parent.attributes.frameOrientation }
90 |
91 | return switch (parent.attributes.deviceOrientation, parent.attributes.cameraPosition) {
92 | case (.portrait, .front): .left
93 | case (.portrait, .back): .right
94 | case (.landscapeLeft, .back): .down
95 | case (.landscapeRight, .back): .up
96 | case (.landscapeLeft, .front) where parent.attributes.mirrorOutput: .up
97 | case (.landscapeLeft, .front): .upMirrored
98 | case (.landscapeRight, .front) where parent.attributes.mirrorOutput: .down
99 | case (.landscapeRight, .front): .downMirrored
100 | default: .right
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/CameraManager+VideoOutput.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraManager+VideoOutput.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | @preconcurrency import AVKit
13 | import SwiftUI
14 | import MijickTimer
15 |
16 | @MainActor class CameraManagerVideoOutput: NSObject {
17 | private(set) var parent: CameraManager!
18 | private(set) var output: AVCaptureMovieFileOutput = .init()
19 | private(set) var timer: MTimer = .init(.camera)
20 | private(set) var recordingTime: MTime = .zero
21 | private(set) var firstRecordedFrame: UIImage?
22 | }
23 |
24 | // MARK: Setup
25 | extension CameraManagerVideoOutput {
26 | func setup(parent: CameraManager) throws(MCameraError) {
27 | self.parent = parent
28 | try parent.captureSession.add(output: output)
29 | }
30 | }
31 |
32 | // MARK: Reset
33 | extension CameraManagerVideoOutput {
34 | func reset() {
35 | timer.reset()
36 | }
37 | }
38 |
39 |
40 | // MARK: - CAPTURE VIDEO
41 |
42 |
43 |
44 | // MARK: Toggle
45 | extension CameraManagerVideoOutput {
46 | func toggleRecording() { switch output.isRecording {
47 | case true: stopRecording()
48 | case false: startRecording()
49 | }}
50 | }
51 |
52 | // MARK: Start Recording
53 | private extension CameraManagerVideoOutput {
54 | func startRecording() {
55 | guard let url = prepareUrlForVideoRecording() else { return }
56 |
57 | configureOutput()
58 | storeLastFrame()
59 | output.startRecording(to: url, recordingDelegate: self)
60 | startRecordingTimer()
61 | parent.objectWillChange.send()
62 | }
63 | }
64 | private extension CameraManagerVideoOutput {
65 | func prepareUrlForVideoRecording() -> URL? {
66 | FileManager.prepareURLForVideoOutput()
67 | }
68 | func configureOutput() {
69 | guard let connection = output.connection(with: .video), connection.isVideoMirroringSupported else { return }
70 |
71 | connection.isVideoMirrored = parent.attributes.mirrorOutput ? parent.attributes.cameraPosition != .front : parent.attributes.cameraPosition == .front
72 | connection.videoOrientation = parent.attributes.deviceOrientation
73 | }
74 | func storeLastFrame() {
75 | guard let texture = parent.cameraMetalView.currentDrawable?.texture,
76 | let ciImage = CIImage(mtlTexture: texture, options: nil),
77 | let cgImage = parent.cameraMetalView.ciContext.createCGImage(ciImage, from: ciImage.extent)
78 | else { return }
79 |
80 | firstRecordedFrame = UIImage(cgImage: cgImage, scale: 1.0, orientation: parent.attributes.deviceOrientation.toImageOrientation())
81 | }
82 | func startRecordingTimer() { try? timer
83 | .publish(every: 1) { [self] in
84 | recordingTime = $0
85 | parent.objectWillChange.send()
86 | }
87 | .start()
88 | }
89 | }
90 |
91 | // MARK: Stop Recording
92 | private extension CameraManagerVideoOutput {
93 | func stopRecording() {
94 | presentLastFrame()
95 | output.stopRecording()
96 | timer.reset()
97 | }
98 | }
99 | private extension CameraManagerVideoOutput {
100 | func presentLastFrame() {
101 | let firstRecordedFrame = MCameraMedia(data: firstRecordedFrame)
102 | parent.setCapturedMedia(firstRecordedFrame)
103 | }
104 | }
105 |
106 | // MARK: Receive Data
107 | extension CameraManagerVideoOutput: @preconcurrency AVCaptureFileOutputRecordingDelegate {
108 | func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: (any Error)?) { Task {
109 | let videoURL = try await prepareVideo(outputFileURL: outputFileURL, cameraFilters: parent.attributes.cameraFilters)
110 | let capturedVideo = MCameraMedia(data: videoURL)
111 |
112 | await Task.sleep(seconds: Animation.duration)
113 | parent.setCapturedMedia(capturedVideo)
114 | }}
115 | }
116 | private extension CameraManagerVideoOutput {
117 | func prepareVideo(outputFileURL: URL, cameraFilters: [CIFilter]) async throws -> URL {
118 | if cameraFilters.isEmpty { return outputFileURL }
119 |
120 | let asset = AVAsset(url: outputFileURL)
121 | let videoComposition = try await AVVideoComposition.applyFilters(to: asset) { self.applyFiltersToVideo($0, cameraFilters) }
122 | let fileUrl = FileManager.prepareURLForVideoOutput()
123 | let exportSession = prepareAssetExportSession(asset, fileUrl, videoComposition)
124 |
125 | try await exportVideo(exportSession, fileUrl)
126 | return fileUrl ?? outputFileURL
127 | }
128 | }
129 | private extension CameraManagerVideoOutput {
130 | nonisolated func applyFiltersToVideo(_ request: AVAsynchronousCIImageFilteringRequest, _ filters: [CIFilter]) {
131 | let videoFrame = prepareVideoFrame(request, filters)
132 | request.finish(with: videoFrame, context: nil)
133 | }
134 | nonisolated func exportVideo(_ exportSession: AVAssetExportSession?, _ fileUrl: URL?) async throws { if let fileUrl {
135 | if #available(iOS 18, *) { try await exportSession?.export(to: fileUrl, as: .mov) }
136 | else { await exportSession?.export() }
137 | }}
138 | }
139 | private extension CameraManagerVideoOutput {
140 | nonisolated func prepareVideoFrame(_ request: AVAsynchronousCIImageFilteringRequest, _ filters: [CIFilter]) -> CIImage { request
141 | .sourceImage
142 | .clampedToExtent()
143 | .applyingFilters(filters)
144 | }
145 | nonisolated func prepareAssetExportSession(_ asset: AVAsset, _ fileUrl: URL?, _ composition: AVVideoComposition?) -> AVAssetExportSession? {
146 | let export = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset1920x1080)
147 | export?.outputFileType = .mov
148 | export?.outputURL = fileUrl
149 | export?.videoComposition = composition
150 | return export
151 | }
152 | }
153 |
154 |
155 | // MARK: - HELPERS
156 | fileprivate extension MTimerID {
157 | static let camera: MTimerID = .init(rawValue: "mijick-camera")
158 | }
159 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/CameraManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraManager.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 | import AVKit
14 |
15 | @MainActor public class CameraManager: NSObject, ObservableObject {
16 | @Published var attributes: CameraManagerAttributes = .init()
17 |
18 | // MARK: Input
19 | private(set) var captureSession: any CaptureSession
20 | private(set) var frontCameraInput: (any CaptureDeviceInput)?
21 | private(set) var backCameraInput: (any CaptureDeviceInput)?
22 |
23 | // MARK: Output
24 | private(set) var photoOutput: CameraManagerPhotoOutput = .init()
25 | private(set) var videoOutput: CameraManagerVideoOutput = .init()
26 |
27 | // MARK: UI Elements
28 | private(set) var cameraView: UIView!
29 | private(set) var cameraLayer: AVCaptureVideoPreviewLayer = .init()
30 | private(set) var cameraMetalView: CameraMetalView = .init()
31 | private(set) var cameraGridView: CameraGridView = .init()
32 |
33 | // MARK: Others
34 | private(set) var permissionsManager: CameraManagerPermissionsManager = .init()
35 | private(set) var motionManager: CameraManagerMotionManager = .init()
36 | private(set) var notificationCenterManager: CameraManagerNotificationCenter = .init()
37 |
38 | // MARK: Initializer
39 | init(captureSession: CS, captureDeviceInputType: CDI.Type) {
40 | self.captureSession = captureSession
41 | self.frontCameraInput = CDI.get(mediaType: .video, position: .front)
42 | self.backCameraInput = CDI.get(mediaType: .video, position: .back)
43 | }
44 | }
45 |
46 | // MARK: Initialize
47 | extension CameraManager {
48 | func initialize(in view: UIView) {
49 | cameraView = view
50 | }
51 | }
52 |
53 | // MARK: Setup
54 | extension CameraManager {
55 | func setup() async throws(MCameraError) {
56 | try await permissionsManager.requestAccess(parent: self)
57 |
58 | setupCameraLayer()
59 | try setupDeviceInputs()
60 | try setupDeviceOutput()
61 | try setupFrameRecorder()
62 | notificationCenterManager.setup(parent: self)
63 | motionManager.setup(parent: self)
64 | try cameraMetalView.setup(parent: self)
65 | cameraGridView.setup(parent: self)
66 |
67 | startSession()
68 | }
69 | }
70 | private extension CameraManager {
71 | func setupCameraLayer() {
72 | captureSession.sessionPreset = attributes.resolution
73 |
74 | cameraLayer.session = captureSession as? AVCaptureSession
75 | cameraLayer.videoGravity = .resizeAspectFill
76 | cameraLayer.isHidden = true
77 | cameraView.layer.addSublayer(cameraLayer)
78 | }
79 | func setupDeviceInputs() throws(MCameraError) {
80 | try captureSession.add(input: getCameraInput())
81 | if let audioInput = getAudioInput() { try captureSession.add(input: audioInput) }
82 | }
83 | func setupDeviceOutput() throws(MCameraError) {
84 | try photoOutput.setup(parent: self)
85 | try videoOutput.setup(parent: self)
86 | }
87 | func setupFrameRecorder() throws(MCameraError) {
88 | let captureVideoOutput = AVCaptureVideoDataOutput()
89 | captureVideoOutput.setSampleBufferDelegate(cameraMetalView, queue: .main)
90 |
91 | try captureSession.add(output: captureVideoOutput)
92 | }
93 | func startSession() { Task {
94 | guard let device = getCameraInput()?.device else { return }
95 |
96 | try await startCaptureSession()
97 | try setupDevice(device)
98 | resetAttributes(device: device)
99 | cameraMetalView.performCameraEntranceAnimation()
100 | }}
101 | }
102 | private extension CameraManager {
103 | func getAudioInput() -> (any CaptureDeviceInput)? {
104 | guard attributes.isAudioSourceAvailable,
105 | let deviceInput = frontCameraInput ?? backCameraInput
106 | else { return nil }
107 |
108 | let captureDeviceInputType = type(of: deviceInput)
109 | let audioInput = captureDeviceInputType.get(mediaType: .audio, position: .unspecified)
110 | return audioInput
111 | }
112 | nonisolated func startCaptureSession() async throws {
113 | await captureSession.startRunning()
114 | }
115 | func setupDevice(_ device: any CaptureDevice) throws {
116 | try device.lockForConfiguration()
117 | device.setExposureMode(attributes.cameraExposure.mode, duration: attributes.cameraExposure.duration, iso: attributes.cameraExposure.iso)
118 | device.setExposureTargetBias(attributes.cameraExposure.targetBias)
119 | device.setFrameRate(attributes.frameRate)
120 | device.setZoomFactor(attributes.zoomFactor)
121 | device.setLightMode(attributes.lightMode)
122 | device.hdrMode = attributes.hdrMode
123 | device.unlockForConfiguration()
124 | }
125 | }
126 |
127 | // MARK: Cancel
128 | extension CameraManager {
129 | func cancel() {
130 | captureSession = captureSession.stopRunningAndReturnNewInstance()
131 | motionManager.reset()
132 | videoOutput.reset()
133 | notificationCenterManager.reset()
134 | }
135 | }
136 |
137 |
138 | // MARK: - LIVE ACTIONS
139 |
140 |
141 |
142 | // MARK: Capture Output
143 | extension CameraManager {
144 | func captureOutput() {
145 | guard !isChanging else { return }
146 |
147 | switch attributes.outputType {
148 | case .photo: photoOutput.capture()
149 | case .video: videoOutput.toggleRecording()
150 | }
151 | }
152 | }
153 |
154 | // MARK: Set Captured Media
155 | extension CameraManager {
156 | func setCapturedMedia(_ capturedMedia: MCameraMedia?) { withAnimation(.mSpring) {
157 | attributes.capturedMedia = capturedMedia
158 | }}
159 | }
160 |
161 | // MARK: Set Camera Output
162 | extension CameraManager {
163 | func setOutputType(_ outputType: CameraOutputType) {
164 | guard outputType != attributes.outputType, !isChanging else { return }
165 | attributes.outputType = outputType
166 | }
167 | }
168 |
169 | // MARK: Set Camera Position
170 | extension CameraManager {
171 | func setCameraPosition(_ position: CameraPosition) async throws {
172 | guard position != attributes.cameraPosition, !isChanging else { return }
173 |
174 | await cameraMetalView.beginCameraFlipAnimation()
175 | try changeCameraInput(position)
176 | resetAttributesWhenChangingCamera(position)
177 | await cameraMetalView.finishCameraFlipAnimation()
178 | }
179 | }
180 | private extension CameraManager {
181 | func changeCameraInput(_ position: CameraPosition) throws {
182 | if let input = getCameraInput() { captureSession.remove(input: input) }
183 | try captureSession.add(input: getCameraInput(position))
184 | }
185 | func resetAttributesWhenChangingCamera(_ position: CameraPosition) {
186 | resetAttributes(device: getCameraInput(position)?.device)
187 | attributes.cameraPosition = position
188 | }
189 | }
190 |
191 | // MARK: Set Camera Zoom
192 | extension CameraManager {
193 | func setCameraZoomFactor(_ zoomFactor: CGFloat) throws {
194 | guard let device = getCameraInput()?.device, zoomFactor != attributes.zoomFactor, !isChanging else { return }
195 |
196 | try setDeviceZoomFactor(zoomFactor, device)
197 | attributes.zoomFactor = device.videoZoomFactor
198 | }
199 | }
200 | private extension CameraManager {
201 | func setDeviceZoomFactor(_ zoomFactor: CGFloat, _ device: any CaptureDevice) throws {
202 | try device.lockForConfiguration()
203 | device.setZoomFactor(zoomFactor)
204 | device.unlockForConfiguration()
205 | }
206 | }
207 |
208 | // MARK: Set Camera Focus
209 | extension CameraManager {
210 | func setCameraFocus(at touchPoint: CGPoint) throws {
211 | guard let device = getCameraInput()?.device, !isChanging else { return }
212 |
213 | let focusPoint = convertTouchPointToFocusPoint(touchPoint)
214 | try setDeviceCameraFocus(focusPoint, device)
215 | cameraMetalView.performCameraFocusAnimation(touchPoint: touchPoint)
216 | }
217 | }
218 | private extension CameraManager {
219 | func convertTouchPointToFocusPoint(_ touchPoint: CGPoint) -> CGPoint { .init(
220 | x: touchPoint.y / cameraView.frame.height,
221 | y: 1 - touchPoint.x / cameraView.frame.width
222 | )}
223 | func setDeviceCameraFocus(_ focusPoint: CGPoint, _ device: any CaptureDevice) throws {
224 | try device.lockForConfiguration()
225 | device.setFocusPointOfInterest(focusPoint)
226 | device.setExposurePointOfInterest(focusPoint)
227 | device.unlockForConfiguration()
228 | }
229 | }
230 |
231 | // MARK: Set Flash Mode
232 | extension CameraManager {
233 | func setFlashMode(_ flashMode: CameraFlashMode) {
234 | guard let device = getCameraInput()?.device, device.hasFlash, flashMode != attributes.flashMode, !isChanging else { return }
235 | attributes.flashMode = flashMode
236 | }
237 | }
238 |
239 | // MARK: Set Light Mode
240 | extension CameraManager {
241 | func setLightMode(_ lightMode: CameraLightMode) throws {
242 | guard let device = getCameraInput()?.device, device.hasTorch, lightMode != attributes.lightMode, !isChanging else { return }
243 |
244 | try setDeviceLightMode(lightMode, device)
245 | attributes.lightMode = device.lightMode
246 | }
247 | }
248 | private extension CameraManager {
249 | func setDeviceLightMode(_ lightMode: CameraLightMode, _ device: any CaptureDevice) throws {
250 | try device.lockForConfiguration()
251 | device.setLightMode(lightMode)
252 | device.unlockForConfiguration()
253 | }
254 | }
255 |
256 | // MARK: Set Mirror Output
257 | extension CameraManager {
258 | func setMirrorOutput(_ mirrorOutput: Bool) {
259 | guard mirrorOutput != attributes.mirrorOutput, !isChanging else { return }
260 | attributes.mirrorOutput = mirrorOutput
261 | }
262 | }
263 |
264 | // MARK: Set Grid Visibility
265 | extension CameraManager {
266 | func setGridVisibility(_ isGridVisible: Bool) {
267 | guard isGridVisible != attributes.isGridVisible, !isChanging else { return }
268 | cameraGridView.setVisibility(isGridVisible)
269 | }
270 | }
271 |
272 | // MARK: Set Camera Filters
273 | extension CameraManager {
274 | func setCameraFilters(_ cameraFilters: [CIFilter]) {
275 | guard cameraFilters != attributes.cameraFilters, !isChanging else { return }
276 | attributes.cameraFilters = cameraFilters
277 | }
278 | }
279 |
280 | // MARK: Set Exposure Mode
281 | extension CameraManager {
282 | func setExposureMode(_ exposureMode: AVCaptureDevice.ExposureMode) throws {
283 | guard let device = getCameraInput()?.device, exposureMode != attributes.cameraExposure.mode, !isChanging else { return }
284 |
285 | try setDeviceExposureMode(exposureMode, device)
286 | attributes.cameraExposure.mode = device.exposureMode
287 | }
288 | }
289 | private extension CameraManager {
290 | func setDeviceExposureMode(_ exposureMode: AVCaptureDevice.ExposureMode, _ device: any CaptureDevice) throws {
291 | try device.lockForConfiguration()
292 | device.setExposureMode(exposureMode, duration: attributes.cameraExposure.duration, iso: attributes.cameraExposure.iso)
293 | device.unlockForConfiguration()
294 | }
295 | }
296 |
297 | // MARK: Set Exposure Duration
298 | extension CameraManager {
299 | func setExposureDuration(_ exposureDuration: CMTime) throws {
300 | guard let device = getCameraInput()?.device, exposureDuration != attributes.cameraExposure.duration, !isChanging else { return }
301 |
302 | try setDeviceExposureDuration(exposureDuration, device)
303 | attributes.cameraExposure.duration = device.exposureDuration
304 | }
305 | }
306 | private extension CameraManager {
307 | func setDeviceExposureDuration(_ exposureDuration: CMTime, _ device: any CaptureDevice) throws {
308 | try device.lockForConfiguration()
309 | device.setExposureMode(.custom, duration: exposureDuration, iso: attributes.cameraExposure.iso)
310 | device.unlockForConfiguration()
311 | }
312 | }
313 |
314 | // MARK: Set ISO
315 | extension CameraManager {
316 | func setISO(_ iso: Float) throws {
317 | guard let device = getCameraInput()?.device, iso != attributes.cameraExposure.iso, !isChanging else { return }
318 |
319 | try setDeviceISO(iso, device)
320 | attributes.cameraExposure.iso = device.iso
321 | }
322 | }
323 | private extension CameraManager {
324 | func setDeviceISO(_ iso: Float, _ device: any CaptureDevice) throws {
325 | try device.lockForConfiguration()
326 | device.setExposureMode(.custom, duration: attributes.cameraExposure.duration, iso: iso)
327 | device.unlockForConfiguration()
328 | }
329 | }
330 |
331 | // MARK: Set Exposure Target Bias
332 | extension CameraManager {
333 | func setExposureTargetBias(_ exposureTargetBias: Float) throws {
334 | guard let device = getCameraInput()?.device, exposureTargetBias != attributes.cameraExposure.targetBias, !isChanging else { return }
335 |
336 | try setDeviceExposureTargetBias(exposureTargetBias, device)
337 | attributes.cameraExposure.targetBias = device.exposureTargetBias
338 | }
339 | }
340 | private extension CameraManager {
341 | func setDeviceExposureTargetBias(_ exposureTargetBias: Float, _ device: any CaptureDevice) throws {
342 | try device.lockForConfiguration()
343 | device.setExposureTargetBias(exposureTargetBias)
344 | device.unlockForConfiguration()
345 | }
346 | }
347 |
348 | // MARK: Set HDR Mode
349 | extension CameraManager {
350 | func setHDRMode(_ hdrMode: CameraHDRMode) throws {
351 | guard let device = getCameraInput()?.device, hdrMode != attributes.hdrMode, !isChanging else { return }
352 |
353 | try setDeviceHDRMode(hdrMode, device)
354 | attributes.hdrMode = hdrMode
355 | }
356 | }
357 | private extension CameraManager {
358 | func setDeviceHDRMode(_ hdrMode: CameraHDRMode, _ device: any CaptureDevice) throws {
359 | try device.lockForConfiguration()
360 | device.hdrMode = hdrMode
361 | device.unlockForConfiguration()
362 | }
363 | }
364 |
365 | // MARK: Set Resolution
366 | extension CameraManager {
367 | func setResolution(_ resolution: AVCaptureSession.Preset) {
368 | guard resolution != attributes.resolution, resolution != attributes.resolution, !isChanging else { return }
369 |
370 | captureSession.sessionPreset = resolution
371 | attributes.resolution = resolution
372 | }
373 | }
374 |
375 | // MARK: Set Frame Rate
376 | extension CameraManager {
377 | func setFrameRate(_ frameRate: Int32) throws {
378 | guard let device = getCameraInput()?.device, frameRate != attributes.frameRate, !isChanging else { return }
379 |
380 | try setDeviceFrameRate(frameRate, device)
381 | attributes.frameRate = device.activeVideoMaxFrameDuration.timescale
382 | }
383 | }
384 | private extension CameraManager {
385 | func setDeviceFrameRate(_ frameRate: Int32, _ device: any CaptureDevice) throws {
386 | try device.lockForConfiguration()
387 | device.setFrameRate(frameRate)
388 | device.unlockForConfiguration()
389 | }
390 | }
391 |
392 |
393 | // MARK: - HELPERS
394 |
395 |
396 |
397 | // MARK: Attributes
398 | extension CameraManager {
399 | var hasFlash: Bool { getCameraInput()?.device.hasFlash ?? false }
400 | var hasLight: Bool { getCameraInput()?.device.hasTorch ?? false }
401 | }
402 | private extension CameraManager {
403 | var isChanging: Bool { cameraMetalView.isAnimating }
404 | }
405 |
406 | // MARK: Methods
407 | extension CameraManager {
408 | func resetAttributes(device: (any CaptureDevice)?) {
409 | guard let device else { return }
410 |
411 | var newAttributes = attributes
412 | newAttributes.cameraExposure.mode = device.exposureMode
413 | newAttributes.cameraExposure.duration = device.exposureDuration
414 | newAttributes.cameraExposure.iso = device.iso
415 | newAttributes.cameraExposure.targetBias = device.exposureTargetBias
416 | newAttributes.frameRate = device.activeVideoMaxFrameDuration.timescale
417 | newAttributes.zoomFactor = device.videoZoomFactor
418 | newAttributes.lightMode = device.lightMode
419 | newAttributes.hdrMode = device.hdrMode
420 |
421 | attributes = newAttributes
422 | }
423 | func getCameraInput(_ position: CameraPosition? = nil) -> (any CaptureDeviceInput)? { switch position ?? attributes.cameraPosition {
424 | case .front: frontCameraInput
425 | case .back: backCameraInput
426 | }}
427 | }
428 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/Helpers/Capture Device Input/CaptureDeviceInput+AVCaptureDeviceInput.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureDeviceInput+AVCaptureDeviceInput.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | extension AVCaptureDeviceInput: CaptureDeviceInput {
15 | static func get(mediaType: AVMediaType, position: AVCaptureDevice.Position?) -> Self? {
16 | let device = { switch mediaType {
17 | case .audio: AVCaptureDevice.default(for: .audio)
18 | case .video where position == .front: AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
19 | case .video where position == .back: AVCaptureDevice.default(for: .video)
20 | default: fatalError()
21 | }}()
22 |
23 | guard let device, let deviceInput = try? Self(device: device) else { return nil }
24 | return deviceInput
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/Helpers/Capture Device Input/CaptureDeviceInput+MockDeviceInput.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureDeviceInput+MockDeviceInput.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | class MockDeviceInput: NSObject, CaptureDeviceInput { required override init() {}
15 | var device: MockCaptureDevice = .init()
16 | }
17 |
18 | // MARK: Methods
19 | extension MockDeviceInput {
20 | static func get(mediaType: AVMediaType, position: AVCaptureDevice.Position?) -> Self? { .init() }
21 | }
22 |
23 | // MARK: Equatable
24 | extension MockDeviceInput {
25 | static func == (lhs: MockDeviceInput, rhs: MockDeviceInput) -> Bool { lhs.device.uniqueID == rhs.device.uniqueID }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/Helpers/Capture Device Input/CaptureDeviceInput.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureDeviceInput.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | protocol CaptureDeviceInput: NSObject {
15 | // MARK: Attributes
16 | associatedtype CD: CaptureDevice
17 | var device: CD { get }
18 |
19 | // MARK: Methods
20 | static func get(mediaType: AVMediaType, position: AVCaptureDevice.Position?) -> Self?
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/Helpers/Capture Device/CaptureDevice+AVCaptureDevice.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureDevice+AVCaptureDevice.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | // MARK: Getters
15 | extension AVCaptureDevice: CaptureDevice {
16 | var minExposureDuration: CMTime { activeFormat.minExposureDuration }
17 | var maxExposureDuration: CMTime { activeFormat.maxExposureDuration }
18 | var minISO: Float { activeFormat.minISO }
19 | var maxISO: Float { activeFormat.maxISO }
20 | var minFrameRate: Float64? { activeFormat.videoSupportedFrameRateRanges.first?.minFrameRate }
21 | var maxFrameRate: Float64? { activeFormat.videoSupportedFrameRateRanges.first?.maxFrameRate }
22 | }
23 |
24 | // MARK: Getters & Setters
25 | extension AVCaptureDevice {
26 | var lightMode: CameraLightMode {
27 | get { torchMode == .off ? .off : .on }
28 | set { torchMode = newValue == .off ? .off : .on }
29 | }
30 | var hdrMode: CameraHDRMode {
31 | get {
32 | if automaticallyAdjustsVideoHDREnabled { return .auto }
33 | else if isVideoHDREnabled { return .on }
34 | else { return .off }
35 | }
36 | set {
37 | automaticallyAdjustsVideoHDREnabled = newValue == .auto
38 | if newValue != .auto { isVideoHDREnabled = newValue == .on }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/Helpers/Capture Device/CaptureDevice+MockCaptureDevice.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureDevice+MockCaptureDevice.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | class MockCaptureDevice: NSObject, CaptureDevice {
15 | // MARK: Getters
16 | var uniqueID: String = UUID().uuidString
17 | var exposureDuration: CMTime { _exposureDuration }
18 | var exposureTargetBias: Float { _exposureTargetBias }
19 | var iso: Float { _iso }
20 | var minAvailableVideoZoomFactor: CGFloat { 1 }
21 | var maxAvailableVideoZoomFactor: CGFloat { 3.876 }
22 | var minExposureDuration: CMTime { .init(value: 1, timescale: 1000) }
23 | var maxExposureDuration: CMTime { .init(value: 1, timescale: 5) }
24 | var minISO: Float { 1 }
25 | var maxISO: Float { 10 }
26 | var minExposureTargetBias: Float { 0.1 }
27 | var maxExposureTargetBias: Float { 199 }
28 | var minFrameRate: Float64? { 15 }
29 | var maxFrameRate: Float64? { 60 }
30 | var hasFlash: Bool { true }
31 | var hasTorch: Bool { true }
32 | var isExposurePointOfInterestSupported: Bool { true }
33 | var isFocusPointOfInterestSupported: Bool { true }
34 |
35 | // MARK: Setters
36 | var videoZoomFactor: CGFloat = 1
37 | var focusMode: AVCaptureDevice.FocusMode = .autoFocus
38 | var focusPointOfInterest: CGPoint = .zero
39 | var exposurePointOfInterest: CGPoint = .zero
40 | var lightMode: CameraLightMode = .off
41 | var activeVideoMinFrameDuration: CMTime = .init()
42 | var activeVideoMaxFrameDuration: CMTime = .init()
43 | var exposureMode: AVCaptureDevice.ExposureMode = .continuousAutoExposure
44 | var hdrMode: CameraHDRMode = .auto
45 |
46 | // MARK: Methods
47 | func lockForConfiguration() throws { return }
48 | func unlockForConfiguration() { return }
49 | func isExposureModeSupported(_ exposureMode: AVCaptureDevice.ExposureMode) -> Bool { true }
50 | func setExposureModeCustom(duration: CMTime, iso: Float, completionHandler: ((CMTime) -> Void)?) {
51 | _exposureDuration = duration
52 | _iso = iso
53 | }
54 | func setExposureTargetBias(_ bias: Float, completionHandler handler: ((CMTime) -> ())?) {
55 | _exposureTargetBias = bias
56 | }
57 |
58 | // MARK: Private Attributes
59 | private var _exposureDuration: CMTime = .init()
60 | private var _exposureTargetBias: Float = 0
61 | private var _iso: Float = 0
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/Helpers/Capture Device/CaptureDevice.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureDevice.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | protocol CaptureDevice: NSObject {
15 | // MARK: Getters
16 | var uniqueID: String { get }
17 | var exposureDuration: CMTime { get }
18 | var exposureTargetBias: Float { get }
19 | var iso: Float { get }
20 | var minAvailableVideoZoomFactor: CGFloat { get }
21 | var maxAvailableVideoZoomFactor: CGFloat { get }
22 | var minExposureDuration: CMTime { get }
23 | var maxExposureDuration: CMTime { get }
24 | var minISO: Float { get }
25 | var maxISO: Float { get }
26 | var minExposureTargetBias: Float { get }
27 | var maxExposureTargetBias: Float { get }
28 | var minFrameRate: Float64? { get }
29 | var maxFrameRate: Float64? { get }
30 | var hasFlash: Bool { get }
31 | var hasTorch: Bool { get }
32 | var isExposurePointOfInterestSupported: Bool { get }
33 | var isFocusPointOfInterestSupported: Bool { get }
34 |
35 | // MARK: Getters & Setters
36 | var videoZoomFactor: CGFloat { get set }
37 | var focusMode: AVCaptureDevice.FocusMode { get set }
38 | var focusPointOfInterest: CGPoint { get set }
39 | var exposurePointOfInterest: CGPoint { get set }
40 | var lightMode: CameraLightMode { get set }
41 | var activeVideoMinFrameDuration: CMTime { get set }
42 | var activeVideoMaxFrameDuration: CMTime { get set }
43 | var exposureMode: AVCaptureDevice.ExposureMode { get set }
44 | var hdrMode: CameraHDRMode { get set }
45 |
46 | // MARK: Methods
47 | func lockForConfiguration() throws
48 | func unlockForConfiguration()
49 | func isExposureModeSupported(_ exposureMode: AVCaptureDevice.ExposureMode) -> Bool
50 | func setExposureModeCustom(duration: CMTime, iso: Float, completionHandler: ((CMTime) -> Void)?)
51 | func setExposureTargetBias(_ bias: Float, completionHandler handler: ((CMTime) -> ())?)
52 | }
53 |
54 |
55 | // MARK: - METHODS
56 |
57 |
58 |
59 | // MARK: Set Zoom Factor
60 | extension CaptureDevice {
61 | func setZoomFactor(_ factor: CGFloat) {
62 | let factor = max(min(factor, min(maxAvailableVideoZoomFactor, 5)), minAvailableVideoZoomFactor)
63 | videoZoomFactor = factor
64 | }
65 | }
66 |
67 | // MARK: Set Focus Point Of Interest
68 | extension CaptureDevice {
69 | func setFocusPointOfInterest(_ point: CGPoint) {
70 | guard isFocusPointOfInterestSupported else { return }
71 |
72 | focusPointOfInterest = point
73 | focusMode = .autoFocus
74 | }
75 | }
76 |
77 | // MARK: Set Exposure Point Of Interest
78 | extension CaptureDevice {
79 | func setExposurePointOfInterest(_ point: CGPoint) {
80 | guard isExposurePointOfInterestSupported else { return }
81 |
82 | exposurePointOfInterest = point
83 | exposureMode = .autoExpose
84 | }
85 | }
86 |
87 | // MARK: Set Light Mode
88 | extension CaptureDevice {
89 | func setLightMode(_ mode: CameraLightMode) {
90 | guard hasTorch else { return }
91 | lightMode = mode
92 | }
93 | }
94 |
95 | // MARK: Set Frame Rate
96 | extension CaptureDevice {
97 | func setFrameRate(_ frameRate: Int32) {
98 | guard let minFrameRate, let maxFrameRate else { return }
99 |
100 | let frameRate = max(min(frameRate, Int32(maxFrameRate)), Int32(minFrameRate))
101 |
102 | activeVideoMinFrameDuration = CMTime(value: 1, timescale: frameRate)
103 | activeVideoMaxFrameDuration = CMTime(value: 1, timescale: frameRate)
104 | }
105 | }
106 |
107 | // MARK: Set Exposure Mode
108 | extension CaptureDevice {
109 | func setExposureMode(_ mode: AVCaptureDevice.ExposureMode, duration: CMTime, iso: Float) {
110 | guard isExposureModeSupported(mode) else { return }
111 |
112 | exposureMode = mode
113 |
114 | guard mode == .custom else { return }
115 |
116 | let duration = max(min(duration, maxExposureDuration), minExposureDuration)
117 | let iso = max(min(iso, maxISO), minISO)
118 |
119 | setExposureModeCustom(duration: duration, iso: iso, completionHandler: nil)
120 | }
121 | }
122 |
123 | // MARK: Set Exposure Target Bias
124 | extension CaptureDevice {
125 | func setExposureTargetBias(_ bias: Float) {
126 | guard isExposureModeSupported(.custom) else { return }
127 |
128 | let bias = max(min(bias, maxExposureTargetBias), minExposureTargetBias)
129 | setExposureTargetBias(bias, completionHandler: nil)
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/Helpers/Capture Session/CaptureSession+AVCaptureSession.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureSession+AVCaptureSession.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | extension AVCaptureSession: @unchecked @retroactive Sendable {}
15 | extension AVCaptureSession: CaptureSession {
16 | var deviceInputs: [any CaptureDeviceInput] { inputs as? [any CaptureDeviceInput] ?? [] }
17 | }
18 |
19 |
20 | // MARK: - METHODS
21 |
22 |
23 |
24 | extension AVCaptureSession {
25 | func stopRunningAndReturnNewInstance() -> any CaptureSession {
26 | self.stopRunning()
27 | return AVCaptureSession()
28 | }
29 | }
30 | extension AVCaptureSession {
31 | func add(input: (any CaptureDeviceInput)?) throws(MCameraError) {
32 | guard let input = input as? AVCaptureDeviceInput else { throw .cannotSetupInput }
33 | if canAddInput(input) { addInput(input) }
34 | }
35 | func remove(input: (any CaptureDeviceInput)?) {
36 | guard let input = input as? AVCaptureDeviceInput else { return }
37 | removeInput(input)
38 | }
39 | }
40 | extension AVCaptureSession {
41 | func add(output: AVCaptureOutput?) throws(MCameraError) {
42 | guard let output else { throw .cannotSetupOutput }
43 | if canAddOutput(output) { addOutput(output) }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/Helpers/Capture Session/CaptureSession+MockCaptureSession.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureSession+MockCaptureSession.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | extension MockCaptureSession: @unchecked Sendable {}
15 | class MockCaptureSession: NSObject, CaptureSession { required override init() {}
16 | // MARK: Attributes
17 | var isRunning: Bool { _isRunning }
18 | var deviceInputs: [any CaptureDeviceInput] { _deviceInputs }
19 | var outputs: [AVCaptureOutput] { _outputs }
20 | var sessionPreset: AVCaptureSession.Preset = .cif352x288
21 |
22 | // MARK: Private Attributes
23 | private var _isRunning: Bool = false
24 | private var _deviceInputs: [any CaptureDeviceInput] = []
25 | private var _outputs: [AVCaptureOutput] = []
26 | }
27 |
28 |
29 | // MARK: - METHODS
30 |
31 |
32 |
33 | extension MockCaptureSession {
34 | func startRunning() { Task { @MainActor in
35 | _isRunning = true
36 | }}
37 | func stopRunningAndReturnNewInstance() -> any CaptureSession {
38 | _isRunning = false
39 | return MockCaptureSession()
40 | }
41 | }
42 | extension MockCaptureSession {
43 | func add(input: (any CaptureDeviceInput)?) throws(MCameraError) {
44 | guard let input = input as? MockDeviceInput, !_deviceInputs.contains(where: { input == $0 }) else { throw .cannotSetupInput }
45 | _deviceInputs.append(input)
46 | }
47 | func remove(input: (any CaptureDeviceInput)?) {
48 | guard let input = input as? MockDeviceInput, let index = _deviceInputs.firstIndex(where: { $0.device.uniqueID == input.device.uniqueID }) else { return }
49 | _deviceInputs.remove(at: index)
50 | }
51 | }
52 | extension MockCaptureSession {
53 | func add(output: AVCaptureOutput?) throws(MCameraError) {
54 | guard let output, !outputs.contains(output) else { throw .cannotSetupOutput }
55 | _outputs.append(output)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/Internal/Manager/Helpers/Capture Session/CaptureSession.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureSession.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | protocol CaptureSession: Sendable {
15 | // MARK: Attributes
16 | var isRunning: Bool { get }
17 | var deviceInputs: [any CaptureDeviceInput] { get }
18 | var outputs: [AVCaptureOutput] { get }
19 | var sessionPreset: AVCaptureSession.Preset { get set }
20 |
21 | // MARK: Methods
22 | func startRunning()
23 | func stopRunningAndReturnNewInstance() -> CaptureSession
24 | func add(input: (any CaptureDeviceInput)?) throws(MCameraError)
25 | func remove(input: (any CaptureDeviceInput)?)
26 | func add(output: AVCaptureOutput?) throws(MCameraError)
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/Internal/Miscellaneous/Typealiases.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Typealiases.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | public typealias CameraScreenBuilder = @MainActor (CameraManager, Namespace.ID, _ closeMCameraAction: @escaping () -> ()) -> any MCameraScreen
15 | public typealias CapturedMediaScreenBuilder = @MainActor (MCameraMedia, Namespace.ID, _ retakeAction: @escaping () -> (), _ acceptMediaAction: @escaping () -> ()) -> any MCapturedMediaScreen
16 | public typealias ErrorScreenBuilder = @MainActor (MCameraError, _ closeMCameraAction: @escaping () -> ()) -> any MCameraErrorScreen
17 |
--------------------------------------------------------------------------------
/Sources/Internal/Models/CameraExposure.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraExposure.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import AVKit
13 |
14 | struct CameraExposure {
15 | var duration: CMTime = .zero
16 | var targetBias: Float = 0
17 | var iso: Float = 0
18 | var mode: AVCaptureDevice.ExposureMode = .autoExpose
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Internal/Models/MCameraMedia.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MCameraMedia.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | public struct MCameraMedia: Sendable {
15 | let image: UIImage?
16 | let video: URL?
17 |
18 | init?(data: Any?) {
19 | if let image = data as? UIImage { self.image = image; self.video = nil }
20 | else if let video = data as? URL { self.video = video; self.image = nil }
21 | else { return nil }
22 | }
23 | }
24 |
25 | // MARK: Equatable
26 | extension MCameraMedia: Equatable {
27 | public static func == (lhs: MCameraMedia, rhs: MCameraMedia) -> Bool { lhs.image == rhs.image && lhs.video == rhs.video }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Camera View/CameraView+Bridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraView+Bridge.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | struct CameraBridgeView: UIViewRepresentable {
15 | let cameraManager: CameraManager
16 | let inputView: UIView = .init()
17 | }
18 | extension CameraBridgeView {
19 | func makeUIView(context: Context) -> some UIView {
20 | cameraManager.initialize(in: inputView)
21 | setupTapGesture(context)
22 | setupPinchGesture(context)
23 | return inputView
24 | }
25 | func updateUIView(_ uiView: UIViewType, context: Context) {}
26 | func makeCoordinator() -> Coordinator { .init(self) }
27 | }
28 | private extension CameraBridgeView {
29 | func setupTapGesture(_ context: Context) {
30 | let tapRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.onTapGesture))
31 | inputView.addGestureRecognizer(tapRecognizer)
32 | }
33 | func setupPinchGesture(_ context: Context) {
34 | let pinchRecognizer = UIPinchGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.onPinchGesture))
35 | inputView.addGestureRecognizer(pinchRecognizer)
36 | }
37 | }
38 |
39 | // MARK: Equatable
40 | extension CameraBridgeView: Equatable {
41 | nonisolated static func ==(lhs: Self, rhs: Self) -> Bool { true }
42 | }
43 |
44 |
45 | // MARK: - GESTURES
46 | extension CameraBridgeView { class Coordinator: NSObject { init(_ parent: CameraBridgeView) { self.parent = parent }
47 | let parent: CameraBridgeView
48 | }}
49 |
50 | // MARK: On Tap
51 | extension CameraBridgeView.Coordinator {
52 | @MainActor @objc func onTapGesture(_ tap: UITapGestureRecognizer) {
53 | do {
54 | let touchPoint = tap.location(in: parent.inputView)
55 | try parent.cameraManager.setCameraFocus(at: touchPoint)
56 | } catch {}
57 | }
58 | }
59 |
60 | // MARK: On Pinch
61 | extension CameraBridgeView.Coordinator {
62 | @MainActor @objc func onPinchGesture(_ pinch: UIPinchGestureRecognizer) { if pinch.state == .changed {
63 | do {
64 | let desiredZoomFactor = parent.cameraManager.attributes.zoomFactor + atan2(pinch.velocity, 33)
65 | try parent.cameraManager.setCameraZoomFactor(desiredZoomFactor)
66 | } catch {}
67 | }}
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Camera View/CameraView+FocusIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraView+FocusIndicator.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | @MainActor class CameraFocusIndicatorView {
15 | var image: UIImage = .init(resource: .mijickIconCrosshair)
16 | var tintColor: UIColor = .init(resource: .mijickBackgroundYellow)
17 | var size: CGFloat = 96
18 | }
19 |
20 | // MARK: Create
21 | extension CameraFocusIndicatorView {
22 | func create(at touchPoint: CGPoint) -> UIImageView {
23 | let focusIndicator = UIImageView(image: image)
24 | focusIndicator.contentMode = .scaleAspectFit
25 | focusIndicator.tintColor = tintColor
26 | focusIndicator.frame.size = .init(width: size, height: size)
27 | focusIndicator.frame.origin.x = touchPoint.x - size / 2
28 | focusIndicator.frame.origin.y = touchPoint.y - size / 2
29 | focusIndicator.transform = .init(scaleX: 0, y: 0)
30 | focusIndicator.tag = .focusIndicatorTag
31 | return focusIndicator
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Camera View/CameraView+Grid.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraView+Grid.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | class CameraGridView: UIView {
15 | var parent: CameraManager!
16 | }
17 |
18 | // MARK: Setup
19 | extension CameraGridView {
20 | func setup(parent: CameraManager) {
21 | self.parent = parent
22 | self.alpha = parent.attributes.isGridVisible ? 1 : 0
23 | self.addToParent(parent.cameraView)
24 | }
25 | }
26 |
27 | // MARK: Set Visibility
28 | extension CameraGridView {
29 | func setVisibility(_ isVisible: Bool) {
30 | UIView.animate(withDuration: 0.2) { self.alpha = isVisible ? 1 : 0 }
31 | parent.attributes.isGridVisible = isVisible
32 | }
33 | }
34 |
35 | // MARK: Draw
36 | extension CameraGridView {
37 | override func draw(_ rect: CGRect) {
38 | clearOldLayersBeforeDraw()
39 |
40 | let firstColumnPath = UIBezierPath()
41 | firstColumnPath.move(to: CGPoint(x: bounds.width / 3, y: 0))
42 | firstColumnPath.addLine(to: CGPoint(x: bounds.width / 3, y: bounds.height))
43 | let firstColumnLayer = createGridLayer()
44 | firstColumnLayer.path = firstColumnPath.cgPath
45 | layer.addSublayer(firstColumnLayer)
46 |
47 | let secondColumnPath = UIBezierPath()
48 | secondColumnPath.move(to: CGPoint(x: (2 * bounds.width) / 3, y: 0))
49 | secondColumnPath.addLine(to: CGPoint(x: (2 * bounds.width) / 3, y: bounds.height))
50 | let secondColumnLayer = createGridLayer()
51 | secondColumnLayer.path = secondColumnPath.cgPath
52 | layer.addSublayer(secondColumnLayer)
53 |
54 | let firstRowPath = UIBezierPath()
55 | firstRowPath.move(to: CGPoint(x: 0, y: bounds.height / 3))
56 | firstRowPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height / 3))
57 | let firstRowLayer = createGridLayer()
58 | firstRowLayer.path = firstRowPath.cgPath
59 | layer.addSublayer(firstRowLayer)
60 |
61 | let secondRowPath = UIBezierPath()
62 | secondRowPath.move(to: CGPoint(x: 0, y: ( 2 * bounds.height) / 3))
63 | secondRowPath.addLine(to: CGPoint(x: bounds.width, y: ( 2 * bounds.height) / 3))
64 | let secondRowLayer = createGridLayer()
65 | secondRowLayer.path = secondRowPath.cgPath
66 | layer.addSublayer(secondRowLayer)
67 | }
68 | }
69 | private extension CameraGridView {
70 | func clearOldLayersBeforeDraw() {
71 | layer.sublayers?.removeAll()
72 | layer.backgroundColor = .none
73 | }
74 | func createGridLayer() -> CAShapeLayer {
75 | let shapeLayer = CAShapeLayer()
76 | shapeLayer.strokeColor = UIColor(white: 1.0, alpha: 0.2).cgColor
77 | shapeLayer.frame = bounds
78 | shapeLayer.fillColor = nil
79 | return shapeLayer
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Camera View/CameraView+Metal.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraView+Metal.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 | import MetalKit
14 | import AVKit
15 |
16 | @MainActor class CameraMetalView: MTKView {
17 | private(set) var parent: CameraManager!
18 | private(set) var ciContext: CIContext!
19 | private(set) var commandQueue: MTLCommandQueue!
20 | private(set) var currentFrame: CIImage?
21 | private(set) var focusIndicator: CameraFocusIndicatorView = .init()
22 | private(set) var isAnimating: Bool = false
23 | }
24 |
25 | // MARK: Setup
26 | extension CameraMetalView {
27 | func setup(parent: CameraManager) throws(MCameraError) {
28 | guard let metalDevice = MTLCreateSystemDefaultDevice() else { throw .cannotSetupMetalDevice }
29 |
30 | self.assignInitialValues(parent: parent, metalDevice: metalDevice)
31 | self.configureMetalView(metalDevice: metalDevice)
32 | self.addToParent(parent.cameraView)
33 | }
34 | }
35 | private extension CameraMetalView {
36 | func assignInitialValues(parent: CameraManager, metalDevice: MTLDevice) {
37 | self.parent = parent
38 | self.ciContext = CIContext(mtlDevice: metalDevice)
39 | self.commandQueue = metalDevice.makeCommandQueue()
40 | }
41 | func configureMetalView(metalDevice: MTLDevice) {
42 | self.parent.cameraView.alpha = 0
43 |
44 | self.delegate = self
45 | self.device = metalDevice
46 | self.isPaused = true
47 | self.enableSetNeedsDisplay = false
48 | self.framebufferOnly = false
49 | self.autoResizeDrawable = false
50 | self.contentMode = .scaleAspectFill
51 | self.clipsToBounds = true
52 | }
53 | }
54 |
55 |
56 | // MARK: - ANIMATIONS
57 |
58 |
59 |
60 | // MARK: Camera Entrance
61 | extension CameraMetalView {
62 | func performCameraEntranceAnimation() { UIView.animate(withDuration: 0.33) { [self] in
63 | parent.cameraView.alpha = 1
64 | }}
65 | }
66 |
67 | // MARK: Image Capture
68 | extension CameraMetalView {
69 | func performImageCaptureAnimation() {
70 | let blackMatte = createBlackMatte()
71 |
72 | parent.cameraView.addSubview(blackMatte)
73 | animateBlackMatte(blackMatte)
74 | }
75 | }
76 | private extension CameraMetalView {
77 | func createBlackMatte() -> UIView {
78 | let view = UIView()
79 | view.frame = parent.cameraView.frame
80 | view.backgroundColor = .init(resource: .mijickBackgroundPrimary)
81 | view.alpha = 0
82 | return view
83 | }
84 | func animateBlackMatte(_ view: UIView) {
85 | UIView.animate(withDuration: 0.16, animations: { view.alpha = 1 }) { _ in
86 | UIView.animate(withDuration: 0.16, animations: { view.alpha = 0 }) { _ in
87 | view.removeFromSuperview()
88 | }
89 | }
90 | }
91 | }
92 |
93 | // MARK: Camera Flip
94 | extension CameraMetalView {
95 | func beginCameraFlipAnimation() async {
96 | let snapshot = createSnapshot()
97 | isAnimating = true
98 | insertBlurView(snapshot)
99 | animateBlurFlip()
100 |
101 | await Task.sleep(seconds: 0.01)
102 | }
103 | func finishCameraFlipAnimation() async {
104 | guard let blurView = parent.cameraView.viewWithTag(.blurViewTag) else { return }
105 |
106 | await Task.sleep(seconds: 0.44)
107 | UIView.animate(withDuration: 0.3, animations: { blurView.alpha = 0 }) { [self] _ in
108 | blurView.removeFromSuperview()
109 | isAnimating = false
110 | }
111 | }
112 | }
113 | private extension CameraMetalView {
114 | func createSnapshot() -> UIImage? {
115 | guard let currentFrame else { return nil }
116 |
117 | let image = UIImage(ciImage: currentFrame)
118 | return image
119 | }
120 | func insertBlurView(_ snapshot: UIImage?) {
121 | let blurView = UIImageView(frame: parent.cameraView.frame)
122 | blurView.image = snapshot
123 | blurView.contentMode = .scaleAspectFill
124 | blurView.clipsToBounds = true
125 | blurView.tag = .blurViewTag
126 | blurView.applyBlurEffect(style: .regular)
127 |
128 | parent.cameraView.addSubview(blurView)
129 | }
130 | func animateBlurFlip() {
131 | UIView.transition(with: parent.cameraView, duration: 0.44, options: cameraFlipAnimationTransition) {}
132 | }
133 | }
134 | private extension CameraMetalView {
135 | var cameraFlipAnimationTransition: UIView.AnimationOptions { parent.attributes.cameraPosition == .back ? .transitionFlipFromLeft : .transitionFlipFromRight }
136 | }
137 |
138 | // MARK: Camera Focus
139 | extension CameraMetalView {
140 | func performCameraFocusAnimation(touchPoint: CGPoint) {
141 | removeExistingFocusIndicatorAnimations()
142 |
143 | let focusIndicator = focusIndicator.create(at: touchPoint)
144 | parent.cameraView.addSubview(focusIndicator)
145 | animateFocusIndicator(focusIndicator)
146 | }
147 | }
148 | private extension CameraMetalView {
149 | func removeExistingFocusIndicatorAnimations() { if let view = parent.cameraView.viewWithTag(.focusIndicatorTag) {
150 | view.removeFromSuperview()
151 | }}
152 | func animateFocusIndicator(_ focusIndicator: UIImageView) {
153 | UIView.animate(withDuration: 0.44, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, animations: { focusIndicator.transform = .init(scaleX: 1, y: 1) }) { _ in
154 | UIView.animate(withDuration: 0.44, delay: 1.44, animations: { focusIndicator.alpha = 0.2 }) { _ in
155 | UIView.animate(withDuration: 0.44, delay: 1.44, animations: { focusIndicator.alpha = 0 })
156 | }
157 | }
158 | }
159 | }
160 |
161 | // MARK: Camera Orientation
162 | extension CameraMetalView {
163 | func beginCameraOrientationAnimation(if shouldAnimate: Bool) async { if shouldAnimate {
164 | parent.cameraView.alpha = 0
165 | await Task.sleep(seconds: 0.1)
166 | }}
167 | func finishCameraOrientationAnimation(if shouldAnimate: Bool) { if shouldAnimate {
168 | UIView.animate(withDuration: 0.2, delay: 0.1) { self.parent.cameraView.alpha = 1 }
169 | }}
170 | }
171 |
172 |
173 | // MARK: - CAPTURING FRAMES
174 |
175 |
176 |
177 | // MARK: Capture
178 | extension CameraMetalView: @preconcurrency AVCaptureVideoDataOutputSampleBufferDelegate {
179 | func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
180 | guard let cvImageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
181 |
182 | let currentFrame = captureCurrentFrame(cvImageBuffer)
183 | let currentFrameWithFiltersApplied = applyingFiltersToCurrentFrame(currentFrame)
184 | redrawCameraView(currentFrameWithFiltersApplied)
185 | }
186 | }
187 | private extension CameraMetalView {
188 | func captureCurrentFrame(_ cvImageBuffer: CVImageBuffer) -> CIImage {
189 | let currentFrame = CIImage(cvImageBuffer: cvImageBuffer)
190 | return currentFrame.oriented(parent.attributes.frameOrientation)
191 | }
192 | func applyingFiltersToCurrentFrame(_ currentFrame: CIImage) -> CIImage {
193 | currentFrame.applyingFilters(parent.attributes.cameraFilters)
194 | }
195 | func redrawCameraView(_ frame: CIImage) {
196 | currentFrame = frame
197 | draw()
198 | }
199 | }
200 |
201 | // MARK: Draw
202 | extension CameraMetalView: MTKViewDelegate {
203 | func draw(in view: MTKView) {
204 | guard let commandBuffer = commandQueue.makeCommandBuffer(),
205 | let ciImage = currentFrame,
206 | let currentDrawable = view.currentDrawable
207 | else { return }
208 |
209 | changeDrawableSize(view, ciImage)
210 | renderView(view, currentDrawable, commandBuffer, ciImage)
211 | commitBuffer(currentDrawable, commandBuffer)
212 | }
213 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
214 | }
215 | private extension CameraMetalView {
216 | func changeDrawableSize(_ view: MTKView, _ ciImage: CIImage) {
217 | view.drawableSize = ciImage.extent.size
218 | }
219 | func renderView(_ view: MTKView, _ currentDrawable: any CAMetalDrawable, _ commandBuffer: any MTLCommandBuffer, _ ciImage: CIImage) { ciContext.render(
220 | ciImage,
221 | to: currentDrawable.texture,
222 | commandBuffer: commandBuffer,
223 | bounds: .init(origin: .zero, size: view.drawableSize),
224 | colorSpace: CGColorSpaceCreateDeviceRGB()
225 | )}
226 | func commitBuffer(_ currentDrawable: any CAMetalDrawable, _ commandBuffer: any MTLCommandBuffer) {
227 | commandBuffer.present(currentDrawable)
228 | commandBuffer.commit()
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Camera/DefaultCameraScreen+BottomBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCameraScreen+BottomBar.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension DefaultCameraScreen { struct BottomBar: View {
15 | let parent: DefaultCameraScreen
16 |
17 |
18 | var body: some View {
19 | ZStack(alignment: .top) {
20 | createOutputTypeSwitch()
21 | createButtons()
22 | }
23 | .frame(maxWidth: .infinity)
24 | .padding(.bottom, 44)
25 | .padding(.horizontal, 32)
26 | }
27 | }}
28 | private extension DefaultCameraScreen.BottomBar {
29 | @ViewBuilder func createOutputTypeSwitch() -> some View { if isOutputTypeSwitchActive {
30 | DefaultCameraScreen.CameraOutputSwitch(parent: parent)
31 | .offset(y: -80)
32 | }}
33 | func createButtons() -> some View {
34 | ZStack {
35 | createLightButton()
36 | createCaptureButton()
37 | createChangeCameraPositionButton()
38 | }.frame(height: 72)
39 | }
40 | }
41 | private extension DefaultCameraScreen.BottomBar {
42 | @ViewBuilder func createLightButton() -> some View { if isLightButtonActive {
43 | BottomButton(
44 | icon: .mijickIconLight,
45 | iconColor: lightButtonIconColor,
46 | backgroundColor: .init(.mijickBackgroundSecondary),
47 | rotationAngle: parent.iconAngle,
48 | action: changeLightMode
49 | )
50 | .frame(maxWidth: .infinity, alignment: .leading)
51 | .transition(.scale)
52 | }}
53 | @ViewBuilder func createCaptureButton() -> some View { if isCaptureButtonActive {
54 | DefaultCameraScreen.CaptureButton(
55 | outputType: parent.cameraOutputType,
56 | isRecording: parent.isRecording,
57 | action: parent.captureOutput
58 | )
59 | .transition(.scale)
60 | }}
61 | @ViewBuilder func createChangeCameraPositionButton() -> some View { if isChangeCameraPositionButtonActive {
62 | BottomButton(
63 | icon: .mijickIconChangeCamera,
64 | iconColor: changeCameraPositionButtonIconColor,
65 | backgroundColor: .init(.mijickBackgroundSecondary),
66 | rotationAngle: parent.iconAngle,
67 | action: changeCameraPosition
68 | )
69 | .frame(maxWidth: .infinity, alignment: .trailing)
70 | .transition(.scale)
71 | }}
72 | }
73 |
74 | private extension DefaultCameraScreen.BottomBar {
75 | func changeLightMode() {
76 | do { try parent.setLightMode(parent.lightMode.next()) }
77 | catch {}
78 | }
79 | func changeCameraPosition() { Task {
80 | do { try await parent.setCameraPosition(parent.cameraPosition.next()) }
81 | catch {}
82 | }}
83 | }
84 |
85 | private extension DefaultCameraScreen.BottomBar {
86 | var lightButtonIconColor: Color { switch parent.lightMode {
87 | case .on: .init(.mijickBackgroundYellow)
88 | case .off: .init(.mijickBackgroundInverted)
89 | }}
90 | var changeCameraPositionButtonIconColor: Color { .init(.mijickBackgroundInverted) }
91 | }
92 | private extension DefaultCameraScreen.BottomBar {
93 | var isOutputTypeSwitchActive: Bool { parent.config.cameraOutputSwitchAllowed && parent.cameraManager.captureSession.isRunning && !parent.isRecording }
94 | var isLightButtonActive: Bool { parent.config.lightButtonAllowed && parent.hasLight && parent.cameraManager.captureSession.isRunning && !parent.isRecording }
95 | var isCaptureButtonActive: Bool { parent.config.captureButtonAllowed && parent.cameraManager.captureSession.isRunning }
96 | var isChangeCameraPositionButtonActive: Bool { parent.config.cameraPositionButtonAllowed && parent.cameraManager.captureSession.isRunning && !parent.isRecording }
97 | }
98 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Camera/DefaultCameraScreen+ButtonScaleStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCameraScreen+ButtonScaleStyle.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | struct ButtonScaleStyle: ButtonStyle {
15 | func makeBody(configuration: Configuration) -> some View { configuration
16 | .label
17 | .scaleEffect(configuration.isPressed ? 0.96 : 1)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Camera/DefaultCameraScreen+CameraOutputSwitch.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCameraScreen+CameraOutputSwitch.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension DefaultCameraScreen { struct CameraOutputSwitch: View {
15 | let parent: DefaultCameraScreen
16 |
17 |
18 | var body: some View {
19 | HStack(spacing: 4) {
20 | createOutputTypeButton(.video)
21 | createOutputTypeButton(.photo)
22 | }
23 | .padding(8)
24 | .background(Color(.mijickBackgroundPrimary50))
25 | .mask(Capsule())
26 | }
27 | }}
28 | private extension DefaultCameraScreen.CameraOutputSwitch {
29 | func createOutputTypeButton(_ outputType: CameraOutputType) -> some View {
30 | Button(icon: getOutputTypeButtonIcon(outputType), active: isOutputTypeButtonActive(outputType)) {
31 | parent.setOutputType(outputType)
32 | }
33 | .rotationEffect(parent.iconAngle)
34 | }
35 | }
36 |
37 | private extension DefaultCameraScreen.CameraOutputSwitch {
38 | func getOutputTypeButtonIcon(_ outputType: CameraOutputType) -> ImageResource { switch outputType {
39 | case .photo: return .mijickIconPhoto
40 | case .video: return .mijickIconVideo
41 | }}
42 | func isOutputTypeButtonActive(_ outputType: CameraOutputType) -> Bool {
43 | outputType == parent.cameraOutputType
44 | }
45 | }
46 |
47 |
48 | // MARK: Button
49 | fileprivate struct Button: View {
50 | let icon: ImageResource
51 | let active: Bool
52 | let action: () -> ()
53 |
54 |
55 | var body: some View {
56 | SwiftUI.Button(action: action, label: createButtonLabel).buttonStyle(ButtonScaleStyle())
57 | }
58 | }
59 | private extension Button {
60 | func createButtonLabel() -> some View {
61 | Image(icon)
62 | .resizable()
63 | .frame(width: iconSize, height: iconSize)
64 | .foregroundColor(iconColor)
65 | .padding(8)
66 | .background(Color(.mijickBackgroundSecondary))
67 | .mask(Circle())
68 | }
69 | }
70 | private extension Button {
71 | var iconSize: CGFloat { switch active {
72 | case true: 28
73 | case false: 20
74 | }}
75 | var iconColor: Color { switch active {
76 | case true: .init(.mijickBackgroundYellow)
77 | case false: .init(.mijickTextTertiary)
78 | }}
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Camera/DefaultCameraScreen+CaptureButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCameraScreen+CaptureButton.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension DefaultCameraScreen { struct CaptureButton: View {
15 | let outputType: CameraOutputType
16 | let isRecording: Bool
17 | let action: () -> ()
18 |
19 |
20 | var body: some View {
21 | Button(action: action, label: createButtonLabel).buttonStyle(ButtonScaleStyle())
22 | }
23 | }}
24 | private extension DefaultCameraScreen.CaptureButton {
25 | func createButtonLabel() -> some View {
26 | ZStack {
27 | createBackground()
28 | createBorders()
29 | }.frame(width: 72, height: 72)
30 | }
31 | }
32 | private extension DefaultCameraScreen.CaptureButton {
33 | func createBackground() -> some View {
34 | RoundedRectangle(cornerRadius: backgroundCornerRadius, style: .continuous)
35 | .fill(backgroundColor)
36 | .padding(backgroundPadding)
37 | }
38 | func createBorders() -> some View {
39 | Circle().stroke(Color(.mijickBackgroundInverted), lineWidth: 2.5)
40 | }
41 | }
42 | private extension DefaultCameraScreen.CaptureButton {
43 | var backgroundColor: Color { switch outputType {
44 | case .photo: .init(.mijickBackgroundInverted)
45 | case .video: .init(.mijickBackgroundRed)
46 | }}
47 | var backgroundCornerRadius: CGFloat { switch isRecording {
48 | case true: 6
49 | case false: 36
50 | }}
51 | var backgroundPadding: CGFloat { switch isRecording {
52 | case true: 20
53 | case false: 4
54 | }}
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Camera/DefaultCameraScreen+Config.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCameraScreen+Config.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import Foundation
13 |
14 | extension DefaultCameraScreen { class Config {
15 | var captureButtonAllowed: Bool = true
16 | var cameraOutputSwitchAllowed: Bool = true
17 | var cameraPositionButtonAllowed: Bool = true
18 | var flashButtonAllowed: Bool = true
19 | var lightButtonAllowed: Bool = true
20 | var flipButtonAllowed: Bool = true
21 | var gridButtonAllowed: Bool = true
22 | var closeButtonAllowed: Bool = true
23 | }}
24 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Camera/DefaultCameraScreen+TopBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCameraScreen+TopBar.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension DefaultCameraScreen { struct TopBar: View {
15 | let parent: DefaultCameraScreen
16 |
17 |
18 | var body: some View { if isTopBarActive {
19 | ZStack {
20 | createCloseButton()
21 | createCentralView()
22 | createRightSideView()
23 | }
24 | .frame(maxWidth: .infinity)
25 | .padding(.top, topPadding)
26 | .padding(.bottom, 8)
27 | .padding(.horizontal, 20)
28 | .background(Color(.mijickBackgroundPrimary80))
29 | .transition(.move(edge: .top))
30 | }}
31 | }}
32 | private extension DefaultCameraScreen.TopBar {
33 | @ViewBuilder func createCloseButton() -> some View { if isCloseButtonActive {
34 | CloseButton(action: parent.closeMCameraAction)
35 | .frame(maxWidth: .infinity, alignment: .leading)
36 | }}
37 | @ViewBuilder func createCentralView() -> some View { if isCentralViewActive {
38 | Text(parent.recordingTime.toString())
39 | .font(.system(size: 20, weight: .medium))
40 | .foregroundColor(.init(.mijickTextPrimary))
41 | }}
42 | @ViewBuilder func createRightSideView() -> some View { if isRightSideViewActive {
43 | HStack(spacing: 12) {
44 | createGridButton()
45 | createFlipOutputButton()
46 | createFlashButton()
47 | }
48 | .frame(maxWidth: .infinity, alignment: .trailing)
49 | }}
50 | }
51 | private extension DefaultCameraScreen.TopBar {
52 | @ViewBuilder func createGridButton() -> some View { if isGridButtonActive {
53 | DefaultCameraScreen.TopButton(
54 | icon: gridButtonIcon,
55 | iconRotationAngle: parent.iconAngle,
56 | action: changeGridVisibility
57 | )
58 | }}
59 | @ViewBuilder func createFlipOutputButton() -> some View { if isFlipOutputButtonActive {
60 | DefaultCameraScreen.TopButton(
61 | icon: flipButtonIcon,
62 | iconRotationAngle: parent.iconAngle,
63 | action: changeMirrorOutput
64 | )
65 | }}
66 | @ViewBuilder func createFlashButton() -> some View { if isFlashButtonActive {
67 | DefaultCameraScreen.TopButton(
68 | icon: flashButtonIcon,
69 | iconRotationAngle: parent.iconAngle,
70 | action: changeFlashMode
71 | )
72 | }}
73 | }
74 |
75 | private extension DefaultCameraScreen.TopBar {
76 | func changeGridVisibility() {
77 | parent.setGridVisibility(!parent.isGridVisible)
78 | }
79 | func changeMirrorOutput() {
80 | parent.setMirrorOutput(!parent.isOutputMirrored)
81 | }
82 | func changeFlashMode() {
83 | parent.setFlashMode(parent.flashMode.next())
84 | }
85 | }
86 |
87 | private extension DefaultCameraScreen.TopBar {
88 | var topPadding: CGFloat { switch parent.deviceOrientation {
89 | case .portrait, .portraitUpsideDown: return 40
90 | default: return 20
91 | }}
92 | }
93 | private extension DefaultCameraScreen.TopBar {
94 | var gridButtonIcon: ImageResource { switch parent.isGridVisible {
95 | case true: .mijickIconGridOn
96 | case false: .mijickIconGridOff
97 | }}
98 | var flipButtonIcon: ImageResource { switch parent.isOutputMirrored {
99 | case true: .mijickIconFlipOn
100 | case false: .mijickIconFlipOff
101 | }}
102 | var flashButtonIcon: ImageResource { switch parent.flashMode {
103 | case .off: .mijickIconFlashOff
104 | case .on: .mijickIconFlashOn
105 | case .auto: .mijickIconFlashAuto
106 | }}
107 | }
108 | private extension DefaultCameraScreen.TopBar {
109 | var isTopBarActive: Bool { parent.cameraManager.captureSession.isRunning }
110 | var isCloseButtonActive: Bool { parent.config.closeButtonAllowed && !parent.isRecording }
111 | var isCentralViewActive: Bool { parent.isRecording }
112 | var isRightSideViewActive: Bool { !parent.isRecording }
113 | var isGridButtonActive: Bool { parent.config.gridButtonAllowed }
114 | var isFlipOutputButtonActive: Bool { parent.config.flipButtonAllowed && parent.cameraPosition == .front }
115 | var isFlashButtonActive: Bool { parent.config.flashButtonAllowed && parent.hasFlash && parent.cameraOutputType == .photo }
116 | }
117 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Camera/DefaultCameraScreen+TopButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCameraScreen+TopButton.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension DefaultCameraScreen { struct TopButton: View {
15 | let icon: ImageResource
16 | let iconRotationAngle: Angle
17 | let action: () -> ()
18 |
19 |
20 | var body: some View {
21 | Button(action: action, label: createButtonLabel)
22 | }
23 | }}
24 | private extension DefaultCameraScreen.TopButton {
25 | func createButtonLabel() -> some View {
26 | Image(icon)
27 | .resizable()
28 | .frame(width: 16, height: 16)
29 | .foregroundColor(Color(.mijickBackgroundInverted))
30 | .rotationEffect(iconRotationAngle)
31 | .frame(width: 32, height: 32)
32 | .background(Color(.mijickBackgroundSecondary))
33 | .mask(Circle())
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Camera/DefaultCameraScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCameraScreen.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | public struct DefaultCameraScreen: MCameraScreen {
15 | @ObservedObject public var cameraManager: CameraManager
16 | public let namespace: Namespace.ID
17 | public let closeMCameraAction: () -> ()
18 | var config: Config = .init()
19 |
20 |
21 | public var body: some View {
22 | ZStack {
23 | createContentView()
24 | createTopBar()
25 | createBottomBar()
26 | }
27 | .ignoresSafeArea()
28 | .frame(maxWidth: .infinity, maxHeight: .infinity)
29 | .background(Color(.mijickBackgroundPrimary).ignoresSafeArea())
30 | .statusBarHidden()
31 | .animation(.mSpring)
32 | }
33 | }
34 | private extension DefaultCameraScreen {
35 | func createTopBar() -> some View {
36 | DefaultCameraScreen.TopBar(parent: self)
37 | .frame(maxHeight: .infinity, alignment: .top)
38 | }
39 | func createContentView() -> some View {
40 | createCameraOutputView()
41 | .ignoresSafeArea()
42 | }
43 | func createBottomBar() -> some View {
44 | DefaultCameraScreen.BottomBar(parent: self)
45 | .frame(maxHeight: .infinity, alignment: .bottom)
46 | }
47 | }
48 |
49 | extension DefaultCameraScreen {
50 | var iconAngle: Angle { switch isOrientationLocked {
51 | case true: deviceOrientation.getAngle()
52 | case false: .zero
53 | }}
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Captured Media/DefaultCapturedMediaScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCapturedMediaScreen.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 | import AVKit
14 |
15 | struct DefaultCapturedMediaScreen: MCapturedMediaScreen {
16 | let capturedMedia: MCameraMedia
17 | let namespace: Namespace.ID
18 | let retakeAction: () -> ()
19 | let acceptMediaAction: () -> ()
20 | @State private var player: AVPlayer = .init()
21 | @State private var isInitialized: Bool = false
22 |
23 |
24 | var body: some View {
25 | ZStack {
26 | createContentView()
27 | createButtons()
28 | }
29 | .frame(maxWidth: .infinity, maxHeight: .infinity)
30 | .background(Color(.mijickBackgroundPrimary).ignoresSafeArea())
31 | .animation(.mSpring, value: isInitialized)
32 | .onAppear { isInitialized = true }
33 | }
34 | }
35 | private extension DefaultCapturedMediaScreen {
36 | @ViewBuilder func createContentView() -> some View { if isInitialized {
37 | if let image = capturedMedia.getImage() { createImageView(image) }
38 | else if let video = capturedMedia.getVideo() { createVideoView(video) }
39 | }}
40 | func createButtons() -> some View {
41 | HStack(spacing: 32) {
42 | createRetakeButton()
43 | createSaveButton()
44 | }
45 | .padding(.top, 12)
46 | .padding(.bottom, 4)
47 | .frame(maxHeight: .infinity, alignment: .bottom)
48 | .padding(.bottom, 8)
49 | }
50 | }
51 | private extension DefaultCapturedMediaScreen {
52 | func createImageView(_ image: UIImage) -> some View {
53 | Image(uiImage: image)
54 | .resizable()
55 | .aspectRatio(contentMode: .fit)
56 | .ignoresSafeArea()
57 | .transition(.scale(scale: 1.1))
58 | }
59 | func createVideoView(_ video: URL) -> some View {
60 | VideoPlayer(player: player)
61 | .onAppear { onVideoAppear(video) }
62 | }
63 | @ViewBuilder func createRetakeButton() -> some View { if isInitialized {
64 | BottomButton(
65 | icon: .mijickIconCancel,
66 | iconColor: .init(.mijickBackgroundInverted),
67 | backgroundColor: .init(.mijickBackgroundSecondary),
68 | rotationAngle: .zero,
69 | action: retakeAction
70 | )
71 | .transition(.scale)
72 | }}
73 | @ViewBuilder func createSaveButton() -> some View { if isInitialized {
74 | BottomButton(
75 | icon: .mijickIconCheck,
76 | iconColor: .init(.mijickBackgroundPrimary),
77 | backgroundColor: .init(.mijickBackgroundInverted),
78 | rotationAngle: .zero,
79 | action: acceptMediaAction
80 | )
81 | .transition(.scale)
82 | }}
83 | }
84 |
85 | private extension DefaultCapturedMediaScreen {
86 | func onVideoAppear(_ url: URL) {
87 | player = .init(url: url)
88 | player.play()
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Common Components/DefaultScreen+BottomButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultScreen+BottomButton.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | struct BottomButton: View {
15 | let icon: ImageResource
16 | let iconColor: Color
17 | let backgroundColor: Color
18 | let rotationAngle: Angle
19 | let action: () -> ()
20 |
21 |
22 | var body: some View {
23 | Button(action: action, label: createButtonLabel).buttonStyle(ButtonScaleStyle())
24 | }
25 | }
26 | private extension BottomButton {
27 | func createButtonLabel() -> some View {
28 | Image(icon)
29 | .resizable()
30 | .frame(width: 26, height: 26)
31 | .foregroundColor(iconColor)
32 | .rotationEffect(rotationAngle)
33 | .frame(width: 52, height: 52)
34 | .background(backgroundColor)
35 | .mask(Circle())
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Common Components/DefaultScreen+CloseButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultScreen+CloseButton.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | struct CloseButton: View {
15 | let action: () -> ()
16 |
17 |
18 | var body: some View {
19 | Button(action: action, label: createButtonLabel)
20 | }
21 | }
22 | private extension CloseButton {
23 | func createButtonLabel() -> some View {
24 | Image(.mijickIconCancel)
25 | .resizable()
26 | .frame(width: 24, height: 24)
27 | .foregroundColor(Color(.mijickBackgroundInverted))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/Default Screens/Error/DefaultCameraErrorScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCameraErrorScreen.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | struct DefaultCameraErrorScreen: MCameraErrorScreen {
15 | let error: MCameraError
16 | let closeMCameraAction: () -> ()
17 |
18 |
19 | var body: some View {
20 | VStack(spacing: 0) {
21 | Spacer().frame(height: 8)
22 | createCloseButton()
23 | Spacer()
24 | createTitle()
25 | Spacer().frame(height: 16)
26 | createDescription()
27 | Spacer().frame(height: 32)
28 | createOpenSettingsButton()
29 | Spacer()
30 | }
31 | .frame(maxWidth: .infinity, maxHeight: .infinity)
32 | .background(Color(.mijickBackgroundPrimary).ignoresSafeArea())
33 | }
34 | }
35 | private extension DefaultCameraErrorScreen {
36 | func createCloseButton() -> some View {
37 | CloseButton(action: closeMCameraAction)
38 | .frame(maxWidth: .infinity, alignment: .leading)
39 | .padding(.leading, 20)
40 | }
41 | func createTitle() -> some View {
42 | Text(title)
43 | .font(.system(size: 20, weight: .bold))
44 | .foregroundColor(.init(.mijickTextPrimary))
45 | .multilineTextAlignment(.center)
46 | .fixedSize(horizontal: false, vertical: true)
47 | .padding(.horizontal, 64)
48 | }
49 | func createDescription() -> some View {
50 | Text(description)
51 | .font(.system(size: 16, weight: .regular))
52 | .foregroundColor(.init(.mijickTextSecondary))
53 | .lineSpacing(4)
54 | .multilineTextAlignment(.center)
55 | .fixedSize(horizontal: false, vertical: true)
56 | .padding(.horizontal, 32)
57 | }
58 | func createOpenSettingsButton() -> some View {
59 | Button(action: openAppSettings) {
60 | Text(openSettingsButton)
61 | .font(.system(size: 16, weight: .bold))
62 | .foregroundColor(Color(.mijickTextBrand))
63 | }
64 | }
65 | }
66 |
67 | private extension DefaultCameraErrorScreen {
68 | var title: String { switch error {
69 | case .microphonePermissionsNotGranted: NSLocalizedString("Enable Microphone Access", comment: "")
70 | case .cameraPermissionsNotGranted: NSLocalizedString("Enable Camera Access", comment: "")
71 | default: ""
72 | }}
73 | var description: String { switch error {
74 | case .microphonePermissionsNotGranted: Bundle.main.infoDictionary?["NSMicrophoneUsageDescription"] as? String ?? ""
75 | case .cameraPermissionsNotGranted: Bundle.main.infoDictionary?["NSCameraUsageDescription"] as? String ?? ""
76 | default: ""
77 | }}
78 | var openSettingsButton: String { NSLocalizedString("Open Settings", comment: "") }
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/MCamera/MCamera+Config.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MCamera+Config.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | extension MCamera { @MainActor class Config {
15 | // MARK: Screens
16 | var cameraScreen: CameraScreenBuilder = DefaultCameraScreen.init
17 | var capturedMediaScreen: CapturedMediaScreenBuilder? = DefaultCapturedMediaScreen.init
18 | var errorScreen: ErrorScreenBuilder = DefaultCameraErrorScreen.init
19 |
20 | // MARK: Actions
21 | var imageCapturedAction: (UIImage, MCamera.Controller) -> () = { _,_ in }
22 | var videoCapturedAction: (URL, MCamera.Controller) -> () = { _,_ in }
23 | var closeMCameraAction: () -> () = {}
24 |
25 | // MARK: Others
26 | var appDelegate: MApplicationDelegate.Type? = nil
27 | var isCameraConfigured: Bool = false
28 | }}
29 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/MCamera/MCamera+Controller.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MCamera+Controller.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import Foundation
13 |
14 | extension MCamera { @MainActor public struct Controller {
15 | let mCamera: MCamera
16 | }}
17 |
--------------------------------------------------------------------------------
/Sources/Internal/UI/MCamera/MCamera.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MCamera.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | /**
15 | A view that displays a camera with state-specific screens.
16 |
17 | By default, it includes three screens that change depending on the status of the camera; **Error Screen**, **Camera Screen** and **Captured Media Screen**.
18 |
19 | Handles issues related to asking for permissions, and if permissions are not granted, it displays the **Error Screen**.
20 |
21 | Optionally shows the **Captured Media Screen**, which is displayed after the user captures an image or video.
22 |
23 |
24 | # Customization
25 | All of the MCamera's default settings can be changed during initialisation.
26 | - important: To start a camera session, simply call the ``startSession()`` method. For more details, see the **Usage** section.
27 |
28 | ## Camera Screens
29 | Use one of the methods below to change the default screens:
30 | - ``setCameraScreen(_:)``
31 | - ``setCapturedMediaScreen(_:)``
32 | - ``setErrorScreen(_:)``
33 |
34 | - tip: To disable displaying captured media, call the ``setCapturedMediaScreen(_:)`` method with a nil value.
35 |
36 | ## Actions after capturing media
37 | Use one of the methods below to set actions that will be called after capturing media:
38 | - ``onImageCaptured(_:)``
39 | - ``onVideoCaptured(_:)``
40 | - note: If there is no **Captured Media Screen**, the action is called immediately after the media is captured, otherwise it is triggered after the user accepts the captured media in the **Captured Media Screen**.
41 |
42 | ## Camera Configuration
43 | To change the initial camera settings, use the following methods:
44 | - ``setCameraOutputType(_:)``
45 | - ``setCameraPosition(_:)``
46 | - ``setAudioAvailability(_:)``
47 | - ``setZoomFactor(_:)``
48 | - ``setFlashMode(_:)``
49 | - ``setLightMode(_:)``
50 | - ``setResolution(_:)``
51 | - ``setFrameRate(_:)``
52 | - ``setCameraExposureDuration(_:)``
53 | - ``setCameraTargetBias(_:)``
54 | - ``setCameraISO(_:)``
55 | - ``setCameraExposureMode(_:)``
56 | - ``setCameraHDRMode(_:)``
57 | - ``setCameraFilters(_:)``
58 | - ``setMirrorOutput(_:)``
59 | - ``setGridVisibility(_:)``
60 | - ``setFocusImage(_:)``
61 | - ``setFocusImageColor(_:)``
62 | - ``setFocusImageSize(_:)``
63 | - important: Note that if you try to set a value that exceeds the camera's capabilities, the camera will automatically set the closest possible value and show you which value has been set.
64 |
65 | ## Other
66 | There are other methods that you can use to customize your experience:
67 | - ``setCloseMCameraAction(_:)``
68 | - ``lockCameraInPortraitOrientation(_:)``
69 |
70 | # Usage
71 | ```swift
72 | struct ContentView: View {
73 | var body: some View {
74 | MCamera()
75 | .setCameraFilters([.init(name: "CISepiaTone")!])
76 | .setCameraPosition(.back)
77 | .setCameraOutputType(.video)
78 | .setAudioAvailability(false)
79 | .setResolution(.hd4K3840x2160)
80 | .setFrameRate(30)
81 | .setZoomFactor(1.2)
82 | .setCameraISO(3)
83 | .setCameraTargetBias(1.2)
84 | .setLightMode(.on)
85 | .setFlashMode(.auto)
86 |
87 | // MUST BE CALLED!
88 | .startSession()
89 | }
90 | }
91 | ```
92 | */
93 | public struct MCamera: View {
94 | @ObservedObject var manager: CameraManager
95 | @Namespace var namespace
96 | var config: Config = .init()
97 |
98 |
99 | public var body: some View { if config.isCameraConfigured {
100 | ZStack(content: createContent)
101 | .onDisappear(perform: onDisappear)
102 | .onChange(of: manager.attributes.capturedMedia, perform: onCapturedMediaChange)
103 | }}
104 | }
105 | private extension MCamera {
106 | @ViewBuilder func createContent() -> some View {
107 | if let error = manager.attributes.error { createErrorScreen(error) }
108 | else if let capturedMedia = manager.attributes.capturedMedia, config.capturedMediaScreen != nil { createCapturedMediaScreen(capturedMedia) }
109 | else { createCameraScreen() }
110 | }
111 | }
112 | private extension MCamera {
113 | func createErrorScreen(_ error: MCameraError) -> some View {
114 | config.errorScreen(error, config.closeMCameraAction).erased()
115 | }
116 | func createCapturedMediaScreen(_ media: MCameraMedia) -> some View {
117 | config.capturedMediaScreen?(media, namespace, onCapturedMediaRejected, onCapturedMediaAccepted)
118 | .erased()
119 | .onAppear(perform: onCaptureMediaScreenAppear)
120 | }
121 | func createCameraScreen() -> some View {
122 | config.cameraScreen(manager, namespace, config.closeMCameraAction)
123 | .erased()
124 | .onAppear(perform: onCameraAppear)
125 | .onDisappear(perform: onCameraDisappear)
126 | }
127 | }
128 |
129 |
130 | // MARK: - ACTIONS
131 |
132 |
133 |
134 | // MARK: MCamera
135 | private extension MCamera {
136 | func onDisappear() {
137 | lockScreenOrientation(nil)
138 | manager.cancel()
139 | }
140 | func onCapturedMediaChange(_ capturedMedia: MCameraMedia?) {
141 | guard let capturedMedia, config.capturedMediaScreen == nil else { return }
142 | notifyUserOfMediaCaptured(capturedMedia)
143 | }
144 | }
145 | private extension MCamera {
146 | func lockScreenOrientation(_ orientation: UIInterfaceOrientationMask?) {
147 | config.appDelegate?.orientationLock = orientation ?? .all
148 | UINavigationController.attemptRotationToDeviceOrientation()
149 | }
150 | func notifyUserOfMediaCaptured(_ capturedMedia: MCameraMedia) {
151 | if let image = capturedMedia.getImage() { config.imageCapturedAction(image, .init(mCamera: self)) }
152 | else if let video = capturedMedia.getVideo() { config.videoCapturedAction(video, .init(mCamera: self)) }
153 | }
154 | }
155 |
156 | // MARK: Camera Screen
157 | private extension MCamera {
158 | func onCameraAppear() { Task {
159 | do {
160 | try await manager.setup()
161 | lockScreenOrientation(.portrait)
162 | } catch { print("(MijickCamera) ERROR DURING SETUP: \(error)") }
163 | }}
164 | func onCameraDisappear() {
165 | manager.cancel()
166 | }
167 | }
168 |
169 | // MARK: Captured Media Screen
170 | private extension MCamera {
171 | func onCaptureMediaScreenAppear() {
172 | lockScreenOrientation(nil)
173 | }
174 | func onCapturedMediaRejected() {
175 | manager.setCapturedMedia(nil)
176 | }
177 | func onCapturedMediaAccepted() {
178 | guard let capturedMedia = manager.attributes.capturedMedia else { return }
179 | notifyUserOfMediaCaptured(capturedMedia)
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/Sources/Public/Camera Settings/Public+CameraSettings+MApplicationDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+CameraSettings+MApplicationDelegate.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | /**
15 | Locks the screen in portrait mode when the Camera Screen is active.
16 |
17 | See ``MCamera/lockCameraInPortraitOrientation(_:)`` for more details.
18 | - note: Blocks the rotation of the entire screen on which the **MCamera** is located.
19 |
20 | ## Usage
21 | ```swift
22 | @main struct App_Main: App {
23 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
24 |
25 | var body: some Scene {
26 | WindowGroup(content: ContentView.init)
27 | }
28 | }
29 |
30 | // MARK: App Delegate
31 | class AppDelegate: NSObject, MApplicationDelegate {
32 | static var orientationLock = UIInterfaceOrientationMask.all
33 |
34 | func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { AppDelegate.orientationLock }
35 | }
36 |
37 | // MARK: Content View
38 | struct ContentView: View {
39 | var body: some View {
40 | MCamera()
41 | .lockCameraInPortraitOrientation(AppDelegate.self)
42 |
43 | // MUST BE CALLED!
44 | .startSession()
45 | }
46 | }
47 | ```
48 | */
49 | public protocol MApplicationDelegate: UIApplicationDelegate {
50 | static var orientationLock: UIInterfaceOrientationMask { get set }
51 |
52 | func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/Public/Camera Settings/Public+CameraSettings+MCamera.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+CameraSettings+MCamera.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 | import AVKit
14 |
15 | // MARK: Initializer
16 | public extension MCamera {
17 | init() { self.init(manager: .init(
18 | captureSession: AVCaptureSession(),
19 | captureDeviceInputType: AVCaptureDeviceInput.self
20 | ))}
21 | }
22 |
23 |
24 | // MARK: - METHODS
25 |
26 |
27 |
28 | // MARK: Changing Default Screens
29 | public extension MCamera {
30 | /**
31 | Changes the camera screen to a selected one.
32 |
33 | For more details and tips on creating your own **Camera Screen**, see the ``MCameraScreen`` documentation.
34 |
35 | - tip: To hide selected buttons and controls on the screen, use the method with DefaultCameraScreen as argument. For a code example, please refer to Usage -> Default Camera Screen Customization section.
36 |
37 |
38 | # Usage
39 |
40 | ## New Camera Screen
41 | ```swift
42 | struct ContentView: View {
43 | var body: some View {
44 | MCamera()
45 | .setCameraScreen(CustomCameraScreen.init)
46 |
47 | // MUST BE CALLED!
48 | .startSession()
49 | }
50 | }
51 | ```
52 |
53 | ## Default Camera Screen Customization
54 | ```swift
55 | struct ContentView: View {
56 | var body: some View {
57 | MCamera()
58 | .setCameraScreen {
59 | DefaultCameraScreen(cameraManager: $0, namespace: $1, closeMCameraAction: $2)
60 | .captureButtonAllowed(false)
61 | .cameraOutputSwitchAllowed(false)
62 | .lightButtonAllowed(false)
63 | }
64 |
65 | // MUST BE CALLED!
66 | .startSession()
67 | }
68 | }
69 | ```
70 | */
71 | func setCameraScreen(_ builder: @escaping CameraScreenBuilder) -> Self { config.cameraScreen = builder; return self }
72 |
73 | /**
74 | Changes the captured media screen to a selected one.
75 |
76 | For more details and tips on creating your own **Captured Media Screen**, see the ``MCapturedMediaScreen`` documentation.
77 |
78 | - tip: To disable displaying captured media, call the method with a nil value.
79 |
80 |
81 | # Usage
82 |
83 | ## New Captured Media Screen
84 | ```swift
85 | struct ContentView: View {
86 | var body: some View {
87 | MCamera()
88 | .setCapturedMediaScreen(DefaultCapturedMediaScreen.init)
89 |
90 | // MUST BE CALLED!
91 | .startSession()
92 | }
93 | }
94 | ```
95 |
96 | ## No Captured Media Screen
97 | ```swift
98 | struct ContentView: View {
99 | var body: some View {
100 | MCamera()
101 | .setCapturedMediaScreen(nil)
102 |
103 | // MUST BE CALLED!
104 | .startSession()
105 | }
106 | }
107 | ```
108 | */
109 | func setCapturedMediaScreen(_ builder: CapturedMediaScreenBuilder?) -> Self { config.capturedMediaScreen = builder; return self }
110 |
111 | /**
112 | Changes the error screen to a selected one.
113 |
114 | For more details and tips on creating your own **Error Screen**, see the ``MCameraErrorScreen`` documentation.
115 |
116 |
117 | ## Usage
118 | ```swift
119 | struct ContentView: View {
120 | var body: some View {
121 | MCamera()
122 | .setErrorScreen(CustomCameraErrorScreen.init)
123 |
124 | // MUST BE CALLED!
125 | .startSession()
126 | }
127 | }
128 | ```
129 | */
130 | func setErrorScreen(_ builder: @escaping ErrorScreenBuilder) -> Self { config.errorScreen = builder; return self }
131 | }
132 |
133 | // MARK: Changing Initial Values
134 | public extension MCamera {
135 | /**
136 | Changes the initial camera output type.
137 |
138 | For available options, please refer to the ``CameraOutputType`` documentation.
139 | */
140 | func setCameraOutputType(_ cameraOutputType: CameraOutputType) -> Self { manager.attributes.outputType = cameraOutputType; return self }
141 |
142 | /**
143 | Changes the initial camera position.
144 |
145 | For available options, please refer to the ``CameraPosition`` documentation.
146 |
147 | - note: If the selected camera position is not available, the camera will not be changed.
148 | */
149 | func setCameraPosition(_ cameraPosition: CameraPosition) -> Self { manager.attributes.cameraPosition = cameraPosition; return self }
150 |
151 | /**
152 | Definies whether the audio source is available.
153 |
154 | If disabled, the camera will not record audio, and will not ask for permission to access the microphone.
155 | */
156 | func setAudioAvailability(_ isAvailable: Bool) -> Self { manager.attributes.isAudioSourceAvailable = isAvailable; return self }
157 |
158 | /**
159 | Changes the initial camera zoom level.
160 |
161 | - note: If the zoom factor is out of bounds, it will be set to the closest available value.
162 | */
163 | func setZoomFactor(_ zoomFactor: CGFloat) -> Self { manager.attributes.zoomFactor = zoomFactor; return self }
164 |
165 | /**
166 | Changes the initial camera flash mode.
167 |
168 | For available options, please refer to the ``CameraFlashMode`` documentation.
169 |
170 | - note: If the selected flash mode is not available, the flash mode will not be changed.
171 | */
172 | func setFlashMode(_ flashMode: CameraFlashMode) -> Self { manager.attributes.flashMode = flashMode; return self }
173 |
174 | /**
175 | Changes the initial light (torch / flashlight) mode.
176 |
177 | For available options, please refer to the ``CameraLightMode`` documentation.
178 |
179 | - note: If the selected light mode is not available, the light mode will not be changed.
180 | */
181 | func setLightMode(_ lightMode: CameraLightMode) -> Self { manager.attributes.lightMode = lightMode; return self }
182 |
183 | /**
184 | Changes the initial camera resolution.
185 |
186 | - important: Changing the resolution may affect the maximum frame rate that can be set.
187 | */
188 | func setResolution(_ resolution: AVCaptureSession.Preset) -> Self { manager.attributes.resolution = resolution; return self }
189 |
190 | /**
191 | Changes the initial camera frame rate.
192 |
193 | - note: Depending on the resolution of the camera and the current specifications of the device, there are some restrictions on the frame rate that can be set.
194 | If you set a frame rate that exceeds the camera's capabilities, the library will automatically set the closest possible value and show you which value has been set (``MCameraScreen/frameRate``).
195 | */
196 | func setFrameRate(_ frameRate: Int32) -> Self { manager.attributes.frameRate = frameRate; return self }
197 |
198 | /**
199 | Changes the initial camera exposure duration.
200 |
201 | - note: If the exposure duration is out of bounds, it will be set to the closest available value.
202 | */
203 | func setCameraExposureDuration(_ duration: CMTime) -> Self { manager.attributes.cameraExposure.duration = duration; return self }
204 |
205 | /**
206 | Changes the initial camera target bias.
207 |
208 | - note: If the target bias is out of bounds, it will be set to the closest available value.
209 | */
210 | func setCameraTargetBias(_ targetBias: Float) -> Self { manager.attributes.cameraExposure.targetBias = targetBias; return self }
211 |
212 | /**
213 | Changes the initial camera ISO.
214 |
215 | - note: If the ISO is out of bounds, it will be set to the closest available value.
216 | */
217 | func setCameraISO(_ iso: Float) -> Self { manager.attributes.cameraExposure.iso = iso; return self }
218 |
219 | /**
220 | Changes the initial camera exposure mode.
221 |
222 | - note: If the exposure mode is not supported, the exposure mode will not be changed.
223 | */
224 | func setCameraExposureMode(_ exposureMode: AVCaptureDevice.ExposureMode) -> Self { manager.attributes.cameraExposure.mode = exposureMode; return self }
225 |
226 | /**
227 | Changes the initial camera HDR mode.
228 |
229 | For available options, please refer to the ``CameraHDRMode`` documentation.
230 | */
231 | func setCameraHDRMode(_ hdrMode: CameraHDRMode) -> Self { manager.attributes.hdrMode = hdrMode; return self }
232 |
233 | /**
234 | Changes the initial camera filters.
235 |
236 | - important: Setting multiple filters simultaneously can affect the performance of the camera.
237 | */
238 | func setCameraFilters(_ filters: [CIFilter]) -> Self { manager.attributes.cameraFilters = filters; return self }
239 |
240 | /**
241 | Changes the initial mirror output setting.
242 | */
243 | func setMirrorOutput(_ shouldMirror: Bool) -> Self { manager.attributes.mirrorOutput = shouldMirror; return self }
244 |
245 | /**
246 | Changes the initial grid visibility setting.
247 | */
248 | func setGridVisibility(_ shouldShowGrid: Bool) -> Self { manager.attributes.isGridVisible = shouldShowGrid; return self }
249 |
250 | /**
251 | Changes the shape of the focus indicator visible when touching anywhere on the camera screen.
252 | */
253 | func setFocusImage(_ image: UIImage) -> Self { manager.cameraMetalView.focusIndicator.image = image; return self }
254 |
255 | /**
256 | Changes the color of the focus indicator visible when touching anywhere on the camera screen.
257 | */
258 | func setFocusImageColor(_ color: UIColor) -> Self { manager.cameraMetalView.focusIndicator.tintColor = color; return self }
259 |
260 | /**
261 | Changes the size of the focus indicator visible when touching anywhere on the camera.
262 | */
263 | func setFocusImageSize(_ size: CGFloat) -> Self { manager.cameraMetalView.focusIndicator.size = size; return self }
264 | }
265 |
266 | // MARK: Actions
267 | public extension MCamera {
268 | /**
269 | Indicates how the MCamera can be closed.
270 |
271 | ## Usage
272 | ```swift
273 | struct ContentView: View {
274 | @State private var isSheetPresented: Bool = false
275 |
276 |
277 | var body: some View {
278 | Button(action: { isSheetPresented = true }) {
279 | Text("Click me!")
280 | }
281 | .fullScreenCover(isPresented: $isSheetPresented) {
282 | MCamera()
283 | .setResolution(.hd1920x1080)
284 | .setCloseMCameraAction { isSheetPresented = false }
285 |
286 | // MUST BE CALLED!
287 | .startSession()
288 | }
289 | }
290 | }
291 | ```
292 | */
293 | func setCloseMCameraAction(_ action: @escaping () -> ()) -> Self { config.closeMCameraAction = action; return self }
294 |
295 | /**
296 | Defines action that is called when an image is captured.
297 |
298 | MCameraController can be used to perform additional actions related to MCamera, such as closing MCamera or returning to the camera screen.
299 | See ``Controller`` for more information.
300 |
301 | - note: The action is called immediately if **Captured Media Screen** is nil, otherwise after the user accepts the photo.
302 |
303 |
304 | ## Usage
305 | ```swift
306 | struct ContentView: View {
307 | var body: some View {
308 | MCamera()
309 | .onImageCaptured { image, controller in
310 | saveImageInGallery(image)
311 | controller.reopenCameraScreen()
312 | }
313 |
314 | // MUST BE CALLED!
315 | .startSession()
316 | }
317 | }
318 | ```
319 | */
320 | func onImageCaptured(_ action: @escaping (UIImage, MCamera.Controller) -> ()) -> Self { config.imageCapturedAction = action; return self }
321 |
322 | /**
323 | Defines action that is called when a video is captured.
324 |
325 | MCameraController can be used to perform additional actions related to MCamera, such as closing MCamera or returning to the camera screen.
326 | See ``Controller`` for more information.
327 |
328 | - note: The action is called immediately if **Captured Media Screen** is nil, otherwise after the user accepts the video.
329 |
330 |
331 | ## Usage
332 | ```swift
333 | struct ContentView: View {
334 | var body: some View {
335 | MCamera()
336 | .onVideoCaptured { video, controller in
337 | saveVideoInGallery(video)
338 | controller.reopenCameraScreen()
339 | }
340 |
341 | // MUST BE CALLED!
342 | .startSession()
343 | }
344 | }
345 | ```
346 | */
347 | func onVideoCaptured(_ action: @escaping (URL, MCamera.Controller) -> ()) -> Self { config.videoCapturedAction = action; return self }
348 | }
349 |
350 | // MARK: Others
351 | public extension MCamera {
352 | /**
353 | Locks the screen in portrait mode when the Camera Screen is active.
354 |
355 | See ``MApplicationDelegate`` for more details.
356 | - note: Blocks the rotation of the entire screen on which the **MCamera** is located.
357 |
358 | ## Usage
359 | ```swift
360 | @main struct App_Main: App {
361 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
362 |
363 | var body: some Scene {
364 | WindowGroup(content: ContentView.init)
365 | }
366 | }
367 |
368 | // MARK: App Delegate
369 | class AppDelegate: NSObject, MApplicationDelegate {
370 | static var orientationLock = UIInterfaceOrientationMask.all
371 |
372 | func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { AppDelegate.orientationLock }
373 | }
374 |
375 | // MARK: Content View
376 | struct ContentView: View {
377 | var body: some View {
378 | MCamera()
379 | .lockCameraInPortraitOrientation(AppDelegate.self)
380 |
381 | // MUST BE CALLED!
382 | .startSession()
383 | }
384 | }
385 | ```
386 | */
387 | func lockCameraInPortraitOrientation(_ appDelegate: MApplicationDelegate.Type) -> Self { config.appDelegate = appDelegate; manager.attributes.orientationLocked = true; return self }
388 |
389 | /**
390 | Starts the camera session.
391 |
392 | - important: This method must be called to start the camera.
393 | */
394 | func startSession() -> some View { config.isCameraConfigured = true; return self }
395 | }
396 |
--------------------------------------------------------------------------------
/Sources/Public/Camera Settings/Public+CameraSettings+MCameraController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+CameraSettings+MCameraController.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import Foundation
13 |
14 | // MARK: Available Actions
15 | public extension MCamera.Controller {
16 | /**
17 | Closes the MCamera.
18 |
19 | See ``MCamera/setCloseMCameraAction(_:)`` for more details.
20 | */
21 | func closeMCamera() { mCamera.config.closeMCameraAction() }
22 |
23 | /**
24 | Opens the Camera Screen.
25 | */
26 | func reopenCameraScreen() { mCamera.manager.setCapturedMedia(nil) }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/Public/Models/Public+Model+CameraUtilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+Model+CameraUtilities.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: Camera Output Type
15 | public enum CameraOutputType: CaseIterable {
16 | case photo
17 | case video
18 | }
19 |
20 | // MARK: Camera Position
21 | public enum CameraPosition: CaseIterable {
22 | case back
23 | case front
24 | }
25 |
26 | // MARK: Camera Flash Mode
27 | public enum CameraFlashMode: CaseIterable {
28 | case off
29 | case on
30 | case auto
31 | }
32 |
33 | // MARK: Camera Light Mode
34 | public enum CameraLightMode: CaseIterable {
35 | case off
36 | case on
37 | }
38 |
39 | // MARK: Camera HDR Mode
40 | public enum CameraHDRMode: CaseIterable {
41 | case off
42 | case on
43 | case auto
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/Public/Models/Public+Model+MCameraError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+Model+MCameraError.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import Foundation
13 |
14 | public enum MCameraError: Error {
15 | case microphonePermissionsNotGranted, cameraPermissionsNotGranted
16 | case cannotSetupInput, cannotSetupOutput, cannotSetupMetalDevice
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Public/Models/Public+Model+MCameraMedia.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+Model+MCameraMedia.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: Getters
15 | public extension MCameraMedia {
16 | /**
17 | Gets the image from the media object.
18 | */
19 | func getImage() -> UIImage? { image }
20 |
21 | /**
22 | Gets the video URL from the media object.
23 | */
24 | func getVideo() -> URL? { video }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/Public/UI/Public+UI+DefaultCameraScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+UI+DefaultCameraScreen.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | // MARK: Initializer
15 | public extension DefaultCameraScreen {
16 | init(cameraManager: CameraManager, namespace: Namespace.ID, closeMCameraAction: @escaping () -> Void) {
17 | self.init(cameraManager: cameraManager, namespace: namespace, closeMCameraAction: closeMCameraAction, config: .init())
18 | }
19 | }
20 |
21 | // MARK: Methods
22 | public extension DefaultCameraScreen {
23 | func captureButtonAllowed(_ value: Bool) -> Self { config.captureButtonAllowed = value; return self }
24 | func cameraOutputSwitchAllowed(_ value: Bool) -> Self { config.cameraOutputSwitchAllowed = value; return self }
25 | func cameraPositionButtonAllowed(_ value: Bool) -> Self { config.cameraPositionButtonAllowed = value; return self }
26 | func flashButtonAllowed(_ value: Bool) -> Self { config.flashButtonAllowed = value; return self }
27 | func lightButtonAllowed(_ value: Bool) -> Self { config.lightButtonAllowed = value; return self }
28 | func flipButtonAllowed(_ value: Bool) -> Self { config.flipButtonAllowed = value; return self }
29 | func gridButtonAllowed(_ value: Bool) -> Self { config.gridButtonAllowed = value; return self }
30 | func closeButtonAllowed(_ value: Bool) -> Self { config.closeButtonAllowed = value; return self }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/Public/UI/Public+UI+MCameraErrorScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+UI+MCameraErrorScreen.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | /**
15 | Screen that displays an error message if one or more camera permissions are denied by the user.
16 |
17 | - important: A view conforming to **MCameraErrorScreen** has to be passed directly to ``MCamera``. See ``MCamera/setErrorScreen(_:)`` for more details.
18 |
19 |
20 | ## Usage
21 | ```swift
22 | struct ContentView: View {
23 | var body: some View {
24 | MCamera()
25 | .setErrorScreen(CustomCameraErrorScreen.init)
26 |
27 | // MUST BE CALLED!
28 | .startSession()
29 | }
30 | }
31 |
32 | // MARK: Custom Camera Error Screen
33 | struct CustomCameraErrorScreen: MCameraErrorScreen {
34 | let error: MCameraError
35 | let closeMCameraAction: () -> ()
36 |
37 |
38 | var body: some View {
39 | Button(action: openAppSettings) { Text("Open Settings") }
40 | }
41 | }
42 | ```
43 | */
44 | public protocol MCameraErrorScreen: View {
45 | var error: MCameraError { get }
46 | var closeMCameraAction: () -> () { get }
47 | }
48 |
49 | // MARK: Methods
50 | public extension MCameraErrorScreen {
51 | func openAppSettings() { if let url = URL(string: UIApplication.openSettingsURLString) {
52 | UIApplication.shared.open(url)
53 | }}
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/Public/UI/Public+UI+MCameraScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+UI+MCameraScreen.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 | import AVFoundation
14 | import MijickTimer
15 |
16 | /**
17 | Screen that displays the camera view and manages camera actions.
18 |
19 | - important: A view conforming to **MCameraScreen** has to be passed directly to ``MCamera``. See ``MCamera/setCameraScreen(_:)`` for more details.
20 |
21 | ## Usage
22 | ```swift
23 | struct ContentView: View {
24 | var body: some View {
25 | MCamera()
26 | .setCameraScreen(CustomCameraErrorScreen.init)
27 |
28 | // MUST BE CALLED!
29 | .startSession()
30 | }
31 | }
32 |
33 | // MARK: Custom Camera Screen
34 | struct CustomCameraScreen: MCameraScreen {
35 | @ObservedObject var cameraManager: CameraManager
36 | let namespace: Namespace.ID
37 | let closeMCameraAction: () -> ()
38 |
39 |
40 | var body: some View {
41 | VStack(spacing: 0) {
42 | createNavigationBar()
43 | createCameraOutputView()
44 | createCaptureButton()
45 | }
46 | }
47 | }
48 | private extension CustomCameraScreen {
49 | func createNavigationBar() -> some View {
50 | Text("This is a Custom Camera View")
51 | .padding(.top, 12)
52 | .padding(.bottom, 12)
53 | }
54 | func createCaptureButton() -> some View {
55 | Button(action: captureOutput) { Text("Click to capture") }
56 | .padding(.top, 12)
57 | .padding(.bottom, 12)
58 | }
59 | }
60 | ```
61 | */
62 | public protocol MCameraScreen: View {
63 | var cameraManager: CameraManager { get }
64 | var namespace: Namespace.ID { get }
65 | var closeMCameraAction: () -> () { get }
66 | }
67 |
68 | // MARK: Methods
69 | public extension MCameraScreen {
70 | /**
71 | View that displays the camera output.
72 |
73 | ## Usage
74 | ```swift
75 | struct CustomCameraScreen: MCameraScreen {
76 | @ObservedObject var cameraManager: CameraManager
77 | let namespace: Namespace.ID
78 | let closeMCameraAction: () -> ()
79 |
80 |
81 | var body: some View {
82 | (...)
83 | createCameraOutputView()
84 | (...)
85 | }
86 | }
87 | ```
88 | */
89 | func createCameraOutputView() -> some View { CameraBridgeView(cameraManager: cameraManager).equatable() }
90 | }
91 | public extension MCameraScreen {
92 | /**
93 | Capture the current camera output.
94 |
95 | The output type depends on what ``cameraOutputType`` is set to.
96 | */
97 | func captureOutput() { cameraManager.captureOutput() }
98 |
99 | /**
100 | Set the output type of the camera.
101 |
102 | For available options, please refer to the ``CameraOutputType`` documentation.
103 | */
104 | func setOutputType(_ outputType: CameraOutputType) { cameraManager.setOutputType(outputType) }
105 |
106 | /**
107 | Set the camera position.
108 |
109 | For available options, please refer to the ``CameraPosition`` documentation.
110 |
111 | - note: If the selected camera position is not available, the camera will not be changed.
112 | */
113 | func setCameraPosition(_ cameraPosition: CameraPosition) async throws { try await cameraManager.setCameraPosition(cameraPosition) }
114 |
115 | /**
116 | Set the zoom factor of the camera.
117 |
118 | - note: If the zoom factor is out of bounds, it will be set to the closest available value.
119 | */
120 | func setZoomFactor(_ zoomFactor: CGFloat) throws { try cameraManager.setCameraZoomFactor(zoomFactor) }
121 |
122 | /**
123 | Set the flash mode of the camera.
124 |
125 | For available options, please refer to the ``CameraFlashMode`` documentation.
126 |
127 | - note: If the selected flash mode is not available, the flash mode will not be changed.
128 | */
129 | func setFlashMode(_ flashMode: CameraFlashMode) { cameraManager.setFlashMode(flashMode) }
130 |
131 | /**
132 | Set the light mode of the camera.
133 |
134 | For available options, please refer to the ``CameraLightMode`` documentation.
135 |
136 | - note: If the selected light mode is not available, the light mode will not be changed.
137 | */
138 | func setLightMode(_ lightMode: CameraLightMode) throws { try cameraManager.setLightMode(lightMode) }
139 |
140 | /**
141 | Set the camera resolution.
142 |
143 | - important: Changing the resolution may affect the maximum frame rate that can be set.
144 | */
145 | func setResolution(_ resolution: AVCaptureSession.Preset) { cameraManager.setResolution(resolution) }
146 |
147 | /**
148 | Set the camera frame rate.
149 |
150 | - important: Changing the resolution may affect the maximum frame rate that can be set.
151 | - note: If the frame rate is out of bounds, it will be set to the closest available value.
152 | */
153 | func setFrameRate(_ frameRate: Int32) throws { try cameraManager.setFrameRate(frameRate) }
154 |
155 | /**
156 | Set the camera exposure duration.
157 |
158 | - note: If the exposure duration is out of bounds, it will be set to the closest available value.
159 | */
160 | func setExposureDuration(_ exposureDuration: CMTime) throws { try cameraManager.setExposureDuration(exposureDuration) }
161 |
162 | /**
163 | Set the camera exposure target bias.
164 |
165 | - note: If the target bias is out of bounds, it will be set to the closest available value.
166 | */
167 | func setExposureTargetBias(_ exposureTargetBias: Float) throws { try cameraManager.setExposureTargetBias(exposureTargetBias) }
168 |
169 | /**
170 | Set the camera ISO.
171 |
172 | - note: If the ISO is out of bounds, it will be set to the closest available value.
173 | */
174 | func setISO(_ iso: Float) throws { try cameraManager.setISO(iso) }
175 |
176 | /**
177 | Set the camera exposure mode.
178 |
179 | - note: If the exposure mode is not supported, the exposure mode will not be changed.
180 | */
181 | func setExposureMode(_ exposureMode: AVCaptureDevice.ExposureMode) throws { try cameraManager.setExposureMode(exposureMode) }
182 |
183 | /**
184 | Set the camera HDR mode.
185 |
186 | For available options, please refer to the ``CameraHDRMode`` documentation.
187 | */
188 | func setHDRMode(_ hdrMode: CameraHDRMode) throws { try cameraManager.setHDRMode(hdrMode) }
189 |
190 | /**
191 | Set the camera filters to be applied to the camera output.
192 |
193 | - important: Setting multiple filters simultaneously can affect the performance of the camera.
194 | */
195 | func setCameraFilters(_ filters: [CIFilter]) { cameraManager.setCameraFilters(filters) }
196 |
197 | /**
198 | Set whether the camera output should be mirrored.
199 | */
200 | func setMirrorOutput(_ shouldMirror: Bool) { cameraManager.setMirrorOutput(shouldMirror) }
201 |
202 | /**
203 | Set whether the camera grid should be visible.
204 | */
205 | func setGridVisibility(_ shouldShowGrid: Bool) { cameraManager.setGridVisibility(shouldShowGrid) }
206 | }
207 |
208 | // MARK: Attributes
209 | public extension MCameraScreen {
210 | var cameraOutputType: CameraOutputType { cameraManager.attributes.outputType }
211 | var cameraPosition: CameraPosition { cameraManager.attributes.cameraPosition }
212 | var zoomFactor: CGFloat { cameraManager.attributes.zoomFactor }
213 | var flashMode: CameraFlashMode { cameraManager.attributes.flashMode }
214 | var lightMode: CameraLightMode { cameraManager.attributes.lightMode }
215 | var resolution: AVCaptureSession.Preset { cameraManager.attributes.resolution }
216 | var frameRate: Int32 { cameraManager.attributes.frameRate }
217 | var exposureDuration: CMTime { cameraManager.attributes.cameraExposure.duration }
218 | var exposureTargetBias: Float { cameraManager.attributes.cameraExposure.targetBias }
219 | var iso: Float { cameraManager.attributes.cameraExposure.iso }
220 | var exposureMode: AVCaptureDevice.ExposureMode { cameraManager.attributes.cameraExposure.mode }
221 | var hdrMode: CameraHDRMode { cameraManager.attributes.hdrMode }
222 | var cameraFilters: [CIFilter] { cameraManager.attributes.cameraFilters }
223 | var isOutputMirrored: Bool { cameraManager.attributes.mirrorOutput }
224 | var isGridVisible: Bool { cameraManager.attributes.isGridVisible }
225 | }
226 | public extension MCameraScreen {
227 | var hasFlash: Bool { cameraManager.hasFlash }
228 | var hasLight: Bool { cameraManager.hasLight }
229 | var recordingTime: MTime { cameraManager.videoOutput.recordingTime }
230 | var isRecording: Bool { cameraManager.videoOutput.timer.timerStatus == .running }
231 | var isOrientationLocked: Bool { cameraManager.attributes.orientationLocked || cameraManager.attributes.userBlockedScreenRotation }
232 | var deviceOrientation: AVCaptureVideoOrientation { cameraManager.attributes.deviceOrientation }
233 | }
234 |
--------------------------------------------------------------------------------
/Sources/Public/UI/Public+UI+MCapturedMediaScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Public+UI+MCapturedMediaScreen.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import SwiftUI
13 |
14 | /**
15 | Screen that displays the captured media.
16 |
17 | - important: A view conforming to **MCapturedMediaScreen** has to be passed directly to ``MCamera``. See ``MCamera/setCapturedMediaScreen(_:)`` for more details.
18 |
19 |
20 | ## Usage
21 | ```swift
22 | struct ContentView: View {
23 | var body: some View {
24 | MCamera()
25 | .setCapturedMediaScreen(CustomCapturedMediaScreen.init)
26 |
27 | // MUST BE CALLED!
28 | .startSession()
29 | }
30 | }
31 |
32 | // MARK: Custom Captured Media Screen
33 | struct CustomCapturedMediaScreen: MCapturedMediaScreen {
34 | let capturedMedia: MCameraMedia
35 | let namespace: Namespace.ID
36 | let retakeAction: () -> ()
37 | let acceptMediaAction: () -> ()
38 |
39 |
40 | var body: some View {
41 | VStack(spacing: 0) {
42 | Spacer()
43 | createContentView()
44 | Spacer()
45 | createButtons()
46 | }
47 | }
48 | }
49 | private extension CustomCapturedMediaScreen {
50 | func createContentView() -> some View { ZStack {
51 | if let image = capturedMedia.getImage() { createImageView(image) }
52 | else { EmptyView() }
53 | }}
54 | func createButtons() -> some View {
55 | HStack(spacing: 24) {
56 | createRetakeButton()
57 | createSaveButton()
58 | }
59 | }
60 | }
61 | private extension CustomCapturedMediaScreen {
62 | func createImageView(_ image: UIImage) -> some View {
63 | Image(uiImage: image)
64 | .resizable()
65 | .aspectRatio(contentMode: .fit)
66 | .ignoresSafeArea()
67 | }
68 | func createRetakeButton() -> some View {
69 | Button(action: retakeAction) { Text("Retake") }
70 | }
71 | func createSaveButton() -> some View {
72 | Button(action: acceptMediaAction) { Text("Save") }
73 | }
74 | }
75 | ```
76 | */
77 | public protocol MCapturedMediaScreen: View {
78 | var capturedMedia: MCameraMedia { get }
79 | var namespace: Namespace.ID { get }
80 | var retakeAction: () -> () { get }
81 | var acceptMediaAction: () -> () { get }
82 | }
83 |
--------------------------------------------------------------------------------
/Tests/Tests+CameraManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tests+CameraManager.swift of MijickCamera
3 | //
4 | // Created by Tomasz Kurylik. Sending ❤️ from Kraków!
5 | // - Mail: tomasz.kurylik@mijick.com
6 | // - GitHub: https://github.com/FulcrumOne
7 | // - Medium: https://medium.com/@mijick
8 | //
9 | // Copyright ©2024 Mijick. All rights reserved.
10 |
11 |
12 | import Testing
13 | import SwiftUI
14 | @testable import MijickCamera
15 |
16 | @MainActor @Suite("Camera Manager Tests") struct CameraManagerTests {
17 | var cameraManager: CameraManager = .init(
18 | captureSession: MockCaptureSession(),
19 | captureDeviceInputType: MockDeviceInput.self
20 | )
21 | }
22 |
23 | // MARK: Setup
24 | extension CameraManagerTests {
25 | @Test("Setup: Default Attributes") func setupWithDefaultAttributes() async throws {
26 | try await setupCamera()
27 |
28 | #expect(cameraManager.captureSession.isRunning == true)
29 | #expect(cameraManager.captureSession.deviceInputs.count == 2)
30 | #expect(cameraManager.photoOutput.parent != nil)
31 | #expect(cameraManager.videoOutput.parent != nil)
32 | #expect(cameraManager.captureSession.outputs.count == 3)
33 | #expect(cameraManager.cameraView != nil)
34 | #expect(cameraManager.cameraLayer.isHidden == true)
35 | #expect(cameraManager.cameraMetalView.parent != nil)
36 | #expect(cameraManager.cameraGridView.parent != nil)
37 | #expect(cameraManager.motionManager.manager.accelerometerUpdateInterval > 0)
38 | #expect(cameraManager.notificationCenterManager.parent != nil)
39 | }
40 | @Test("Setup: Custom Attributes") func setupWithCustomAttributes() async throws {
41 | cameraManager.attributes.cameraPosition = .front
42 | cameraManager.attributes.zoomFactor = 2137
43 | cameraManager.attributes.lightMode = .on
44 | cameraManager.attributes.resolution = .hd1280x720
45 | cameraManager.attributes.frameRate = 666
46 | cameraManager.attributes.cameraExposure.duration = .init(value: 1, timescale: 10)
47 | cameraManager.attributes.cameraExposure.targetBias = 0.66
48 | cameraManager.attributes.cameraExposure.iso = 2000
49 | cameraManager.attributes.cameraExposure.mode = .custom
50 | cameraManager.attributes.hdrMode = .off
51 | cameraManager.attributes.isGridVisible = false
52 |
53 | try await setupCamera()
54 |
55 | #expect(currentDevice.uniqueID == cameraManager.frontCameraInput?.device.uniqueID)
56 | #expect(currentDevice.videoZoomFactor == currentDevice.maxAvailableVideoZoomFactor)
57 | #expect(currentDevice.lightMode == .on)
58 | #expect(cameraManager.captureSession.sessionPreset == .hd1280x720)
59 | #expect(currentDevice.activeVideoMinFrameDuration == .init(value: 1, timescale: Int32(currentDevice.maxFrameRate!)))
60 | #expect(currentDevice.activeVideoMaxFrameDuration == .init(value: 1, timescale: Int32(currentDevice.maxFrameRate!)))
61 | #expect(currentDevice.exposureDuration == .init(value: 1, timescale: 10))
62 | #expect(currentDevice.exposureTargetBias == 0.66)
63 | #expect(currentDevice.iso == currentDevice.maxISO)
64 | #expect(currentDevice.exposureMode == .custom)
65 | #expect(currentDevice.hdrMode == .off)
66 | #expect(cameraManager.cameraGridView.alpha == 0)
67 |
68 | #expect(cameraManager.attributes.zoomFactor == currentDevice.maxAvailableVideoZoomFactor)
69 | #expect(cameraManager.attributes.frameRate == Int32(currentDevice.maxFrameRate!))
70 | #expect(cameraManager.attributes.cameraExposure.iso == currentDevice.maxISO)
71 | }
72 | @Test("Setup: Audio Source Unavailable") func setupWithAudioSourceUnavailable() async throws {
73 | cameraManager.attributes.isAudioSourceAvailable = false
74 | try await setupCamera()
75 |
76 | #expect(cameraManager.captureSession.deviceInputs.count == 1)
77 | }
78 | }
79 |
80 | // MARK: Cancel
81 | extension CameraManagerTests {
82 | @Test("Cancel Camera Session") func cancelCameraSession() async throws {
83 | try await setupCamera()
84 | cameraManager.cancel()
85 |
86 | #expect(cameraManager.captureSession.isRunning == false)
87 | #expect(cameraManager.captureSession.deviceInputs.count == 0)
88 | #expect(cameraManager.captureSession.outputs.count == 0)
89 | }
90 | }
91 |
92 | // MARK: Set Camera Output
93 | extension CameraManagerTests {
94 | @Test("Set Camera Output") func setCameraOutput() async throws {
95 | try await setupCamera()
96 |
97 | cameraManager.setOutputType(.photo)
98 | #expect(cameraManager.attributes.outputType == .photo)
99 |
100 | cameraManager.setOutputType(.video)
101 | #expect(cameraManager.attributes.outputType == .video)
102 | }
103 | }
104 |
105 | // MARK: Set Camera Position
106 | extension CameraManagerTests {
107 | @Test("Set Camera Position") func setCameraPosition() async throws {
108 | try await setupCamera()
109 |
110 | try await cameraManager.setCameraPosition(.front)
111 | #expect(cameraManager.captureSession.deviceInputs.count == 2)
112 | #expect(currentDevice.uniqueID == cameraManager.frontCameraInput?.device.uniqueID)
113 | #expect(cameraManager.attributes.cameraPosition == .front)
114 |
115 | await Task.sleep(seconds: 0.5)
116 |
117 | try await cameraManager.setCameraPosition(.back)
118 | #expect(cameraManager.captureSession.deviceInputs.count == 2)
119 | #expect(currentDevice.uniqueID == cameraManager.backCameraInput?.device.uniqueID)
120 | #expect(cameraManager.attributes.cameraPosition == .back)
121 |
122 | await Task.sleep(seconds: 0.5)
123 |
124 | try cameraManager.setCameraZoomFactor(3.2)
125 | try await cameraManager.setCameraPosition(.front)
126 | #expect(currentDevice.videoZoomFactor == 1)
127 | #expect(cameraManager.attributes.zoomFactor == 1)
128 | }
129 | }
130 |
131 | // MARK: Set Camera Zoom
132 | extension CameraManagerTests {
133 | @Test("Set Camera Zoom") func setCameraZoom() async throws {
134 | try await setupCamera()
135 |
136 | try cameraManager.setCameraZoomFactor(2.137)
137 | #expect(currentDevice.videoZoomFactor == 2.137)
138 | #expect(cameraManager.attributes.zoomFactor == 2.137)
139 |
140 | try cameraManager.setCameraZoomFactor(0.2137)
141 | #expect(currentDevice.videoZoomFactor == currentDevice.minAvailableVideoZoomFactor)
142 | #expect(cameraManager.attributes.zoomFactor == currentDevice.minAvailableVideoZoomFactor)
143 |
144 | try cameraManager.setCameraZoomFactor(213.7)
145 | #expect(currentDevice.videoZoomFactor == currentDevice.maxAvailableVideoZoomFactor)
146 | #expect(cameraManager.attributes.zoomFactor == currentDevice.maxAvailableVideoZoomFactor)
147 | }
148 | }
149 |
150 | // MARK: Set Camera Focus
151 | extension CameraManagerTests {
152 | @Test("Set Camera Focus") func setCameraFocus() async throws {
153 | try await setupCamera()
154 |
155 | let point = CGPoint(x: 213.7, y: 21.37)
156 | let expectedPoint = CGPoint(x: point.y / cameraManager.cameraView.frame.height, y: 1 - point.x / cameraManager.cameraView.frame.width)
157 |
158 | try cameraManager.setCameraFocus(at: point)
159 | #expect(currentDevice.focusPointOfInterest == expectedPoint)
160 | #expect(currentDevice.exposurePointOfInterest == expectedPoint)
161 | #expect(currentDevice.focusMode == .autoFocus)
162 | #expect(currentDevice.exposureMode == .autoExpose)
163 | #expect(cameraManager.cameraView.subviews.filter { $0.tag == .focusIndicatorTag }.count == 1)
164 | }
165 | }
166 |
167 | // MARK: Set Flash Mode
168 | extension CameraManagerTests {
169 | @Test("Set Flash Mode") func setFlashMode() async throws {
170 | try await setupCamera()
171 |
172 | cameraManager.setFlashMode(.on)
173 | #expect(cameraManager.attributes.flashMode == .on)
174 |
175 | cameraManager.setFlashMode(.auto)
176 | #expect(cameraManager.attributes.flashMode == .auto)
177 |
178 | cameraManager.setFlashMode(.off)
179 | #expect(cameraManager.attributes.flashMode == .off)
180 | }
181 | }
182 |
183 | // MARK: Set Light Mode
184 | extension CameraManagerTests {
185 | @Test("Set Light Mode") func setLightMode() async throws {
186 | try await setupCamera()
187 |
188 | try cameraManager.setLightMode(.on)
189 | #expect(currentDevice.lightMode == .on)
190 | #expect(cameraManager.attributes.lightMode == .on)
191 |
192 | try cameraManager.setLightMode(.off)
193 | #expect(currentDevice.lightMode == .off)
194 | #expect(cameraManager.attributes.lightMode == .off)
195 | }
196 | }
197 |
198 | // MARK: Set Mirror Output
199 | extension CameraManagerTests {
200 | @Test("Set Mirror Output") func setMirrorOutput() async throws {
201 | try await setupCamera()
202 |
203 | cameraManager.setMirrorOutput(true)
204 | #expect(cameraManager.attributes.mirrorOutput == true)
205 |
206 | cameraManager.setMirrorOutput(false)
207 | #expect(cameraManager.attributes.mirrorOutput == false)
208 | }
209 | }
210 |
211 | // MARK: Set Grid Visibility
212 | extension CameraManagerTests {
213 | @Test("Set Grid Visibility") func setGridVisibility() async throws {
214 | try await setupCamera()
215 |
216 | cameraManager.setGridVisibility(true)
217 | #expect(cameraManager.cameraGridView.alpha == 1)
218 | #expect(cameraManager.attributes.isGridVisible == true)
219 |
220 | cameraManager.setGridVisibility(false)
221 | #expect(cameraManager.cameraGridView.alpha == 0)
222 | #expect(cameraManager.attributes.isGridVisible == false)
223 | }
224 | }
225 |
226 | // MARK: Set Camera Filters
227 | extension CameraManagerTests {
228 | @Test("Set Camera Filters") func setCameraFilters() async throws {
229 | try await setupCamera()
230 |
231 | cameraManager.setCameraFilters([.init(name: "CISepiaTone")!])
232 | #expect(cameraManager.attributes.cameraFilters.count == 1)
233 | }
234 | }
235 |
236 | // MARK: Set Exposure Mode
237 | extension CameraManagerTests {
238 | @Test("Set Exposure Mode") func setExposureMode() async throws {
239 | try await setupCamera()
240 |
241 | try cameraManager.setExposureMode(.continuousAutoExposure)
242 | #expect(currentDevice.exposureMode == .continuousAutoExposure)
243 | #expect(cameraManager.attributes.cameraExposure.mode == .continuousAutoExposure)
244 |
245 | try cameraManager.setExposureMode(.autoExpose)
246 | #expect(currentDevice.exposureMode == .autoExpose)
247 | #expect(cameraManager.attributes.cameraExposure.mode == .autoExpose)
248 |
249 | try cameraManager.setExposureMode(.custom)
250 | #expect(currentDevice.exposureMode == .custom)
251 | #expect(cameraManager.attributes.cameraExposure.mode == .custom)
252 | }
253 | }
254 |
255 | // MARK: Set Exposure Duration
256 | extension CameraManagerTests {
257 | @Test("Set Exposure Duration") func setExposureDuration() async throws {
258 | try await setupCamera()
259 |
260 | try cameraManager.setExposureDuration(.init(value: 1, timescale: 33))
261 | #expect(currentDevice.exposureDuration == .init(value: 1, timescale: 33))
262 | #expect(currentDevice.exposureMode == .custom)
263 | #expect(cameraManager.attributes.cameraExposure.duration == .init(value: 1, timescale: 33))
264 |
265 | try cameraManager.setExposureDuration(.init(value: 1, timescale: 100000))
266 | #expect(currentDevice.exposureDuration == currentDevice.minExposureDuration)
267 | #expect(currentDevice.exposureMode == .custom)
268 | #expect(cameraManager.attributes.cameraExposure.duration == currentDevice.minExposureDuration)
269 |
270 | try cameraManager.setExposureDuration(.init(value: 1, timescale: 2))
271 | #expect(currentDevice.exposureDuration == currentDevice.maxExposureDuration)
272 | #expect(currentDevice.exposureMode == .custom)
273 | #expect(cameraManager.attributes.cameraExposure.duration == currentDevice.maxExposureDuration)
274 | }
275 | }
276 |
277 | // MARK: Set ISO
278 | extension CameraManagerTests {
279 | @Test("Set ISO") func setISO() async throws {
280 | try await setupCamera()
281 |
282 | try cameraManager.setISO(1)
283 | #expect(currentDevice.iso == 1)
284 | #expect(currentDevice.exposureMode == .custom)
285 | #expect(cameraManager.attributes.cameraExposure.iso == 1)
286 |
287 | try cameraManager.setISO(-2137)
288 | #expect(currentDevice.iso == currentDevice.minISO)
289 | #expect(currentDevice.exposureMode == .custom)
290 | #expect(cameraManager.attributes.cameraExposure.iso == currentDevice.minISO)
291 |
292 | try cameraManager.setISO(2137)
293 | #expect(currentDevice.iso == currentDevice.maxISO)
294 | #expect(currentDevice.exposureMode == .custom)
295 | #expect(cameraManager.attributes.cameraExposure.iso == currentDevice.maxISO)
296 | }
297 | }
298 |
299 | // MARK: Set Exposure Target Bias
300 | extension CameraManagerTests {
301 | @Test("Set Exposure Target Bias") func setExposureTargetBias() async throws {
302 | try await setupCamera()
303 |
304 | try cameraManager.setExposureTargetBias(1)
305 | #expect(currentDevice.exposureTargetBias == 1)
306 | #expect(cameraManager.attributes.cameraExposure.targetBias == 1)
307 |
308 | try cameraManager.setExposureTargetBias(-2137)
309 | #expect(currentDevice.exposureTargetBias == currentDevice.minExposureTargetBias)
310 | #expect(cameraManager.attributes.cameraExposure.targetBias == currentDevice.minExposureTargetBias)
311 |
312 | try cameraManager.setExposureTargetBias(2137)
313 | #expect(currentDevice.exposureTargetBias == currentDevice.maxExposureTargetBias)
314 | #expect(cameraManager.attributes.cameraExposure.targetBias == currentDevice.maxExposureTargetBias)
315 | }
316 | }
317 |
318 | // MARK: Set HDR Mode
319 | extension CameraManagerTests {
320 | @Test("Set HDR Mode") func setHDRMode() async throws {
321 | try await setupCamera()
322 |
323 | try cameraManager.setHDRMode(.on)
324 | #expect(currentDevice.hdrMode == .on)
325 | #expect(cameraManager.attributes.hdrMode == .on)
326 |
327 | try cameraManager.setHDRMode(.off)
328 | #expect(currentDevice.hdrMode == .off)
329 | #expect(cameraManager.attributes.hdrMode == .off)
330 |
331 | try cameraManager.setHDRMode(.auto)
332 | #expect(currentDevice.hdrMode == .auto)
333 | #expect(cameraManager.attributes.hdrMode == .auto)
334 | }
335 | }
336 |
337 | // MARK: Set Resolution
338 | extension CameraManagerTests {
339 | @Test("Set Resolution") func setResolution() async throws {
340 | try await setupCamera()
341 |
342 | cameraManager.setResolution(.hd1280x720)
343 | #expect(cameraManager.captureSession.sessionPreset == .hd1280x720)
344 | #expect(cameraManager.attributes.resolution == .hd1280x720)
345 |
346 | cameraManager.setResolution(.hd1920x1080)
347 | #expect(cameraManager.captureSession.sessionPreset == .hd1920x1080)
348 | #expect(cameraManager.attributes.resolution == .hd1920x1080)
349 |
350 | cameraManager.setResolution(.cif352x288)
351 | #expect(cameraManager.captureSession.sessionPreset == .cif352x288)
352 | #expect(cameraManager.attributes.resolution == .cif352x288)
353 | }
354 | }
355 |
356 | // MARK: Set Frame Rate
357 | extension CameraManagerTests {
358 | @Test("Set Frame Rate") func setFrameRate() async throws {
359 | try await setupCamera()
360 |
361 | try cameraManager.setFrameRate(45)
362 | #expect(currentDevice.activeVideoMinFrameDuration == .init(value: 1, timescale: 45))
363 | #expect(currentDevice.activeVideoMaxFrameDuration == .init(value: 1, timescale: 45))
364 | #expect(cameraManager.attributes.frameRate == 45)
365 |
366 | try cameraManager.setFrameRate(10)
367 | #expect(currentDevice.activeVideoMinFrameDuration.timescale == Int32(currentDevice.minFrameRate!))
368 | #expect(currentDevice.activeVideoMaxFrameDuration.timescale == Int32(currentDevice.minFrameRate!))
369 | #expect(cameraManager.attributes.frameRate == Int32(currentDevice.minFrameRate!))
370 |
371 | try cameraManager.setFrameRate(100)
372 | #expect(currentDevice.activeVideoMinFrameDuration.timescale == Int32(currentDevice.maxFrameRate!))
373 | #expect(currentDevice.activeVideoMaxFrameDuration.timescale == Int32(currentDevice.maxFrameRate!))
374 | #expect(cameraManager.attributes.frameRate == Int32(currentDevice.maxFrameRate!))
375 | }
376 | }
377 |
378 |
379 | // MARK: Helpers
380 | private extension CameraManagerTests {
381 | func setupCamera() async throws {
382 | let cameraView = UIView(frame: .init(origin: .zero, size: .init(width: 1000, height: 1000)))
383 |
384 | cameraManager.initialize(in: cameraView)
385 | try await cameraManager.setup()
386 | await Task.sleep(seconds: 10)
387 | }
388 | }
389 | private extension CameraManagerTests {
390 | var currentDevice: any CaptureDevice { cameraManager.getCameraInput()!.device }
391 | }
392 |
--------------------------------------------------------------------------------