├── .gitignore ├── README.md ├── SwiftCamera.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── rolando.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── SwiftCamera ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── CameraPreview.swift ├── CameraService+Enums.swift ├── CameraService+Extensions.swift ├── CameraService.swift ├── ContentView.swift ├── ImageResizer.swift ├── Info.plist ├── PhotoCaptureProcessor.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── SwiftCameraApp.swift ├── SwiftCameraTests ├── Info.plist └── SwiftCameraTests.swift └── SwiftCameraUITests ├── Info.plist └── SwiftCameraUITests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | ### Swift ### 2 | # Xcode 3 | # 4 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 5 | 6 | ## User settings 7 | xcuserdata/ 8 | 9 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 10 | *.xcscmblueprint 11 | *.xccheckout 12 | 13 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 14 | build/ 15 | DerivedData/ 16 | *.moved-aside 17 | *.pbxuser 18 | !default.pbxuser 19 | *.mode1v3 20 | !default.mode1v3 21 | *.mode2v3 22 | !default.mode2v3 23 | *.perspectivev3 24 | !default.perspectivev3 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | 29 | ## App packaging 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | Packages/ 41 | Package.pins 42 | Package.resolved 43 | # *.xcodeproj 44 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 45 | # hence it is not needed unless you have added a package configuration file to your project 46 | .swiftpm 47 | 48 | .build/ 49 | 50 | # CocoaPods 51 | # We recommend against adding the Pods directory to your .gitignore. However 52 | # you should judge for yourself, the pros and cons are mentioned at: 53 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 54 | # Pods/ 55 | # Add this line if you want to avoid checking in source code from the Xcode workspace 56 | # *.xcworkspace 57 | 58 | # Carthage 59 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 60 | # Carthage/Checkouts 61 | 62 | Carthage/Build/ 63 | 64 | # Accio dependency management 65 | Dependencies/ 66 | .accio/ 67 | 68 | # fastlane 69 | # It is recommended to not store the screenshots in the git repo. 70 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 71 | # For more information about the recommended setup visit: 72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 73 | 74 | fastlane/report.xml 75 | fastlane/Preview.html 76 | fastlane/screenshots/**/*.png 77 | fastlane/test_output 78 | 79 | # Code Injection 80 | # After new code Injection tools there's a generated folder /iOSInjectionProject 81 | # https://github.com/johnno1962/injectionforxcode 82 | 83 | iOSInjectionProject/ 84 | 85 | ### SwiftPackageManager ### 86 | Packages 87 | xcuserdata 88 | *.xcodeproj 89 | 90 | 91 | ### SwiftPM ### 92 | 93 | 94 | ### Xcode ### 95 | # Xcode 96 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 97 | 98 | 99 | 100 | 101 | ## Gcc Patch 102 | /*.gcno 103 | 104 | ### Xcode Patch ### 105 | *.xcodeproj/* 106 | !*.xcodeproj/project.pbxproj 107 | !*.xcodeproj/xcshareddata/ 108 | !*.xcworkspace/contents.xcworkspacedata 109 | **/xcshareddata/WorkspaceSettings.xcsettings 110 | 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Effortless SwiftUI-- Camera 2 | 3 | ![alt text](https://cdn-images-1.medium.com/max/2600/1*dQ6PJ2f9GfIO3iuKDNZxqA.png) 4 | This is the project repo for the [Effortless SwiftUI-- Camera](https://rorodriguez116.medium.com/effortless-swiftui-camera-d7a74abde37e) tutorial. 5 | 6 | SwiftUI has proven to be a really awesome new framework to build and design apps in a quick and reliable way. Nonetheless, it is still in its infancy and Apple needs to add more support for other framework integrations as it did recently with the new Sign In With Apple button in SwiftUI 2.0. In this project we'll learn how to integrate AVFoundation's AVCameraSession with SwiftUI so we can create reusable, extendable modern components for apps that need to use our device's cameras. 7 | 8 | # Features 9 | - Camera feed. 10 | - Tap to focus. 11 | - Drag to zoom in/out. 12 | - Thumbnail preview. 13 | - Save capture to photo library. 14 | - Switch between front and back facing cameras. 15 | - Set flash on/off 16 | 17 | # Try it out! 18 | You can download de whole project from here, if you'd like to read the article for this project click [here](https://rorodriguez116.medium.com/effortless-swiftui-camera-d7a74abde37e): 19 | 20 | # Swift Package 21 | There's a swift package related to this project so you can use all the mentioned features in your app with your custom UI! Check it out [here](https://github.com/rorodriguez116/Camera-SwiftUI). 22 | 23 | License 24 | ---- 25 | 26 | MIT 27 | 28 | -------------------------------------------------------------------------------- /SwiftCamera.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A9E6E4EF253B5EA8008B4A33 /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E6E4EE253B5EA8008B4A33 /* CameraPreview.swift */; }; 11 | A9FE17852539494300D547C7 /* SwiftCameraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE17842539494300D547C7 /* SwiftCameraApp.swift */; }; 12 | A9FE17872539494300D547C7 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE17862539494300D547C7 /* ContentView.swift */; }; 13 | A9FE17892539494700D547C7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A9FE17882539494700D547C7 /* Assets.xcassets */; }; 14 | A9FE178C2539494700D547C7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A9FE178B2539494700D547C7 /* Preview Assets.xcassets */; }; 15 | A9FE17972539494700D547C7 /* SwiftCameraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE17962539494700D547C7 /* SwiftCameraTests.swift */; }; 16 | A9FE17A22539494700D547C7 /* SwiftCameraUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE17A12539494700D547C7 /* SwiftCameraUITests.swift */; }; 17 | A9FE17B725394F1E00D547C7 /* CameraService+Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE17B225394F1C00D547C7 /* CameraService+Enums.swift */; }; 18 | A9FE17B825394F1E00D547C7 /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE17B325394F1D00D547C7 /* ImageResizer.swift */; }; 19 | A9FE17B925394F1E00D547C7 /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE17B425394F1D00D547C7 /* CameraService.swift */; }; 20 | A9FE17BA25394F1E00D547C7 /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE17B525394F1E00D547C7 /* PhotoCaptureProcessor.swift */; }; 21 | A9FE17BB25394F1E00D547C7 /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE17B625394F1E00D547C7 /* CameraService+Extensions.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXContainerItemProxy section */ 25 | A9FE17932539494700D547C7 /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = A9FE17792539494300D547C7 /* Project object */; 28 | proxyType = 1; 29 | remoteGlobalIDString = A9FE17802539494300D547C7; 30 | remoteInfo = SwiftCamera; 31 | }; 32 | A9FE179E2539494700D547C7 /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = A9FE17792539494300D547C7 /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = A9FE17802539494300D547C7; 37 | remoteInfo = SwiftCamera; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXFileReference section */ 42 | A9E6E4EE253B5EA8008B4A33 /* CameraPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = ""; }; 43 | A9FE17812539494300D547C7 /* SwiftCamera.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftCamera.app; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | A9FE17842539494300D547C7 /* SwiftCameraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftCameraApp.swift; sourceTree = ""; }; 45 | A9FE17862539494300D547C7 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 46 | A9FE17882539494700D547C7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47 | A9FE178B2539494700D547C7 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 48 | A9FE178D2539494700D547C7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | A9FE17922539494700D547C7 /* SwiftCameraTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftCameraTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | A9FE17962539494700D547C7 /* SwiftCameraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftCameraTests.swift; sourceTree = ""; }; 51 | A9FE17982539494700D547C7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 52 | A9FE179D2539494700D547C7 /* SwiftCameraUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftCameraUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | A9FE17A12539494700D547C7 /* SwiftCameraUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftCameraUITests.swift; sourceTree = ""; }; 54 | A9FE17A32539494700D547C7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | A9FE17B225394F1C00D547C7 /* CameraService+Enums.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Enums.swift"; sourceTree = ""; }; 56 | A9FE17B325394F1D00D547C7 /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = ""; }; 57 | A9FE17B425394F1D00D547C7 /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = ""; }; 58 | A9FE17B525394F1E00D547C7 /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = ""; }; 59 | A9FE17B625394F1E00D547C7 /* CameraService+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Extensions.swift"; sourceTree = ""; }; 60 | /* End PBXFileReference section */ 61 | 62 | /* Begin PBXFrameworksBuildPhase section */ 63 | A9FE177E2539494300D547C7 /* Frameworks */ = { 64 | isa = PBXFrameworksBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | A9FE178F2539494700D547C7 /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | A9FE179A2539494700D547C7 /* Frameworks */ = { 78 | isa = PBXFrameworksBuildPhase; 79 | buildActionMask = 2147483647; 80 | files = ( 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | /* End PBXFrameworksBuildPhase section */ 85 | 86 | /* Begin PBXGroup section */ 87 | A9FE17782539494300D547C7 = { 88 | isa = PBXGroup; 89 | children = ( 90 | A9FE17832539494300D547C7 /* SwiftCamera */, 91 | A9FE17952539494700D547C7 /* SwiftCameraTests */, 92 | A9FE17A02539494700D547C7 /* SwiftCameraUITests */, 93 | A9FE17822539494300D547C7 /* Products */, 94 | ); 95 | sourceTree = ""; 96 | }; 97 | A9FE17822539494300D547C7 /* Products */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | A9FE17812539494300D547C7 /* SwiftCamera.app */, 101 | A9FE17922539494700D547C7 /* SwiftCameraTests.xctest */, 102 | A9FE179D2539494700D547C7 /* SwiftCameraUITests.xctest */, 103 | ); 104 | name = Products; 105 | sourceTree = ""; 106 | }; 107 | A9FE17832539494300D547C7 /* SwiftCamera */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | A9FE17842539494300D547C7 /* SwiftCameraApp.swift */, 111 | A9FE17862539494300D547C7 /* ContentView.swift */, 112 | A9FE17B425394F1D00D547C7 /* CameraService.swift */, 113 | A9FE17B225394F1C00D547C7 /* CameraService+Enums.swift */, 114 | A9FE17B625394F1E00D547C7 /* CameraService+Extensions.swift */, 115 | A9FE17B325394F1D00D547C7 /* ImageResizer.swift */, 116 | A9FE17B525394F1E00D547C7 /* PhotoCaptureProcessor.swift */, 117 | A9FE17882539494700D547C7 /* Assets.xcassets */, 118 | A9FE178D2539494700D547C7 /* Info.plist */, 119 | A9FE178A2539494700D547C7 /* Preview Content */, 120 | A9E6E4EE253B5EA8008B4A33 /* CameraPreview.swift */, 121 | ); 122 | path = SwiftCamera; 123 | sourceTree = ""; 124 | }; 125 | A9FE178A2539494700D547C7 /* Preview Content */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | A9FE178B2539494700D547C7 /* Preview Assets.xcassets */, 129 | ); 130 | path = "Preview Content"; 131 | sourceTree = ""; 132 | }; 133 | A9FE17952539494700D547C7 /* SwiftCameraTests */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | A9FE17962539494700D547C7 /* SwiftCameraTests.swift */, 137 | A9FE17982539494700D547C7 /* Info.plist */, 138 | ); 139 | path = SwiftCameraTests; 140 | sourceTree = ""; 141 | }; 142 | A9FE17A02539494700D547C7 /* SwiftCameraUITests */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | A9FE17A12539494700D547C7 /* SwiftCameraUITests.swift */, 146 | A9FE17A32539494700D547C7 /* Info.plist */, 147 | ); 148 | path = SwiftCameraUITests; 149 | sourceTree = ""; 150 | }; 151 | /* End PBXGroup section */ 152 | 153 | /* Begin PBXNativeTarget section */ 154 | A9FE17802539494300D547C7 /* SwiftCamera */ = { 155 | isa = PBXNativeTarget; 156 | buildConfigurationList = A9FE17A62539494700D547C7 /* Build configuration list for PBXNativeTarget "SwiftCamera" */; 157 | buildPhases = ( 158 | A9FE177D2539494300D547C7 /* Sources */, 159 | A9FE177E2539494300D547C7 /* Frameworks */, 160 | A9FE177F2539494300D547C7 /* Resources */, 161 | ); 162 | buildRules = ( 163 | ); 164 | dependencies = ( 165 | ); 166 | name = SwiftCamera; 167 | productName = SwiftCamera; 168 | productReference = A9FE17812539494300D547C7 /* SwiftCamera.app */; 169 | productType = "com.apple.product-type.application"; 170 | }; 171 | A9FE17912539494700D547C7 /* SwiftCameraTests */ = { 172 | isa = PBXNativeTarget; 173 | buildConfigurationList = A9FE17A92539494700D547C7 /* Build configuration list for PBXNativeTarget "SwiftCameraTests" */; 174 | buildPhases = ( 175 | A9FE178E2539494700D547C7 /* Sources */, 176 | A9FE178F2539494700D547C7 /* Frameworks */, 177 | A9FE17902539494700D547C7 /* Resources */, 178 | ); 179 | buildRules = ( 180 | ); 181 | dependencies = ( 182 | A9FE17942539494700D547C7 /* PBXTargetDependency */, 183 | ); 184 | name = SwiftCameraTests; 185 | productName = SwiftCameraTests; 186 | productReference = A9FE17922539494700D547C7 /* SwiftCameraTests.xctest */; 187 | productType = "com.apple.product-type.bundle.unit-test"; 188 | }; 189 | A9FE179C2539494700D547C7 /* SwiftCameraUITests */ = { 190 | isa = PBXNativeTarget; 191 | buildConfigurationList = A9FE17AC2539494700D547C7 /* Build configuration list for PBXNativeTarget "SwiftCameraUITests" */; 192 | buildPhases = ( 193 | A9FE17992539494700D547C7 /* Sources */, 194 | A9FE179A2539494700D547C7 /* Frameworks */, 195 | A9FE179B2539494700D547C7 /* Resources */, 196 | ); 197 | buildRules = ( 198 | ); 199 | dependencies = ( 200 | A9FE179F2539494700D547C7 /* PBXTargetDependency */, 201 | ); 202 | name = SwiftCameraUITests; 203 | productName = SwiftCameraUITests; 204 | productReference = A9FE179D2539494700D547C7 /* SwiftCameraUITests.xctest */; 205 | productType = "com.apple.product-type.bundle.ui-testing"; 206 | }; 207 | /* End PBXNativeTarget section */ 208 | 209 | /* Begin PBXProject section */ 210 | A9FE17792539494300D547C7 /* Project object */ = { 211 | isa = PBXProject; 212 | attributes = { 213 | LastSwiftUpdateCheck = 1200; 214 | LastUpgradeCheck = 1200; 215 | TargetAttributes = { 216 | A9FE17802539494300D547C7 = { 217 | CreatedOnToolsVersion = 12.0.1; 218 | }; 219 | A9FE17912539494700D547C7 = { 220 | CreatedOnToolsVersion = 12.0.1; 221 | TestTargetID = A9FE17802539494300D547C7; 222 | }; 223 | A9FE179C2539494700D547C7 = { 224 | CreatedOnToolsVersion = 12.0.1; 225 | TestTargetID = A9FE17802539494300D547C7; 226 | }; 227 | }; 228 | }; 229 | buildConfigurationList = A9FE177C2539494300D547C7 /* Build configuration list for PBXProject "SwiftCamera" */; 230 | compatibilityVersion = "Xcode 9.3"; 231 | developmentRegion = en; 232 | hasScannedForEncodings = 0; 233 | knownRegions = ( 234 | en, 235 | Base, 236 | ); 237 | mainGroup = A9FE17782539494300D547C7; 238 | productRefGroup = A9FE17822539494300D547C7 /* Products */; 239 | projectDirPath = ""; 240 | projectRoot = ""; 241 | targets = ( 242 | A9FE17802539494300D547C7 /* SwiftCamera */, 243 | A9FE17912539494700D547C7 /* SwiftCameraTests */, 244 | A9FE179C2539494700D547C7 /* SwiftCameraUITests */, 245 | ); 246 | }; 247 | /* End PBXProject section */ 248 | 249 | /* Begin PBXResourcesBuildPhase section */ 250 | A9FE177F2539494300D547C7 /* Resources */ = { 251 | isa = PBXResourcesBuildPhase; 252 | buildActionMask = 2147483647; 253 | files = ( 254 | A9FE178C2539494700D547C7 /* Preview Assets.xcassets in Resources */, 255 | A9FE17892539494700D547C7 /* Assets.xcassets in Resources */, 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | }; 259 | A9FE17902539494700D547C7 /* Resources */ = { 260 | isa = PBXResourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | A9FE179B2539494700D547C7 /* Resources */ = { 267 | isa = PBXResourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | /* End PBXResourcesBuildPhase section */ 274 | 275 | /* Begin PBXSourcesBuildPhase section */ 276 | A9FE177D2539494300D547C7 /* Sources */ = { 277 | isa = PBXSourcesBuildPhase; 278 | buildActionMask = 2147483647; 279 | files = ( 280 | A9FE17B925394F1E00D547C7 /* CameraService.swift in Sources */, 281 | A9E6E4EF253B5EA8008B4A33 /* CameraPreview.swift in Sources */, 282 | A9FE17BB25394F1E00D547C7 /* CameraService+Extensions.swift in Sources */, 283 | A9FE17B825394F1E00D547C7 /* ImageResizer.swift in Sources */, 284 | A9FE17BA25394F1E00D547C7 /* PhotoCaptureProcessor.swift in Sources */, 285 | A9FE17872539494300D547C7 /* ContentView.swift in Sources */, 286 | A9FE17852539494300D547C7 /* SwiftCameraApp.swift in Sources */, 287 | A9FE17B725394F1E00D547C7 /* CameraService+Enums.swift in Sources */, 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | }; 291 | A9FE178E2539494700D547C7 /* Sources */ = { 292 | isa = PBXSourcesBuildPhase; 293 | buildActionMask = 2147483647; 294 | files = ( 295 | A9FE17972539494700D547C7 /* SwiftCameraTests.swift in Sources */, 296 | ); 297 | runOnlyForDeploymentPostprocessing = 0; 298 | }; 299 | A9FE17992539494700D547C7 /* Sources */ = { 300 | isa = PBXSourcesBuildPhase; 301 | buildActionMask = 2147483647; 302 | files = ( 303 | A9FE17A22539494700D547C7 /* SwiftCameraUITests.swift in Sources */, 304 | ); 305 | runOnlyForDeploymentPostprocessing = 0; 306 | }; 307 | /* End PBXSourcesBuildPhase section */ 308 | 309 | /* Begin PBXTargetDependency section */ 310 | A9FE17942539494700D547C7 /* PBXTargetDependency */ = { 311 | isa = PBXTargetDependency; 312 | target = A9FE17802539494300D547C7 /* SwiftCamera */; 313 | targetProxy = A9FE17932539494700D547C7 /* PBXContainerItemProxy */; 314 | }; 315 | A9FE179F2539494700D547C7 /* PBXTargetDependency */ = { 316 | isa = PBXTargetDependency; 317 | target = A9FE17802539494300D547C7 /* SwiftCamera */; 318 | targetProxy = A9FE179E2539494700D547C7 /* PBXContainerItemProxy */; 319 | }; 320 | /* End PBXTargetDependency section */ 321 | 322 | /* Begin XCBuildConfiguration section */ 323 | A9FE17A42539494700D547C7 /* Debug */ = { 324 | isa = XCBuildConfiguration; 325 | buildSettings = { 326 | ALWAYS_SEARCH_USER_PATHS = NO; 327 | CLANG_ANALYZER_NONNULL = YES; 328 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 329 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 330 | CLANG_CXX_LIBRARY = "libc++"; 331 | CLANG_ENABLE_MODULES = YES; 332 | CLANG_ENABLE_OBJC_ARC = YES; 333 | CLANG_ENABLE_OBJC_WEAK = YES; 334 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 335 | CLANG_WARN_BOOL_CONVERSION = YES; 336 | CLANG_WARN_COMMA = YES; 337 | CLANG_WARN_CONSTANT_CONVERSION = YES; 338 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 339 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 340 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 341 | CLANG_WARN_EMPTY_BODY = YES; 342 | CLANG_WARN_ENUM_CONVERSION = YES; 343 | CLANG_WARN_INFINITE_RECURSION = YES; 344 | CLANG_WARN_INT_CONVERSION = YES; 345 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 346 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 347 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 348 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 349 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 350 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 351 | CLANG_WARN_STRICT_PROTOTYPES = YES; 352 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 353 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 354 | CLANG_WARN_UNREACHABLE_CODE = YES; 355 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 356 | COPY_PHASE_STRIP = NO; 357 | DEBUG_INFORMATION_FORMAT = dwarf; 358 | ENABLE_STRICT_OBJC_MSGSEND = YES; 359 | ENABLE_TESTABILITY = YES; 360 | GCC_C_LANGUAGE_STANDARD = gnu11; 361 | GCC_DYNAMIC_NO_PIC = NO; 362 | GCC_NO_COMMON_BLOCKS = YES; 363 | GCC_OPTIMIZATION_LEVEL = 0; 364 | GCC_PREPROCESSOR_DEFINITIONS = ( 365 | "DEBUG=1", 366 | "$(inherited)", 367 | ); 368 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 369 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 370 | GCC_WARN_UNDECLARED_SELECTOR = YES; 371 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 372 | GCC_WARN_UNUSED_FUNCTION = YES; 373 | GCC_WARN_UNUSED_VARIABLE = YES; 374 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 375 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 376 | MTL_FAST_MATH = YES; 377 | ONLY_ACTIVE_ARCH = YES; 378 | SDKROOT = iphoneos; 379 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 380 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 381 | }; 382 | name = Debug; 383 | }; 384 | A9FE17A52539494700D547C7 /* Release */ = { 385 | isa = XCBuildConfiguration; 386 | buildSettings = { 387 | ALWAYS_SEARCH_USER_PATHS = NO; 388 | CLANG_ANALYZER_NONNULL = YES; 389 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 390 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 391 | CLANG_CXX_LIBRARY = "libc++"; 392 | CLANG_ENABLE_MODULES = YES; 393 | CLANG_ENABLE_OBJC_ARC = YES; 394 | CLANG_ENABLE_OBJC_WEAK = YES; 395 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 396 | CLANG_WARN_BOOL_CONVERSION = YES; 397 | CLANG_WARN_COMMA = YES; 398 | CLANG_WARN_CONSTANT_CONVERSION = YES; 399 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 400 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 401 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 402 | CLANG_WARN_EMPTY_BODY = YES; 403 | CLANG_WARN_ENUM_CONVERSION = YES; 404 | CLANG_WARN_INFINITE_RECURSION = YES; 405 | CLANG_WARN_INT_CONVERSION = YES; 406 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 407 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 408 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 409 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 410 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 411 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 412 | CLANG_WARN_STRICT_PROTOTYPES = YES; 413 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 414 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 415 | CLANG_WARN_UNREACHABLE_CODE = YES; 416 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 417 | COPY_PHASE_STRIP = NO; 418 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 419 | ENABLE_NS_ASSERTIONS = NO; 420 | ENABLE_STRICT_OBJC_MSGSEND = YES; 421 | GCC_C_LANGUAGE_STANDARD = gnu11; 422 | GCC_NO_COMMON_BLOCKS = YES; 423 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 424 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 425 | GCC_WARN_UNDECLARED_SELECTOR = YES; 426 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 427 | GCC_WARN_UNUSED_FUNCTION = YES; 428 | GCC_WARN_UNUSED_VARIABLE = YES; 429 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 430 | MTL_ENABLE_DEBUG_INFO = NO; 431 | MTL_FAST_MATH = YES; 432 | SDKROOT = iphoneos; 433 | SWIFT_COMPILATION_MODE = wholemodule; 434 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 435 | VALIDATE_PRODUCT = YES; 436 | }; 437 | name = Release; 438 | }; 439 | A9FE17A72539494700D547C7 /* Debug */ = { 440 | isa = XCBuildConfiguration; 441 | buildSettings = { 442 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 443 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 444 | CODE_SIGN_STYLE = Automatic; 445 | DEVELOPMENT_ASSET_PATHS = "\"SwiftCamera/Preview Content\""; 446 | DEVELOPMENT_TEAM = HS2HJ27PLP; 447 | ENABLE_PREVIEWS = YES; 448 | INFOPLIST_FILE = SwiftCamera/Info.plist; 449 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 450 | LD_RUNPATH_SEARCH_PATHS = ( 451 | "$(inherited)", 452 | "@executable_path/Frameworks", 453 | ); 454 | PRODUCT_BUNDLE_IDENTIFIER = pe.rry.SwiftCamera; 455 | PRODUCT_NAME = "$(TARGET_NAME)"; 456 | SWIFT_VERSION = 5.0; 457 | TARGETED_DEVICE_FAMILY = "1,2"; 458 | }; 459 | name = Debug; 460 | }; 461 | A9FE17A82539494700D547C7 /* Release */ = { 462 | isa = XCBuildConfiguration; 463 | buildSettings = { 464 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 465 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 466 | CODE_SIGN_STYLE = Automatic; 467 | DEVELOPMENT_ASSET_PATHS = "\"SwiftCamera/Preview Content\""; 468 | DEVELOPMENT_TEAM = HS2HJ27PLP; 469 | ENABLE_PREVIEWS = YES; 470 | INFOPLIST_FILE = SwiftCamera/Info.plist; 471 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 472 | LD_RUNPATH_SEARCH_PATHS = ( 473 | "$(inherited)", 474 | "@executable_path/Frameworks", 475 | ); 476 | PRODUCT_BUNDLE_IDENTIFIER = pe.rry.SwiftCamera; 477 | PRODUCT_NAME = "$(TARGET_NAME)"; 478 | SWIFT_VERSION = 5.0; 479 | TARGETED_DEVICE_FAMILY = "1,2"; 480 | }; 481 | name = Release; 482 | }; 483 | A9FE17AA2539494700D547C7 /* Debug */ = { 484 | isa = XCBuildConfiguration; 485 | buildSettings = { 486 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 487 | BUNDLE_LOADER = "$(TEST_HOST)"; 488 | CODE_SIGN_STYLE = Automatic; 489 | DEVELOPMENT_TEAM = HS2HJ27PLP; 490 | INFOPLIST_FILE = SwiftCameraTests/Info.plist; 491 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 492 | LD_RUNPATH_SEARCH_PATHS = ( 493 | "$(inherited)", 494 | "@executable_path/Frameworks", 495 | "@loader_path/Frameworks", 496 | ); 497 | PRODUCT_BUNDLE_IDENTIFIER = pe.rry.SwiftCameraTests; 498 | PRODUCT_NAME = "$(TARGET_NAME)"; 499 | SWIFT_VERSION = 5.0; 500 | TARGETED_DEVICE_FAMILY = "1,2"; 501 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftCamera.app/SwiftCamera"; 502 | }; 503 | name = Debug; 504 | }; 505 | A9FE17AB2539494700D547C7 /* Release */ = { 506 | isa = XCBuildConfiguration; 507 | buildSettings = { 508 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 509 | BUNDLE_LOADER = "$(TEST_HOST)"; 510 | CODE_SIGN_STYLE = Automatic; 511 | DEVELOPMENT_TEAM = HS2HJ27PLP; 512 | INFOPLIST_FILE = SwiftCameraTests/Info.plist; 513 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 514 | LD_RUNPATH_SEARCH_PATHS = ( 515 | "$(inherited)", 516 | "@executable_path/Frameworks", 517 | "@loader_path/Frameworks", 518 | ); 519 | PRODUCT_BUNDLE_IDENTIFIER = pe.rry.SwiftCameraTests; 520 | PRODUCT_NAME = "$(TARGET_NAME)"; 521 | SWIFT_VERSION = 5.0; 522 | TARGETED_DEVICE_FAMILY = "1,2"; 523 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftCamera.app/SwiftCamera"; 524 | }; 525 | name = Release; 526 | }; 527 | A9FE17AD2539494700D547C7 /* Debug */ = { 528 | isa = XCBuildConfiguration; 529 | buildSettings = { 530 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 531 | CODE_SIGN_STYLE = Automatic; 532 | DEVELOPMENT_TEAM = HS2HJ27PLP; 533 | INFOPLIST_FILE = SwiftCameraUITests/Info.plist; 534 | LD_RUNPATH_SEARCH_PATHS = ( 535 | "$(inherited)", 536 | "@executable_path/Frameworks", 537 | "@loader_path/Frameworks", 538 | ); 539 | PRODUCT_BUNDLE_IDENTIFIER = pe.rry.SwiftCameraUITests; 540 | PRODUCT_NAME = "$(TARGET_NAME)"; 541 | SWIFT_VERSION = 5.0; 542 | TARGETED_DEVICE_FAMILY = "1,2"; 543 | TEST_TARGET_NAME = SwiftCamera; 544 | }; 545 | name = Debug; 546 | }; 547 | A9FE17AE2539494700D547C7 /* Release */ = { 548 | isa = XCBuildConfiguration; 549 | buildSettings = { 550 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 551 | CODE_SIGN_STYLE = Automatic; 552 | DEVELOPMENT_TEAM = HS2HJ27PLP; 553 | INFOPLIST_FILE = SwiftCameraUITests/Info.plist; 554 | LD_RUNPATH_SEARCH_PATHS = ( 555 | "$(inherited)", 556 | "@executable_path/Frameworks", 557 | "@loader_path/Frameworks", 558 | ); 559 | PRODUCT_BUNDLE_IDENTIFIER = pe.rry.SwiftCameraUITests; 560 | PRODUCT_NAME = "$(TARGET_NAME)"; 561 | SWIFT_VERSION = 5.0; 562 | TARGETED_DEVICE_FAMILY = "1,2"; 563 | TEST_TARGET_NAME = SwiftCamera; 564 | }; 565 | name = Release; 566 | }; 567 | /* End XCBuildConfiguration section */ 568 | 569 | /* Begin XCConfigurationList section */ 570 | A9FE177C2539494300D547C7 /* Build configuration list for PBXProject "SwiftCamera" */ = { 571 | isa = XCConfigurationList; 572 | buildConfigurations = ( 573 | A9FE17A42539494700D547C7 /* Debug */, 574 | A9FE17A52539494700D547C7 /* Release */, 575 | ); 576 | defaultConfigurationIsVisible = 0; 577 | defaultConfigurationName = Release; 578 | }; 579 | A9FE17A62539494700D547C7 /* Build configuration list for PBXNativeTarget "SwiftCamera" */ = { 580 | isa = XCConfigurationList; 581 | buildConfigurations = ( 582 | A9FE17A72539494700D547C7 /* Debug */, 583 | A9FE17A82539494700D547C7 /* Release */, 584 | ); 585 | defaultConfigurationIsVisible = 0; 586 | defaultConfigurationName = Release; 587 | }; 588 | A9FE17A92539494700D547C7 /* Build configuration list for PBXNativeTarget "SwiftCameraTests" */ = { 589 | isa = XCConfigurationList; 590 | buildConfigurations = ( 591 | A9FE17AA2539494700D547C7 /* Debug */, 592 | A9FE17AB2539494700D547C7 /* Release */, 593 | ); 594 | defaultConfigurationIsVisible = 0; 595 | defaultConfigurationName = Release; 596 | }; 597 | A9FE17AC2539494700D547C7 /* Build configuration list for PBXNativeTarget "SwiftCameraUITests" */ = { 598 | isa = XCConfigurationList; 599 | buildConfigurations = ( 600 | A9FE17AD2539494700D547C7 /* Debug */, 601 | A9FE17AE2539494700D547C7 /* Release */, 602 | ); 603 | defaultConfigurationIsVisible = 0; 604 | defaultConfigurationName = Release; 605 | }; 606 | /* End XCConfigurationList section */ 607 | }; 608 | rootObject = A9FE17792539494300D547C7 /* Project object */; 609 | } 610 | -------------------------------------------------------------------------------- /SwiftCamera.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftCamera.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftCamera.xcodeproj/xcuserdata/rolando.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftCamera.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SwiftCamera/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftCamera/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SwiftCamera/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftCamera/CameraPreview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraPreview.swift 3 | // SwiftCamera 4 | // 5 | // Created by Rolando Rodriguez on 10/17/20. 6 | // 7 | 8 | import SwiftUI 9 | import AVFoundation 10 | 11 | struct CameraPreview: UIViewRepresentable { 12 | class VideoPreviewView: UIView { 13 | override class var layerClass: AnyClass { 14 | AVCaptureVideoPreviewLayer.self 15 | } 16 | 17 | var videoPreviewLayer: AVCaptureVideoPreviewLayer { 18 | return layer as! AVCaptureVideoPreviewLayer 19 | } 20 | } 21 | 22 | let session: AVCaptureSession 23 | 24 | func makeUIView(context: Context) -> VideoPreviewView { 25 | let view = VideoPreviewView() 26 | view.backgroundColor = .black 27 | view.videoPreviewLayer.cornerRadius = 0 28 | view.videoPreviewLayer.session = session 29 | view.videoPreviewLayer.connection?.videoOrientation = .portrait 30 | 31 | return view 32 | } 33 | 34 | func updateUIView(_ uiView: VideoPreviewView, context: Context) { 35 | 36 | } 37 | } 38 | 39 | struct CameraPreview_Previews: PreviewProvider { 40 | static var previews: some View { 41 | CameraPreview(session: AVCaptureSession()) 42 | .frame(height: 300) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SwiftCamera/CameraService+Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraService+Enums.swift 3 | // SwiftCamera 4 | // 5 | // Created by Rolando Rodriguez on 10/15/20. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: CameraService Enums 11 | extension CameraService { 12 | enum LivePhotoMode { 13 | case on 14 | case off 15 | } 16 | 17 | enum DepthDataDeliveryMode { 18 | case on 19 | case off 20 | } 21 | 22 | enum PortraitEffectsMatteDeliveryMode { 23 | case on 24 | case off 25 | } 26 | 27 | enum SessionSetupResult { 28 | case success 29 | case notAuthorized 30 | case configurationFailed 31 | } 32 | 33 | enum CaptureMode: Int { 34 | case photo = 0 35 | case movie = 1 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SwiftCamera/CameraService+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraService+Extensions.swift 3 | // SwiftCamera 4 | // 5 | // Created by Rolando Rodriguez on 10/15/20. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import AVFoundation 11 | 12 | extension AVCaptureVideoOrientation { 13 | init?(deviceOrientation: UIDeviceOrientation) { 14 | switch deviceOrientation { 15 | case .portrait: self = .portrait 16 | case .portraitUpsideDown: self = .portraitUpsideDown 17 | case .landscapeLeft: self = .landscapeRight 18 | case .landscapeRight: self = .landscapeLeft 19 | default: return nil 20 | } 21 | } 22 | 23 | init?(interfaceOrientation: UIInterfaceOrientation) { 24 | switch interfaceOrientation { 25 | case .portrait: self = .portrait 26 | case .portraitUpsideDown: self = .portraitUpsideDown 27 | case .landscapeLeft: self = .landscapeLeft 28 | case .landscapeRight: self = .landscapeRight 29 | default: return nil 30 | } 31 | } 32 | } 33 | 34 | extension AVCaptureDevice.DiscoverySession { 35 | var uniqueDevicePositionsCount: Int { 36 | 37 | var uniqueDevicePositions = [AVCaptureDevice.Position]() 38 | 39 | for device in devices where !uniqueDevicePositions.contains(device.position) { 40 | uniqueDevicePositions.append(device.position) 41 | } 42 | 43 | return uniqueDevicePositions.count 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SwiftCamera/CameraService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraService.swift 3 | // SwiftCamera 4 | // 5 | // Created by Rolando Rodriguez on 10/15/20. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import AVFoundation 11 | import Photos 12 | import UIKit 13 | 14 | // MARK: Class Camera Service, handles setup of AVFoundation needed for a basic camera app. 15 | public struct Photo: Identifiable, Equatable { 16 | // The ID of the captured photo 17 | public var id: String 18 | // Data representation of the captured photo 19 | public var originalData: Data 20 | 21 | public init(id: String = UUID().uuidString, originalData: Data) { 22 | self.id = id 23 | self.originalData = originalData 24 | } 25 | } 26 | 27 | public struct AlertError { 28 | public var title: String = "" 29 | public var message: String = "" 30 | public var primaryButtonTitle = "Accept" 31 | public var secondaryButtonTitle: String? 32 | public var primaryAction: (() -> ())? 33 | public var secondaryAction: (() -> ())? 34 | 35 | public init(title: String = "", message: String = "", primaryButtonTitle: String = "Accept", secondaryButtonTitle: String? = nil, primaryAction: (() -> ())? = nil, secondaryAction: (() -> ())? = nil) { 36 | self.title = title 37 | self.message = message 38 | self.primaryAction = primaryAction 39 | self.primaryButtonTitle = primaryButtonTitle 40 | self.secondaryAction = secondaryAction 41 | } 42 | } 43 | 44 | extension Photo { 45 | public var compressedData: Data? { 46 | ImageResizer(targetWidth: 800).resize(data: originalData)?.jpegData(compressionQuality: 0.5) 47 | } 48 | public var thumbnailData: Data? { 49 | ImageResizer(targetWidth: 100).resize(data: originalData)?.jpegData(compressionQuality: 0.5) 50 | } 51 | public var thumbnailImage: UIImage? { 52 | guard let data = thumbnailData else { return nil } 53 | return UIImage(data: data) 54 | } 55 | public var image: UIImage? { 56 | guard let data = compressedData else { return nil } 57 | return UIImage(data: data) 58 | } 59 | } 60 | 61 | public class CameraService { 62 | typealias PhotoCaptureSessionID = String 63 | 64 | // MARK: Observed Properties UI must react to 65 | 66 | // 1. 67 | @Published public var flashMode: AVCaptureDevice.FlashMode = .off 68 | // 2. 69 | @Published public var shouldShowAlertView = false 70 | // 3. 71 | @Published public var shouldShowSpinner = false 72 | // 4. 73 | @Published public var willCapturePhoto = false 74 | // 5. 75 | @Published public var isCameraButtonDisabled = true 76 | // 6. 77 | @Published public var isCameraUnavailable = true 78 | // 8. 79 | @Published public var photo: Photo? 80 | 81 | 82 | // MARK: Alert properties 83 | public var alertError: AlertError = AlertError() 84 | 85 | // MARK: Session Management Properties 86 | 87 | // 9 88 | public let session = AVCaptureSession() 89 | // 10 90 | var isSessionRunning = false 91 | // 12 92 | var isConfigured = false 93 | // 13 94 | var setupResult: SessionSetupResult = .success 95 | // 14 96 | // Communicate with the session and other session objects on this queue. 97 | private let sessionQueue = DispatchQueue(label: "session queue") 98 | 99 | @objc dynamic var videoDeviceInput: AVCaptureDeviceInput! 100 | 101 | // MARK: Device Configuration Properties 102 | private let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDualCamera, .builtInTrueDepthCamera], mediaType: .video, position: .unspecified) 103 | 104 | // MARK: Capturing Photos 105 | 106 | private let photoOutput = AVCapturePhotoOutput() 107 | 108 | private var inProgressPhotoCaptureDelegates = [Int64: PhotoCaptureProcessor]() 109 | 110 | // MARK: KVO and Notifications Properties 111 | 112 | private var keyValueObservations = [NSKeyValueObservation]() 113 | 114 | 115 | public func configure() { 116 | /* 117 | Setup the capture session. 118 | In general, it's not safe to mutate an AVCaptureSession or any of its 119 | inputs, outputs, or connections from multiple threads at the same time. 120 | 121 | Don't perform these tasks on the main queue because 122 | AVCaptureSession.startRunning() is a blocking call, which can 123 | take a long time. Dispatch session setup to the sessionQueue, so 124 | that the main queue isn't blocked, which keeps the UI responsive. 125 | */ 126 | sessionQueue.async { 127 | self.configureSession() 128 | } 129 | } 130 | 131 | // MARK: Checks for user's permisions 132 | public func checkForPermissions() { 133 | 134 | switch AVCaptureDevice.authorizationStatus(for: .video) { 135 | case .authorized: 136 | // The user has previously granted access to the camera. 137 | break 138 | case .notDetermined: 139 | /* 140 | The user has not yet been presented with the option to grant 141 | video access. Suspend the session queue to delay session 142 | setup until the access request has completed. 143 | */ 144 | sessionQueue.suspend() 145 | AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in 146 | if !granted { 147 | self.setupResult = .notAuthorized 148 | } 149 | self.sessionQueue.resume() 150 | }) 151 | 152 | default: 153 | // The user has previously denied access. 154 | setupResult = .notAuthorized 155 | 156 | DispatchQueue.main.async { 157 | self.alertError = AlertError(title: "Camera Access", message: "SwiftCamera doesn't have access to use your camera, please update your privacy settings.", primaryButtonTitle: "Settings", secondaryButtonTitle: nil, primaryAction: { 158 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, 159 | options: [:], completionHandler: nil) 160 | 161 | }, secondaryAction: nil) 162 | self.shouldShowAlertView = true 163 | self.isCameraUnavailable = true 164 | self.isCameraButtonDisabled = true 165 | } 166 | } 167 | } 168 | 169 | // MARK: Session Management 170 | 171 | // Call this on the session queue. 172 | /// - Tag: ConfigureSession 173 | private func configureSession() { 174 | if setupResult != .success { 175 | return 176 | } 177 | 178 | session.beginConfiguration() 179 | 180 | session.sessionPreset = .photo 181 | 182 | // Add video input. 183 | do { 184 | var defaultVideoDevice: AVCaptureDevice? 185 | 186 | if let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) { 187 | // If a rear dual camera is not available, default to the rear wide angle camera. 188 | defaultVideoDevice = backCameraDevice 189 | } else if let frontCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) { 190 | // If the rear wide angle camera isn't available, default to the front wide angle camera. 191 | defaultVideoDevice = frontCameraDevice 192 | } 193 | 194 | guard let videoDevice = defaultVideoDevice else { 195 | print("Default video device is unavailable.") 196 | setupResult = .configurationFailed 197 | session.commitConfiguration() 198 | return 199 | } 200 | 201 | let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice) 202 | 203 | if session.canAddInput(videoDeviceInput) { 204 | session.addInput(videoDeviceInput) 205 | self.videoDeviceInput = videoDeviceInput 206 | 207 | } else { 208 | print("Couldn't add video device input to the session.") 209 | setupResult = .configurationFailed 210 | session.commitConfiguration() 211 | return 212 | } 213 | } catch { 214 | print("Couldn't create video device input: \(error)") 215 | setupResult = .configurationFailed 216 | session.commitConfiguration() 217 | return 218 | } 219 | 220 | // Add the photo output. 221 | if session.canAddOutput(photoOutput) { 222 | session.addOutput(photoOutput) 223 | 224 | photoOutput.isHighResolutionCaptureEnabled = true 225 | photoOutput.maxPhotoQualityPrioritization = .quality 226 | 227 | } else { 228 | print("Could not add photo output to the session") 229 | setupResult = .configurationFailed 230 | session.commitConfiguration() 231 | return 232 | } 233 | 234 | session.commitConfiguration() 235 | 236 | self.isConfigured = true 237 | 238 | self.start() 239 | } 240 | 241 | // MARK: Device Configuration 242 | 243 | /// - Tag: ChangeCamera 244 | public func changeCamera() { 245 | // MARK: Here disable all camera operation related buttons due to configuration is due upon and must not be interrupted 246 | DispatchQueue.main.async { 247 | self.isCameraButtonDisabled = true 248 | } 249 | // 250 | 251 | sessionQueue.async { 252 | let currentVideoDevice = self.videoDeviceInput.device 253 | let currentPosition = currentVideoDevice.position 254 | 255 | let preferredPosition: AVCaptureDevice.Position 256 | let preferredDeviceType: AVCaptureDevice.DeviceType 257 | 258 | switch currentPosition { 259 | case .unspecified, .front: 260 | preferredPosition = .back 261 | preferredDeviceType = .builtInWideAngleCamera 262 | 263 | case .back: 264 | preferredPosition = .front 265 | preferredDeviceType = .builtInWideAngleCamera 266 | 267 | @unknown default: 268 | print("Unknown capture position. Defaulting to back, dual-camera.") 269 | preferredPosition = .back 270 | preferredDeviceType = .builtInWideAngleCamera 271 | } 272 | let devices = self.videoDeviceDiscoverySession.devices 273 | var newVideoDevice: AVCaptureDevice? = nil 274 | 275 | // First, seek a device with both the preferred position and device type. Otherwise, seek a device with only the preferred position. 276 | if let device = devices.first(where: { $0.position == preferredPosition && $0.deviceType == preferredDeviceType }) { 277 | newVideoDevice = device 278 | } else if let device = devices.first(where: { $0.position == preferredPosition }) { 279 | newVideoDevice = device 280 | } 281 | 282 | if let videoDevice = newVideoDevice { 283 | do { 284 | let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice) 285 | 286 | self.session.beginConfiguration() 287 | 288 | // Remove the existing device input first, because AVCaptureSession doesn't support 289 | // simultaneous use of the rear and front cameras. 290 | self.session.removeInput(self.videoDeviceInput) 291 | 292 | if self.session.canAddInput(videoDeviceInput) { 293 | self.session.addInput(videoDeviceInput) 294 | self.videoDeviceInput = videoDeviceInput 295 | } else { 296 | self.session.addInput(self.videoDeviceInput) 297 | } 298 | 299 | if let connection = self.photoOutput.connection(with: .video) { 300 | if connection.isVideoStabilizationSupported { 301 | connection.preferredVideoStabilizationMode = .auto 302 | } 303 | } 304 | 305 | self.photoOutput.maxPhotoQualityPrioritization = .quality 306 | 307 | self.session.commitConfiguration() 308 | } catch { 309 | print("Error occurred while creating video device input: \(error)") 310 | } 311 | } 312 | 313 | DispatchQueue.main.async { 314 | // MARK: Here enable capture button due to successfull setup 315 | self.isCameraButtonDisabled = false 316 | } 317 | } 318 | } 319 | 320 | public func focus(at focusPoint: CGPoint){ 321 | // let focusPoint = self.videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: point) 322 | 323 | let device = self.videoDeviceInput.device 324 | do { 325 | try device.lockForConfiguration() 326 | if device.isFocusPointOfInterestSupported { 327 | device.focusPointOfInterest = focusPoint 328 | device.exposurePointOfInterest = focusPoint 329 | device.exposureMode = .continuousAutoExposure 330 | device.focusMode = .continuousAutoFocus 331 | device.unlockForConfiguration() 332 | } 333 | } 334 | catch { 335 | print(error.localizedDescription) 336 | } 337 | } 338 | 339 | /// - Tag: Stop capture session 340 | 341 | public func stop(completion: (() -> ())? = nil) { 342 | sessionQueue.async { 343 | if self.isSessionRunning { 344 | if self.setupResult == .success { 345 | self.session.stopRunning() 346 | self.isSessionRunning = self.session.isRunning 347 | 348 | if !self.session.isRunning { 349 | DispatchQueue.main.async { 350 | self.isCameraButtonDisabled = true 351 | self.isCameraUnavailable = true 352 | completion?() 353 | } 354 | } 355 | } 356 | } 357 | } 358 | } 359 | 360 | /// - Tag: Start capture session 361 | 362 | public func start() { 363 | // We use our capture session queue to ensure our UI runs smoothly on the main thread. 364 | sessionQueue.async { 365 | if !self.isSessionRunning && self.isConfigured { 366 | switch self.setupResult { 367 | case .success: 368 | self.session.startRunning() 369 | self.isSessionRunning = self.session.isRunning 370 | 371 | if self.session.isRunning { 372 | DispatchQueue.main.async { 373 | self.isCameraButtonDisabled = false 374 | self.isCameraUnavailable = false 375 | } 376 | } 377 | 378 | case .configurationFailed, .notAuthorized: 379 | print("Application not authorized to use camera") 380 | 381 | DispatchQueue.main.async { 382 | self.alertError = AlertError(title: "Camera Error", message: "Camera configuration failed. Either your device camera is not available or its missing permissions", primaryButtonTitle: "Accept", secondaryButtonTitle: nil, primaryAction: nil, secondaryAction: nil) 383 | self.shouldShowAlertView = true 384 | self.isCameraButtonDisabled = true 385 | self.isCameraUnavailable = true 386 | } 387 | } 388 | } 389 | } 390 | } 391 | 392 | public func set(zoom: CGFloat){ 393 | let factor = zoom < 1 ? 1 : zoom 394 | let device = self.videoDeviceInput.device 395 | 396 | do { 397 | try device.lockForConfiguration() 398 | device.videoZoomFactor = factor 399 | device.unlockForConfiguration() 400 | } 401 | catch { 402 | print(error.localizedDescription) 403 | } 404 | } 405 | 406 | // MARK: Capture Photo 407 | 408 | /// - Tag: CapturePhoto 409 | public func capturePhoto() { 410 | if self.setupResult != .configurationFailed { 411 | self.isCameraButtonDisabled = true 412 | 413 | sessionQueue.async { 414 | if let photoOutputConnection = self.photoOutput.connection(with: .video) { 415 | photoOutputConnection.videoOrientation = .portrait 416 | } 417 | var photoSettings = AVCapturePhotoSettings() 418 | 419 | // Capture HEIF photos when supported. Enable according to user settings and high-resolution photos. 420 | if self.photoOutput.availablePhotoCodecTypes.contains(.hevc) { 421 | photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc]) 422 | } 423 | 424 | // Sets the flash option for this capture. 425 | if self.videoDeviceInput.device.isFlashAvailable { 426 | photoSettings.flashMode = self.flashMode 427 | } 428 | 429 | photoSettings.isHighResolutionPhotoEnabled = true 430 | 431 | // Sets the preview thumbnail pixel format 432 | if !photoSettings.__availablePreviewPhotoPixelFormatTypes.isEmpty { 433 | photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: photoSettings.__availablePreviewPhotoPixelFormatTypes.first!] 434 | } 435 | 436 | photoSettings.photoQualityPrioritization = .quality 437 | 438 | let photoCaptureProcessor = PhotoCaptureProcessor(with: photoSettings, willCapturePhotoAnimation: { [weak self] in 439 | // Tells the UI to flash the screen to signal that SwiftCamera took a photo. 440 | DispatchQueue.main.async { 441 | self?.willCapturePhoto = true 442 | } 443 | 444 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { 445 | self?.willCapturePhoto = false 446 | } 447 | 448 | }, completionHandler: { [weak self] (photoCaptureProcessor) in 449 | // When the capture is complete, remove a reference to the photo capture delegate so it can be deallocated. 450 | if let data = photoCaptureProcessor.photoData { 451 | self?.photo = Photo(originalData: data) 452 | print("passing photo") 453 | } else { 454 | print("No photo data") 455 | } 456 | 457 | self?.isCameraButtonDisabled = false 458 | 459 | self?.sessionQueue.async { 460 | self?.inProgressPhotoCaptureDelegates[photoCaptureProcessor.requestedPhotoSettings.uniqueID] = nil 461 | } 462 | }, photoProcessingHandler: { [weak self] animate in 463 | // Animates a spinner while photo is processing 464 | if animate { 465 | self?.shouldShowSpinner = true 466 | } else { 467 | self?.shouldShowSpinner = false 468 | } 469 | }) 470 | 471 | // The photo output holds a weak reference to the photo capture delegate and stores it in an array to maintain a strong reference. 472 | self.inProgressPhotoCaptureDelegates[photoCaptureProcessor.requestedPhotoSettings.uniqueID] = photoCaptureProcessor 473 | self.photoOutput.capturePhoto(with: photoSettings, delegate: photoCaptureProcessor) 474 | } 475 | } 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /SwiftCamera/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SwiftCamera 4 | // 5 | // Created by Rolando Rodriguez on 10/15/20. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import AVFoundation 11 | 12 | final class CameraModel: ObservableObject { 13 | private let service = CameraService() 14 | 15 | @Published var photo: Photo! 16 | 17 | @Published var showAlertError = false 18 | 19 | @Published var isFlashOn = false 20 | 21 | @Published var willCapturePhoto = false 22 | 23 | var alertError: AlertError! 24 | 25 | var session: AVCaptureSession 26 | 27 | private var subscriptions = Set() 28 | 29 | init() { 30 | self.session = service.session 31 | 32 | service.$photo.sink { [weak self] (photo) in 33 | guard let pic = photo else { return } 34 | self?.photo = pic 35 | } 36 | .store(in: &self.subscriptions) 37 | 38 | service.$shouldShowAlertView.sink { [weak self] (val) in 39 | self?.alertError = self?.service.alertError 40 | self?.showAlertError = val 41 | } 42 | .store(in: &self.subscriptions) 43 | 44 | service.$flashMode.sink { [weak self] (mode) in 45 | self?.isFlashOn = mode == .on 46 | } 47 | .store(in: &self.subscriptions) 48 | 49 | service.$willCapturePhoto.sink { [weak self] (val) in 50 | self?.willCapturePhoto = val 51 | } 52 | .store(in: &self.subscriptions) 53 | } 54 | 55 | func configure() { 56 | service.checkForPermissions() 57 | service.configure() 58 | } 59 | 60 | func capturePhoto() { 61 | service.capturePhoto() 62 | } 63 | 64 | func flipCamera() { 65 | service.changeCamera() 66 | } 67 | 68 | func zoom(with factor: CGFloat) { 69 | service.set(zoom: factor) 70 | } 71 | 72 | func switchFlash() { 73 | service.flashMode = service.flashMode == .on ? .off : .on 74 | } 75 | } 76 | 77 | struct CameraView: View { 78 | @StateObject var model = CameraModel() 79 | 80 | @State var currentZoomFactor: CGFloat = 1.0 81 | 82 | var captureButton: some View { 83 | Button(action: { 84 | model.capturePhoto() 85 | }, label: { 86 | Circle() 87 | .foregroundColor(.white) 88 | .frame(width: 80, height: 80, alignment: .center) 89 | .overlay( 90 | Circle() 91 | .stroke(Color.black.opacity(0.8), lineWidth: 2) 92 | .frame(width: 65, height: 65, alignment: .center) 93 | ) 94 | }) 95 | } 96 | 97 | var capturedPhotoThumbnail: some View { 98 | Group { 99 | if model.photo != nil { 100 | Image(uiImage: model.photo.image!) 101 | .resizable() 102 | .aspectRatio(contentMode: .fill) 103 | .frame(width: 60, height: 60) 104 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) 105 | .animation(.spring()) 106 | 107 | } else { 108 | RoundedRectangle(cornerRadius: 10) 109 | .frame(width: 60, height: 60, alignment: .center) 110 | .foregroundColor(.black) 111 | } 112 | } 113 | } 114 | 115 | var flipCameraButton: some View { 116 | Button(action: { 117 | model.flipCamera() 118 | }, label: { 119 | Circle() 120 | .foregroundColor(Color.gray.opacity(0.2)) 121 | .frame(width: 45, height: 45, alignment: .center) 122 | .overlay( 123 | Image(systemName: "camera.rotate.fill") 124 | .foregroundColor(.white)) 125 | }) 126 | } 127 | 128 | var body: some View { 129 | GeometryReader { reader in 130 | ZStack { 131 | Color.black.edgesIgnoringSafeArea(.all) 132 | 133 | VStack { 134 | Button(action: { 135 | model.switchFlash() 136 | }, label: { 137 | Image(systemName: model.isFlashOn ? "bolt.fill" : "bolt.slash.fill") 138 | .font(.system(size: 20, weight: .medium, design: .default)) 139 | }) 140 | .accentColor(model.isFlashOn ? .yellow : .white) 141 | 142 | CameraPreview(session: model.session) 143 | .gesture( 144 | DragGesture().onChanged({ (val) in 145 | // Only accept vertical drag 146 | if abs(val.translation.height) > abs(val.translation.width) { 147 | // Get the percentage of vertical screen space covered by drag 148 | let percentage: CGFloat = -(val.translation.height / reader.size.height) 149 | // Calculate new zoom factor 150 | let calc = currentZoomFactor + percentage 151 | // Limit zoom factor to a maximum of 5x and a minimum of 1x 152 | let zoomFactor: CGFloat = min(max(calc, 1), 5) 153 | // Store the newly calculated zoom factor 154 | currentZoomFactor = zoomFactor 155 | // Sets the zoom factor to the capture device session 156 | model.zoom(with: zoomFactor) 157 | } 158 | }) 159 | ) 160 | .onAppear { 161 | model.configure() 162 | } 163 | .alert(isPresented: $model.showAlertError, content: { 164 | Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: { 165 | model.alertError.primaryAction?() 166 | })) 167 | }) 168 | .overlay( 169 | Group { 170 | if model.willCapturePhoto { 171 | Color.black 172 | } 173 | } 174 | ) 175 | .animation(.easeInOut) 176 | 177 | 178 | HStack { 179 | capturedPhotoThumbnail 180 | 181 | Spacer() 182 | 183 | captureButton 184 | 185 | Spacer() 186 | 187 | flipCameraButton 188 | 189 | } 190 | .padding(.horizontal, 20) 191 | } 192 | } 193 | } 194 | } 195 | } 196 | 197 | struct ContentView_Previews: PreviewProvider { 198 | static var previews: some View { 199 | CameraView() 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /SwiftCamera/ImageResizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageResizer.swift 3 | // SwiftCamera 4 | // 5 | // Created by Rolando Rodriguez on 10/15/20. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | enum ImageResizingError: Error { 12 | case cannotRetrieveFromURL 13 | case cannotRetrieveFromData 14 | } 15 | 16 | public struct ImageResizer { 17 | var targetWidth: CGFloat 18 | 19 | public func resize(at url: URL) -> UIImage? { 20 | guard let image = UIImage(contentsOfFile: url.path) else { 21 | return nil 22 | } 23 | 24 | return self.resize(image: image) 25 | } 26 | 27 | public func resize(image: UIImage) -> UIImage { 28 | let originalSize = image.size 29 | let targetSize = CGSize(width: targetWidth, height: targetWidth*originalSize.height/originalSize.width) 30 | let renderer = UIGraphicsImageRenderer(size: targetSize) 31 | return renderer.image { (context) in 32 | image.draw(in: CGRect(origin: .zero, size: targetSize)) 33 | } 34 | } 35 | 36 | public func resize(data: Data) -> UIImage? { 37 | guard let image = UIImage(data: data) else {return nil} 38 | return resize(image: image ) 39 | } 40 | } 41 | 42 | struct MemorySizer { 43 | static func size(of data: Data) -> String { 44 | let bcf = ByteCountFormatter() 45 | bcf.allowedUnits = [.useMB] // optional: restricts the units to MB only 46 | bcf.countStyle = .file 47 | let string = bcf.string(fromByteCount: Int64(data.count)) 48 | return string 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SwiftCamera/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPhotoLibraryUsageDescription 6 | SwiftCamera captures photos and saves them into your photo library 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSCameraUsageDescription 26 | SwiftCamera needs access to your camera to capture photo and video 27 | UIApplicationSceneManifest 28 | 29 | UIApplicationSupportsMultipleScenes 30 | 31 | 32 | UIApplicationSupportsIndirectInputEvents 33 | 34 | UILaunchScreen 35 | 36 | UIRequiredDeviceCapabilities 37 | 38 | armv7 39 | 40 | UISupportedInterfaceOrientations 41 | 42 | UIInterfaceOrientationPortrait 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /SwiftCamera/PhotoCaptureProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoCaptureProcessor.swift 3 | // abseil 4 | // 5 | // Created by Rolando Rodriguez on 10/15/20. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | 11 | class PhotoCaptureProcessor: NSObject { 12 | 13 | lazy var context = CIContext() 14 | 15 | private(set) var requestedPhotoSettings: AVCapturePhotoSettings 16 | 17 | private let willCapturePhotoAnimation: () -> Void 18 | 19 | private let completionHandler: (PhotoCaptureProcessor) -> Void 20 | 21 | private let photoProcessingHandler: (Bool) -> Void 22 | 23 | // The actual captured photo's data 24 | var photoData: Data? 25 | 26 | // The maximum time lapse before telling UI to show a spinner 27 | private var maxPhotoProcessingTime: CMTime? 28 | 29 | // Init takes multiple closures to be called in each step of the photco capture process 30 | init(with requestedPhotoSettings: AVCapturePhotoSettings, willCapturePhotoAnimation: @escaping () -> Void, completionHandler: @escaping (PhotoCaptureProcessor) -> Void, photoProcessingHandler: @escaping (Bool) -> Void) { 31 | 32 | self.requestedPhotoSettings = requestedPhotoSettings 33 | self.willCapturePhotoAnimation = willCapturePhotoAnimation 34 | self.completionHandler = completionHandler 35 | self.photoProcessingHandler = photoProcessingHandler 36 | } 37 | } 38 | 39 | extension PhotoCaptureProcessor: AVCapturePhotoCaptureDelegate { 40 | 41 | // This extension adopts AVCapturePhotoCaptureDelegate protocol methods. 42 | 43 | /// - Tag: WillBeginCapture 44 | func photoOutput(_ output: AVCapturePhotoOutput, willBeginCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings) { 45 | maxPhotoProcessingTime = resolvedSettings.photoProcessingTimeRange.start + resolvedSettings.photoProcessingTimeRange.duration 46 | } 47 | 48 | /// - Tag: WillCapturePhoto 49 | func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) { 50 | DispatchQueue.main.async { 51 | self.willCapturePhotoAnimation() 52 | } 53 | 54 | guard let maxPhotoProcessingTime = maxPhotoProcessingTime else { 55 | return 56 | } 57 | 58 | // Show a spinner if processing time exceeds one second. 59 | let oneSecond = CMTime(seconds: 2, preferredTimescale: 1) 60 | if maxPhotoProcessingTime > oneSecond { 61 | DispatchQueue.main.async { 62 | self.photoProcessingHandler(true) 63 | } 64 | } 65 | } 66 | 67 | /// - Tag: DidFinishProcessingPhoto 68 | func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { 69 | 70 | DispatchQueue.main.async { 71 | self.photoProcessingHandler(false) 72 | } 73 | 74 | if let error = error { 75 | print("Error capturing photo: \(error)") 76 | } else { 77 | photoData = photo.fileDataRepresentation() 78 | } 79 | } 80 | 81 | // MARK: Saves capture to photo library 82 | func saveToPhotoLibrary(_ photoData: Data) { 83 | 84 | PHPhotoLibrary.requestAuthorization { status in 85 | if status == .authorized { 86 | PHPhotoLibrary.shared().performChanges({ 87 | let options = PHAssetResourceCreationOptions() 88 | let creationRequest = PHAssetCreationRequest.forAsset() 89 | options.uniformTypeIdentifier = self.requestedPhotoSettings.processedFileType.map { $0.rawValue } 90 | creationRequest.addResource(with: .photo, data: photoData, options: options) 91 | 92 | 93 | }, completionHandler: { _, error in 94 | if let error = error { 95 | print("Error occurred while saving photo to photo library: \(error)") 96 | } 97 | 98 | DispatchQueue.main.async { 99 | self.completionHandler(self) 100 | } 101 | } 102 | ) 103 | } else { 104 | DispatchQueue.main.async { 105 | self.completionHandler(self) 106 | } 107 | } 108 | } 109 | } 110 | 111 | /// - Tag: DidFinishCapture 112 | func photoOutput(_ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) { 113 | if let error = error { 114 | print("Error capturing photo: \(error)") 115 | DispatchQueue.main.async { 116 | self.completionHandler(self) 117 | } 118 | return 119 | } else { 120 | guard let data = photoData else { 121 | DispatchQueue.main.async { 122 | self.completionHandler(self) 123 | } 124 | return 125 | } 126 | 127 | self.saveToPhotoLibrary(data) 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /SwiftCamera/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftCamera/SwiftCameraApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftCameraApp.swift 3 | // SwiftCamera 4 | // 5 | // Created by Rolando Rodriguez on 10/15/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SwiftCameraApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | CameraView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SwiftCameraTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SwiftCameraTests/SwiftCameraTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftCameraTests.swift 3 | // SwiftCameraTests 4 | // 5 | // Created by Rolando Rodriguez on 10/15/20. 6 | // 7 | 8 | import XCTest 9 | @testable import SwiftCamera 10 | 11 | class SwiftCameraTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /SwiftCameraUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SwiftCameraUITests/SwiftCameraUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftCameraUITests.swift 3 | // SwiftCameraUITests 4 | // 5 | // Created by Rolando Rodriguez on 10/15/20. 6 | // 7 | 8 | import XCTest 9 | 10 | class SwiftCameraUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | --------------------------------------------------------------------------------