├── .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 | MijickCamera Hero 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 | Labels 28 |

29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 50 | 53 | 56 | 57 | 58 |
Camera PositionMedia CapturingGesturesFilters
45 | 46 | 48 | 49 | 51 | 52 | 54 | 55 |
59 | 60 |

61 | 62 | 63 |

64 | 65 | 66 | Visit our Website 67 | 68 | 69 | 70 | Join us on Discord 71 | 72 | 73 | 74 | Follow us on LinkedIn 75 | 76 | 77 | 78 | See our other frameworks 79 | 80 | 81 | 82 | Read us on Medium 83 | 84 | 85 | 86 | Buy us a coffee 87 | 88 |

89 | 90 | 91 | # ✨ Features 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 |
🙏🏻Automatically handles permissions
🖼️Image capture
🎬️Video capture (with or without sound)
📸Camera position changes
🔍️Supports manual zoom
👁️Supports manual focus
🎞️Changeable frame rate
📺️Changeable camera resolution
🙈Camera filters
🔦Torch
📸Flash
⏱️Other camera settings (exposure duration, target bias, ISO, HDR mode and more)
☢️Displays error screen if permissions are not granted
🖼️Displays captured media screen
📱Modern and minimalistic UI
🕺Beautiful animations
🚧Fully customizable screens
🤏🏼Gestures support
📲Blocks screen orientation change
⚡️Supports Swift 6
🚀... and others
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 | Code Example 1 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 | Code Example 4 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 | Code Example 1 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 | --------------------------------------------------------------------------------