├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CameraManager.podspec ├── CameraManager.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── CameraManager.xcscheme │ └── camera.xcscheme ├── Example App ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── ImageViewController.swift ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-57x57@1x.png │ │ ├── Icon-App-57x57@2x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-72x72@1x.png │ │ ├── Icon-App-72x72@2x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ ├── Icon-Small-50x50@1x.png │ │ ├── Icon-Small-50x50@2x.png │ │ ├── copy.png │ │ ├── imaginary copy 2-1.png │ │ ├── imaginary copy 2-2.png │ │ ├── imaginary copy 2-3.png │ │ ├── imaginary copy 2-4.png │ │ ├── imaginary copy 2.png │ │ ├── imaginary copy-1.png │ │ ├── imaginary copy.png │ │ └── logo.jpeg │ ├── Contents.json │ ├── flash_auto.imageset │ │ ├── Contents.json │ │ └── flash-auto.png │ ├── flash_off.imageset │ │ ├── Contents.json │ │ └── flash-off.png │ ├── flash_on.imageset │ │ ├── Contents.json │ │ └── flash-on.png │ ├── logo.imageset │ │ ├── Contents.json │ │ ├── imaginary-1.png │ │ ├── imaginary-2.png │ │ └── imaginary.png │ ├── output_image.imageset │ │ ├── Contents.json │ │ └── output-image.png │ ├── output_video.imageset │ │ ├── Contents.json │ │ └── icons8-video-call.png │ └── switch_camera.imageset │ │ ├── Contents.json │ │ └── switch-camera.png ├── Info.plist └── ViewController.swift ├── LICENSE ├── Package.swift ├── README.md └── Sources ├── CameraManager.h ├── CameraManager.swift └── Info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 35 | # 36 | # Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | .DS_Store 45 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [5.1.3](https://github.com/imaginary-cloud/CameraManager/tree/5.1.3) - 2020-04-20 7 | 8 | ### Fixed 9 | 10 | - Remove unnecessary calls to `_orientationChanged` 11 | - Change videoStabilisationMode in between recordings 12 | - [Change the main class to open to permit inheritance](https://github.com/imaginary-cloud/CameraManager/pull/225) 13 | 14 | ### Added 15 | 16 | - [Add property to get the stabilisation mode currently active](https://github.com/imaginary-cloud/CameraManager/issues/177) 17 | 18 | ## [5.1.2](https://github.com/imaginary-cloud/CameraManager/tree/5.1.2) - 2020-04-16 19 | 20 | ### Fixed 21 | 22 | - Fix location metadata and front image flipping when capturing a video 23 | 24 | ## [5.1.1](https://github.com/imaginary-cloud/CameraManager/tree/5.1.1) - 2020-04-15 25 | 26 | ### Fixed 27 | 28 | - Minor fixes when changing between outputs 29 | - Fix bug when changing exposure duration not calling `unlockForConfiguration` 30 | 31 | ## [5.1.0](https://github.com/imaginary-cloud/CameraManager/tree/5.1.0) - 2020-04-06 32 | 33 | ### Fixed 34 | 35 | - [`capturePictureWithCompletion` called twice when `writeFilesToPhoneLibrary = true`](https://github.com/imaginary-cloud/CameraManager/issues/187) 36 | - [Camera freeze caused by orientation changes](https://github.com/imaginary-cloud/CameraManager/pull/204) 37 | - [Location data not stored in EXIF data](https://github.com/imaginary-cloud/CameraManager/issues/213) 38 | - [Support flash mode selection on front camera](https://github.com/imaginary-cloud/CameraManager/pull/214) 39 | 40 | ### Added 41 | 42 | - [Set camera output quality using AVCaptureSession.Preset](https://github.com/imaginary-cloud/CameraManager/pull/195) 43 | - [Add support for scanning QR codes](https://github.com/imaginary-cloud/CameraManager/pull/199) 44 | 45 | ## [5.0.0](https://github.com/imaginary-cloud/CameraManager/tree/5.0.0) - 2020-04-01 46 | 47 | ### Changed 48 | 49 | - [Syntax update for Swift 5.2](https://github.com/imaginary-cloud/CameraManager/issues/189) 50 | 51 | ### Fixed 52 | 53 | - [Fix Swift Package Manager support](https://github.com/imaginary-cloud/CameraManager/issues/215) 54 | 55 | ## [4.4.0](https://github.com/imaginary-cloud/CameraManager/tree/4.4.0) - 2019-03-12 56 | 57 | ### Changed 58 | 59 | - Change callbacks to the swifty way of reporting success and errors (pull request #185) 60 | 61 | ## [4.3.1](https://github.com/imaginary-cloud/CameraManager/tree/4.3.1) - 2019-03-08 62 | 63 | ### Fixed 64 | 65 | - Swift Support (pull request #173 and #168) 66 | - Example App layout supports iPhone X (pull request #173) 67 | 68 | ## [4.3.0](https://github.com/imaginary-cloud/CameraManager/tree/4.3.0) - 2018-08-17 69 | 70 | ### Added 71 | 72 | - Add video stabilization mode (pull request #160) 73 | 74 | ## [4.2.3](https://github.com/imaginary-cloud/CameraManager/tree/4.2.3) - 2018-07-27 75 | 76 | ### Fixed 77 | 78 | - Deinit cause crash when `cameraDevice = .front` (#157) 79 | 80 | ### Changed 81 | 82 | - Add note to documentation when a property has a default value 83 | 84 | ## [4.2.2](https://github.com/imaginary-cloud/CameraManager/tree/4.2.2) - 2018-07-26 85 | 86 | ### Added 87 | 88 | - Add `CONTRIBUTING.md` 89 | 90 | ### Fixed 91 | 92 | - When recording is finished `videoURL` is always nil (#158) 93 | 94 | ## [4.2.1](https://github.com/imaginary-cloud/CameraManager/tree/4.2.1) - 2018-06-20 95 | 96 | ### Added 97 | 98 | - Add `CODE_OF_CONDUCT.md` 99 | 100 | ### Fixed 101 | 102 | - Fix flash for video (#152) 103 | 104 | ## [4.2.0](https://github.com/imaginary-cloud/CameraManager/tree/4.2.0) - 2018-06-19 105 | 106 | ### Added 107 | 108 | - Add properties for setting custom album names for image and video recordings (pull request #147) 109 | - Add Exposure slider (pull request #148) 110 | 111 | ### Fixed 112 | 113 | - Orientation bug (pull request #150) 114 | - Video orientation not set after camera switch (#64) 115 | - Memory leak in Example App 116 | 117 | ## [4.1.1](https://github.com/imaginary-cloud/CameraManager/tree/4.1.1) - 2018-05-16 118 | 119 | ### Fixed 120 | 121 | - License 122 | 123 | ## [4.1.0](https://github.com/imaginary-cloud/CameraManager/tree/4.1.0) - 2018-05-15 124 | 125 | ### Added 126 | 127 | - Add EXIF metadata for images and video - including location (#131) 128 | - New layout for Example App 129 | 130 | ### Fixed 131 | 132 | - Fix Crash (pull request #141) 133 | - Fix image orientation when taking pictures with device rotation lock active 134 | 135 | ## [4.0.2](https://github.com/imaginary-cloud/CameraManager/tree/4.0.2) - 2018-05-07 136 | 137 | ### Fixed 138 | 139 | - Using CoreMotion to get real device orientation, fixing no sound when switching camera (pull request #134) 140 | - Fixed take picture on iPad with front camera(landscape, UpsideDown… (pull request #139) 141 | 142 | - Bugs (issue #115) 143 | - Not able to to get sound while recording from front camera (#116) 144 | - Urgent: toggle camera back to front no sound coming in recorded video (issue #118) 145 | - Orientation issues (landscape) (issue #135) 146 | - Resuming capture session improvement (issue #136) 147 | - Issue with focus area (issue #137) 148 | - Repeat crash CameraManager.swift line 430 in release (issue #138) 149 | 150 | ## [4.0.1](https://github.com/imaginary-cloud/CameraManager/tree/4.0.1) - 2017-11-18 151 | 152 | ### Added 153 | 154 | - Add @discardableResult modifiers to addPreviewLayerToView (pull request #132) 155 | 156 | ### Fixed 157 | 158 | - Fix shouldEnableTapToFocus function (pull request #133) 159 | 160 | ## [4.0.0](https://github.com/imaginary-cloud/CameraManager/tree/4.0.0) - 2017-10-22 161 | 162 | ### Changed 163 | 164 | - Syntax update for Swift 4.0 (pull request #125) 165 | 166 | ### Fixed 167 | 168 | - Add gesture recognizers on the main thread (pull request #123) 169 | 170 | ## [3.2.0](https://github.com/imaginary-cloud/CameraManager/tree/3.2.0) - 2017-07-03 171 | 172 | ### Added 173 | 174 | - Add location data to videos (pull request #110) 175 | - Optional location permissions (pull request #110) 176 | 177 | ## [3.1.4](https://github.com/imaginary-cloud/CameraManager/tree/3.1.4) - 2017-06-14 178 | 179 | ### Added 180 | 181 | - Add properties `focusMode` and `exposureMode` (pull request #106) 182 | - Add property `animateShutter` to disable shutter animation 183 | 184 | ### Fixed 185 | 186 | - FlashMode on front camera (issue #82) 187 | - Zoom of front camera not working (issue #84) 188 | - Getting same video URL, when simultaneously recording video (issue #108) 189 | 190 | ## [3.1.3](https://github.com/imaginary-cloud/CameraManager/tree/3.1.3) - 2017-05-15 191 | 192 | ### Added 193 | 194 | - Add two new properties `shouldEnableTapToFocus` and `shouldEnablePinchToZoom` (pull request #106) 195 | 196 | ## [3.1.2](https://github.com/imaginary-cloud/CameraManager/tree/3.1.2) - 2017-05-02 197 | 198 | ### Changed 199 | 200 | - New option to flip image took by front camera (pull request #104) 201 | - Fixes possible hang after requesting permission (pull request #98) 202 | 203 | ## [3.1.1](https://github.com/imaginary-cloud/CameraManager/tree/3.1.1) - 2017-03-15 204 | 205 | ### Changed 206 | 207 | - Refactor to avoid implicit unwrapped optionals (pull request #94) 208 | 209 | ## [3.1.0](https://github.com/imaginary-cloud/CameraManager/tree/3.1.0) - 2017-02-11 210 | 211 | ### Added 212 | 213 | - Flip animation and tap to focus (pull request #72) 214 | - Icons and splash image to example 215 | 216 | ## [3.0.0](https://github.com/imaginary-cloud/CameraManager/tree/3.0.0) - 2016-09-16 217 | 218 | ### Changed 219 | 220 | - Syntax update for Swift 3.0. 221 | 222 | ## [2.2.4](https://github.com/imaginary-cloud/CameraManager/tree/2.2.4) - 2016-07-06 223 | 224 | ### Added 225 | 226 | - Add error checking. 227 | 228 | ### Changed 229 | 230 | - Fixes completion typos and suggests renamed functions. 231 | 232 | ## [2.2.3](https://github.com/imaginary-cloud/CameraManager/tree/2.2.3) - 2016-05-12 233 | 234 | ### Changed 235 | 236 | - Fixed zoom in StillImage Mode. 237 | 238 | - Minor refactoring 239 | 240 | ## [2.2.2](https://github.com/imaginary-cloud/CameraManager/tree/2.2.2) - 2016-03-07 241 | 242 | ### Added 243 | 244 | - `CHANGELOG.md` file. 245 | 246 | ## [2.2.1](https://github.com/imaginary-cloud/CameraManager/tree/2.2.1) - 2016-03-02 247 | 248 | ### Added 249 | 250 | - Initial support for the Swift Package Manager. 251 | 252 | ## [2.2.0](https://github.com/imaginary-cloud/CameraManager/tree/2.2.0) - 2016-02-19 253 | 254 | ### Added 255 | 256 | - Zoom support. 257 | 258 | ### Changed 259 | 260 | - Fixed spelling of `embeddingView`. 261 | 262 | ## [2.1.3](https://github.com/imaginary-cloud/CameraManager/tree/2.1.3) - 2016-01-08 263 | 264 | ### Changed 265 | 266 | - No sound in video with more than 10 seconds fixed. 267 | 268 | - Fixed `NewCameraOutputMode` not passed during init. 269 | 270 | ## [2.1.2](https://github.com/imaginary-cloud/CameraManager/tree/2.1.2) - 2015-12-24 271 | 272 | ### Added 273 | 274 | - Property `cameraIsReady`. 275 | 276 | - Completion block `addPreviewLayerToView`. 277 | 278 | ## [2.1.1](https://github.com/imaginary-cloud/CameraManager/tree/2.1.1) - 2015-12-11 279 | 280 | ### Added 281 | 282 | - Ability to disable responding to device orientation changes. 283 | 284 | ## [2.1.0](https://github.com/imaginary-cloud/CameraManager/tree/2.1) - 2015-11-20 285 | 286 | ### Added 287 | 288 | - Properties `recordedDuration` and `recordedFileSize`. 289 | 290 | ## [2.0.2](https://github.com/imaginary-cloud/CameraManager/tree/2.0.2) - 2015-11-17 291 | 292 | ### Fixed 293 | 294 | - iOS 9.0.1 bug. 295 | 296 | ## [2.0.1](https://github.com/imaginary-cloud/CameraManager/tree/2.0.1) - 2015-09-17 297 | 298 | ### Changed 299 | 300 | - Syntax updates. 301 | 302 | ## [2.0.0](https://github.com/imaginary-cloud/CameraManager/tree/2.0.0) - 2015-07-30 303 | 304 | ### Changed 305 | 306 | - Syntax update for Swift 2.0. 307 | 308 | ## [1.0.14](https://github.com/imaginary-cloud/CameraManager/tree/1.0.14) - 2015-07-17 309 | 310 | ### Changed 311 | 312 | - Small fixes. 313 | 314 | ## [1.0.13](https://github.com/imaginary-cloud/CameraManager/tree/1.0.13) - 2015-05-12 315 | 316 | ### Added 317 | 318 | - Support for installing via Carthage. 319 | 320 | - Property `hasFlash`. 321 | 322 | ### Changed 323 | 324 | - Syntax update for Swift 1.2. 325 | 326 | ## [1.0.12](https://github.com/imaginary-cloud/CameraManager/tree/1.0.12) - 2015-03-23 327 | 328 | ### Added 329 | 330 | - Incremental flash mode. 331 | 332 | - Content localization. 333 | 334 | ### Changed 335 | 336 | - Torch is set to correct state according to the current flash mode. 337 | 338 | - `README.md` update. 339 | 340 | ## [1.0.11](https://github.com/imaginary-cloud/CameraManager/tree/1.0.11) - 2015-03-20 341 | 342 | ### Added 343 | 344 | - Property `showAccessPermissionPopupAutomatically`, to determine if you want the user to be asked about camera permissions automatically or manually. 345 | 346 | - Error handling in capture completion blocks. 347 | 348 | ## [1.0.10](https://github.com/imaginary-cloud/CameraManager/tree/1.0.10) - 2015-03-19 349 | 350 | ### Added 351 | 352 | - Camera state returned when adding the preview layer. 353 | 354 | ### Changed 355 | 356 | - `README.md` update. 357 | 358 | ## [1.0.9](https://github.com/imaginary-cloud/CameraManager/tree/1.0.9) - 2015-03-10 359 | 360 | ### Changed 361 | 362 | - CameraManager class made public. 363 | 364 | ## [1.0.8](https://github.com/imaginary-cloud/CameraManager/tree/1.0.8) - 2015-02-24 365 | 366 | ### Fixed 367 | 368 | - Wrong orientation when camera preview starts in landscape mode. 369 | 370 | - Crash when trying to capture a still image. 371 | 372 | - Orientation detection failure after stop and resume of a capture session. 373 | 374 | - Bug which prevented from recording audio. 375 | 376 | ## [1.0.7](https://github.com/imaginary-cloud/CameraManager/tree/1.0.7) - 2014-10-30 377 | 378 | ### Added 379 | 380 | - Version compatible with XCode 6.1. 381 | 382 | ### Changed 383 | 384 | - `README.md` update. 385 | 386 | - Swift syntax updates to resolve compile errors. 387 | 388 | ## [1.0.6](https://github.com/imaginary-cloud/CameraManager/tree/1.0.6) - 2014-10-28 389 | 390 | ### Added 391 | 392 | - Check for valid capture session. 393 | 394 | ### Changed 395 | 396 | - Fixed video orientation change. 397 | 398 | ## [1.0.5](https://github.com/imaginary-cloud/CameraManager/tree/1.0.5) - 2014-10-22 399 | 400 | ### Changed 401 | 402 | - Enhanced Camera lifecyle. 403 | 404 | - Orientation observers only added if needed. 405 | 406 | ## [1.0.4](https://github.com/imaginary-cloud/CameraManager/tree/1.0.4) - 2014-10-16 407 | 408 | ### Added 409 | 410 | - Restart session. 411 | 412 | ### Changed 413 | 414 | - `README.md` update. 415 | 416 | ## [1.0.3](https://github.com/imaginary-cloud/CameraManager/tree/1.0.3) - 2014-10-15 417 | 418 | ### Added 419 | 420 | - Property `writeFilesToPhoneLibrary` to conditionally write to user library. 421 | 422 | ### Changed 423 | 424 | - Resources only recreated when needed. 425 | 426 | - `README.md` update. 427 | 428 | ## [1.0.2](https://github.com/imaginary-cloud/CameraManager/tree/1.0.2) - 2014-10-15 429 | 430 | ### Added 431 | 432 | - `CameraManager.podspec` file. 433 | 434 | ## [1.0.1](https://github.com/imaginary-cloud/CameraManager/tree/1.0.1) - 2014-10-15 435 | 436 | ### Changed 437 | 438 | - Optional initializer for `addPreviewLayerToView`. 439 | 440 | ## [1.0.0](https://github.com/imaginary-cloud/CameraManager/tree/1.0.0) - 2014-10-15 441 | 442 | ### Added 443 | 444 | - Front and back camera selection. 445 | 446 | - Support for multiple flash modes. 447 | 448 | - Video recording, with or without mic. 449 | 450 | - Support for multiple camera output quality. 451 | 452 | - Preview layer follows interface orientation changes. 453 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # [Imaginary Cloud's](https://www.imaginarycloud.com) Code of Conduct 2 | 3 | To make sure that we provide an open and welcoming environment, we enforce in our projects a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 4 | 5 | ## Behavior 6 | 7 | How to create a positive environment: 8 | 9 | * Using welcoming and inclusive language; 10 | * Being respectful of differing viewpoints and experiences; 11 | * Gracefully accepting constructive criticism; 12 | * Focusing on what is best for the community; 13 | * Showing empathy towards other community members. 14 | 15 | Unacceptable behavior by participants include: 16 | 17 | * The use of sexualized language or imagery and unwelcome sexual attention or advances; 18 | * Trolling, insulting/derogatory comments, and personal or political attacks; 19 | * Public or private harassment; 20 | * Publishing others' private information, such as a physical or electronic address, without explicit permission; 21 | * Other conduct which could reasonably be considered inappropriate in a professional setting. 22 | 23 | ## Responsibilities 24 | 25 | We are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 26 | 27 | We have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that we deem inappropriate, threatening, offensive, or harmful. 28 | 29 | ## Scope 30 | 31 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 32 | 33 | ## Enforcement 34 | 35 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@imaginarycloud. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 36 | 37 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 38 | 39 | ## Attribution 40 | 41 | This Code of Conduct is inspired by the [Contributor Covenant](http://contributor-covenant.org/version/1/4), version 1.4. 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # [Imaginary Cloud's](https://www.imaginarycloud.com) Contribution Guidelines 2 | 3 | We accept pull requests from everyone and you're very welcome to do it. Just be sure that you're following the Imaginary Clouds's Code of Conduct and everything will run smoothly thereafter. 4 | 5 | ## General Guidelines 6 | 7 | * Fork the repository; 8 | * Provide a thorough description of the feature behavior; 9 | * If you're submiting a bug fix, provide a thorough description of its behavior or expected behavior; 10 | * Push to fork and submit a pull request. 11 | -------------------------------------------------------------------------------- /CameraManager.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "CameraManager" 3 | s.version = "5.1.3" 4 | s.summary = "This is a simple Swift class to provide all the configurations you need to create custom camera view in your app. Just drag, drop and use." 5 | s.requires_arc = true 6 | s.homepage = "https://github.com/imaginary-cloud/CameraManager" 7 | s.license = 'MIT' 8 | s.author = { "torrao" => "rtorrao@imaginarycloud.com" } 9 | s.source = { :git => "https://github.com/imaginary-cloud/CameraManager.git", :tag => "5.1.3" } 10 | s.social_media_url = 'http://www.imaginarycloud.com/' 11 | s.platform = :ios, '9.0' 12 | s.pod_target_xcconfig = { "SWIFT_VERSION" => "5.2" } 13 | s.swift_version = '5.2' 14 | s.source_files = 'Sources/CameraManager.swift' 15 | end 16 | -------------------------------------------------------------------------------- /CameraManager.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 02A8524E2433A7BC001F560A /* CameraManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454C1F6619E8316A00C81915 /* CameraManager.swift */; }; 11 | 454C1F4719E82E2500C81915 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454C1F4619E82E2500C81915 /* AppDelegate.swift */; }; 12 | 454C1F4919E82E2500C81915 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454C1F4819E82E2500C81915 /* ViewController.swift */; }; 13 | 454C1F4C19E82E2500C81915 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 454C1F4A19E82E2500C81915 /* Main.storyboard */; }; 14 | 454C1F4E19E82E2500C81915 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 454C1F4D19E82E2500C81915 /* Images.xcassets */; }; 15 | 454C1F5119E82E2500C81915 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 454C1F4F19E82E2500C81915 /* LaunchScreen.xib */; }; 16 | 45A23C181A656BDC00FB48F3 /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A23C171A656BDC00FB48F3 /* ImageViewController.swift */; }; 17 | D71DE8861AD677A7001E62F1 /* CameraManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D71DE8851AD677A7001E62F1 /* CameraManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 18 | D71DE8981AD677A8001E62F1 /* CameraManager.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D71DE8811AD677A7001E62F1 /* CameraManager.framework */; }; 19 | D71DE8991AD677A8001E62F1 /* CameraManager.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D71DE8811AD677A7001E62F1 /* CameraManager.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | D71DE8961AD677A8001E62F1 /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = 454C1F3919E82E2500C81915 /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = D71DE8801AD677A7001E62F1; 28 | remoteInfo = CameraManager; 29 | }; 30 | /* End PBXContainerItemProxy section */ 31 | 32 | /* Begin PBXCopyFilesBuildPhase section */ 33 | D71DE89D1AD677A8001E62F1 /* Embed Frameworks */ = { 34 | isa = PBXCopyFilesBuildPhase; 35 | buildActionMask = 2147483647; 36 | dstPath = ""; 37 | dstSubfolderSpec = 10; 38 | files = ( 39 | D71DE8991AD677A8001E62F1 /* CameraManager.framework in Embed Frameworks */, 40 | ); 41 | name = "Embed Frameworks"; 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXCopyFilesBuildPhase section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | 454C1F4119E82E2500C81915 /* camera.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = camera.app; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 454C1F4519E82E2500C81915 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | 454C1F4619E82E2500C81915 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 50 | 454C1F4819E82E2500C81915 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 51 | 454C1F4B19E82E2500C81915 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 52 | 454C1F4D19E82E2500C81915 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 53 | 454C1F5019E82E2500C81915 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 54 | 454C1F6619E8316A00C81915 /* CameraManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraManager.swift; sourceTree = ""; }; 55 | 45A23C171A656BDC00FB48F3 /* ImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = ""; }; 56 | D71DE8811AD677A7001E62F1 /* CameraManager.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CameraManager.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | D71DE8841AD677A7001E62F1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 58 | D71DE8851AD677A7001E62F1 /* CameraManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CameraManager.h; sourceTree = ""; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | 454C1F3E19E82E2500C81915 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | D71DE8981AD677A8001E62F1 /* CameraManager.framework in Frameworks */, 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | D71DE87D1AD677A7001E62F1 /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXFrameworksBuildPhase section */ 78 | 79 | /* Begin PBXGroup section */ 80 | 454C1F3819E82E2500C81915 = { 81 | isa = PBXGroup; 82 | children = ( 83 | 454C1F4319E82E2500C81915 /* Example App */, 84 | D71DE8821AD677A7001E62F1 /* Sources */, 85 | 454C1F4219E82E2500C81915 /* Products */, 86 | ); 87 | sourceTree = ""; 88 | }; 89 | 454C1F4219E82E2500C81915 /* Products */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 454C1F4119E82E2500C81915 /* camera.app */, 93 | D71DE8811AD677A7001E62F1 /* CameraManager.framework */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | 454C1F4319E82E2500C81915 /* Example App */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 454C1F4619E82E2500C81915 /* AppDelegate.swift */, 102 | 454C1F4819E82E2500C81915 /* ViewController.swift */, 103 | 45A23C171A656BDC00FB48F3 /* ImageViewController.swift */, 104 | 454C1F4D19E82E2500C81915 /* Images.xcassets */, 105 | 454C1F4F19E82E2500C81915 /* LaunchScreen.xib */, 106 | 454C1F4A19E82E2500C81915 /* Main.storyboard */, 107 | 454C1F4419E82E2500C81915 /* Supporting Files */, 108 | ); 109 | path = "Example App"; 110 | sourceTree = ""; 111 | }; 112 | 454C1F4419E82E2500C81915 /* Supporting Files */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 454C1F4519E82E2500C81915 /* Info.plist */, 116 | ); 117 | name = "Supporting Files"; 118 | sourceTree = ""; 119 | }; 120 | D71DE8821AD677A7001E62F1 /* Sources */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 454C1F6619E8316A00C81915 /* CameraManager.swift */, 124 | D71DE8851AD677A7001E62F1 /* CameraManager.h */, 125 | D71DE8831AD677A7001E62F1 /* Supporting Files */, 126 | ); 127 | path = Sources; 128 | sourceTree = ""; 129 | }; 130 | D71DE8831AD677A7001E62F1 /* Supporting Files */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | D71DE8841AD677A7001E62F1 /* Info.plist */, 134 | ); 135 | name = "Supporting Files"; 136 | sourceTree = ""; 137 | }; 138 | /* End PBXGroup section */ 139 | 140 | /* Begin PBXHeadersBuildPhase section */ 141 | D71DE87E1AD677A7001E62F1 /* Headers */ = { 142 | isa = PBXHeadersBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | D71DE8861AD677A7001E62F1 /* CameraManager.h in Headers */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | /* End PBXHeadersBuildPhase section */ 150 | 151 | /* Begin PBXNativeTarget section */ 152 | 454C1F4019E82E2500C81915 /* camera */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = 454C1F6019E82E2500C81915 /* Build configuration list for PBXNativeTarget "camera" */; 155 | buildPhases = ( 156 | 454C1F3D19E82E2500C81915 /* Sources */, 157 | 454C1F3E19E82E2500C81915 /* Frameworks */, 158 | 454C1F3F19E82E2500C81915 /* Resources */, 159 | D71DE89D1AD677A8001E62F1 /* Embed Frameworks */, 160 | ); 161 | buildRules = ( 162 | ); 163 | dependencies = ( 164 | D71DE8971AD677A8001E62F1 /* PBXTargetDependency */, 165 | ); 166 | name = camera; 167 | productName = camera; 168 | productReference = 454C1F4119E82E2500C81915 /* camera.app */; 169 | productType = "com.apple.product-type.application"; 170 | }; 171 | D71DE8801AD677A7001E62F1 /* CameraManager */ = { 172 | isa = PBXNativeTarget; 173 | buildConfigurationList = D71DE89A1AD677A8001E62F1 /* Build configuration list for PBXNativeTarget "CameraManager" */; 174 | buildPhases = ( 175 | D71DE87C1AD677A7001E62F1 /* Sources */, 176 | D71DE87D1AD677A7001E62F1 /* Frameworks */, 177 | D71DE87E1AD677A7001E62F1 /* Headers */, 178 | D71DE87F1AD677A7001E62F1 /* Resources */, 179 | ); 180 | buildRules = ( 181 | ); 182 | dependencies = ( 183 | ); 184 | name = CameraManager; 185 | productName = CameraManager; 186 | productReference = D71DE8811AD677A7001E62F1 /* CameraManager.framework */; 187 | productType = "com.apple.product-type.framework"; 188 | }; 189 | /* End PBXNativeTarget section */ 190 | 191 | /* Begin PBXProject section */ 192 | 454C1F3919E82E2500C81915 /* Project object */ = { 193 | isa = PBXProject; 194 | attributes = { 195 | LastSwiftUpdateCheck = 0700; 196 | LastUpgradeCheck = 1130; 197 | ORGANIZATIONNAME = imaginaryCloud; 198 | TargetAttributes = { 199 | 454C1F4019E82E2500C81915 = { 200 | CreatedOnToolsVersion = 6.0.1; 201 | DevelopmentTeam = QM7HJTY23M; 202 | LastSwiftMigration = 1130; 203 | ProvisioningStyle = Automatic; 204 | }; 205 | D71DE8801AD677A7001E62F1 = { 206 | CreatedOnToolsVersion = 6.3; 207 | LastSwiftMigration = 1130; 208 | }; 209 | }; 210 | }; 211 | buildConfigurationList = 454C1F3C19E82E2500C81915 /* Build configuration list for PBXProject "CameraManager" */; 212 | compatibilityVersion = "Xcode 3.2"; 213 | developmentRegion = en; 214 | hasScannedForEncodings = 0; 215 | knownRegions = ( 216 | en, 217 | Base, 218 | ); 219 | mainGroup = 454C1F3819E82E2500C81915; 220 | productRefGroup = 454C1F4219E82E2500C81915 /* Products */; 221 | projectDirPath = ""; 222 | projectRoot = ""; 223 | targets = ( 224 | 454C1F4019E82E2500C81915 /* camera */, 225 | D71DE8801AD677A7001E62F1 /* CameraManager */, 226 | ); 227 | }; 228 | /* End PBXProject section */ 229 | 230 | /* Begin PBXResourcesBuildPhase section */ 231 | 454C1F3F19E82E2500C81915 /* Resources */ = { 232 | isa = PBXResourcesBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | 454C1F4C19E82E2500C81915 /* Main.storyboard in Resources */, 236 | 454C1F5119E82E2500C81915 /* LaunchScreen.xib in Resources */, 237 | 454C1F4E19E82E2500C81915 /* Images.xcassets in Resources */, 238 | ); 239 | runOnlyForDeploymentPostprocessing = 0; 240 | }; 241 | D71DE87F1AD677A7001E62F1 /* Resources */ = { 242 | isa = PBXResourcesBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | /* End PBXResourcesBuildPhase section */ 249 | 250 | /* Begin PBXSourcesBuildPhase section */ 251 | 454C1F3D19E82E2500C81915 /* Sources */ = { 252 | isa = PBXSourcesBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | 454C1F4919E82E2500C81915 /* ViewController.swift in Sources */, 256 | 45A23C181A656BDC00FB48F3 /* ImageViewController.swift in Sources */, 257 | 454C1F4719E82E2500C81915 /* AppDelegate.swift in Sources */, 258 | ); 259 | runOnlyForDeploymentPostprocessing = 0; 260 | }; 261 | D71DE87C1AD677A7001E62F1 /* Sources */ = { 262 | isa = PBXSourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | 02A8524E2433A7BC001F560A /* CameraManager.swift in Sources */, 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | }; 269 | /* End PBXSourcesBuildPhase section */ 270 | 271 | /* Begin PBXTargetDependency section */ 272 | D71DE8971AD677A8001E62F1 /* PBXTargetDependency */ = { 273 | isa = PBXTargetDependency; 274 | target = D71DE8801AD677A7001E62F1 /* CameraManager */; 275 | targetProxy = D71DE8961AD677A8001E62F1 /* PBXContainerItemProxy */; 276 | }; 277 | /* End PBXTargetDependency section */ 278 | 279 | /* Begin PBXVariantGroup section */ 280 | 454C1F4A19E82E2500C81915 /* Main.storyboard */ = { 281 | isa = PBXVariantGroup; 282 | children = ( 283 | 454C1F4B19E82E2500C81915 /* Base */, 284 | ); 285 | name = Main.storyboard; 286 | sourceTree = ""; 287 | }; 288 | 454C1F4F19E82E2500C81915 /* LaunchScreen.xib */ = { 289 | isa = PBXVariantGroup; 290 | children = ( 291 | 454C1F5019E82E2500C81915 /* Base */, 292 | ); 293 | name = LaunchScreen.xib; 294 | sourceTree = ""; 295 | }; 296 | /* End PBXVariantGroup section */ 297 | 298 | /* Begin XCBuildConfiguration section */ 299 | 454C1F5E19E82E2500C81915 /* Debug */ = { 300 | isa = XCBuildConfiguration; 301 | buildSettings = { 302 | ALWAYS_SEARCH_USER_PATHS = NO; 303 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 304 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 305 | CLANG_CXX_LIBRARY = "libc++"; 306 | CLANG_ENABLE_MODULES = YES; 307 | CLANG_ENABLE_OBJC_ARC = YES; 308 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 309 | CLANG_WARN_BOOL_CONVERSION = YES; 310 | CLANG_WARN_COMMA = YES; 311 | CLANG_WARN_CONSTANT_CONVERSION = YES; 312 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 313 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 314 | CLANG_WARN_EMPTY_BODY = YES; 315 | CLANG_WARN_ENUM_CONVERSION = YES; 316 | CLANG_WARN_INFINITE_RECURSION = YES; 317 | CLANG_WARN_INT_CONVERSION = YES; 318 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 319 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 320 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 321 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 322 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 323 | CLANG_WARN_STRICT_PROTOTYPES = YES; 324 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 325 | CLANG_WARN_UNREACHABLE_CODE = YES; 326 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 327 | "CODE_SIGN_ENTITLEMENTS[sdk=iphoneos*]" = ""; 328 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 329 | COPY_PHASE_STRIP = NO; 330 | ENABLE_STRICT_OBJC_MSGSEND = YES; 331 | ENABLE_TESTABILITY = YES; 332 | GCC_C_LANGUAGE_STANDARD = gnu99; 333 | GCC_DYNAMIC_NO_PIC = NO; 334 | GCC_NO_COMMON_BLOCKS = YES; 335 | GCC_OPTIMIZATION_LEVEL = 0; 336 | GCC_PREPROCESSOR_DEFINITIONS = ( 337 | "DEBUG=1", 338 | "$(inherited)", 339 | ); 340 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 341 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 342 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 343 | GCC_WARN_UNDECLARED_SELECTOR = YES; 344 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 345 | GCC_WARN_UNUSED_FUNCTION = YES; 346 | GCC_WARN_UNUSED_VARIABLE = YES; 347 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 348 | MTL_ENABLE_DEBUG_INFO = YES; 349 | ONLY_ACTIVE_ARCH = YES; 350 | SDKROOT = iphoneos; 351 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 352 | SWIFT_VERSION = 5.0; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | }; 355 | name = Debug; 356 | }; 357 | 454C1F5F19E82E2500C81915 /* Release */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | ALWAYS_SEARCH_USER_PATHS = NO; 361 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 362 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 363 | CLANG_CXX_LIBRARY = "libc++"; 364 | CLANG_ENABLE_MODULES = YES; 365 | CLANG_ENABLE_OBJC_ARC = YES; 366 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 367 | CLANG_WARN_BOOL_CONVERSION = YES; 368 | CLANG_WARN_COMMA = YES; 369 | CLANG_WARN_CONSTANT_CONVERSION = YES; 370 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 371 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 372 | CLANG_WARN_EMPTY_BODY = YES; 373 | CLANG_WARN_ENUM_CONVERSION = YES; 374 | CLANG_WARN_INFINITE_RECURSION = YES; 375 | CLANG_WARN_INT_CONVERSION = YES; 376 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 377 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 378 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 379 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 380 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 381 | CLANG_WARN_STRICT_PROTOTYPES = YES; 382 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 383 | CLANG_WARN_UNREACHABLE_CODE = YES; 384 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 385 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 386 | COPY_PHASE_STRIP = YES; 387 | ENABLE_NS_ASSERTIONS = NO; 388 | ENABLE_STRICT_OBJC_MSGSEND = YES; 389 | GCC_C_LANGUAGE_STANDARD = gnu99; 390 | GCC_NO_COMMON_BLOCKS = YES; 391 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 392 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 393 | GCC_WARN_UNDECLARED_SELECTOR = YES; 394 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 395 | GCC_WARN_UNUSED_FUNCTION = YES; 396 | GCC_WARN_UNUSED_VARIABLE = YES; 397 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 398 | MTL_ENABLE_DEBUG_INFO = NO; 399 | SDKROOT = iphoneos; 400 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 401 | SWIFT_VERSION = 5.0; 402 | TARGETED_DEVICE_FAMILY = "1,2"; 403 | VALIDATE_PRODUCT = YES; 404 | }; 405 | name = Release; 406 | }; 407 | 454C1F6119E82E2500C81915 /* Debug */ = { 408 | isa = XCBuildConfiguration; 409 | buildSettings = { 410 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 411 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 412 | BUILD_LIBRARY_FOR_DISTRIBUTION = NO; 413 | CODE_SIGN_IDENTITY = "iPhone Developer"; 414 | CODE_SIGN_STYLE = Automatic; 415 | DEVELOPMENT_TEAM = QM7HJTY23M; 416 | INFOPLIST_FILE = "Example App/Info.plist"; 417 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 418 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 419 | MARKETING_VERSION = 5.1.3; 420 | PRODUCT_BUNDLE_IDENTIFIER = cameraDemo; 421 | PRODUCT_NAME = "$(TARGET_NAME)"; 422 | PROVISIONING_PROFILE_SPECIFIER = ""; 423 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 424 | SWIFT_VERSION = 5.0; 425 | }; 426 | name = Debug; 427 | }; 428 | 454C1F6219E82E2500C81915 /* Release */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 432 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 433 | BUILD_LIBRARY_FOR_DISTRIBUTION = NO; 434 | CODE_SIGN_IDENTITY = "iPhone Developer"; 435 | CODE_SIGN_STYLE = Automatic; 436 | DEVELOPMENT_TEAM = QM7HJTY23M; 437 | INFOPLIST_FILE = camera/Info.plist; 438 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 439 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 440 | MARKETING_VERSION = 5.1.3; 441 | PRODUCT_BUNDLE_IDENTIFIER = cameraDemo; 442 | PRODUCT_NAME = "$(TARGET_NAME)"; 443 | PROVISIONING_PROFILE_SPECIFIER = ""; 444 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 445 | SWIFT_VERSION = 5.0; 446 | }; 447 | name = Release; 448 | }; 449 | D71DE89B1AD677A8001E62F1 /* Debug */ = { 450 | isa = XCBuildConfiguration; 451 | buildSettings = { 452 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 453 | CURRENT_PROJECT_VERSION = 1; 454 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 455 | DEFINES_MODULE = YES; 456 | DYLIB_COMPATIBILITY_VERSION = 1; 457 | DYLIB_CURRENT_VERSION = 1; 458 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 459 | GCC_NO_COMMON_BLOCKS = YES; 460 | GCC_PREPROCESSOR_DEFINITIONS = ( 461 | "DEBUG=1", 462 | "$(inherited)", 463 | ); 464 | INFOPLIST_FILE = Sources/Info.plist; 465 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 466 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 467 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 468 | PRODUCT_BUNDLE_IDENTIFIER = "com.imaginarycloud.$(PRODUCT_NAME:rfc1034identifier)"; 469 | PRODUCT_NAME = "$(TARGET_NAME)"; 470 | SKIP_INSTALL = YES; 471 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 472 | SWIFT_VERSION = 5.0; 473 | VERSIONING_SYSTEM = "apple-generic"; 474 | VERSION_INFO_PREFIX = ""; 475 | }; 476 | name = Debug; 477 | }; 478 | D71DE89C1AD677A8001E62F1 /* Release */ = { 479 | isa = XCBuildConfiguration; 480 | buildSettings = { 481 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 482 | COPY_PHASE_STRIP = NO; 483 | CURRENT_PROJECT_VERSION = 1; 484 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 485 | DEFINES_MODULE = YES; 486 | DYLIB_COMPATIBILITY_VERSION = 1; 487 | DYLIB_CURRENT_VERSION = 1; 488 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 489 | GCC_NO_COMMON_BLOCKS = YES; 490 | INFOPLIST_FILE = Sources/Info.plist; 491 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 492 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 493 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 494 | PRODUCT_BUNDLE_IDENTIFIER = "com.imaginarycloud.$(PRODUCT_NAME:rfc1034identifier)"; 495 | PRODUCT_NAME = "$(TARGET_NAME)"; 496 | SKIP_INSTALL = YES; 497 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 498 | SWIFT_VERSION = 5.0; 499 | VERSIONING_SYSTEM = "apple-generic"; 500 | VERSION_INFO_PREFIX = ""; 501 | }; 502 | name = Release; 503 | }; 504 | /* End XCBuildConfiguration section */ 505 | 506 | /* Begin XCConfigurationList section */ 507 | 454C1F3C19E82E2500C81915 /* Build configuration list for PBXProject "CameraManager" */ = { 508 | isa = XCConfigurationList; 509 | buildConfigurations = ( 510 | 454C1F5E19E82E2500C81915 /* Debug */, 511 | 454C1F5F19E82E2500C81915 /* Release */, 512 | ); 513 | defaultConfigurationIsVisible = 0; 514 | defaultConfigurationName = Release; 515 | }; 516 | 454C1F6019E82E2500C81915 /* Build configuration list for PBXNativeTarget "camera" */ = { 517 | isa = XCConfigurationList; 518 | buildConfigurations = ( 519 | 454C1F6119E82E2500C81915 /* Debug */, 520 | 454C1F6219E82E2500C81915 /* Release */, 521 | ); 522 | defaultConfigurationIsVisible = 0; 523 | defaultConfigurationName = Release; 524 | }; 525 | D71DE89A1AD677A8001E62F1 /* Build configuration list for PBXNativeTarget "CameraManager" */ = { 526 | isa = XCConfigurationList; 527 | buildConfigurations = ( 528 | D71DE89B1AD677A8001E62F1 /* Debug */, 529 | D71DE89C1AD677A8001E62F1 /* Release */, 530 | ); 531 | defaultConfigurationIsVisible = 0; 532 | defaultConfigurationName = Release; 533 | }; 534 | /* End XCConfigurationList section */ 535 | }; 536 | rootObject = 454C1F3919E82E2500C81915 /* Project object */; 537 | } 538 | -------------------------------------------------------------------------------- /CameraManager.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CameraManager.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CameraManager.xcodeproj/xcshareddata/xcschemes/CameraManager.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /CameraManager.xcodeproj/xcshareddata/xcschemes/camera.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 45 | 47 | 53 | 54 | 55 | 56 | 62 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Example App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // camera 4 | // 5 | // Created by Natalia Terlecka on 10/10/14. 6 | // Copyright (c) 2014 Imaginary Cloud. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | func applicationWillResignActive(_ application: UIApplication) { 21 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 22 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 23 | } 24 | 25 | func applicationDidEnterBackground(_ application: UIApplication) { 26 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 27 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 28 | } 29 | 30 | func applicationWillEnterForeground(_ application: UIApplication) { 31 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 32 | } 33 | 34 | func applicationDidBecomeActive(_ application: UIApplication) { 35 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 36 | } 37 | 38 | func applicationWillTerminate(_ application: UIApplication) { 39 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Example App/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example App/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 81 | 82 | 83 | 84 | 105 | 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 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /Example App/ImageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageViewController.swift 3 | // camera 4 | // 5 | // Created by Natalia Terlecka on 13/01/15. 6 | // Copyright (c) 2015 Imaginary Cloud. All rights reserved. 7 | // 8 | 9 | import CameraManager 10 | import UIKit 11 | 12 | class ImageViewController: UIViewController { 13 | var image: UIImage? 14 | var cameraManager: CameraManager? 15 | @IBOutlet var imageView: UIImageView! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | navigationController?.navigationBar.isHidden = true 20 | 21 | guard let validImage = image else { 22 | return 23 | } 24 | 25 | imageView.image = validImage 26 | 27 | if cameraManager?.cameraDevice == .front { 28 | switch validImage.imageOrientation { 29 | case .up, .down: 30 | imageView.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi)) 31 | default: 32 | break 33 | } 34 | } 35 | } 36 | 37 | override func didReceiveMemoryWarning() { 38 | super.didReceiveMemoryWarning() 39 | // Dispose of any resources that can be recreated. 40 | } 41 | 42 | @IBAction func closeButtonTapped(_: Any) { 43 | navigationController?.popViewController(animated: true) 44 | } 45 | 46 | override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { 47 | return .portrait 48 | } 49 | 50 | override var shouldAutorotate: Bool { 51 | return false 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-App-20x20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "Icon-App-20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "Icon-App-29x29@1x.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "imaginary copy 2-4.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "imaginary copy 2-3.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "imaginary copy 2-2.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "Icon-App-40x40@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "Icon-App-57x57@1x.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "Icon-App-57x57@2x.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "imaginary copy 2-1.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "imaginary copy 2.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "Icon-App-20x20@1x.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "20x20" 74 | }, 75 | { 76 | "idiom" : "ipad", 77 | "scale" : "2x", 78 | "size" : "20x20" 79 | }, 80 | { 81 | "filename" : "imaginary copy-1.png", 82 | "idiom" : "ipad", 83 | "scale" : "1x", 84 | "size" : "29x29" 85 | }, 86 | { 87 | "filename" : "imaginary copy.png", 88 | "idiom" : "ipad", 89 | "scale" : "2x", 90 | "size" : "29x29" 91 | }, 92 | { 93 | "filename" : "Icon-App-40x40@1x.png", 94 | "idiom" : "ipad", 95 | "scale" : "1x", 96 | "size" : "40x40" 97 | }, 98 | { 99 | "filename" : "copy.png", 100 | "idiom" : "ipad", 101 | "scale" : "2x", 102 | "size" : "40x40" 103 | }, 104 | { 105 | "filename" : "Icon-Small-50x50@1x.png", 106 | "idiom" : "ipad", 107 | "scale" : "1x", 108 | "size" : "50x50" 109 | }, 110 | { 111 | "filename" : "Icon-Small-50x50@2x.png", 112 | "idiom" : "ipad", 113 | "scale" : "2x", 114 | "size" : "50x50" 115 | }, 116 | { 117 | "filename" : "Icon-App-72x72@1x.png", 118 | "idiom" : "ipad", 119 | "scale" : "1x", 120 | "size" : "72x72" 121 | }, 122 | { 123 | "filename" : "Icon-App-72x72@2x.png", 124 | "idiom" : "ipad", 125 | "scale" : "2x", 126 | "size" : "72x72" 127 | }, 128 | { 129 | "filename" : "Icon-App-76x76@1x.png", 130 | "idiom" : "ipad", 131 | "scale" : "1x", 132 | "size" : "76x76" 133 | }, 134 | { 135 | "filename" : "Icon-App-76x76@2x.png", 136 | "idiom" : "ipad", 137 | "scale" : "2x", 138 | "size" : "76x76" 139 | }, 140 | { 141 | "filename" : "Icon-App-83.5x83.5@2x.png", 142 | "idiom" : "ipad", 143 | "scale" : "2x", 144 | "size" : "83.5x83.5" 145 | }, 146 | { 147 | "filename" : "logo.jpeg", 148 | "idiom" : "ios-marketing", 149 | "scale" : "1x", 150 | "size" : "1024x1024" 151 | }, 152 | { 153 | "filename" : "Icon-App-60x60@2x.png", 154 | "idiom" : "car", 155 | "scale" : "2x", 156 | "size" : "60x60" 157 | }, 158 | { 159 | "filename" : "Icon-App-60x60@3x.png", 160 | "idiom" : "car", 161 | "scale" : "3x", 162 | "size" : "60x60" 163 | }, 164 | { 165 | "idiom" : "watch", 166 | "role" : "notificationCenter", 167 | "scale" : "2x", 168 | "size" : "24x24", 169 | "subtype" : "38mm" 170 | }, 171 | { 172 | "idiom" : "watch", 173 | "role" : "notificationCenter", 174 | "scale" : "2x", 175 | "size" : "27.5x27.5", 176 | "subtype" : "42mm" 177 | }, 178 | { 179 | "filename" : "Icon-App-29x29@2x.png", 180 | "idiom" : "watch", 181 | "role" : "companionSettings", 182 | "scale" : "2x", 183 | "size" : "29x29" 184 | }, 185 | { 186 | "filename" : "Icon-App-29x29@3x.png", 187 | "idiom" : "watch", 188 | "role" : "companionSettings", 189 | "scale" : "3x", 190 | "size" : "29x29" 191 | }, 192 | { 193 | "filename" : "Icon-App-40x40@2x.png", 194 | "idiom" : "watch", 195 | "role" : "appLauncher", 196 | "scale" : "2x", 197 | "size" : "40x40", 198 | "subtype" : "38mm" 199 | }, 200 | { 201 | "idiom" : "watch", 202 | "role" : "appLauncher", 203 | "scale" : "2x", 204 | "size" : "44x44", 205 | "subtype" : "40mm" 206 | }, 207 | { 208 | "idiom" : "watch", 209 | "role" : "appLauncher", 210 | "scale" : "2x", 211 | "size" : "50x50", 212 | "subtype" : "44mm" 213 | }, 214 | { 215 | "idiom" : "watch", 216 | "role" : "quickLook", 217 | "scale" : "2x", 218 | "size" : "86x86", 219 | "subtype" : "38mm" 220 | }, 221 | { 222 | "idiom" : "watch", 223 | "role" : "quickLook", 224 | "scale" : "2x", 225 | "size" : "98x98", 226 | "subtype" : "42mm" 227 | }, 228 | { 229 | "idiom" : "watch", 230 | "role" : "quickLook", 231 | "scale" : "2x", 232 | "size" : "108x108", 233 | "subtype" : "44mm" 234 | }, 235 | { 236 | "idiom" : "watch-marketing", 237 | "scale" : "1x", 238 | "size" : "1024x1024" 239 | } 240 | ], 241 | "info" : { 242 | "author" : "xcode", 243 | "version" : 1 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/copy.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/imaginary copy 2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/imaginary copy 2-1.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/imaginary copy 2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/imaginary copy 2-2.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/imaginary copy 2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/imaginary copy 2-3.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/imaginary copy 2-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/imaginary copy 2-4.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/imaginary copy 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/imaginary copy 2.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/imaginary copy-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/imaginary copy-1.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/imaginary copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/imaginary copy.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/AppIcon.appiconset/logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/AppIcon.appiconset/logo.jpeg -------------------------------------------------------------------------------- /Example App/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example App/Images.xcassets/flash_auto.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "flash-auto.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example App/Images.xcassets/flash_auto.imageset/flash-auto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/flash_auto.imageset/flash-auto.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/flash_off.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "flash-off.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example App/Images.xcassets/flash_off.imageset/flash-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/flash_off.imageset/flash-off.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/flash_on.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "flash-on.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example App/Images.xcassets/flash_on.imageset/flash-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/flash_on.imageset/flash-on.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "imaginary-2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "imaginary-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "imaginary.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example App/Images.xcassets/logo.imageset/imaginary-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/logo.imageset/imaginary-1.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/logo.imageset/imaginary-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/logo.imageset/imaginary-2.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/logo.imageset/imaginary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/logo.imageset/imaginary.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/output_image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "output-image.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example App/Images.xcassets/output_image.imageset/output-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/output_image.imageset/output-image.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/output_video.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "icons8-video-call.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example App/Images.xcassets/output_video.imageset/icons8-video-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/output_video.imageset/icons8-video-call.png -------------------------------------------------------------------------------- /Example App/Images.xcassets/switch_camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "switch-camera.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example App/Images.xcassets/switch_camera.imageset/switch-camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imaginary-cloud/CameraManager/660d7b8326f43ab1721620cd4729b3928c8a767b/Example App/Images.xcassets/switch_camera.imageset/switch-camera.png -------------------------------------------------------------------------------- /Example App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSCameraUsageDescription 26 | CameraManager will use your camera to take pictures/video. 27 | NSLocationWhenInUseUsageDescription 28 | CameraManager will use your location to save EXIF data. 29 | NSMicrophoneUsageDescription 30 | CameraManager will use your microphone to record audio for videos. 31 | NSPhotoLibraryUsageDescription 32 | CameraManager will access your photo library to save pictures/video. 33 | UILaunchStoryboardName 34 | LaunchScreen 35 | UIMainStoryboardFile 36 | Main 37 | UIRequiredDeviceCapabilities 38 | 39 | armv7 40 | 41 | UIRequiresFullScreen 42 | 43 | UIStatusBarHidden 44 | 45 | UIStatusBarStyle 46 | UIStatusBarStyleDefault 47 | UISupportedInterfaceOrientations 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | UIViewControllerBasedStatusBarAppearance 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Example App/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // camera 4 | // 5 | // Created by Natalia Terlecka on 10/10/14. 6 | // Copyright (c) 2014 Imaginary Cloud. All rights reserved. 7 | // 8 | 9 | import CameraManager 10 | import CoreLocation 11 | import UIKit 12 | 13 | class ViewController: UIViewController { 14 | // MARK: - Constants 15 | 16 | let cameraManager = CameraManager() 17 | 18 | // MARK: - @IBOutlets 19 | 20 | @IBOutlet var headerView: UIView! 21 | @IBOutlet var flashModeImageView: UIImageView! 22 | @IBOutlet var outputImageView: UIImageView! 23 | @IBOutlet var cameraTypeImageView: UIImageView! 24 | @IBOutlet var qualityLabel: UILabel! 25 | 26 | @IBOutlet var cameraView: UIView! 27 | @IBOutlet var askForPermissionsLabel: UILabel! 28 | 29 | @IBOutlet var footerView: UIView! 30 | @IBOutlet var cameraButton: UIButton! 31 | @IBOutlet var locationButton: UIButton! 32 | 33 | let darkBlue = UIColor(red: 4 / 255, green: 14 / 255, blue: 26 / 255, alpha: 1) 34 | let lightBlue = UIColor(red: 24 / 255, green: 125 / 255, blue: 251 / 255, alpha: 1) 35 | let redColor = UIColor(red: 229 / 255, green: 77 / 255, blue: 67 / 255, alpha: 1) 36 | 37 | // MARK: - UIViewController 38 | 39 | override func viewDidLoad() { 40 | super.viewDidLoad() 41 | 42 | setupCameraManager() 43 | 44 | navigationController?.navigationBar.isHidden = true 45 | 46 | askForPermissionsLabel.isHidden = true 47 | askForPermissionsLabel.backgroundColor = lightBlue 48 | askForPermissionsLabel.textColor = .white 49 | askForPermissionsLabel.isUserInteractionEnabled = true 50 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(askForCameraPermissions)) 51 | askForPermissionsLabel.addGestureRecognizer(tapGesture) 52 | 53 | footerView.backgroundColor = darkBlue 54 | headerView.backgroundColor = darkBlue 55 | 56 | if CLLocationManager.locationServicesEnabled() { 57 | switch CLLocationManager.authorizationStatus() { 58 | case .authorizedAlways, .authorizedWhenInUse: 59 | cameraManager.shouldUseLocationServices = true 60 | locationButton.isHidden = true 61 | default: 62 | cameraManager.shouldUseLocationServices = false 63 | } 64 | } 65 | 66 | let currentCameraState = cameraManager.currentCameraStatus() 67 | 68 | if currentCameraState == .notDetermined { 69 | askForPermissionsLabel.isHidden = false 70 | } else if currentCameraState == .ready { 71 | addCameraToView() 72 | } else { 73 | askForPermissionsLabel.isHidden = false 74 | } 75 | 76 | flashModeImageView.image = UIImage(named: "flash_off") 77 | if cameraManager.hasFlash { 78 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(changeFlashMode)) 79 | flashModeImageView.addGestureRecognizer(tapGesture) 80 | } 81 | 82 | outputImageView.image = UIImage(named: "output_video") 83 | let outputGesture = UITapGestureRecognizer(target: self, action: #selector(outputModeButtonTapped)) 84 | outputImageView.addGestureRecognizer(outputGesture) 85 | 86 | cameraTypeImageView.image = UIImage(named: "switch_camera") 87 | let cameraTypeGesture = UITapGestureRecognizer(target: self, action: #selector(changeCameraDevice)) 88 | cameraTypeImageView.addGestureRecognizer(cameraTypeGesture) 89 | 90 | qualityLabel.isUserInteractionEnabled = true 91 | let qualityGesture = UITapGestureRecognizer(target: self, action: #selector(changeCameraQuality)) 92 | qualityLabel.addGestureRecognizer(qualityGesture) 93 | } 94 | 95 | override func viewWillAppear(_ animated: Bool) { 96 | super.viewWillAppear(animated) 97 | 98 | navigationController?.navigationBar.isHidden = true 99 | cameraManager.resumeCaptureSession() 100 | cameraManager.startQRCodeDetection { result in 101 | switch result { 102 | case .success(let value): 103 | print(value) 104 | case .failure(let error): 105 | print(error.localizedDescription) 106 | } 107 | } 108 | } 109 | 110 | override func viewWillDisappear(_ animated: Bool) { 111 | super.viewWillDisappear(animated) 112 | cameraManager.stopQRCodeDetection() 113 | cameraManager.stopCaptureSession() 114 | } 115 | 116 | // MARK: - ViewController 117 | fileprivate func setupCameraManager() { 118 | cameraManager.shouldEnableExposure = true 119 | 120 | cameraManager.writeFilesToPhoneLibrary = false 121 | 122 | cameraManager.shouldFlipFrontCameraImage = false 123 | cameraManager.showAccessPermissionPopupAutomatically = false 124 | } 125 | 126 | 127 | fileprivate func addCameraToView() { 128 | cameraManager.addPreviewLayerToView(cameraView, newCameraOutputMode: CameraOutputMode.videoWithMic) 129 | cameraManager.showErrorBlock = { [weak self] (erTitle: String, erMessage: String) -> Void in 130 | 131 | let alertController = UIAlertController(title: erTitle, message: erMessage, preferredStyle: .alert) 132 | alertController.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: { (_) -> Void in })) 133 | 134 | self?.present(alertController, animated: true, completion: nil) 135 | } 136 | } 137 | 138 | // MARK: - @IBActions 139 | 140 | @IBAction func changeFlashMode(_ sender: UIButton) { 141 | switch cameraManager.changeFlashMode() { 142 | case .off: 143 | flashModeImageView.image = UIImage(named: "flash_off") 144 | case .on: 145 | flashModeImageView.image = UIImage(named: "flash_on") 146 | case .auto: 147 | flashModeImageView.image = UIImage(named: "flash_auto") 148 | } 149 | } 150 | 151 | @IBAction func recordButtonTapped(_ sender: UIButton) { 152 | switch cameraManager.cameraOutputMode { 153 | case .stillImage: 154 | cameraManager.capturePictureWithCompletion { result in 155 | switch result { 156 | case .failure: 157 | self.cameraManager.showErrorBlock("Error occurred", "Cannot save picture.") 158 | case .success(let content): 159 | 160 | let vc: ImageViewController? = self.storyboard?.instantiateViewController(withIdentifier: "ImageVC") as? ImageViewController 161 | if let validVC: ImageViewController = vc, 162 | case let capturedData = content.asData { 163 | print(capturedData!.printExifData()) 164 | let capturedImage = UIImage(data: capturedData!)! 165 | validVC.image = capturedImage 166 | validVC.cameraManager = self.cameraManager 167 | self.navigationController?.pushViewController(validVC, animated: true) 168 | } 169 | } 170 | } 171 | case .videoWithMic, .videoOnly: 172 | cameraButton.isSelected = !cameraButton.isSelected 173 | cameraButton.setTitle("", for: UIControl.State.selected) 174 | 175 | cameraButton.backgroundColor = cameraButton.isSelected ? redColor : lightBlue 176 | if sender.isSelected { 177 | cameraManager.startRecordingVideo() 178 | } else { 179 | cameraManager.stopVideoRecording { (_, error) -> Void in 180 | if error != nil { 181 | self.cameraManager.showErrorBlock("Error occurred", "Cannot save video.") 182 | } 183 | } 184 | } 185 | } 186 | } 187 | 188 | @IBAction func locateMeButtonTapped(_ sender: Any) { 189 | cameraManager.shouldUseLocationServices = true 190 | locationButton.isHidden = true 191 | } 192 | 193 | @IBAction func outputModeButtonTapped(_ sender: UIButton) { 194 | cameraManager.cameraOutputMode = cameraManager.cameraOutputMode == CameraOutputMode.videoWithMic ? CameraOutputMode.stillImage : CameraOutputMode.videoWithMic 195 | switch cameraManager.cameraOutputMode { 196 | case .stillImage: 197 | cameraButton.isSelected = false 198 | cameraButton.backgroundColor = lightBlue 199 | outputImageView.image = UIImage(named: "output_image") 200 | case .videoWithMic, .videoOnly: 201 | outputImageView.image = UIImage(named: "output_video") 202 | } 203 | } 204 | 205 | @IBAction func changeCameraDevice() { 206 | cameraManager.cameraDevice = cameraManager.cameraDevice == CameraDevice.front ? CameraDevice.back : CameraDevice.front 207 | } 208 | 209 | @IBAction func askForCameraPermissions() { 210 | cameraManager.askUserForCameraPermission { permissionGranted in 211 | 212 | if permissionGranted { 213 | self.askForPermissionsLabel.isHidden = true 214 | self.askForPermissionsLabel.alpha = 0 215 | self.addCameraToView() 216 | } else { 217 | if #available(iOS 10.0, *) { 218 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) 219 | } else { 220 | // Fallback on earlier versions 221 | } 222 | } 223 | } 224 | } 225 | 226 | @IBAction func changeCameraQuality() { 227 | switch cameraManager.cameraOutputQuality { 228 | case .high: 229 | qualityLabel.text = "Medium" 230 | cameraManager.cameraOutputQuality = .medium 231 | case .medium: 232 | qualityLabel.text = "Low" 233 | cameraManager.cameraOutputQuality = .low 234 | case .low: 235 | qualityLabel.text = "High" 236 | cameraManager.cameraOutputQuality = .high 237 | default: 238 | qualityLabel.text = "High" 239 | cameraManager.cameraOutputQuality = .high 240 | } 241 | } 242 | } 243 | 244 | public extension Data { 245 | func printExifData() { 246 | let cfdata: CFData = self as CFData 247 | let imageSourceRef = CGImageSourceCreateWithData(cfdata, nil) 248 | let imageProperties = CGImageSourceCopyMetadataAtIndex(imageSourceRef!, 0, nil)! 249 | 250 | let mutableMetadata = CGImageMetadataCreateMutableCopy(imageProperties)! 251 | 252 | CGImageMetadataEnumerateTagsUsingBlock(mutableMetadata, nil, nil) { _, tag in 253 | print(CGImageMetadataTagCopyName(tag)!, ":", CGImageMetadataTagCopyValue(tag)!) 254 | return true 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Imaginary Cloud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 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: "CameraManager", 8 | platforms: [ 9 | .iOS(.v10) 10 | ], 11 | products: [ 12 | .library( 13 | name: "CameraManager", 14 | targets: ["CameraManager"]), 15 | ], 16 | dependencies: [], 17 | targets: [ 18 | .target( 19 | name: "CameraManager", 20 | dependencies: [], 21 | path: "Sources", 22 | sources: ["CameraManager.swift"] 23 | ) 24 | ], 25 | swiftLanguageVersions: [.v5] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Camera Manager 2 | 3 | [![CocoaPods](https://img.shields.io/cocoapods/v/CameraManager.svg)](https://github.com/imaginary-cloud/CameraManager) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 4 | 5 | This is a simple Swift class to provide all the configurations you need to create custom camera view in your app. 6 | It follows orientation change and updates UI accordingly, supports front and rear camera selection, pinch to zoom, tap to focus, exposure slider, different flash modes, inputs and outputs and QRCode detection. 7 | Just drag, drop and use. 8 | 9 | We've also written a blog post about it. You can read it [here](https://www.imaginarycloud.com/blog/camera-manager/). 10 | 11 | ## Installation with CocoaPods 12 | 13 | The easiest way to install the CameraManager is with [CocoaPods](http://cocoapods.org) 14 | 15 | ### Podfile 16 | 17 | ```ruby 18 | use_frameworks! 19 | 20 | pod 'CameraManager', '~> 5.1' 21 | ``` 22 | 23 | ## Installation with Swift Package Manager 24 | 25 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. 26 | 27 | Add `CameraManager` as a dependency in your `Package.swift` file: 28 | 29 | ``` 30 | import PackageDescription 31 | 32 | let package = Package( 33 | dependencies: [ 34 | .Package(url: "https://github.com/imaginary-cloud/CameraManager", from: "5.1.3") 35 | ] 36 | ) 37 | ``` 38 | 39 | ## Installation with Carthage 40 | 41 | [Carthage](https://github.com/Carthage/Carthage) is another dependency management tool written in Swift. 42 | 43 | Add the following line to your Cartfile: 44 | 45 | ``` 46 | github "imaginary-cloud/CameraManager" >= 5.1 47 | ``` 48 | 49 | And run `carthage update` to build the dynamic framework. 50 | 51 | ## How to use 52 | 53 | To use it you just add the preview layer to your desired view, you'll get back the state of the camera if it's unavailable, ready or the user denied access to it. Have in mind that in order to retain the AVCaptureSession you will need to retain cameraManager instance somewhere, ex. as an instance constant. 54 | 55 | ```swift 56 | let cameraManager = CameraManager() 57 | cameraManager.addPreviewLayerToView(self.cameraView) 58 | 59 | ``` 60 | 61 | To shoot image all you need to do is call: 62 | 63 | ```swift 64 | cameraManager.capturePictureWithCompletion({ result in 65 | switch result { 66 | case .failure: 67 | // error handling 68 | case .success(let content): 69 | self.myImage = content.asImage; 70 | } 71 | }) 72 | ``` 73 | 74 | To record video you call: 75 | 76 | ```swift 77 | cameraManager.startRecordingVideo() 78 | cameraManager.stopVideoRecording({ (videoURL, recordError) -> Void in 79 | guard let videoURL = videoURL else { 80 | //Handle error of no recorded video URL 81 | } 82 | do { 83 | try FileManager.default.copyItem(at: videoURL, to: self.myVideoURL) 84 | } 85 | catch { 86 | //Handle error occured during copy 87 | } 88 | }) 89 | ``` 90 | 91 | To zoom in manually: 92 | 93 | ```swift 94 | let zoomScale = CGFloat(2.0) 95 | cameraManager.zoom(zoomScale) 96 | ``` 97 | 98 | ### Properties 99 | 100 | You can set input device to front or back camera. `(Default: .Back)` 101 | 102 | ```swift 103 | cameraManager.cameraDevice = .front || .back 104 | ``` 105 | 106 | You can specify if the front camera image should be horizontally fliped. `(Default: false)` 107 | 108 | ```swift 109 | cameraManager.shouldFlipFrontCameraImage = true || false 110 | ``` 111 | 112 | You can enable or disable gestures on camera preview. `(Default: true)` 113 | 114 | ```swift 115 | cameraManager.shouldEnableTapToFocus = true || false 116 | cameraManager.shouldEnablePinchToZoom = true || false 117 | cameraManager.shouldEnableExposure = true || false 118 | ``` 119 | 120 | You can set output format to Image, video or video with audio. `(Default: .stillImage)` 121 | 122 | ```swift 123 | cameraManager.cameraOutputMode = .stillImage || .videoWithMic || .videoOnly 124 | ``` 125 | 126 | You can set the quality based on the [AVCaptureSession.Preset values](https://developer.apple.com/documentation/avfoundation/avcapturesession/preset) `(Default: .high)` 127 | 128 | ```swift 129 | cameraManager.cameraOutputQuality = .low || .medium || .high || * 130 | ``` 131 | 132 | `*` check all the possible values [here](https://developer.apple.com/documentation/avfoundation/avcapturesession/preset) 133 | 134 | You can also check if you can set a specific preset value: 135 | 136 | ```swift 137 | if .cameraManager.canSetPreset(preset: .hd1280x720) { 138 | cameraManager.cameraOutputQuality = .hd1280x720 139 | } else { 140 | cameraManager.cameraOutputQuality = .high 141 | } 142 | ``` 143 | 144 | You can specify the focus mode. `(Default: .continuousAutoFocus)` 145 | 146 | ```swift 147 | cameraManager.focusMode = .autoFocus || .continuousAutoFocus || .locked 148 | ``` 149 | 150 | You can specifiy the exposure mode. `(Default: .continuousAutoExposure)` 151 | 152 | ```swift 153 | cameraManager.exposureMode = .autoExpose || .continuousAutoExposure || .locked || .custom 154 | ``` 155 | 156 | You can change the flash mode (it will also set corresponding flash mode). `(Default: .off)` 157 | 158 | ```swift 159 | cameraManager.flashMode = .off || .on || .auto 160 | ``` 161 | 162 | You can specify the stabilisation mode to be used during a video record session. `(Default: .auto)` 163 | 164 | ```swift 165 | cameraManager.videoStabilisationMode = .auto || .cinematic 166 | ``` 167 | 168 | You can get the video stabilization mode currently active. If video stabilization is neither supported or active it will return `.off`. 169 | 170 | ```swift 171 | cameraManager.activeVideoStabilisationMode 172 | ``` 173 | 174 | You can enable location services for storing GPS location when saving to Camera Roll. `(Default: false)` 175 | 176 | ```swift 177 | cameraManager.shouldUseLocationServices = true || false 178 | ``` 179 | 180 | In case you use location it's mandatory to add `NSLocationWhenInUseUsageDescription` key to the `Info.plist` in your app. [More Info](https://developer.apple.com/documentation/uikit/protecting_the_user_s_privacy) 181 | 182 | For getting the gps location when calling `capturePictureWithCompletion` you should use the `CaptureResult` as `data` (see [Example App](https://github.com/imaginary-cloud/CameraManager/blob/master/Example%20App/ViewController.swift)). 183 | 184 | You can specify if you want to save the files to phone library. `(Default: true)` 185 | 186 | ```swift 187 | cameraManager.writeFilesToPhoneLibrary = true || false 188 | ``` 189 | 190 | You can specify the album names for image and video recordings. 191 | 192 | ```swift 193 | cameraManager.imageAlbumName = "Image Album Name" 194 | cameraManager.videoAlbumName = "Video Album Name" 195 | ``` 196 | 197 | You can specify if you want to disable animations. `(Default: true)` 198 | 199 | ```swift 200 | cameraManager.animateShutter = true || false 201 | cameraManager.animateCameraDeviceChange = true || false 202 | ``` 203 | 204 | You can specify if you want the user to be asked about camera permissions automatically when you first try to use the camera or manually. `(Default: true)` 205 | 206 | ```swift 207 | cameraManager.showAccessPermissionPopupAutomatically = true || false 208 | ``` 209 | 210 | To check if the device supports flash call: 211 | 212 | ```swift 213 | cameraManager.hasFlash 214 | ``` 215 | 216 | To change flash mode to the next available one you can use this handy function which will also return current value for you to update the UI accordingly: 217 | 218 | ```swift 219 | cameraManager.changeFlashMode() 220 | ``` 221 | 222 | You can even setUp your custom block to handle error messages: 223 | It can be customized to be presented on the Window root view controller, for example. 224 | 225 | ```swift 226 | cameraManager.showErrorBlock = { (erTitle: String, erMessage: String) -> Void in 227 | var alertController = UIAlertController(title: erTitle, message: erMessage, preferredStyle: .alert) 228 | alertController.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: { (alertAction) -> Void in 229 | })) 230 | 231 | let topController = UIApplication.shared.keyWindow?.rootViewController 232 | 233 | if (topController != nil) { 234 | topController?.present(alertController, animated: true, completion: { () -> Void in 235 | // 236 | }) 237 | } 238 | 239 | } 240 | ``` 241 | 242 | You can set if you want to detect QR codes: 243 | 244 | ```swift 245 | cameraManager.startQRCodeDetection { (result) in 246 | switch result { 247 | case .success(let value): 248 | print(value) 249 | case .failure(let error): 250 | print(error.localizedDescription) 251 | } 252 | } 253 | ``` 254 | 255 | and don't forget to call `cameraManager.stopQRCodeDetection()` whenever you done detecting. 256 | 257 | ## Support 258 | 259 | Supports iOS 9 and above. Xcode 11.4 is required to build the latest code written in Swift 5.2. 260 | 261 | Now it's compatible with latest Swift syntax, so if you're using any Swift version prior to 5 make sure to use one of the previously tagged releases: 262 | 263 | - for Swift 4 see: [v4.4.0](https://github.com/imaginary-cloud/CameraManager/tree/4.4.0). 264 | - for Swift 3 see: [v3.2.0](https://github.com/imaginary-cloud/CameraManager/tree/3.2.0). 265 | 266 | ## License 267 | 268 | Copyright © 2010-2020 [Imaginary Cloud](https://www.imaginarycloud.com/?utm_source=github). This library is licensed under the MIT license. 269 | 270 | ## About Imaginary Cloud 271 | 272 | [![Imaginary Cloud](https://s3.eu-central-1.amazonaws.com/imaginary-images/Logo_IC_readme.svg)](https://www.imaginarycloud.com/?utm_source=github) 273 | 274 | At Imaginary Cloud, we build world-class web & mobile apps. Our Front-end developers and UI/UX designers are ready to create or scale your digital product. Take a look at our [website](https://www.imaginarycloud.com/?utm_source=github) and [get in touch!](https://www.imaginarycloud.com/contacts/?utm_source=github) We'll take it from there. 275 | -------------------------------------------------------------------------------- /Sources/CameraManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // CameraManager.h 3 | // CameraManager 4 | // 5 | // Created by Lex Tang on 4/9/15. 6 | // Copyright (c) 2015 Imaginary Cloud. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for CameraManager. 12 | FOUNDATION_EXPORT double CameraManagerVersionNumber; 13 | 14 | //! Project version string for CameraManager. 15 | FOUNDATION_EXPORT const unsigned char CameraManagerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/CameraManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraManager.swift 3 | // camera 4 | // 5 | // Created by Natalia Terlecka on 10/10/14. 6 | // Copyright (c) 2014 Imaginary Cloud. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | import CoreImage 11 | import CoreLocation 12 | import CoreMotion 13 | import ImageIO 14 | import MobileCoreServices 15 | import Photos 16 | import PhotosUI 17 | import UIKit 18 | 19 | public enum CameraState { 20 | case ready, accessDenied, noDeviceFound, notDetermined 21 | } 22 | 23 | public enum CameraDevice { 24 | case front, back 25 | } 26 | 27 | public enum CameraFlashMode: Int { 28 | case off, on, auto 29 | } 30 | 31 | public enum CameraOutputMode { 32 | case stillImage, videoWithMic, videoOnly 33 | } 34 | 35 | public enum CaptureResult { 36 | case success(content: CaptureContent) 37 | case failure(Error) 38 | 39 | init(_ image: UIImage) { 40 | self = .success(content: .image(image)) 41 | } 42 | 43 | init(_ data: Data) { 44 | self = .success(content: .imageData(data)) 45 | } 46 | 47 | init(_ asset: PHAsset) { 48 | self = .success(content: .asset(asset)) 49 | } 50 | 51 | var imageData: Data? { 52 | if case let .success(content) = self { 53 | return content.asData 54 | } else { 55 | return nil 56 | } 57 | } 58 | } 59 | 60 | public enum CaptureContent { 61 | case imageData(Data) 62 | case image(UIImage) 63 | case asset(PHAsset) 64 | } 65 | 66 | extension CaptureContent { 67 | public var asImage: UIImage? { 68 | switch self { 69 | case let .image(image): return image 70 | case let .imageData(data): return UIImage(data: data) 71 | case let .asset(asset): 72 | if let data = getImageData(fromAsset: asset) { 73 | return UIImage(data: data) 74 | } else { 75 | return nil 76 | } 77 | } 78 | } 79 | 80 | public var asData: Data? { 81 | switch self { 82 | case let .image(image): return image.jpegData(compressionQuality: 1.0) 83 | case let .imageData(data): return data 84 | case let .asset(asset): return getImageData(fromAsset: asset) 85 | } 86 | } 87 | 88 | private func getImageData(fromAsset asset: PHAsset) -> Data? { 89 | var imageData: Data? 90 | let manager = PHImageManager.default() 91 | let options = PHImageRequestOptions() 92 | options.version = .original 93 | options.isSynchronous = true 94 | manager.requestImageData(for: asset, options: options) { data, _, _, _ in 95 | 96 | imageData = data 97 | } 98 | return imageData 99 | } 100 | } 101 | 102 | public enum CaptureError: Error { 103 | case noImageData 104 | case invalidImageData 105 | case noVideoConnection 106 | case noSampleBuffer 107 | case assetNotSaved 108 | } 109 | 110 | /// Class for handling iDevices custom camera usage 111 | open class CameraManager: NSObject, AVCaptureFileOutputRecordingDelegate, UIGestureRecognizerDelegate { 112 | // MARK: - Public properties 113 | 114 | // Property for custom image album name. 115 | open var imageAlbumName: String? 116 | 117 | // Property for custom image album name. 118 | open var videoAlbumName: String? 119 | 120 | /// Property for capture session to customize camera settings. 121 | open var captureSession: AVCaptureSession? 122 | 123 | /** 124 | Property to determine if the manager should show the error for the user. If you want to show the errors yourself set this to false. If you want to add custom error UI set showErrorBlock property. 125 | - note: Default value is **false** 126 | */ 127 | open var showErrorsToUsers = false 128 | 129 | /// Property to determine if the manager should show the camera permission popup immediatly when it's needed or you want to show it manually. Default value is true. Be carful cause using the camera requires permission, if you set this value to false and don't ask manually you won't be able to use the camera. 130 | open var showAccessPermissionPopupAutomatically = true 131 | 132 | /// A block creating UI to present error message to the user. This can be customised to be presented on the Window root view controller, or to pass in the viewController which will present the UIAlertController, for example. 133 | open var showErrorBlock: (_ erTitle: String, _ erMessage: String) -> Void = { (erTitle: String, erMessage: String) -> Void in 134 | 135 | var alertController = UIAlertController(title: erTitle, message: erMessage, preferredStyle: .alert) 136 | alertController.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: { (_) -> Void in })) 137 | 138 | if let topController = UIApplication.shared.keyWindow?.rootViewController { 139 | topController.present(alertController, animated: true, completion: nil) 140 | } 141 | } 142 | 143 | open func canSetPreset(preset: AVCaptureSession.Preset) -> Bool? { 144 | if let validCaptureSession = captureSession { 145 | return validCaptureSession.canSetSessionPreset(preset) 146 | } 147 | return nil 148 | } 149 | 150 | /** 151 | Property to determine if manager should write the resources to the phone library. 152 | - note: Default value is **true** 153 | */ 154 | open var writeFilesToPhoneLibrary = true 155 | 156 | /** 157 | Property to determine if manager should follow device orientation. 158 | - note: Default value is **true** 159 | */ 160 | open var shouldRespondToOrientationChanges = true { 161 | didSet { 162 | if shouldRespondToOrientationChanges { 163 | _startFollowingDeviceOrientation() 164 | } else { 165 | _stopFollowingDeviceOrientation() 166 | } 167 | } 168 | } 169 | 170 | /** 171 | Property to determine if manager should horizontally flip image took by front camera. 172 | - note: Default value is **false** 173 | */ 174 | open var shouldFlipFrontCameraImage = false 175 | 176 | /** 177 | Property to determine if manager should keep view with the same bounds when the orientation changes. 178 | - note: Default value is **false** 179 | */ 180 | open var shouldKeepViewAtOrientationChanges = false 181 | 182 | /** 183 | Property to determine if manager should enable tap to focus on camera preview. 184 | - note: Default value is **true** 185 | */ 186 | open var shouldEnableTapToFocus = true { 187 | didSet { 188 | focusGesture.isEnabled = shouldEnableTapToFocus 189 | } 190 | } 191 | 192 | /** 193 | Property to determine if manager should enable pinch to zoom on camera preview. 194 | - note: Default value is **true** 195 | */ 196 | open var shouldEnablePinchToZoom = true { 197 | didSet { 198 | zoomGesture.isEnabled = shouldEnablePinchToZoom 199 | } 200 | } 201 | 202 | /** 203 | Property to determine if manager should enable pan to change exposure/brightness. 204 | - note: Default value is **true** 205 | */ 206 | open var shouldEnableExposure = true { 207 | didSet { 208 | exposureGesture.isEnabled = shouldEnableExposure 209 | } 210 | } 211 | 212 | /// Property to determine if the camera is ready to use. 213 | open var cameraIsReady: Bool { 214 | return cameraIsSetup 215 | } 216 | 217 | /// Property to determine if current device has front camera. 218 | open var hasFrontCamera: Bool = { 219 | let frontDevices = AVCaptureDevice.videoDevices.filter { $0.position == .front } 220 | return !frontDevices.isEmpty 221 | }() 222 | 223 | /// Property to determine if current device has flash. 224 | open var hasFlash: Bool = { 225 | let hasFlashDevices = AVCaptureDevice.videoDevices.filter { $0.hasFlash } 226 | return !hasFlashDevices.isEmpty 227 | }() 228 | 229 | /** 230 | Property to enable or disable flip animation when switch between back and front camera. 231 | - note: Default value is **true** 232 | */ 233 | open var animateCameraDeviceChange: Bool = true 234 | 235 | /** 236 | Property to enable or disable shutter animation when taking a picture. 237 | - note: Default value is **true** 238 | */ 239 | open var animateShutter: Bool = true 240 | 241 | /** 242 | Property to enable or disable location services. Location services in camera is used for EXIF data. 243 | - note: Default value is **false** 244 | */ 245 | open var shouldUseLocationServices: Bool = false { 246 | didSet { 247 | if shouldUseLocationServices { 248 | self.locationManager = CameraLocationManager() 249 | } 250 | } 251 | } 252 | 253 | /// Property to change camera device between front and back. 254 | open var cameraDevice: CameraDevice = .back { 255 | didSet { 256 | if cameraIsSetup, cameraDevice != oldValue { 257 | if animateCameraDeviceChange { 258 | _doFlipAnimation() 259 | } 260 | _updateCameraDevice(cameraDevice) 261 | _updateIlluminationMode(flashMode) 262 | _setupMaxZoomScale() 263 | _zoom(0) 264 | _orientationChanged() 265 | } 266 | } 267 | } 268 | 269 | /// Property to change camera flash mode. 270 | open var flashMode: CameraFlashMode = .off { 271 | didSet { 272 | if cameraIsSetup && flashMode != oldValue { 273 | _updateIlluminationMode(flashMode) 274 | } 275 | } 276 | } 277 | 278 | /// Property to change camera output quality. 279 | open var cameraOutputQuality: AVCaptureSession.Preset = .high { 280 | didSet { 281 | if cameraIsSetup && cameraOutputQuality != oldValue { 282 | _updateCameraQualityMode(cameraOutputQuality) 283 | } 284 | } 285 | } 286 | 287 | /// Property to change camera output. 288 | open var cameraOutputMode: CameraOutputMode = .stillImage { 289 | didSet { 290 | if cameraIsSetup { 291 | if cameraOutputMode != oldValue { 292 | _setupOutputMode(cameraOutputMode, oldCameraOutputMode: oldValue) 293 | } 294 | _setupMaxZoomScale() 295 | _zoom(0) 296 | } 297 | } 298 | } 299 | 300 | /// Property to check video recording duration when in progress. 301 | open var recordedDuration: CMTime { return movieOutput?.recordedDuration ?? CMTime.zero } 302 | 303 | /// Property to check video recording file size when in progress. 304 | open var recordedFileSize: Int64 { return movieOutput?.recordedFileSize ?? 0 } 305 | 306 | /// Property to set focus mode when tap to focus is used (_focusStart). 307 | open var focusMode: AVCaptureDevice.FocusMode = .continuousAutoFocus 308 | 309 | /// Property to set exposure mode when tap to focus is used (_focusStart). 310 | open var exposureMode: AVCaptureDevice.ExposureMode = .continuousAutoExposure 311 | 312 | /// Property to set video stabilisation mode during a video record session 313 | open var videoStabilisationMode: AVCaptureVideoStabilizationMode = .auto { 314 | didSet { 315 | if oldValue != videoStabilisationMode { 316 | _setupVideoConnection() 317 | } 318 | } 319 | } 320 | 321 | // Property to get the stabilization mode currently active 322 | open var activeVideoStabilisationMode: AVCaptureVideoStabilizationMode { 323 | if let movieOutput = movieOutput { 324 | for connection in movieOutput.connections { 325 | for port in connection.inputPorts { 326 | if port.mediaType == AVMediaType.video { 327 | let videoConnection = connection as AVCaptureConnection 328 | return videoConnection.activeVideoStabilizationMode 329 | } 330 | } 331 | } 332 | } 333 | 334 | return .off 335 | } 336 | 337 | // MARK: - Private properties 338 | 339 | fileprivate var locationManager: CameraLocationManager? 340 | 341 | fileprivate weak var embeddingView: UIView? 342 | fileprivate var videoCompletion: ((_ videoURL: URL?, _ error: NSError?) -> Void)? 343 | 344 | fileprivate var sessionQueue: DispatchQueue = DispatchQueue(label: "CameraSessionQueue", attributes: []) 345 | 346 | fileprivate lazy var frontCameraDevice: AVCaptureDevice? = { 347 | AVCaptureDevice.videoDevices.filter { $0.position == .front }.first 348 | }() 349 | 350 | fileprivate lazy var backCameraDevice: AVCaptureDevice? = { 351 | AVCaptureDevice.videoDevices.filter { $0.position == .back }.first 352 | }() 353 | 354 | fileprivate lazy var mic: AVCaptureDevice? = { 355 | AVCaptureDevice.default(for: AVMediaType.audio) 356 | }() 357 | 358 | fileprivate var stillImageOutput: AVCaptureStillImageOutput? 359 | fileprivate var movieOutput: AVCaptureMovieFileOutput? 360 | fileprivate var previewLayer: AVCaptureVideoPreviewLayer? 361 | fileprivate var library: PHPhotoLibrary? 362 | 363 | fileprivate var cameraIsSetup = false 364 | fileprivate var cameraIsObservingDeviceOrientation = false 365 | 366 | fileprivate var zoomScale = CGFloat(1.0) 367 | fileprivate var beginZoomScale = CGFloat(1.0) 368 | fileprivate var maxZoomScale = CGFloat(1.0) 369 | 370 | fileprivate func _tempFilePath() -> URL { 371 | let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("tempMovie\(Date().timeIntervalSince1970)").appendingPathExtension("mp4") 372 | return tempURL 373 | } 374 | 375 | fileprivate var coreMotionManager: CMMotionManager! 376 | 377 | /// Real device orientation from DeviceMotion 378 | fileprivate var deviceOrientation: UIDeviceOrientation = .portrait 379 | 380 | // MARK: - CameraManager 381 | 382 | /** 383 | Inits a capture session and adds a preview layer to the given view. Preview layer bounds will automaticaly be set to match given view. Default session is initialized with still image output. 384 | 385 | :param: view The view you want to add the preview layer to 386 | :param: cameraOutputMode The mode you want capturesession to run image / video / video and microphone 387 | :param: completion Optional completion block 388 | 389 | :returns: Current state of the camera: Ready / AccessDenied / NoDeviceFound / NotDetermined. 390 | */ 391 | @discardableResult open func addPreviewLayerToView(_ view: UIView) -> CameraState { 392 | return addPreviewLayerToView(view, newCameraOutputMode: cameraOutputMode) 393 | } 394 | 395 | @discardableResult open func addPreviewLayerToView(_ view: UIView, newCameraOutputMode: CameraOutputMode) -> CameraState { 396 | return addLayerPreviewToView(view, newCameraOutputMode: newCameraOutputMode, completion: nil) 397 | } 398 | 399 | @discardableResult open func addLayerPreviewToView(_ view: UIView, newCameraOutputMode: CameraOutputMode, completion: (() -> Void)?) -> CameraState { 400 | if _canLoadCamera() { 401 | if let _ = embeddingView { 402 | if let validPreviewLayer = previewLayer { 403 | validPreviewLayer.removeFromSuperlayer() 404 | } 405 | } 406 | if cameraIsSetup { 407 | _addPreviewLayerToView(view) 408 | cameraOutputMode = newCameraOutputMode 409 | if let validCompletion = completion { 410 | validCompletion() 411 | } 412 | } else { 413 | _setupCamera { 414 | self._addPreviewLayerToView(view) 415 | self.cameraOutputMode = newCameraOutputMode 416 | if let validCompletion = completion { 417 | validCompletion() 418 | } 419 | } 420 | } 421 | } 422 | return _checkIfCameraIsAvailable() 423 | } 424 | 425 | /** 426 | Zoom in to the requested scale. 427 | */ 428 | open func zoom(_ scale: CGFloat) { 429 | _zoom(scale) 430 | } 431 | 432 | /** 433 | Asks the user for camera permissions. Only works if the permissions are not yet determined. Note that it'll also automaticaly ask about the microphone permissions if you selected VideoWithMic output. 434 | 435 | :param: completion Completion block with the result of permission request 436 | */ 437 | open func askUserForCameraPermission(_ completion: @escaping (Bool) -> Void) { 438 | AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (allowedAccess) -> Void in 439 | if self.cameraOutputMode == .videoWithMic { 440 | AVCaptureDevice.requestAccess(for: AVMediaType.audio, completionHandler: { (allowedAccess) -> Void in 441 | DispatchQueue.main.async { () -> Void in 442 | completion(allowedAccess) 443 | } 444 | }) 445 | } else { 446 | DispatchQueue.main.async { () -> Void in 447 | completion(allowedAccess) 448 | } 449 | } 450 | }) 451 | } 452 | 453 | /** 454 | Stops running capture session but all setup devices, inputs and outputs stay for further reuse. 455 | */ 456 | open func stopCaptureSession() { 457 | captureSession?.stopRunning() 458 | _stopFollowingDeviceOrientation() 459 | } 460 | 461 | /** 462 | Resumes capture session. 463 | */ 464 | open func resumeCaptureSession() { 465 | if let validCaptureSession = captureSession { 466 | if !validCaptureSession.isRunning, cameraIsSetup { 467 | sessionQueue.async { 468 | validCaptureSession.startRunning() 469 | self._startFollowingDeviceOrientation() 470 | } 471 | } 472 | } else { 473 | if _canLoadCamera() { 474 | if cameraIsSetup { 475 | stopAndRemoveCaptureSession() 476 | } 477 | _setupCamera { 478 | if let validEmbeddingView = self.embeddingView { 479 | self._addPreviewLayerToView(validEmbeddingView) 480 | } 481 | self._startFollowingDeviceOrientation() 482 | } 483 | } 484 | } 485 | } 486 | 487 | /** 488 | Stops running capture session and removes all setup devices, inputs and outputs. 489 | */ 490 | open func stopAndRemoveCaptureSession() { 491 | stopCaptureSession() 492 | let oldAnimationValue = animateCameraDeviceChange 493 | animateCameraDeviceChange = false 494 | cameraDevice = .back 495 | cameraIsSetup = false 496 | previewLayer = nil 497 | captureSession = nil 498 | frontCameraDevice = nil 499 | backCameraDevice = nil 500 | mic = nil 501 | stillImageOutput = nil 502 | movieOutput = nil 503 | animateCameraDeviceChange = oldAnimationValue 504 | } 505 | 506 | /** 507 | Captures still image from currently running capture session. 508 | 509 | :param: imageCompletion Completion block containing the captured UIImage 510 | */ 511 | @available(*, deprecated) 512 | open func capturePictureWithCompletion(_ imageCompletion: @escaping (UIImage?, NSError?) -> Void) { 513 | func completion(_ result: CaptureResult) { 514 | switch result { 515 | case let .success(content): 516 | imageCompletion(content.asImage, nil) 517 | case .failure: 518 | imageCompletion(nil, NSError()) 519 | } 520 | } 521 | 522 | capturePictureWithCompletion(completion) 523 | } 524 | 525 | /** 526 | Captures still image from currently running capture session. 527 | 528 | :param: imageCompletion Completion block containing the captured UIImage 529 | */ 530 | open func capturePictureWithCompletion(_ imageCompletion: @escaping (CaptureResult) -> Void) { 531 | capturePictureDataWithCompletion { result in 532 | 533 | guard let imageData = result.imageData else { 534 | if case let .failure(error) = result { 535 | imageCompletion(.failure(error)) 536 | } else { 537 | imageCompletion(.failure(CaptureError.noImageData)) 538 | } 539 | 540 | return 541 | } 542 | 543 | if self.animateShutter { 544 | self._performShutterAnimation { 545 | self._capturePicture(imageData, imageCompletion) 546 | } 547 | } else { 548 | self._capturePicture(imageData, imageCompletion) 549 | } 550 | } 551 | } 552 | 553 | fileprivate func _capturePicture(_ imageData: Data, _ imageCompletion: @escaping (CaptureResult) -> Void) { 554 | guard let img = UIImage(data: imageData) else { 555 | imageCompletion(.failure(NSError())) 556 | return 557 | } 558 | 559 | let image = fixOrientation(withImage: img) 560 | let newImageData = _imageDataWithEXIF(forImage: image, imageData)! as Data 561 | 562 | if writeFilesToPhoneLibrary { 563 | let filePath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("tempImg\(Int(Date().timeIntervalSince1970)).jpg") 564 | 565 | do { 566 | try newImageData.write(to: filePath) 567 | 568 | // make sure that doesn't fail the first time 569 | if PHPhotoLibrary.authorizationStatus() != .authorized { 570 | PHPhotoLibrary.requestAuthorization { status in 571 | if status == PHAuthorizationStatus.authorized { 572 | self._saveImageToLibrary(atFileURL: filePath, imageCompletion) 573 | } 574 | } 575 | } else { 576 | _saveImageToLibrary(atFileURL: filePath, imageCompletion) 577 | } 578 | 579 | } catch { 580 | imageCompletion(.failure(error)) 581 | return 582 | } 583 | } 584 | 585 | imageCompletion(CaptureResult(newImageData)) 586 | } 587 | 588 | fileprivate func _setVideoWithGPS(forLocation location: CLLocation) { 589 | let metadata = AVMutableMetadataItem() 590 | metadata.keySpace = AVMetadataKeySpace.quickTimeMetadata 591 | metadata.key = AVMetadataKey.quickTimeMetadataKeyLocationISO6709 as NSString 592 | metadata.identifier = AVMetadataIdentifier.quickTimeMetadataLocationISO6709 593 | metadata.value = String(format: "%+09.5f%+010.5f%+.0fCRSWGS_84", location.coordinate.latitude, location.coordinate.longitude, location.altitude) as NSString 594 | _getMovieOutput().metadata = [metadata] 595 | } 596 | 597 | fileprivate func _imageDataWithEXIF(forImage _: UIImage, _ data: Data) -> NSData? { 598 | let cfdata: CFData = data as CFData 599 | let source = CGImageSourceCreateWithData(cfdata, nil)! 600 | let UTI: CFString = CGImageSourceGetType(source)! 601 | let mutableData: CFMutableData = NSMutableData(data: data) as CFMutableData 602 | let destination = CGImageDestinationCreateWithData(mutableData, UTI, 1, nil)! 603 | 604 | let imageSourceRef = CGImageSourceCreateWithData(cfdata, nil) 605 | let imageProperties = CGImageSourceCopyMetadataAtIndex(imageSourceRef!, 0, nil)! 606 | 607 | var mutableMetadata = CGImageMetadataCreateMutableCopy(imageProperties)! 608 | 609 | if let location = locationManager?.latestLocation { 610 | mutableMetadata = _gpsMetadata(mutableMetadata, withLocation: location) 611 | } 612 | 613 | let finalMetadata: CGImageMetadata = mutableMetadata 614 | CGImageDestinationAddImageAndMetadata(destination, UIImage(data: data)!.cgImage!, finalMetadata, nil) 615 | CGImageDestinationFinalize(destination) 616 | return mutableData 617 | } 618 | 619 | fileprivate func _gpsMetadata(_ imageMetadata: CGMutableImageMetadata, withLocation location: CLLocation) -> CGMutableImageMetadata { 620 | let altitudeRef = Int(location.altitude < 0.0 ? 1 : 0) 621 | let latitudeRef = location.coordinate.latitude < 0.0 ? "S" : "N" 622 | let longitudeRef = location.coordinate.longitude < 0.0 ? "W" : "E" 623 | 624 | let f = DateFormatter() 625 | f.timeZone = TimeZone(abbreviation: "UTC") 626 | 627 | f.dateFormat = "yyyy:MM:dd" 628 | let isoDate = f.string(from: location.timestamp) 629 | 630 | f.dateFormat = "HH:mm:ss.SSSSSS" 631 | let isoTime = f.string(from: location.timestamp) 632 | 633 | CGImageMetadataSetValueMatchingImageProperty(imageMetadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLatitudeRef, latitudeRef as CFTypeRef) 634 | CGImageMetadataSetValueMatchingImageProperty(imageMetadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLatitude, abs(location.coordinate.latitude) as CFTypeRef) 635 | CGImageMetadataSetValueMatchingImageProperty(imageMetadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLongitudeRef, longitudeRef as CFTypeRef) 636 | CGImageMetadataSetValueMatchingImageProperty(imageMetadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLongitude, abs(location.coordinate.longitude) as CFTypeRef) 637 | CGImageMetadataSetValueMatchingImageProperty(imageMetadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSAltitude, Int(abs(location.altitude)) as CFTypeRef) 638 | CGImageMetadataSetValueMatchingImageProperty(imageMetadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSAltitudeRef, altitudeRef as CFTypeRef) 639 | CGImageMetadataSetValueMatchingImageProperty(imageMetadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSTimeStamp, isoTime as CFTypeRef) 640 | CGImageMetadataSetValueMatchingImageProperty(imageMetadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSDateStamp, isoDate as CFTypeRef) 641 | 642 | return imageMetadata 643 | } 644 | 645 | fileprivate func _saveImageToLibrary(atFileURL filePath: URL, _ imageCompletion: @escaping (CaptureResult) -> Void) { 646 | let location = locationManager?.latestLocation 647 | let date = Date() 648 | 649 | library?.save(imageAtURL: filePath, albumName: imageAlbumName, date: date, location: location) { asset in 650 | 651 | guard let _ = asset else { 652 | return imageCompletion(.failure(CaptureError.assetNotSaved)) 653 | } 654 | } 655 | } 656 | 657 | /** 658 | Captures still image from currently running capture session. 659 | 660 | :param: imageCompletion Completion block containing the captured imageData 661 | */ 662 | @available(*, deprecated) 663 | open func capturePictureDataWithCompletion(_ imageCompletion: @escaping (Data?, NSError?) -> Void) { 664 | func completion(_ result: CaptureResult) { 665 | switch result { 666 | case let .success(content): 667 | imageCompletion(content.asData, nil) 668 | case .failure: 669 | imageCompletion(nil, NSError()) 670 | } 671 | } 672 | capturePictureDataWithCompletion(completion) 673 | } 674 | 675 | /** 676 | Captures still image from currently running capture session. 677 | 678 | :param: imageCompletion Completion block containing the captured imageData 679 | */ 680 | open func capturePictureDataWithCompletion(_ imageCompletion: @escaping (CaptureResult) -> Void) { 681 | guard cameraIsSetup else { 682 | _show(NSLocalizedString("No capture session setup", comment: ""), message: NSLocalizedString("I can't take any picture", comment: "")) 683 | return 684 | } 685 | 686 | guard cameraOutputMode == .stillImage else { 687 | _show(NSLocalizedString("Capture session output mode video", comment: ""), message: NSLocalizedString("I can't take any picture", comment: "")) 688 | return 689 | } 690 | 691 | _updateIlluminationMode(flashMode) 692 | 693 | sessionQueue.async { 694 | let stillImageOutput = self._getStillImageOutput() 695 | if let connection = stillImageOutput.connection(with: AVMediaType.video), 696 | connection.isEnabled { 697 | if self.cameraDevice == CameraDevice.front, connection.isVideoMirroringSupported, 698 | self.shouldFlipFrontCameraImage { 699 | connection.isVideoMirrored = true 700 | } 701 | if connection.isVideoOrientationSupported { 702 | connection.videoOrientation = self._currentCaptureVideoOrientation() 703 | } 704 | 705 | stillImageOutput.captureStillImageAsynchronously(from: connection, completionHandler: { [weak self] sample, error in 706 | 707 | if let error = error { 708 | self?._show(NSLocalizedString("Error", comment: ""), message: error.localizedDescription) 709 | imageCompletion(.failure(error)) 710 | return 711 | } 712 | 713 | guard let sample = sample else { imageCompletion(.failure(CaptureError.noSampleBuffer)); return } 714 | if let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(sample) { 715 | imageCompletion(CaptureResult(imageData)) 716 | } else { 717 | imageCompletion(.failure(CaptureError.noImageData)) 718 | } 719 | 720 | }) 721 | } else { 722 | imageCompletion(.failure(CaptureError.noVideoConnection)) 723 | } 724 | } 725 | } 726 | 727 | fileprivate func _imageOrientation(forDeviceOrientation deviceOrientation: UIDeviceOrientation, isMirrored: Bool) -> UIImage.Orientation { 728 | switch deviceOrientation { 729 | case .landscapeLeft: 730 | return isMirrored ? .upMirrored : .up 731 | case .landscapeRight: 732 | return isMirrored ? .downMirrored : .down 733 | default: 734 | break 735 | } 736 | 737 | return isMirrored ? .leftMirrored : .right 738 | } 739 | 740 | /** 741 | Starts recording a video with or without voice as in the session preset. 742 | */ 743 | open func startRecordingVideo() { 744 | guard cameraOutputMode != .stillImage else { 745 | _show(NSLocalizedString("Capture session output still image", comment: ""), message: NSLocalizedString("I can only take pictures", comment: "")) 746 | return 747 | } 748 | 749 | let videoOutput = _getMovieOutput() 750 | 751 | if shouldUseLocationServices { 752 | 753 | let specs = [kCMMetadataFormatDescriptionMetadataSpecificationKey_Identifier as String: AVMetadataIdentifier.quickTimeMetadataLocationISO6709, 754 | kCMMetadataFormatDescriptionMetadataSpecificationKey_DataType as String: kCMMetadataDataType_QuickTimeMetadataLocation_ISO6709 as String] as [String: Any] 755 | 756 | var locationMetadataDesc: CMFormatDescription? 757 | CMMetadataFormatDescriptionCreateWithMetadataSpecifications(allocator: kCFAllocatorDefault, metadataType: kCMMetadataFormatType_Boxed, metadataSpecifications: [specs] as CFArray, formatDescriptionOut: &locationMetadataDesc) 758 | 759 | // Create the metadata input and add it to the session. 760 | guard let captureSession = captureSession, let locationMetadata = locationMetadataDesc else { 761 | return 762 | } 763 | 764 | let newLocationMetadataInput = AVCaptureMetadataInput(formatDescription: locationMetadata, clock: CMClockGetHostTimeClock()) 765 | captureSession.addInputWithNoConnections(newLocationMetadataInput) 766 | 767 | // Connect the location metadata input to the movie file output. 768 | let inputPort = newLocationMetadataInput.ports[0] 769 | captureSession.addConnection(AVCaptureConnection(inputPorts: [inputPort], output: videoOutput)) 770 | 771 | } 772 | 773 | _updateIlluminationMode(flashMode) 774 | 775 | videoOutput.startRecording(to: _tempFilePath(), recordingDelegate: self) 776 | } 777 | 778 | /** 779 | Stop recording a video. Save it to the cameraRoll and give back the url. 780 | */ 781 | open func stopVideoRecording(_ completion: ((_ videoURL: URL?, _ error: NSError?) -> Void)?) { 782 | if let runningMovieOutput = movieOutput, 783 | runningMovieOutput.isRecording { 784 | videoCompletion = completion 785 | runningMovieOutput.stopRecording() 786 | } 787 | } 788 | 789 | /** 790 | The signature for a handler. 791 | The success value is the string representation of a scanned QR code, if any. 792 | */ 793 | public typealias QRCodeDetectionHandler = (Result) -> Void 794 | 795 | /** 796 | Start detecting QR codes. 797 | */ 798 | open func startQRCodeDetection(_ handler: @escaping QRCodeDetectionHandler) { 799 | guard let captureSession = self.captureSession 800 | else { return } 801 | 802 | let output = AVCaptureMetadataOutput() 803 | 804 | guard captureSession.canAddOutput(output) 805 | else { return } 806 | 807 | qrCodeDetectionHandler = handler 808 | captureSession.addOutput(output) 809 | 810 | // Note: The object types must be set after the output was added to the capture session. 811 | output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) 812 | output.metadataObjectTypes = [.qr, .ean8, .ean13, .pdf417].filter { output.availableMetadataObjectTypes.contains($0) } 813 | } 814 | 815 | /** 816 | Stop detecting QR codes. 817 | */ 818 | open func stopQRCodeDetection() { 819 | qrCodeDetectionHandler = nil 820 | 821 | if let output = qrOutput { 822 | captureSession?.removeOutput(output) 823 | } 824 | qrOutput = nil 825 | } 826 | 827 | /** 828 | The stored handler for QR codes. 829 | */ 830 | private var qrCodeDetectionHandler: QRCodeDetectionHandler? 831 | 832 | /** 833 | The stored meta data output; used to detect QR codes. 834 | */ 835 | private var qrOutput: AVCaptureOutput? 836 | 837 | /** 838 | Check if the device rotation is locked 839 | */ 840 | open func deviceOrientationMatchesInterfaceOrientation() -> Bool { 841 | return deviceOrientation == UIDevice.current.orientation 842 | } 843 | 844 | /** 845 | Current camera status. 846 | 847 | :returns: Current state of the camera: Ready / AccessDenied / NoDeviceFound / NotDetermined 848 | */ 849 | open func currentCameraStatus() -> CameraState { 850 | return _checkIfCameraIsAvailable() 851 | } 852 | 853 | /** 854 | Change current flash mode to next value from available ones. 855 | 856 | :returns: Current flash mode: Off / On / Auto 857 | */ 858 | open func changeFlashMode() -> CameraFlashMode { 859 | guard let newFlashMode = CameraFlashMode(rawValue: (flashMode.rawValue + 1) % 3) else { return flashMode } 860 | flashMode = newFlashMode 861 | return flashMode 862 | } 863 | 864 | /** 865 | Check the camera device has flash 866 | */ 867 | open func hasFlash(for cameraDevice: CameraDevice) -> Bool { 868 | let devices = AVCaptureDevice.videoDevices 869 | for device in devices { 870 | if device.position == .back, cameraDevice == .back { 871 | return device.hasFlash 872 | } else if device.position == .front, cameraDevice == .front { 873 | return device.hasFlash 874 | } 875 | } 876 | return false 877 | } 878 | 879 | // MARK: - AVCaptureFileOutputRecordingDelegate 880 | 881 | public func fileOutput(_: AVCaptureFileOutput, didStartRecordingTo _: URL, from _: [AVCaptureConnection]) { 882 | captureSession?.beginConfiguration() 883 | if flashMode != .off { 884 | _updateIlluminationMode(flashMode) 885 | } 886 | 887 | captureSession?.commitConfiguration() 888 | } 889 | 890 | open func fileOutput(_: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from _: [AVCaptureConnection], error: Error?) { 891 | if let error = error { 892 | _show(NSLocalizedString("Unable to save video to the device", comment: ""), message: error.localizedDescription) 893 | } else { 894 | if writeFilesToPhoneLibrary { 895 | if PHPhotoLibrary.authorizationStatus() == .authorized { 896 | _saveVideoToLibrary(outputFileURL) 897 | } else { 898 | PHPhotoLibrary.requestAuthorization { autorizationStatus in 899 | if autorizationStatus == .authorized { 900 | self._saveVideoToLibrary(outputFileURL) 901 | } 902 | } 903 | } 904 | } else { 905 | _executeVideoCompletionWithURL(outputFileURL, error: error as NSError?) 906 | } 907 | } 908 | } 909 | 910 | fileprivate func _saveVideoToLibrary(_ fileURL: URL) { 911 | let location = locationManager?.latestLocation 912 | let date = Date() 913 | 914 | library?.save(videoAtURL: fileURL, albumName: videoAlbumName, date: date, location: location, completion: { _ in 915 | self._executeVideoCompletionWithURL(fileURL, error: nil) 916 | }) 917 | } 918 | 919 | // MARK: - UIGestureRecognizerDelegate 920 | 921 | fileprivate lazy var zoomGesture = UIPinchGestureRecognizer() 922 | 923 | fileprivate func attachZoom(_ view: UIView) { 924 | DispatchQueue.main.async { 925 | self.zoomGesture.addTarget(self, action: #selector(CameraManager._zoomStart(_:))) 926 | view.addGestureRecognizer(self.zoomGesture) 927 | self.zoomGesture.delegate = self 928 | } 929 | } 930 | 931 | open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 932 | if gestureRecognizer.isKind(of: UIPinchGestureRecognizer.self) { 933 | beginZoomScale = zoomScale 934 | } 935 | 936 | return true 937 | } 938 | 939 | @objc fileprivate func _zoomStart(_ recognizer: UIPinchGestureRecognizer) { 940 | guard let view = embeddingView, 941 | let previewLayer = previewLayer 942 | else { return } 943 | 944 | var allTouchesOnPreviewLayer = true 945 | let numTouch = recognizer.numberOfTouches 946 | 947 | for i in 0 ..< numTouch { 948 | let location = recognizer.location(ofTouch: i, in: view) 949 | let convertedTouch = previewLayer.convert(location, from: previewLayer.superlayer) 950 | if !previewLayer.contains(convertedTouch) { 951 | allTouchesOnPreviewLayer = false 952 | break 953 | } 954 | } 955 | if allTouchesOnPreviewLayer { 956 | _zoom(recognizer.scale) 957 | } 958 | } 959 | 960 | fileprivate func _zoom(_ scale: CGFloat) { 961 | let device: AVCaptureDevice? 962 | 963 | switch cameraDevice { 964 | case .back: 965 | device = backCameraDevice 966 | case .front: 967 | device = frontCameraDevice 968 | } 969 | 970 | do { 971 | let captureDevice = device 972 | try captureDevice?.lockForConfiguration() 973 | 974 | zoomScale = max(1.0, min(beginZoomScale * scale, maxZoomScale)) 975 | 976 | captureDevice?.videoZoomFactor = zoomScale 977 | 978 | captureDevice?.unlockForConfiguration() 979 | 980 | } catch { 981 | print("Error locking configuration") 982 | } 983 | } 984 | 985 | // MARK: - UIGestureRecognizerDelegate 986 | 987 | fileprivate lazy var focusGesture = UITapGestureRecognizer() 988 | 989 | fileprivate func attachFocus(_ view: UIView) { 990 | DispatchQueue.main.async { 991 | self.focusGesture.addTarget(self, action: #selector(CameraManager._focusStart(_:))) 992 | view.addGestureRecognizer(self.focusGesture) 993 | self.focusGesture.delegate = self 994 | } 995 | } 996 | 997 | fileprivate lazy var exposureGesture = UIPanGestureRecognizer() 998 | 999 | fileprivate func attachExposure(_ view: UIView) { 1000 | DispatchQueue.main.async { 1001 | self.exposureGesture.addTarget(self, action: #selector(CameraManager._exposureStart(_:))) 1002 | view.addGestureRecognizer(self.exposureGesture) 1003 | self.exposureGesture.delegate = self 1004 | } 1005 | } 1006 | 1007 | @objc fileprivate func _focusStart(_ recognizer: UITapGestureRecognizer) { 1008 | let device: AVCaptureDevice? 1009 | 1010 | switch cameraDevice { 1011 | case .back: 1012 | device = backCameraDevice 1013 | case .front: 1014 | device = frontCameraDevice 1015 | } 1016 | 1017 | _changeExposureMode(mode: .continuousAutoExposure) 1018 | translationY = 0 1019 | exposureValue = 0.5 1020 | 1021 | if let validDevice = device, 1022 | let validPreviewLayer = previewLayer, 1023 | let view = recognizer.view { 1024 | let pointInPreviewLayer = view.layer.convert(recognizer.location(in: view), to: validPreviewLayer) 1025 | let pointOfInterest = validPreviewLayer.captureDevicePointConverted(fromLayerPoint: pointInPreviewLayer) 1026 | 1027 | do { 1028 | try validDevice.lockForConfiguration() 1029 | 1030 | _showFocusRectangleAtPoint(pointInPreviewLayer, inLayer: validPreviewLayer) 1031 | 1032 | if validDevice.isFocusPointOfInterestSupported { 1033 | validDevice.focusPointOfInterest = pointOfInterest 1034 | } 1035 | 1036 | if validDevice.isExposurePointOfInterestSupported { 1037 | validDevice.exposurePointOfInterest = pointOfInterest 1038 | } 1039 | 1040 | if validDevice.isFocusModeSupported(focusMode) { 1041 | validDevice.focusMode = focusMode 1042 | } 1043 | 1044 | if validDevice.isExposureModeSupported(exposureMode) { 1045 | validDevice.exposureMode = exposureMode 1046 | } 1047 | 1048 | validDevice.unlockForConfiguration() 1049 | } catch { 1050 | print(error) 1051 | } 1052 | } 1053 | } 1054 | 1055 | fileprivate var lastFocusRectangle: CAShapeLayer? 1056 | fileprivate var lastFocusPoint: CGPoint? 1057 | fileprivate func _showFocusRectangleAtPoint(_ focusPoint: CGPoint, inLayer layer: CALayer, withBrightness brightness: Float? = nil) { 1058 | if let lastFocusRectangle = lastFocusRectangle { 1059 | lastFocusRectangle.removeFromSuperlayer() 1060 | self.lastFocusRectangle = nil 1061 | } 1062 | 1063 | let size = CGSize(width: 75, height: 75) 1064 | let rect = CGRect(origin: CGPoint(x: focusPoint.x - size.width / 2.0, y: focusPoint.y - size.height / 2.0), size: size) 1065 | 1066 | let endPath = UIBezierPath(rect: rect) 1067 | endPath.move(to: CGPoint(x: rect.minX + size.width / 2.0, y: rect.minY)) 1068 | endPath.addLine(to: CGPoint(x: rect.minX + size.width / 2.0, y: rect.minY + 5.0)) 1069 | endPath.move(to: CGPoint(x: rect.maxX, y: rect.minY + size.height / 2.0)) 1070 | endPath.addLine(to: CGPoint(x: rect.maxX - 5.0, y: rect.minY + size.height / 2.0)) 1071 | endPath.move(to: CGPoint(x: rect.minX + size.width / 2.0, y: rect.maxY)) 1072 | endPath.addLine(to: CGPoint(x: rect.minX + size.width / 2.0, y: rect.maxY - 5.0)) 1073 | endPath.move(to: CGPoint(x: rect.minX, y: rect.minY + size.height / 2.0)) 1074 | endPath.addLine(to: CGPoint(x: rect.minX + 5.0, y: rect.minY + size.height / 2.0)) 1075 | if brightness != nil { 1076 | endPath.move(to: CGPoint(x: rect.minX + size.width + size.width / 4, y: rect.minY)) 1077 | endPath.addLine(to: CGPoint(x: rect.minX + size.width + size.width / 4, y: rect.minY + size.height)) 1078 | 1079 | endPath.move(to: CGPoint(x: rect.minX + size.width + size.width / 4 - size.width / 16, y: rect.minY + size.height - CGFloat(brightness!) * size.height)) 1080 | endPath.addLine(to: CGPoint(x: rect.minX + size.width + size.width / 4 + size.width / 16, y: rect.minY + size.height - CGFloat(brightness!) * size.height)) 1081 | } 1082 | 1083 | let startPath = UIBezierPath(cgPath: endPath.cgPath) 1084 | let scaleAroundCenterTransform = CGAffineTransform(translationX: -focusPoint.x, y: -focusPoint.y).concatenating(CGAffineTransform(scaleX: 2.0, y: 2.0).concatenating(CGAffineTransform(translationX: focusPoint.x, y: focusPoint.y))) 1085 | startPath.apply(scaleAroundCenterTransform) 1086 | 1087 | let shapeLayer = CAShapeLayer() 1088 | shapeLayer.path = endPath.cgPath 1089 | shapeLayer.fillColor = UIColor.clear.cgColor 1090 | shapeLayer.strokeColor = UIColor(red: 1, green: 0.83, blue: 0, alpha: 0.95).cgColor 1091 | shapeLayer.lineWidth = 1.0 1092 | 1093 | layer.addSublayer(shapeLayer) 1094 | lastFocusRectangle = shapeLayer 1095 | lastFocusPoint = focusPoint 1096 | 1097 | CATransaction.begin() 1098 | 1099 | CATransaction.setAnimationDuration(0.2) 1100 | CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)) 1101 | 1102 | CATransaction.setCompletionBlock { 1103 | if shapeLayer.superlayer != nil { 1104 | shapeLayer.removeFromSuperlayer() 1105 | self.lastFocusRectangle = nil 1106 | } 1107 | } 1108 | if brightness == nil { 1109 | let appearPathAnimation = CABasicAnimation(keyPath: "path") 1110 | appearPathAnimation.fromValue = startPath.cgPath 1111 | appearPathAnimation.toValue = endPath.cgPath 1112 | shapeLayer.add(appearPathAnimation, forKey: "path") 1113 | 1114 | let appearOpacityAnimation = CABasicAnimation(keyPath: "opacity") 1115 | appearOpacityAnimation.fromValue = 0.0 1116 | appearOpacityAnimation.toValue = 1.0 1117 | shapeLayer.add(appearOpacityAnimation, forKey: "opacity") 1118 | } 1119 | 1120 | let disappearOpacityAnimation = CABasicAnimation(keyPath: "opacity") 1121 | disappearOpacityAnimation.fromValue = 1.0 1122 | disappearOpacityAnimation.toValue = 0.0 1123 | disappearOpacityAnimation.beginTime = CACurrentMediaTime() + 0.8 1124 | disappearOpacityAnimation.fillMode = CAMediaTimingFillMode.forwards 1125 | disappearOpacityAnimation.isRemovedOnCompletion = false 1126 | shapeLayer.add(disappearOpacityAnimation, forKey: "opacity") 1127 | 1128 | CATransaction.commit() 1129 | } 1130 | 1131 | var exposureValue: Float = 0.1 // EV 1132 | var translationY: Float = 0 1133 | var startPanPointInPreviewLayer: CGPoint? 1134 | 1135 | let exposureDurationPower: Float = 4.0 // the exposure slider gain 1136 | let exposureMininumDuration: Float64 = 1.0 / 2000.0 1137 | 1138 | @objc fileprivate func _exposureStart(_ gestureRecognizer: UIPanGestureRecognizer) { 1139 | guard gestureRecognizer.view != nil else { return } 1140 | let view = gestureRecognizer.view! 1141 | 1142 | _changeExposureMode(mode: .custom) 1143 | 1144 | let translation = gestureRecognizer.translation(in: view) 1145 | let currentTranslation = translationY + Float(translation.y) 1146 | if gestureRecognizer.state == .ended { 1147 | translationY = currentTranslation 1148 | } 1149 | if currentTranslation < 0 { 1150 | // up - brighter 1151 | exposureValue = 0.5 + min(abs(currentTranslation) / 400, 1) / 2 1152 | } else if currentTranslation >= 0 { 1153 | // down - lower 1154 | exposureValue = 0.5 - min(abs(currentTranslation) / 400, 1) / 2 1155 | } 1156 | _changeExposureDuration(value: exposureValue) 1157 | 1158 | // UI Visualization 1159 | if gestureRecognizer.state == .began { 1160 | if let validPreviewLayer = previewLayer { 1161 | startPanPointInPreviewLayer = view.layer.convert(gestureRecognizer.location(in: view), to: validPreviewLayer) 1162 | } 1163 | } 1164 | 1165 | if let validPreviewLayer = previewLayer, let lastFocusPoint = self.lastFocusPoint { 1166 | _showFocusRectangleAtPoint(lastFocusPoint, inLayer: validPreviewLayer, withBrightness: exposureValue) 1167 | } 1168 | } 1169 | 1170 | // Available modes: 1171 | // .Locked .AutoExpose .ContinuousAutoExposure .Custom 1172 | func _changeExposureMode(mode: AVCaptureDevice.ExposureMode) { 1173 | let device: AVCaptureDevice? 1174 | 1175 | switch cameraDevice { 1176 | case .back: 1177 | device = backCameraDevice 1178 | case .front: 1179 | device = frontCameraDevice 1180 | } 1181 | if device?.exposureMode == mode { 1182 | return 1183 | } 1184 | 1185 | do { 1186 | try device?.lockForConfiguration() 1187 | 1188 | if device?.isExposureModeSupported(mode) == true { 1189 | device?.exposureMode = mode 1190 | } 1191 | device?.unlockForConfiguration() 1192 | 1193 | } catch { 1194 | return 1195 | } 1196 | } 1197 | 1198 | func _changeExposureDuration(value: Float) { 1199 | if cameraIsSetup { 1200 | let device: AVCaptureDevice? 1201 | 1202 | switch cameraDevice { 1203 | case .back: 1204 | device = backCameraDevice 1205 | case .front: 1206 | device = frontCameraDevice 1207 | } 1208 | 1209 | guard let videoDevice = device else { 1210 | return 1211 | } 1212 | 1213 | do { 1214 | try videoDevice.lockForConfiguration() 1215 | 1216 | let p = Float64(pow(value, exposureDurationPower)) // Apply power function to expand slider's low-end range 1217 | let minDurationSeconds = Float64(max(CMTimeGetSeconds(videoDevice.activeFormat.minExposureDuration), exposureMininumDuration)) 1218 | let maxDurationSeconds = Float64(CMTimeGetSeconds(videoDevice.activeFormat.maxExposureDuration)) 1219 | let newDurationSeconds = Float64(p * (maxDurationSeconds - minDurationSeconds)) + minDurationSeconds // Scale from 0-1 slider range to actual duration 1220 | 1221 | if videoDevice.exposureMode == .custom { 1222 | let newExposureTime = CMTimeMakeWithSeconds(Float64(newDurationSeconds), preferredTimescale: 1000 * 1000 * 1000) 1223 | videoDevice.setExposureModeCustom(duration: newExposureTime, iso: AVCaptureDevice.currentISO, completionHandler: nil) 1224 | } 1225 | 1226 | videoDevice.unlockForConfiguration() 1227 | } catch { 1228 | return 1229 | } 1230 | } 1231 | } 1232 | 1233 | // MARK: - CameraManager() 1234 | 1235 | fileprivate func _executeVideoCompletionWithURL(_ url: URL?, error: NSError?) { 1236 | if let validCompletion = videoCompletion { 1237 | validCompletion(url, error) 1238 | videoCompletion = nil 1239 | } 1240 | } 1241 | 1242 | fileprivate func _getMovieOutput() -> AVCaptureMovieFileOutput { 1243 | if movieOutput == nil { 1244 | _createMovieOutput() 1245 | } 1246 | 1247 | return movieOutput! 1248 | } 1249 | 1250 | fileprivate func _createMovieOutput() { 1251 | 1252 | let newMovieOutput = AVCaptureMovieFileOutput() 1253 | newMovieOutput.movieFragmentInterval = CMTime.invalid 1254 | 1255 | movieOutput = newMovieOutput 1256 | 1257 | _setupVideoConnection() 1258 | 1259 | if let captureSession = captureSession, captureSession.canAddOutput(newMovieOutput) { 1260 | captureSession.beginConfiguration() 1261 | captureSession.addOutput(newMovieOutput) 1262 | captureSession.commitConfiguration() 1263 | } 1264 | } 1265 | 1266 | fileprivate func _setupVideoConnection() { 1267 | if let movieOutput = movieOutput { 1268 | for connection in movieOutput.connections { 1269 | for port in connection.inputPorts { 1270 | if port.mediaType == AVMediaType.video { 1271 | let videoConnection = connection as AVCaptureConnection 1272 | // setup video mirroring 1273 | if videoConnection.isVideoMirroringSupported { 1274 | videoConnection.isVideoMirrored = (cameraDevice == CameraDevice.front && shouldFlipFrontCameraImage) 1275 | } 1276 | 1277 | if videoConnection.isVideoStabilizationSupported { 1278 | videoConnection.preferredVideoStabilizationMode = videoStabilisationMode 1279 | } 1280 | } 1281 | } 1282 | } 1283 | } 1284 | } 1285 | 1286 | fileprivate func _getStillImageOutput() -> AVCaptureStillImageOutput { 1287 | if let stillImageOutput = stillImageOutput, let connection = stillImageOutput.connection(with: AVMediaType.video), 1288 | connection.isActive { 1289 | return stillImageOutput 1290 | } 1291 | let newStillImageOutput = AVCaptureStillImageOutput() 1292 | stillImageOutput = newStillImageOutput 1293 | if let captureSession = captureSession, 1294 | captureSession.canAddOutput(newStillImageOutput) { 1295 | captureSession.beginConfiguration() 1296 | captureSession.addOutput(newStillImageOutput) 1297 | captureSession.commitConfiguration() 1298 | } 1299 | return newStillImageOutput 1300 | } 1301 | 1302 | @objc fileprivate func _orientationChanged() { 1303 | var currentConnection: AVCaptureConnection? 1304 | 1305 | switch cameraOutputMode { 1306 | case .stillImage: 1307 | currentConnection = stillImageOutput?.connection(with: AVMediaType.video) 1308 | case .videoOnly, .videoWithMic: 1309 | currentConnection = _getMovieOutput().connection(with: AVMediaType.video) 1310 | if let location = locationManager?.latestLocation { 1311 | _setVideoWithGPS(forLocation: location) 1312 | } 1313 | } 1314 | 1315 | if let validPreviewLayer = previewLayer { 1316 | if !shouldKeepViewAtOrientationChanges { 1317 | if let validPreviewLayerConnection = validPreviewLayer.connection, 1318 | validPreviewLayerConnection.isVideoOrientationSupported { 1319 | validPreviewLayerConnection.videoOrientation = _currentPreviewVideoOrientation() 1320 | } 1321 | } 1322 | if let validOutputLayerConnection = currentConnection, 1323 | validOutputLayerConnection.isVideoOrientationSupported { 1324 | validOutputLayerConnection.videoOrientation = _currentCaptureVideoOrientation() 1325 | } 1326 | if !shouldKeepViewAtOrientationChanges && cameraIsObservingDeviceOrientation { 1327 | DispatchQueue.main.async { () -> Void in 1328 | if let validEmbeddingView = self.embeddingView { 1329 | validPreviewLayer.frame = validEmbeddingView.bounds 1330 | } 1331 | } 1332 | } 1333 | } 1334 | } 1335 | 1336 | fileprivate func _currentCaptureVideoOrientation() -> AVCaptureVideoOrientation { 1337 | if deviceOrientation == .faceDown 1338 | || deviceOrientation == .faceUp 1339 | || deviceOrientation == .unknown { 1340 | return _currentPreviewVideoOrientation() 1341 | } 1342 | 1343 | return _videoOrientation(forDeviceOrientation: deviceOrientation) 1344 | } 1345 | 1346 | fileprivate func _currentPreviewDeviceOrientation() -> UIDeviceOrientation { 1347 | if shouldKeepViewAtOrientationChanges { 1348 | return .portrait 1349 | } 1350 | 1351 | return UIDevice.current.orientation 1352 | } 1353 | 1354 | fileprivate func _currentPreviewVideoOrientation() -> AVCaptureVideoOrientation { 1355 | let orientation = _currentPreviewDeviceOrientation() 1356 | return _videoOrientation(forDeviceOrientation: orientation) 1357 | } 1358 | 1359 | open func resetOrientation() { 1360 | // Main purpose is to reset the preview layer orientation. Problems occur if you are recording landscape, present a modal VC, 1361 | // then turn portriat to dismiss. The preview view is then stuck in a prior orientation and not redrawn. Calling this function 1362 | // will then update the orientation of the preview layer. 1363 | _orientationChanged() 1364 | } 1365 | 1366 | fileprivate func _videoOrientation(forDeviceOrientation deviceOrientation: UIDeviceOrientation) -> AVCaptureVideoOrientation { 1367 | switch deviceOrientation { 1368 | case .landscapeLeft: 1369 | return .landscapeRight 1370 | case .landscapeRight: 1371 | return .landscapeLeft 1372 | case .portraitUpsideDown: 1373 | return .portraitUpsideDown 1374 | case .faceUp: 1375 | /* 1376 | Attempt to keep the existing orientation. If the device was landscape, then face up 1377 | getting the orientation from the stats bar would fail every other time forcing it 1378 | to default to portrait which would introduce flicker into the preview layer. This 1379 | would not happen if it was in portrait then face up 1380 | */ 1381 | if let validPreviewLayer = previewLayer, let connection = validPreviewLayer.connection { 1382 | return connection.videoOrientation // Keep the existing orientation 1383 | } 1384 | // Could not get existing orientation, try to get it from stats bar 1385 | return _videoOrientationFromStatusBarOrientation() 1386 | case .faceDown: 1387 | /* 1388 | Attempt to keep the existing orientation. If the device was landscape, then face down 1389 | getting the orientation from the stats bar would fail every other time forcing it 1390 | to default to portrait which would introduce flicker into the preview layer. This 1391 | would not happen if it was in portrait then face down 1392 | */ 1393 | if let validPreviewLayer = previewLayer, let connection = validPreviewLayer.connection { 1394 | return connection.videoOrientation // Keep the existing orientation 1395 | } 1396 | // Could not get existing orientation, try to get it from stats bar 1397 | return _videoOrientationFromStatusBarOrientation() 1398 | default: 1399 | return .portrait 1400 | } 1401 | } 1402 | 1403 | fileprivate func _videoOrientationFromStatusBarOrientation() -> AVCaptureVideoOrientation { 1404 | var orientation: UIInterfaceOrientation? 1405 | 1406 | DispatchQueue.main.async { 1407 | orientation = UIApplication.shared.statusBarOrientation 1408 | } 1409 | 1410 | /* 1411 | Note - the following would fall into the guard every other call (it is called repeatedly) if the device was 1412 | landscape then face up/down. Did not seem to fail if in portrait first. 1413 | */ 1414 | guard let statusBarOrientation = orientation else { 1415 | return .portrait 1416 | } 1417 | 1418 | switch statusBarOrientation { 1419 | case .landscapeLeft: 1420 | return .landscapeLeft 1421 | case .landscapeRight: 1422 | return .landscapeRight 1423 | case .portrait: 1424 | return .portrait 1425 | case .portraitUpsideDown: 1426 | return .portraitUpsideDown 1427 | default: 1428 | return .portrait 1429 | } 1430 | } 1431 | 1432 | fileprivate func fixOrientation(withImage image: UIImage) -> UIImage { 1433 | guard let cgImage = image.cgImage else { return image } 1434 | 1435 | var isMirrored = false 1436 | let orientation = image.imageOrientation 1437 | if orientation == .rightMirrored 1438 | || orientation == .leftMirrored 1439 | || orientation == .upMirrored 1440 | || orientation == .downMirrored { 1441 | isMirrored = true 1442 | } 1443 | 1444 | let newOrientation = _imageOrientation(forDeviceOrientation: deviceOrientation, isMirrored: isMirrored) 1445 | 1446 | if image.imageOrientation != newOrientation { 1447 | return UIImage(cgImage: cgImage, scale: image.scale, orientation: newOrientation) 1448 | } 1449 | 1450 | return image 1451 | } 1452 | 1453 | fileprivate func _canLoadCamera() -> Bool { 1454 | let currentCameraState = _checkIfCameraIsAvailable() 1455 | return currentCameraState == .ready || (currentCameraState == .notDetermined && showAccessPermissionPopupAutomatically) 1456 | } 1457 | 1458 | fileprivate func _setupCamera(_ completion: @escaping () -> Void) { 1459 | captureSession = AVCaptureSession() 1460 | 1461 | sessionQueue.async { 1462 | if let validCaptureSession = self.captureSession { 1463 | validCaptureSession.beginConfiguration() 1464 | validCaptureSession.sessionPreset = AVCaptureSession.Preset.high 1465 | self._updateCameraDevice(self.cameraDevice) 1466 | self._setupOutputs() 1467 | self._setupOutputMode(self.cameraOutputMode, oldCameraOutputMode: nil) 1468 | self._setupPreviewLayer() 1469 | validCaptureSession.commitConfiguration() 1470 | self._updateIlluminationMode(self.flashMode) 1471 | self._updateCameraQualityMode(self.cameraOutputQuality) 1472 | validCaptureSession.startRunning() 1473 | self._startFollowingDeviceOrientation() 1474 | self.cameraIsSetup = true 1475 | self._orientationChanged() 1476 | 1477 | completion() 1478 | } 1479 | } 1480 | } 1481 | 1482 | fileprivate func _startFollowingDeviceOrientation() { 1483 | if shouldRespondToOrientationChanges, !cameraIsObservingDeviceOrientation { 1484 | coreMotionManager = CMMotionManager() 1485 | coreMotionManager.deviceMotionUpdateInterval = 1 / 30.0 1486 | if coreMotionManager.isDeviceMotionAvailable { 1487 | coreMotionManager.startDeviceMotionUpdates(to: OperationQueue()) { motion, _ in 1488 | guard let motion = motion else { return } 1489 | let x = motion.gravity.x 1490 | let y = motion.gravity.y 1491 | let previousOrientation = self.deviceOrientation 1492 | if fabs(y) >= fabs(x) { 1493 | if y >= 0 { 1494 | self.deviceOrientation = .portraitUpsideDown 1495 | } else { 1496 | self.deviceOrientation = .portrait 1497 | } 1498 | } else { 1499 | if x >= 0 { 1500 | self.deviceOrientation = .landscapeRight 1501 | } else { 1502 | self.deviceOrientation = .landscapeLeft 1503 | } 1504 | } 1505 | if previousOrientation != self.deviceOrientation { 1506 | self._orientationChanged() 1507 | } 1508 | } 1509 | 1510 | cameraIsObservingDeviceOrientation = true 1511 | } else { 1512 | cameraIsObservingDeviceOrientation = false 1513 | } 1514 | } 1515 | } 1516 | 1517 | // fileprivate func updateDeviceOrientation(_ orientation: UIDeviceOrientation) { 1518 | // deviceOrientation = orientation 1519 | // } 1520 | 1521 | fileprivate func _stopFollowingDeviceOrientation() { 1522 | if cameraIsObservingDeviceOrientation { 1523 | coreMotionManager.stopDeviceMotionUpdates() 1524 | cameraIsObservingDeviceOrientation = false 1525 | } 1526 | } 1527 | 1528 | fileprivate func _addPreviewLayerToView(_ view: UIView) { 1529 | embeddingView = view 1530 | attachZoom(view) 1531 | attachFocus(view) 1532 | attachExposure(view) 1533 | 1534 | DispatchQueue.main.async { () -> Void in 1535 | guard let previewLayer = self.previewLayer else { return } 1536 | previewLayer.frame = view.layer.bounds 1537 | view.clipsToBounds = true 1538 | view.layer.addSublayer(previewLayer) 1539 | } 1540 | } 1541 | 1542 | fileprivate func _setupMaxZoomScale() { 1543 | var maxZoom = CGFloat(1.0) 1544 | beginZoomScale = CGFloat(1.0) 1545 | 1546 | if cameraDevice == .back, let backCameraDevice = backCameraDevice { 1547 | maxZoom = backCameraDevice.activeFormat.videoMaxZoomFactor 1548 | } else if cameraDevice == .front, let frontCameraDevice = frontCameraDevice { 1549 | maxZoom = frontCameraDevice.activeFormat.videoMaxZoomFactor 1550 | } 1551 | 1552 | maxZoomScale = maxZoom 1553 | } 1554 | 1555 | fileprivate func _checkIfCameraIsAvailable() -> CameraState { 1556 | let deviceHasCamera = UIImagePickerController.isCameraDeviceAvailable(UIImagePickerController.CameraDevice.rear) || UIImagePickerController.isCameraDeviceAvailable(UIImagePickerController.CameraDevice.front) 1557 | if deviceHasCamera { 1558 | let authorizationStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.video) 1559 | let userAgreedToUseIt = authorizationStatus == .authorized 1560 | if userAgreedToUseIt { 1561 | return .ready 1562 | } else if authorizationStatus == AVAuthorizationStatus.notDetermined { 1563 | return .notDetermined 1564 | } else { 1565 | _show(NSLocalizedString("Camera access denied", comment: ""), message: NSLocalizedString("You need to go to settings app and grant acces to the camera device to use it.", comment: "")) 1566 | return .accessDenied 1567 | } 1568 | } else { 1569 | _show(NSLocalizedString("Camera unavailable", comment: ""), message: NSLocalizedString("The device does not have a camera.", comment: "")) 1570 | return .noDeviceFound 1571 | } 1572 | } 1573 | 1574 | fileprivate func _setupOutputMode(_ newCameraOutputMode: CameraOutputMode, oldCameraOutputMode: CameraOutputMode?) { 1575 | captureSession?.beginConfiguration() 1576 | 1577 | if let cameraOutputToRemove = oldCameraOutputMode { 1578 | // remove current setting 1579 | switch cameraOutputToRemove { 1580 | case .stillImage: 1581 | if let validStillImageOutput = stillImageOutput { 1582 | captureSession?.removeOutput(validStillImageOutput) 1583 | } 1584 | case .videoOnly, .videoWithMic: 1585 | if let validMovieOutput = movieOutput { 1586 | captureSession?.removeOutput(validMovieOutput) 1587 | } 1588 | if cameraOutputToRemove == .videoWithMic { 1589 | _removeMicInput() 1590 | } 1591 | } 1592 | } 1593 | 1594 | _setupOutputs() 1595 | 1596 | // configure new devices 1597 | switch newCameraOutputMode { 1598 | case .stillImage: 1599 | let validStillImageOutput = _getStillImageOutput() 1600 | if let captureSession = captureSession, 1601 | captureSession.canAddOutput(validStillImageOutput) { 1602 | captureSession.addOutput(validStillImageOutput) 1603 | } 1604 | case .videoOnly, .videoWithMic: 1605 | let videoMovieOutput = _getMovieOutput() 1606 | if let captureSession = captureSession, 1607 | captureSession.canAddOutput(videoMovieOutput) { 1608 | captureSession.addOutput(videoMovieOutput) 1609 | } 1610 | 1611 | if newCameraOutputMode == .videoWithMic, 1612 | let validMic = _deviceInputFromDevice(mic) { 1613 | captureSession?.addInput(validMic) 1614 | } 1615 | } 1616 | captureSession?.commitConfiguration() 1617 | _updateCameraQualityMode(cameraOutputQuality) 1618 | _orientationChanged() 1619 | } 1620 | 1621 | fileprivate func _setupOutputs() { 1622 | if stillImageOutput == nil { 1623 | stillImageOutput = AVCaptureStillImageOutput() 1624 | } 1625 | if movieOutput == nil { 1626 | movieOutput = _getMovieOutput() 1627 | } 1628 | if library == nil { 1629 | library = PHPhotoLibrary.shared() 1630 | } 1631 | } 1632 | 1633 | fileprivate func _setupPreviewLayer() { 1634 | if let validCaptureSession = captureSession { 1635 | previewLayer = AVCaptureVideoPreviewLayer(session: validCaptureSession) 1636 | previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill 1637 | } 1638 | } 1639 | 1640 | /** 1641 | Switches between the current and specified camera using a flip animation similar to the one used in the iOS stock camera app. 1642 | */ 1643 | 1644 | fileprivate var cameraTransitionView: UIView? 1645 | fileprivate var transitionAnimating = false 1646 | 1647 | open func _doFlipAnimation() { 1648 | if transitionAnimating { 1649 | return 1650 | } 1651 | 1652 | if let validEmbeddingView = embeddingView, 1653 | let validPreviewLayer = previewLayer { 1654 | var tempView = UIView() 1655 | 1656 | if CameraManager._blurSupported() { 1657 | let blurEffect = UIBlurEffect(style: .light) 1658 | tempView = UIVisualEffectView(effect: blurEffect) 1659 | tempView.frame = validEmbeddingView.bounds 1660 | } else { 1661 | tempView = UIView(frame: validEmbeddingView.bounds) 1662 | tempView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) 1663 | } 1664 | 1665 | validEmbeddingView.insertSubview(tempView, at: Int(validPreviewLayer.zPosition + 1)) 1666 | 1667 | cameraTransitionView = validEmbeddingView.snapshotView(afterScreenUpdates: true) 1668 | 1669 | if let cameraTransitionView = cameraTransitionView { 1670 | validEmbeddingView.insertSubview(cameraTransitionView, at: Int(validEmbeddingView.layer.zPosition + 1)) 1671 | } 1672 | tempView.removeFromSuperview() 1673 | 1674 | transitionAnimating = true 1675 | 1676 | validPreviewLayer.opacity = 0.0 1677 | 1678 | DispatchQueue.main.async { 1679 | self._flipCameraTransitionView() 1680 | } 1681 | } 1682 | } 1683 | 1684 | // MARK: - CameraLocationManager() 1685 | 1686 | public class CameraLocationManager: NSObject, CLLocationManagerDelegate { 1687 | var locationManager = CLLocationManager() 1688 | var latestLocation: CLLocation? 1689 | 1690 | override init() { 1691 | super.init() 1692 | locationManager.delegate = self 1693 | locationManager.requestWhenInUseAuthorization() 1694 | locationManager.distanceFilter = kCLDistanceFilterNone 1695 | locationManager.headingFilter = 5.0 1696 | locationManager.desiredAccuracy = kCLLocationAccuracyBest 1697 | } 1698 | 1699 | func startUpdatingLocation() { 1700 | locationManager.startUpdatingLocation() 1701 | } 1702 | 1703 | func stopUpdatingLocation() { 1704 | locationManager.stopUpdatingLocation() 1705 | } 1706 | 1707 | // MARK: - CLLocationManagerDelegate 1708 | 1709 | public func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 1710 | // Pick the location with best (= smallest value) horizontal accuracy 1711 | latestLocation = locations.sorted { $0.horizontalAccuracy < $1.horizontalAccuracy }.first 1712 | } 1713 | 1714 | public func locationManager(_: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 1715 | if status == .authorizedAlways || status == .authorizedWhenInUse { 1716 | locationManager.startUpdatingLocation() 1717 | } else { 1718 | locationManager.stopUpdatingLocation() 1719 | } 1720 | } 1721 | } 1722 | 1723 | // Determining whether the current device actually supports blurring 1724 | // As seen on: http://stackoverflow.com/a/29997626/2269387 1725 | fileprivate class func _blurSupported() -> Bool { 1726 | var supported = Set() 1727 | supported.insert("iPad") 1728 | supported.insert("iPad1,1") 1729 | supported.insert("iPhone1,1") 1730 | supported.insert("iPhone1,2") 1731 | supported.insert("iPhone2,1") 1732 | supported.insert("iPhone3,1") 1733 | supported.insert("iPhone3,2") 1734 | supported.insert("iPhone3,3") 1735 | supported.insert("iPod1,1") 1736 | supported.insert("iPod2,1") 1737 | supported.insert("iPod2,2") 1738 | supported.insert("iPod3,1") 1739 | supported.insert("iPod4,1") 1740 | supported.insert("iPad2,1") 1741 | supported.insert("iPad2,2") 1742 | supported.insert("iPad2,3") 1743 | supported.insert("iPad2,4") 1744 | supported.insert("iPad3,1") 1745 | supported.insert("iPad3,2") 1746 | supported.insert("iPad3,3") 1747 | 1748 | return !supported.contains(_hardwareString()) 1749 | } 1750 | 1751 | fileprivate class func _hardwareString() -> String { 1752 | var sysinfo = utsname() 1753 | uname(&sysinfo) 1754 | guard let deviceName = String(bytes: Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii)?.trimmingCharacters(in: .controlCharacters) else { 1755 | return "" 1756 | } 1757 | return deviceName 1758 | } 1759 | 1760 | fileprivate func _flipCameraTransitionView() { 1761 | if let cameraTransitionView = cameraTransitionView { 1762 | UIView.transition(with: cameraTransitionView, 1763 | duration: 0.5, 1764 | options: UIView.AnimationOptions.transitionFlipFromLeft, 1765 | animations: nil, 1766 | completion: { (_) -> Void in 1767 | self._removeCameraTransistionView() 1768 | }) 1769 | } 1770 | } 1771 | 1772 | fileprivate func _removeCameraTransistionView() { 1773 | if let cameraTransitionView = cameraTransitionView { 1774 | if let validPreviewLayer = previewLayer { 1775 | validPreviewLayer.opacity = 1.0 1776 | } 1777 | 1778 | UIView.animate(withDuration: 0.5, 1779 | animations: { () -> Void in 1780 | 1781 | cameraTransitionView.alpha = 0.0 1782 | 1783 | }, completion: { (_) -> Void in 1784 | 1785 | self.transitionAnimating = false 1786 | 1787 | cameraTransitionView.removeFromSuperview() 1788 | self.cameraTransitionView = nil 1789 | }) 1790 | } 1791 | } 1792 | 1793 | fileprivate func _updateCameraDevice(_: CameraDevice) { 1794 | if let validCaptureSession = captureSession { 1795 | validCaptureSession.beginConfiguration() 1796 | defer { validCaptureSession.commitConfiguration() } 1797 | let inputs: [AVCaptureInput] = validCaptureSession.inputs 1798 | 1799 | for input in inputs { 1800 | if let deviceInput = input as? AVCaptureDeviceInput, deviceInput.device != mic { 1801 | validCaptureSession.removeInput(deviceInput) 1802 | } 1803 | } 1804 | 1805 | switch cameraDevice { 1806 | case .front: 1807 | if hasFrontCamera { 1808 | if let validFrontDevice = _deviceInputFromDevice(frontCameraDevice), 1809 | !inputs.contains(validFrontDevice) { 1810 | validCaptureSession.addInput(validFrontDevice) 1811 | } 1812 | } 1813 | case .back: 1814 | if let validBackDevice = _deviceInputFromDevice(backCameraDevice), 1815 | !inputs.contains(validBackDevice) { 1816 | validCaptureSession.addInput(validBackDevice) 1817 | } 1818 | } 1819 | } 1820 | } 1821 | 1822 | fileprivate func _updateIlluminationMode(_ mode: CameraFlashMode) { 1823 | if cameraOutputMode != .stillImage { 1824 | _updateTorch(mode) 1825 | } else { 1826 | _updateFlash(mode) 1827 | } 1828 | } 1829 | 1830 | fileprivate func _updateTorch(_: CameraFlashMode) { 1831 | captureSession?.beginConfiguration() 1832 | defer { captureSession?.commitConfiguration() } 1833 | for captureDevice in AVCaptureDevice.videoDevices { 1834 | guard let avTorchMode = AVCaptureDevice.TorchMode(rawValue: flashMode.rawValue) else { continue } 1835 | if captureDevice.isTorchModeSupported(avTorchMode), cameraDevice == .back { 1836 | do { 1837 | try captureDevice.lockForConfiguration() 1838 | 1839 | captureDevice.torchMode = avTorchMode 1840 | captureDevice.unlockForConfiguration() 1841 | 1842 | } catch { 1843 | return 1844 | } 1845 | } 1846 | } 1847 | } 1848 | 1849 | fileprivate func _updateFlash(_ flashMode: CameraFlashMode) { 1850 | captureSession?.beginConfiguration() 1851 | defer { captureSession?.commitConfiguration() } 1852 | for captureDevice in AVCaptureDevice.videoDevices { 1853 | guard let avFlashMode = AVCaptureDevice.FlashMode(rawValue: flashMode.rawValue) else { continue } 1854 | if captureDevice.isFlashModeSupported(avFlashMode) { 1855 | do { 1856 | try captureDevice.lockForConfiguration() 1857 | captureDevice.flashMode = avFlashMode 1858 | captureDevice.unlockForConfiguration() 1859 | } catch { 1860 | return 1861 | } 1862 | } 1863 | } 1864 | } 1865 | 1866 | fileprivate func _performShutterAnimation(_ completion: (() -> Void)?) { 1867 | if let validPreviewLayer = previewLayer { 1868 | DispatchQueue.main.async { 1869 | let duration = 0.1 1870 | 1871 | CATransaction.begin() 1872 | 1873 | if let completion = completion { 1874 | CATransaction.setCompletionBlock(completion) 1875 | } 1876 | 1877 | let fadeOutAnimation = CABasicAnimation(keyPath: "opacity") 1878 | fadeOutAnimation.fromValue = 1.0 1879 | fadeOutAnimation.toValue = 0.0 1880 | validPreviewLayer.add(fadeOutAnimation, forKey: "opacity") 1881 | 1882 | let fadeInAnimation = CABasicAnimation(keyPath: "opacity") 1883 | fadeInAnimation.fromValue = 0.0 1884 | fadeInAnimation.toValue = 1.0 1885 | fadeInAnimation.beginTime = CACurrentMediaTime() + duration * 2.0 1886 | validPreviewLayer.add(fadeInAnimation, forKey: "opacity") 1887 | 1888 | CATransaction.commit() 1889 | } 1890 | } 1891 | } 1892 | 1893 | fileprivate func _updateCameraQualityMode(_ newCameraOutputQuality: AVCaptureSession.Preset) { 1894 | if let validCaptureSession = captureSession { 1895 | var sessionPreset = newCameraOutputQuality 1896 | if newCameraOutputQuality == .high { 1897 | if cameraOutputMode == .stillImage { 1898 | sessionPreset = AVCaptureSession.Preset.photo 1899 | } else { 1900 | sessionPreset = AVCaptureSession.Preset.high 1901 | } 1902 | } 1903 | 1904 | if validCaptureSession.canSetSessionPreset(sessionPreset) { 1905 | validCaptureSession.beginConfiguration() 1906 | validCaptureSession.sessionPreset = sessionPreset 1907 | validCaptureSession.commitConfiguration() 1908 | } else { 1909 | _show(NSLocalizedString("Preset not supported", comment: ""), message: NSLocalizedString("Camera preset not supported. Please try another one.", comment: "")) 1910 | } 1911 | } else { 1912 | _show(NSLocalizedString("Camera error", comment: ""), message: NSLocalizedString("No valid capture session found, I can't take any pictures or videos.", comment: "")) 1913 | } 1914 | } 1915 | 1916 | fileprivate func _removeMicInput() { 1917 | guard let inputs = captureSession?.inputs else { return } 1918 | 1919 | for input in inputs { 1920 | if let deviceInput = input as? AVCaptureDeviceInput, 1921 | deviceInput.device == mic { 1922 | captureSession?.removeInput(deviceInput) 1923 | break 1924 | } 1925 | } 1926 | } 1927 | 1928 | fileprivate func _show(_ title: String, message: String) { 1929 | if showErrorsToUsers { 1930 | DispatchQueue.main.async { () -> Void in 1931 | self.showErrorBlock(title, message) 1932 | } 1933 | } 1934 | } 1935 | 1936 | fileprivate func _deviceInputFromDevice(_ device: AVCaptureDevice?) -> AVCaptureDeviceInput? { 1937 | guard let validDevice = device else { return nil } 1938 | do { 1939 | return try AVCaptureDeviceInput(device: validDevice) 1940 | } catch let outError { 1941 | _show(NSLocalizedString("Device setup error occured", comment: ""), message: "\(outError)") 1942 | return nil 1943 | } 1944 | } 1945 | 1946 | deinit { 1947 | _stopFollowingDeviceOrientation() 1948 | stopAndRemoveCaptureSession() 1949 | } 1950 | } 1951 | 1952 | private extension AVCaptureDevice { 1953 | static var videoDevices: [AVCaptureDevice] { 1954 | return AVCaptureDevice.devices(for: AVMediaType.video) 1955 | } 1956 | } 1957 | 1958 | extension PHPhotoLibrary { 1959 | // MARK: - Public 1960 | 1961 | // finds or creates an album 1962 | 1963 | func getAlbum(name: String, completion: @escaping (PHAssetCollection) -> Void) { 1964 | if let album = findAlbum(name: name) { 1965 | completion(album) 1966 | } else { 1967 | createAlbum(name: name, completion: completion) 1968 | } 1969 | } 1970 | 1971 | func save(imageAtURL: URL, albumName: String?, date: Date = Date(), location: CLLocation? = nil, completion: ((PHAsset?) -> Void)? = nil) { 1972 | func save() { 1973 | if let albumName = albumName { 1974 | getAlbum(name: albumName) { album in 1975 | self.saveImage(imageAtURL: imageAtURL, album: album, date: date, location: location, completion: completion) 1976 | } 1977 | } else { 1978 | saveImage(imageAtURL: imageAtURL, album: nil, date: date, location: location, completion: completion) 1979 | } 1980 | } 1981 | 1982 | if PHPhotoLibrary.authorizationStatus() == .authorized { 1983 | save() 1984 | } else { 1985 | PHPhotoLibrary.requestAuthorization { status in 1986 | if status == .authorized { 1987 | save() 1988 | } 1989 | } 1990 | } 1991 | } 1992 | 1993 | func save(videoAtURL: URL, albumName: String?, date: Date = Date(), location: CLLocation? = nil, completion: ((PHAsset?) -> Void)? = nil) { 1994 | func save() { 1995 | if let albumName = albumName { 1996 | getAlbum(name: albumName) { album in 1997 | self.saveVideo(videoAtURL: videoAtURL, album: album, date: date, location: location, completion: completion) 1998 | } 1999 | } else { 2000 | saveVideo(videoAtURL: videoAtURL, album: nil, date: date, location: location, completion: completion) 2001 | } 2002 | } 2003 | 2004 | if PHPhotoLibrary.authorizationStatus() == .authorized { 2005 | save() 2006 | } else { 2007 | PHPhotoLibrary.requestAuthorization { status in 2008 | if status == .authorized { 2009 | save() 2010 | } 2011 | } 2012 | } 2013 | } 2014 | 2015 | // MARK: - Private 2016 | 2017 | fileprivate func findAlbum(name: String) -> PHAssetCollection? { 2018 | let fetchOptions = PHFetchOptions() 2019 | fetchOptions.predicate = NSPredicate(format: "title = %@", name) 2020 | let fetchResult: PHFetchResult = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions) 2021 | guard let photoAlbum = fetchResult.firstObject else { 2022 | return nil 2023 | } 2024 | return photoAlbum 2025 | } 2026 | 2027 | fileprivate func createAlbum(name: String, completion: @escaping (PHAssetCollection) -> Void) { 2028 | var placeholder: PHObjectPlaceholder? 2029 | 2030 | performChanges({ 2031 | let createAlbumRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: name) 2032 | placeholder = createAlbumRequest.placeholderForCreatedAssetCollection 2033 | }, completionHandler: { _, _ in 2034 | let fetchResult = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [placeholder!.localIdentifier], options: nil) 2035 | completion(fetchResult.firstObject!) 2036 | }) 2037 | } 2038 | 2039 | fileprivate func saveImage(imageAtURL: URL, album: PHAssetCollection?, date: Date = Date(), location: CLLocation? = nil, completion: ((PHAsset?) -> Void)? = nil) { 2040 | var placeholder: PHObjectPlaceholder? 2041 | performChanges({ 2042 | let createAssetRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: imageAtURL)! 2043 | createAssetRequest.creationDate = date 2044 | createAssetRequest.location = location 2045 | if let album = album { 2046 | guard let albumChangeRequest = PHAssetCollectionChangeRequest(for: album), 2047 | let photoPlaceholder = createAssetRequest.placeholderForCreatedAsset else { return } 2048 | placeholder = photoPlaceholder 2049 | let fastEnumeration = NSArray(array: [photoPlaceholder] as [PHObjectPlaceholder]) 2050 | albumChangeRequest.addAssets(fastEnumeration) 2051 | } 2052 | 2053 | }, completionHandler: { success, _ in 2054 | guard let placeholder = placeholder else { 2055 | return 2056 | } 2057 | if success { 2058 | let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [placeholder.localIdentifier], options: nil) 2059 | let asset: PHAsset? = assets.firstObject 2060 | completion?(asset) 2061 | } 2062 | }) 2063 | } 2064 | 2065 | fileprivate func saveVideo(videoAtURL: URL, album: PHAssetCollection?, date: Date = Date(), location: CLLocation? = nil, completion: ((PHAsset?) -> Void)? = nil) { 2066 | var placeholder: PHObjectPlaceholder? 2067 | performChanges({ 2068 | let createAssetRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoAtURL)! 2069 | createAssetRequest.creationDate = date 2070 | createAssetRequest.location = location 2071 | if let album = album { 2072 | guard let albumChangeRequest = PHAssetCollectionChangeRequest(for: album), 2073 | let photoPlaceholder = createAssetRequest.placeholderForCreatedAsset else { return } 2074 | placeholder = photoPlaceholder 2075 | let fastEnumeration = NSArray(array: [photoPlaceholder] as [PHObjectPlaceholder]) 2076 | albumChangeRequest.addAssets(fastEnumeration) 2077 | } 2078 | 2079 | }, completionHandler: { success, _ in 2080 | guard let placeholder = placeholder else { 2081 | completion?(nil) 2082 | return 2083 | } 2084 | if success { 2085 | let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [placeholder.localIdentifier], options: nil) 2086 | let asset: PHAsset? = assets.firstObject 2087 | completion?(asset) 2088 | } else { 2089 | completion?(nil) 2090 | } 2091 | }) 2092 | } 2093 | 2094 | fileprivate func saveImage(image: UIImage, album: PHAssetCollection, completion: ((PHAsset?) -> Void)? = nil) { 2095 | var placeholder: PHObjectPlaceholder? 2096 | performChanges({ 2097 | let createAssetRequest = PHAssetChangeRequest.creationRequestForAsset(from: image) 2098 | createAssetRequest.creationDate = Date() 2099 | guard let albumChangeRequest = PHAssetCollectionChangeRequest(for: album), 2100 | let photoPlaceholder = createAssetRequest.placeholderForCreatedAsset else { return } 2101 | placeholder = photoPlaceholder 2102 | let fastEnumeration = NSArray(array: [photoPlaceholder] as [PHObjectPlaceholder]) 2103 | albumChangeRequest.addAssets(fastEnumeration) 2104 | }, completionHandler: { success, _ in 2105 | guard let placeholder = placeholder else { 2106 | completion?(nil) 2107 | return 2108 | } 2109 | if success { 2110 | let assets: PHFetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [placeholder.localIdentifier], options: nil) 2111 | let asset: PHAsset? = assets.firstObject 2112 | completion?(asset) 2113 | } else { 2114 | completion?(nil) 2115 | } 2116 | }) 2117 | } 2118 | } 2119 | 2120 | extension CameraManager: AVCaptureMetadataOutputObjectsDelegate { 2121 | /** 2122 | Called when a QR code is detected. 2123 | */ 2124 | public func metadataOutput(_: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from _: AVCaptureConnection) { 2125 | // Check if there is a registered handler. 2126 | guard let handler = qrCodeDetectionHandler 2127 | else { return } 2128 | 2129 | // Get the detection result. 2130 | let stringValues = metadataObjects 2131 | .compactMap { $0 as? AVMetadataMachineReadableCodeObject } 2132 | .compactMap { $0.stringValue } 2133 | 2134 | guard let stringValue = stringValues.first 2135 | else { return } 2136 | 2137 | handler(.success(stringValue)) 2138 | } 2139 | } 2140 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | --------------------------------------------------------------------------------