├── .gitignore
├── DemoApp
├── DemoApp
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── DemoAppApp.swift
│ └── ContentView.swift
├── DemoApp.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── project.pbxproj
├── DemoAppUITests
│ ├── DemoAppUITestsLaunchTests.swift
│ └── DemoAppUITests.swift
└── DemoAppTests
│ └── DemoAppTests.swift
├── Tests
├── LinuxMain.swift
└── Camera-SwiftUITests
│ ├── XCTestManifests.swift
│ └── Camera_SwiftUITests.swift
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Sources
└── Camera-SwiftUI
│ ├── Service
│ ├── CameraService+Enums.swift
│ ├── CameraService+Extensions.swift
│ ├── ImageResizer.swift
│ ├── PhotoCaptureProcessor.swift
│ └── CameraService.swift
│ └── View
│ └── CameraPreview.swift
├── Package.swift
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/DemoApp/DemoApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/DemoApp/DemoApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import Camera_SwiftUITests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += Camera_SwiftUITests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/DemoApp/DemoApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/DemoApp/DemoApp/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 |
--------------------------------------------------------------------------------
/Tests/Camera-SwiftUITests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(Camera_SwiftUITests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/DemoApp/DemoApp/DemoAppApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoAppApp.swift
3 | // DemoApp
4 | //
5 | // Created by Rolando Rodriguez on 8/25/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct DemoAppApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | CameraView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/DemoApp/DemoApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tests/Camera-SwiftUITests/Camera_SwiftUITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Camera_SwiftUI
3 |
4 | final class Camera_SwiftUITests: XCTestCase {
5 | func testExample() {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | }
10 |
11 | static var allTests = [
12 | ("testExample", testExample),
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Camera-SwiftUI/Service/CameraService+Enums.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraService+Enums.swift
3 | // Campus
4 | //
5 | // Created by Rolando Rodriguez on 1/11/20.
6 | // Copyright © 2020 Rolando Rodriguez. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: CameraService Enums
12 | extension CameraService {
13 | enum LivePhotoMode {
14 | case on
15 | case off
16 | }
17 |
18 | enum DepthDataDeliveryMode {
19 | case on
20 | case off
21 | }
22 |
23 | enum PortraitEffectsMatteDeliveryMode {
24 | case on
25 | case off
26 | }
27 |
28 | enum SessionSetupResult {
29 | case success
30 | case notAuthorized
31 | case configurationFailed
32 | }
33 |
34 | enum CaptureMode: Int {
35 | case photo = 0
36 | case movie = 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/DemoApp/DemoAppUITests/DemoAppUITestsLaunchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoAppUITestsLaunchTests.swift
3 | // DemoAppUITests
4 | //
5 | // Created by Rolando Rodriguez on 8/25/22.
6 | //
7 |
8 | import XCTest
9 |
10 | class DemoAppUITestsLaunchTests: XCTestCase {
11 |
12 | override class var runsForEachTargetApplicationUIConfiguration: Bool {
13 | true
14 | }
15 |
16 | override func setUpWithError() throws {
17 | continueAfterFailure = false
18 | }
19 |
20 | func testLaunch() throws {
21 | let app = XCUIApplication()
22 | app.launch()
23 |
24 | // Insert steps here to perform after app launch but before taking a screenshot,
25 | // such as logging into a test account or navigating somewhere in the app
26 |
27 | let attachment = XCTAttachment(screenshot: app.screenshot())
28 | attachment.name = "Launch Screen"
29 | attachment.lifetime = .keepAlways
30 | add(attachment)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
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: "Camera-SwiftUI",
8 | platforms: [
9 | .iOS(.v13),
10 | .macOS(.v11)
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, and make them visible to other packages.
14 | .library(
15 | name: "Camera-SwiftUI",
16 | targets: ["Camera-SwiftUI"]),
17 | ],
18 | dependencies: [
19 | // Dependencies declare other packages that this package depends on.
20 | // .package(url: /* package url */, from: "1.0.0"),
21 | ],
22 | targets: [
23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
24 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
25 | .target(
26 | name: "Camera-SwiftUI",
27 | dependencies: []),
28 | .testTarget(
29 | name: "Camera-SwiftUITests",
30 | dependencies: ["Camera-SwiftUI"]),
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/DemoApp/DemoAppTests/DemoAppTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoAppTests.swift
3 | // DemoAppTests
4 | //
5 | // Created by Rolando Rodriguez on 8/25/22.
6 | //
7 |
8 | import XCTest
9 | @testable import DemoApp
10 |
11 | class DemoAppTests: 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 | // Any test you write for XCTest can be annotated as throws and async.
25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
27 | }
28 |
29 | func testPerformanceExample() throws {
30 | // This is an example of a performance test case.
31 | self.measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/DemoApp/DemoAppUITests/DemoAppUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoAppUITests.swift
3 | // DemoAppUITests
4 | //
5 | // Created by Rolando Rodriguez on 8/25/22.
6 | //
7 |
8 | import XCTest
9 |
10 | class DemoAppUITests: 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 XCTAssert and related functions to verify your tests produce the correct results.
31 | }
32 |
33 | func testLaunchPerformance() throws {
34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
35 | // This measures how long it takes to launch your application.
36 | measure(metrics: [XCTApplicationLaunchMetric()]) {
37 | XCUIApplication().launch()
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/Camera-SwiftUI/Service/CameraService+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraService+Extensions.swift
3 | // Campus
4 | //
5 | // Created by Rolando Rodriguez on 1/11/20.
6 | // Copyright © 2020 Rolando Rodriguez. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AVFoundation
12 |
13 | extension AVCaptureVideoOrientation {
14 | init?(deviceOrientation: UIDeviceOrientation) {
15 | switch deviceOrientation {
16 | case .portrait: self = .portrait
17 | case .portraitUpsideDown: self = .portraitUpsideDown
18 | case .landscapeLeft: self = .landscapeRight
19 | case .landscapeRight: self = .landscapeLeft
20 | default: return nil
21 | }
22 | }
23 |
24 | init?(interfaceOrientation: UIInterfaceOrientation) {
25 | switch interfaceOrientation {
26 | case .portrait: self = .portrait
27 | case .portraitUpsideDown: self = .portraitUpsideDown
28 | case .landscapeLeft: self = .landscapeLeft
29 | case .landscapeRight: self = .landscapeRight
30 | default: return nil
31 | }
32 | }
33 | }
34 |
35 | extension AVCaptureDevice.DiscoverySession {
36 | var uniqueDevicePositionsCount: Int {
37 |
38 | var uniqueDevicePositions = [AVCaptureDevice.Position]()
39 |
40 | for device in devices where !uniqueDevicePositions.contains(device.position) {
41 | uniqueDevicePositions.append(device.position)
42 | }
43 |
44 | return uniqueDevicePositions.count
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Camera-SwiftUI
2 |
3 | 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.
4 |
5 | 
6 | This is the package developed and used in the [Effortless SwiftUI-- Camera](https://rorodriguez116.medium.com/effortless-swiftui-camera-d7a74abde37e) tutorial.
7 |
8 |
9 | # Features
10 | - Camera feed.
11 | - Tap to focus.
12 | - Drag to zoom in/out.
13 | - Thumbnail preview.
14 | - Save capture to photo library.
15 | - Switch between front and back facing cameras.
16 | - Set flash on/off
17 |
18 | # Try it out!
19 | You can download the whole example project from [here](https://github.com/rorodriguez116/SwiftCamera). If you'd like to read the article for this project click [here](https://rorodriguez116.medium.com/effortless-swiftui-camera-d7a74abde37e)
20 |
21 | # Installation
22 | This package is and will only be available trough Swift Package Manager. You can add this pacakage as a dependency in Xcode 12.
23 |
24 | Use the following link when Xcode requests the package's repo url:
25 |
26 | https://github.com/rorodriguez116/Camera-SwiftUI.git
27 |
28 | License
29 | ----
30 |
31 | MIT
32 |
33 |
--------------------------------------------------------------------------------
/Sources/Camera-SwiftUI/Service/ImageResizer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageResizer.swift
3 | // Campus
4 | //
5 | // Created by Rolando Rodriguez on 12/21/19.
6 | // Copyright © 2019 Rolando Rodriguez. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | public enum ImageResizingError: Error {
13 | case cannotRetrieveFromURL
14 | case cannotRetrieveFromData
15 | }
16 |
17 | public struct ImageResizer {
18 | public var targetWidth: CGFloat
19 |
20 | public init(targetWidth: CGFloat) {
21 | self.targetWidth = targetWidth
22 | }
23 |
24 | public func resize(at url: URL) -> UIImage? {
25 | guard let image = UIImage(contentsOfFile: url.path) else {
26 | return nil
27 | }
28 |
29 | return self.resize(image: image)
30 | }
31 |
32 | public func resize(image: UIImage) -> UIImage {
33 | let originalSize = image.size
34 | let targetSize = CGSize(width: targetWidth, height: targetWidth*originalSize.height/originalSize.width)
35 | let renderer = UIGraphicsImageRenderer(size: targetSize)
36 | return renderer.image { (context) in
37 | image.draw(in: CGRect(origin: .zero, size: targetSize))
38 | }
39 | }
40 |
41 | public func resize(data: Data) -> UIImage? {
42 | guard let image = UIImage(data: data) else {return nil}
43 | return resize(image: image )
44 | }
45 | }
46 |
47 | public struct MemorySizer {
48 | public static func size(of data: Data) -> String {
49 | let bcf = ByteCountFormatter()
50 | bcf.allowedUnits = [.useMB] // optional: restricts the units to MB only
51 | bcf.countStyle = .file
52 | let string = bcf.string(fromByteCount: Int64(data.count))
53 | return string
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/DemoApp/DemoApp/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" : "2x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "83.5x83.5"
82 | },
83 | {
84 | "idiom" : "ios-marketing",
85 | "scale" : "1x",
86 | "size" : "1024x1024"
87 | }
88 | ],
89 | "info" : {
90 | "author" : "xcode",
91 | "version" : 1
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Sources/Camera-SwiftUI/View/CameraPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraPreview.swift
3 | // Campus
4 | //
5 | // Created by Rolando Rodriguez on 12/17/19.
6 | // Copyright © 2019 Rolando Rodriguez. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 | import SwiftUI
12 |
13 | public struct CameraPreview: UIViewRepresentable {
14 | public class VideoPreviewView: UIView {
15 | public override class var layerClass: AnyClass {
16 | AVCaptureVideoPreviewLayer.self
17 | }
18 |
19 | var videoPreviewLayer: AVCaptureVideoPreviewLayer {
20 | return layer as! AVCaptureVideoPreviewLayer
21 | }
22 |
23 | let focusView: UIView = {
24 | let focusView = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
25 | focusView.layer.borderColor = UIColor.white.cgColor
26 | focusView.layer.borderWidth = 1.5
27 | focusView.layer.cornerRadius = 25
28 | focusView.layer.opacity = 0
29 | focusView.backgroundColor = .clear
30 | return focusView
31 | }()
32 |
33 | @objc func focusAndExposeTap(gestureRecognizer: UITapGestureRecognizer) {
34 | let layerPoint = gestureRecognizer.location(in: gestureRecognizer.view)
35 | let devicePoint = videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: layerPoint)
36 |
37 | let focusCircleDiam: CGFloat = 50
38 | let shiftedLayerPoint = CGPoint(x: layerPoint.x - (focusCircleDiam / 2),
39 | y: layerPoint.y - (focusCircleDiam / 2))
40 |
41 | focusView.layer.frame = CGRect(origin: shiftedLayerPoint, size: CGSize(width: focusCircleDiam, height: focusCircleDiam))
42 |
43 | NotificationCenter.default.post(.init(name: .init("UserDidRequestNewFocusPoint"), object: nil, userInfo: ["devicePoint": devicePoint] as [AnyHashable: Any]))
44 |
45 | UIView.animate(withDuration: 0.3, animations: {
46 | self.focusView.layer.opacity = 1
47 | }) { (completed) in
48 | if completed {
49 | UIView.animate(withDuration: 0.3) {
50 | self.focusView.layer.opacity = 0
51 | }
52 | }
53 | }
54 | }
55 |
56 | public override func layoutSubviews() {
57 | super.layoutSubviews()
58 |
59 | self.layer.addSublayer(focusView.layer)
60 |
61 | let gRecognizer = UITapGestureRecognizer(target: self, action: #selector(VideoPreviewView.focusAndExposeTap(gestureRecognizer:)))
62 | self.addGestureRecognizer(gRecognizer)
63 | }
64 | }
65 |
66 | public let session: AVCaptureSession
67 |
68 | public init(session: AVCaptureSession) {
69 | self.session = session
70 | }
71 |
72 | public func makeUIView(context: Context) -> VideoPreviewView {
73 | let viewFinder = VideoPreviewView()
74 | viewFinder.backgroundColor = .black
75 | viewFinder.videoPreviewLayer.cornerRadius = 0
76 | viewFinder.videoPreviewLayer.session = session
77 | viewFinder.videoPreviewLayer.connection?.videoOrientation = .portrait
78 | return viewFinder
79 | }
80 |
81 | public func updateUIView(_ uiView: VideoPreviewView, context: Context) {
82 |
83 | }
84 | }
85 |
86 | struct CameraPreview_Previews: PreviewProvider {
87 | static var previews: some View {
88 | CameraPreview(session: AVCaptureSession())
89 | .frame(height: 300)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/Camera-SwiftUI/Service/PhotoCaptureProcessor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoCaptureProcessor.swift
3 | // abseil
4 | //
5 | // Created by Rolando Rodriguez on 1/11/20.
6 | //
7 |
8 | import Foundation
9 | import Photos
10 |
11 | class PhotoCaptureProcessor: NSObject {
12 | private(set) var requestedPhotoSettings: AVCapturePhotoSettings
13 |
14 | private let willCapturePhotoAnimation: () -> Void
15 |
16 | lazy var context = CIContext()
17 |
18 | private let completionHandler: (PhotoCaptureProcessor) -> Void
19 |
20 | private let photoProcessingHandler: (Bool) -> Void
21 |
22 | var photoData: Data?
23 |
24 | private var maxPhotoProcessingTime: CMTime?
25 |
26 | init(with requestedPhotoSettings: AVCapturePhotoSettings,
27 | willCapturePhotoAnimation: @escaping () -> Void,
28 | completionHandler: @escaping (PhotoCaptureProcessor) -> Void,
29 | photoProcessingHandler: @escaping (Bool) -> Void) {
30 | self.requestedPhotoSettings = requestedPhotoSettings
31 | self.willCapturePhotoAnimation = willCapturePhotoAnimation
32 | self.completionHandler = completionHandler
33 | self.photoProcessingHandler = photoProcessingHandler
34 | }
35 | }
36 |
37 | extension PhotoCaptureProcessor: AVCapturePhotoCaptureDelegate {
38 | /*
39 | This extension adopts all of the AVCapturePhotoCaptureDelegate protocol methods.
40 | */
41 |
42 | /// - Tag: WillBeginCapture
43 | func photoOutput(_ output: AVCapturePhotoOutput, willBeginCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
44 | maxPhotoProcessingTime = resolvedSettings.photoProcessingTimeRange.start + resolvedSettings.photoProcessingTimeRange.duration
45 | }
46 |
47 | /// - Tag: WillCapturePhoto
48 | func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
49 | DispatchQueue.main.async {
50 | self.willCapturePhotoAnimation()
51 | }
52 |
53 | guard let maxPhotoProcessingTime = maxPhotoProcessingTime else {
54 | return
55 | }
56 |
57 | // Show a spinner if processing time exceeds one second.
58 | let oneSecond = CMTime(seconds: 2, preferredTimescale: 1)
59 | if maxPhotoProcessingTime > oneSecond {
60 | DispatchQueue.main.async {
61 | self.photoProcessingHandler(true)
62 | }
63 | }
64 | }
65 |
66 | /// - Tag: DidFinishProcessingPhoto
67 | func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
68 |
69 | DispatchQueue.main.async {
70 | self.photoProcessingHandler(false)
71 | }
72 |
73 | if let error = error {
74 | print("Error capturing photo: \(error)")
75 | } else {
76 | photoData = photo.fileDataRepresentation()
77 |
78 | }
79 | }
80 |
81 | fileprivate func saveToPhotoLibrary(_ photoData: Data) {
82 | // MARK: Saves capture to photo library
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 | }
120 |
121 | DispatchQueue.main.async {
122 | self.completionHandler(self)
123 | }
124 |
125 | // self.saveToPhotoLibrary(photoData)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/DemoApp/DemoApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // DemoApp
4 | //
5 | // Created by Rolando Rodriguez on 8/25/22.
6 | //
7 |
8 | import SwiftUI
9 | import Combine
10 | import Camera_SwiftUI
11 | import AVFoundation
12 |
13 | final class CameraModel: ObservableObject {
14 | private let service = CameraService()
15 |
16 | @Published var photo: Photo!
17 |
18 | @Published var showAlertError = false
19 |
20 | @Published var isFlashOn = false
21 |
22 | @Published var willCapturePhoto = false
23 |
24 | var alertError: AlertError!
25 |
26 | var session: AVCaptureSession
27 |
28 | private var subscriptions = Set()
29 |
30 | init() {
31 | self.session = service.session
32 |
33 | service.$photo.sink { [weak self] (photo) in
34 | guard let pic = photo else { return }
35 | self?.photo = pic
36 | }
37 | .store(in: &self.subscriptions)
38 |
39 | service.$shouldShowAlertView.sink { [weak self] (val) in
40 | self?.alertError = self?.service.alertError
41 | self?.showAlertError = val
42 | }
43 | .store(in: &self.subscriptions)
44 |
45 | service.$flashMode.sink { [weak self] (mode) in
46 | self?.isFlashOn = mode == .on
47 | }
48 | .store(in: &self.subscriptions)
49 |
50 | service.$willCapturePhoto.sink { [weak self] (val) in
51 | self?.willCapturePhoto = val
52 | }
53 | .store(in: &self.subscriptions)
54 | }
55 |
56 | func configure() {
57 | service.checkForPermissions()
58 | service.configure()
59 | }
60 |
61 | func capturePhoto() {
62 | service.capturePhoto()
63 | }
64 |
65 | func flipCamera() {
66 | service.changeCamera()
67 | }
68 |
69 | func zoom(with factor: CGFloat) {
70 | service.set(zoom: factor)
71 | }
72 |
73 | func switchFlash() {
74 | service.flashMode = service.flashMode == .on ? .off : .on
75 | }
76 | }
77 |
78 | struct CameraView: View {
79 | @StateObject var model = CameraModel()
80 |
81 | @State var currentZoomFactor: CGFloat = 1.0
82 |
83 | var captureButton: some View {
84 | Button(action: {
85 | model.capturePhoto()
86 | }, label: {
87 | Circle()
88 | .foregroundColor(.white)
89 | .frame(width: 80, height: 80, alignment: .center)
90 | .overlay(
91 | Circle()
92 | .stroke(Color.black.opacity(0.8), lineWidth: 2)
93 | .frame(width: 65, height: 65, alignment: .center)
94 | )
95 | })
96 | }
97 |
98 | var capturedPhotoThumbnail: some View {
99 | Group {
100 | if model.photo != nil {
101 | Image(uiImage: model.photo.image!)
102 | .resizable()
103 | .aspectRatio(contentMode: .fill)
104 | .frame(width: 60, height: 60)
105 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
106 | .animation(.spring())
107 |
108 | } else {
109 | RoundedRectangle(cornerRadius: 10)
110 | .frame(width: 60, height: 60, alignment: .center)
111 | .foregroundColor(.black)
112 | }
113 | }
114 | }
115 |
116 | var flipCameraButton: some View {
117 | Button(action: {
118 | model.flipCamera()
119 | }, label: {
120 | Circle()
121 | .foregroundColor(Color.gray.opacity(0.2))
122 | .frame(width: 45, height: 45, alignment: .center)
123 | .overlay(
124 | Image(systemName: "camera.rotate.fill")
125 | .foregroundColor(.white))
126 | })
127 | }
128 |
129 | var body: some View {
130 | NavigationView {
131 | GeometryReader { reader in
132 | ZStack {
133 | Color.black.edgesIgnoringSafeArea(.all)
134 |
135 | VStack {
136 | Button(action: {
137 | model.switchFlash()
138 | }, label: {
139 | Image(systemName: model.isFlashOn ? "bolt.fill" : "bolt.slash.fill")
140 | .font(.system(size: 20, weight: .medium, design: .default))
141 | })
142 | .accentColor(model.isFlashOn ? .yellow : .white)
143 |
144 | CameraPreview(session: model.session)
145 | .gesture(
146 | DragGesture().onChanged({ (val) in
147 | // Only accept vertical drag
148 | if abs(val.translation.height) > abs(val.translation.width) {
149 | // Get the percentage of vertical screen space covered by drag
150 | let percentage: CGFloat = -(val.translation.height / reader.size.height)
151 | // Calculate new zoom factor
152 | let calc = currentZoomFactor + percentage
153 | // Limit zoom factor to a maximum of 5x and a minimum of 1x
154 | let zoomFactor: CGFloat = min(max(calc, 1), 5)
155 | // Store the newly calculated zoom factor
156 | currentZoomFactor = zoomFactor
157 | // Sets the zoom factor to the capture device session
158 | model.zoom(with: zoomFactor)
159 | }
160 | })
161 | )
162 | .onAppear {
163 | model.configure()
164 | }
165 | .alert(isPresented: $model.showAlertError, content: {
166 | Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
167 | model.alertError.primaryAction?()
168 | }))
169 | })
170 | .overlay(
171 | Group {
172 | if model.willCapturePhoto {
173 | Color.black
174 | }
175 | }
176 | )
177 | .animation(.easeInOut)
178 |
179 |
180 | HStack {
181 | NavigationLink(destination: Text("Detail photo")) {
182 | capturedPhotoThumbnail
183 | }
184 |
185 | Spacer()
186 |
187 | captureButton
188 |
189 | Spacer()
190 |
191 | flipCameraButton
192 |
193 | }
194 | .padding(.horizontal, 20)
195 | }
196 | }
197 | }
198 | }
199 | }
200 | }
201 |
202 | struct ContentView_Previews: PreviewProvider {
203 | static var previews: some View {
204 | CameraView()
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/DemoApp/DemoApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | A9E714B928B7892F00A273A7 /* DemoAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E714B828B7892F00A273A7 /* DemoAppApp.swift */; };
11 | A9E714BB28B7892F00A273A7 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E714BA28B7892F00A273A7 /* ContentView.swift */; };
12 | A9E714BD28B7893100A273A7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A9E714BC28B7893100A273A7 /* Assets.xcassets */; };
13 | A9E714C028B7893100A273A7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A9E714BF28B7893100A273A7 /* Preview Assets.xcassets */; };
14 | A9E714CA28B7893100A273A7 /* DemoAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E714C928B7893100A273A7 /* DemoAppTests.swift */; };
15 | A9E714D428B7893100A273A7 /* DemoAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E714D328B7893100A273A7 /* DemoAppUITests.swift */; };
16 | A9E714D628B7893100A273A7 /* DemoAppUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E714D528B7893100A273A7 /* DemoAppUITestsLaunchTests.swift */; };
17 | A9E714E628B789A100A273A7 /* Camera-SwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = A9E714E528B789A100A273A7 /* Camera-SwiftUI */; };
18 | /* End PBXBuildFile section */
19 |
20 | /* Begin PBXContainerItemProxy section */
21 | A9E714C628B7893100A273A7 /* PBXContainerItemProxy */ = {
22 | isa = PBXContainerItemProxy;
23 | containerPortal = A9E714AD28B7892F00A273A7 /* Project object */;
24 | proxyType = 1;
25 | remoteGlobalIDString = A9E714B428B7892F00A273A7;
26 | remoteInfo = DemoApp;
27 | };
28 | A9E714D028B7893100A273A7 /* PBXContainerItemProxy */ = {
29 | isa = PBXContainerItemProxy;
30 | containerPortal = A9E714AD28B7892F00A273A7 /* Project object */;
31 | proxyType = 1;
32 | remoteGlobalIDString = A9E714B428B7892F00A273A7;
33 | remoteInfo = DemoApp;
34 | };
35 | /* End PBXContainerItemProxy section */
36 |
37 | /* Begin PBXFileReference section */
38 | A9E714B528B7892F00A273A7 /* DemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
39 | A9E714B828B7892F00A273A7 /* DemoAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAppApp.swift; sourceTree = ""; };
40 | A9E714BA28B7892F00A273A7 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
41 | A9E714BC28B7893100A273A7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
42 | A9E714BF28B7893100A273A7 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
43 | A9E714C528B7893100A273A7 /* DemoAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DemoAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
44 | A9E714C928B7893100A273A7 /* DemoAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAppTests.swift; sourceTree = ""; };
45 | A9E714CF28B7893100A273A7 /* DemoAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DemoAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
46 | A9E714D328B7893100A273A7 /* DemoAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAppUITests.swift; sourceTree = ""; };
47 | A9E714D528B7893100A273A7 /* DemoAppUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAppUITestsLaunchTests.swift; sourceTree = ""; };
48 | A9E714E328B7898200A273A7 /* Camera-SwiftUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "Camera-SwiftUI"; path = ..; sourceTree = ""; };
49 | /* End PBXFileReference section */
50 |
51 | /* Begin PBXFrameworksBuildPhase section */
52 | A9E714B228B7892F00A273A7 /* Frameworks */ = {
53 | isa = PBXFrameworksBuildPhase;
54 | buildActionMask = 2147483647;
55 | files = (
56 | A9E714E628B789A100A273A7 /* Camera-SwiftUI in Frameworks */,
57 | );
58 | runOnlyForDeploymentPostprocessing = 0;
59 | };
60 | A9E714C228B7893100A273A7 /* Frameworks */ = {
61 | isa = PBXFrameworksBuildPhase;
62 | buildActionMask = 2147483647;
63 | files = (
64 | );
65 | runOnlyForDeploymentPostprocessing = 0;
66 | };
67 | A9E714CC28B7893100A273A7 /* Frameworks */ = {
68 | isa = PBXFrameworksBuildPhase;
69 | buildActionMask = 2147483647;
70 | files = (
71 | );
72 | runOnlyForDeploymentPostprocessing = 0;
73 | };
74 | /* End PBXFrameworksBuildPhase section */
75 |
76 | /* Begin PBXGroup section */
77 | A9E714AC28B7892F00A273A7 = {
78 | isa = PBXGroup;
79 | children = (
80 | A9E714E228B7898200A273A7 /* Packages */,
81 | A9E714B728B7892F00A273A7 /* DemoApp */,
82 | A9E714C828B7893100A273A7 /* DemoAppTests */,
83 | A9E714D228B7893100A273A7 /* DemoAppUITests */,
84 | A9E714B628B7892F00A273A7 /* Products */,
85 | A9E714E428B789A100A273A7 /* Frameworks */,
86 | );
87 | sourceTree = "";
88 | };
89 | A9E714B628B7892F00A273A7 /* Products */ = {
90 | isa = PBXGroup;
91 | children = (
92 | A9E714B528B7892F00A273A7 /* DemoApp.app */,
93 | A9E714C528B7893100A273A7 /* DemoAppTests.xctest */,
94 | A9E714CF28B7893100A273A7 /* DemoAppUITests.xctest */,
95 | );
96 | name = Products;
97 | sourceTree = "";
98 | };
99 | A9E714B728B7892F00A273A7 /* DemoApp */ = {
100 | isa = PBXGroup;
101 | children = (
102 | A9E714B828B7892F00A273A7 /* DemoAppApp.swift */,
103 | A9E714BA28B7892F00A273A7 /* ContentView.swift */,
104 | A9E714BC28B7893100A273A7 /* Assets.xcassets */,
105 | A9E714BE28B7893100A273A7 /* Preview Content */,
106 | );
107 | path = DemoApp;
108 | sourceTree = "";
109 | };
110 | A9E714BE28B7893100A273A7 /* Preview Content */ = {
111 | isa = PBXGroup;
112 | children = (
113 | A9E714BF28B7893100A273A7 /* Preview Assets.xcassets */,
114 | );
115 | path = "Preview Content";
116 | sourceTree = "";
117 | };
118 | A9E714C828B7893100A273A7 /* DemoAppTests */ = {
119 | isa = PBXGroup;
120 | children = (
121 | A9E714C928B7893100A273A7 /* DemoAppTests.swift */,
122 | );
123 | path = DemoAppTests;
124 | sourceTree = "";
125 | };
126 | A9E714D228B7893100A273A7 /* DemoAppUITests */ = {
127 | isa = PBXGroup;
128 | children = (
129 | A9E714D328B7893100A273A7 /* DemoAppUITests.swift */,
130 | A9E714D528B7893100A273A7 /* DemoAppUITestsLaunchTests.swift */,
131 | );
132 | path = DemoAppUITests;
133 | sourceTree = "";
134 | };
135 | A9E714E228B7898200A273A7 /* Packages */ = {
136 | isa = PBXGroup;
137 | children = (
138 | A9E714E328B7898200A273A7 /* Camera-SwiftUI */,
139 | );
140 | name = Packages;
141 | sourceTree = "";
142 | };
143 | A9E714E428B789A100A273A7 /* Frameworks */ = {
144 | isa = PBXGroup;
145 | children = (
146 | );
147 | name = Frameworks;
148 | sourceTree = "";
149 | };
150 | /* End PBXGroup section */
151 |
152 | /* Begin PBXNativeTarget section */
153 | A9E714B428B7892F00A273A7 /* DemoApp */ = {
154 | isa = PBXNativeTarget;
155 | buildConfigurationList = A9E714D928B7893100A273A7 /* Build configuration list for PBXNativeTarget "DemoApp" */;
156 | buildPhases = (
157 | A9E714B128B7892F00A273A7 /* Sources */,
158 | A9E714B228B7892F00A273A7 /* Frameworks */,
159 | A9E714B328B7892F00A273A7 /* Resources */,
160 | );
161 | buildRules = (
162 | );
163 | dependencies = (
164 | );
165 | name = DemoApp;
166 | packageProductDependencies = (
167 | A9E714E528B789A100A273A7 /* Camera-SwiftUI */,
168 | );
169 | productName = DemoApp;
170 | productReference = A9E714B528B7892F00A273A7 /* DemoApp.app */;
171 | productType = "com.apple.product-type.application";
172 | };
173 | A9E714C428B7893100A273A7 /* DemoAppTests */ = {
174 | isa = PBXNativeTarget;
175 | buildConfigurationList = A9E714DC28B7893100A273A7 /* Build configuration list for PBXNativeTarget "DemoAppTests" */;
176 | buildPhases = (
177 | A9E714C128B7893100A273A7 /* Sources */,
178 | A9E714C228B7893100A273A7 /* Frameworks */,
179 | A9E714C328B7893100A273A7 /* Resources */,
180 | );
181 | buildRules = (
182 | );
183 | dependencies = (
184 | A9E714C728B7893100A273A7 /* PBXTargetDependency */,
185 | );
186 | name = DemoAppTests;
187 | productName = DemoAppTests;
188 | productReference = A9E714C528B7893100A273A7 /* DemoAppTests.xctest */;
189 | productType = "com.apple.product-type.bundle.unit-test";
190 | };
191 | A9E714CE28B7893100A273A7 /* DemoAppUITests */ = {
192 | isa = PBXNativeTarget;
193 | buildConfigurationList = A9E714DF28B7893100A273A7 /* Build configuration list for PBXNativeTarget "DemoAppUITests" */;
194 | buildPhases = (
195 | A9E714CB28B7893100A273A7 /* Sources */,
196 | A9E714CC28B7893100A273A7 /* Frameworks */,
197 | A9E714CD28B7893100A273A7 /* Resources */,
198 | );
199 | buildRules = (
200 | );
201 | dependencies = (
202 | A9E714D128B7893100A273A7 /* PBXTargetDependency */,
203 | );
204 | name = DemoAppUITests;
205 | productName = DemoAppUITests;
206 | productReference = A9E714CF28B7893100A273A7 /* DemoAppUITests.xctest */;
207 | productType = "com.apple.product-type.bundle.ui-testing";
208 | };
209 | /* End PBXNativeTarget section */
210 |
211 | /* Begin PBXProject section */
212 | A9E714AD28B7892F00A273A7 /* Project object */ = {
213 | isa = PBXProject;
214 | attributes = {
215 | BuildIndependentTargetsInParallel = 1;
216 | LastSwiftUpdateCheck = 1330;
217 | LastUpgradeCheck = 1330;
218 | TargetAttributes = {
219 | A9E714B428B7892F00A273A7 = {
220 | CreatedOnToolsVersion = 13.3.1;
221 | };
222 | A9E714C428B7893100A273A7 = {
223 | CreatedOnToolsVersion = 13.3.1;
224 | TestTargetID = A9E714B428B7892F00A273A7;
225 | };
226 | A9E714CE28B7893100A273A7 = {
227 | CreatedOnToolsVersion = 13.3.1;
228 | TestTargetID = A9E714B428B7892F00A273A7;
229 | };
230 | };
231 | };
232 | buildConfigurationList = A9E714B028B7892F00A273A7 /* Build configuration list for PBXProject "DemoApp" */;
233 | compatibilityVersion = "Xcode 13.0";
234 | developmentRegion = en;
235 | hasScannedForEncodings = 0;
236 | knownRegions = (
237 | en,
238 | Base,
239 | );
240 | mainGroup = A9E714AC28B7892F00A273A7;
241 | productRefGroup = A9E714B628B7892F00A273A7 /* Products */;
242 | projectDirPath = "";
243 | projectRoot = "";
244 | targets = (
245 | A9E714B428B7892F00A273A7 /* DemoApp */,
246 | A9E714C428B7893100A273A7 /* DemoAppTests */,
247 | A9E714CE28B7893100A273A7 /* DemoAppUITests */,
248 | );
249 | };
250 | /* End PBXProject section */
251 |
252 | /* Begin PBXResourcesBuildPhase section */
253 | A9E714B328B7892F00A273A7 /* Resources */ = {
254 | isa = PBXResourcesBuildPhase;
255 | buildActionMask = 2147483647;
256 | files = (
257 | A9E714C028B7893100A273A7 /* Preview Assets.xcassets in Resources */,
258 | A9E714BD28B7893100A273A7 /* Assets.xcassets in Resources */,
259 | );
260 | runOnlyForDeploymentPostprocessing = 0;
261 | };
262 | A9E714C328B7893100A273A7 /* Resources */ = {
263 | isa = PBXResourcesBuildPhase;
264 | buildActionMask = 2147483647;
265 | files = (
266 | );
267 | runOnlyForDeploymentPostprocessing = 0;
268 | };
269 | A9E714CD28B7893100A273A7 /* Resources */ = {
270 | isa = PBXResourcesBuildPhase;
271 | buildActionMask = 2147483647;
272 | files = (
273 | );
274 | runOnlyForDeploymentPostprocessing = 0;
275 | };
276 | /* End PBXResourcesBuildPhase section */
277 |
278 | /* Begin PBXSourcesBuildPhase section */
279 | A9E714B128B7892F00A273A7 /* Sources */ = {
280 | isa = PBXSourcesBuildPhase;
281 | buildActionMask = 2147483647;
282 | files = (
283 | A9E714BB28B7892F00A273A7 /* ContentView.swift in Sources */,
284 | A9E714B928B7892F00A273A7 /* DemoAppApp.swift in Sources */,
285 | );
286 | runOnlyForDeploymentPostprocessing = 0;
287 | };
288 | A9E714C128B7893100A273A7 /* Sources */ = {
289 | isa = PBXSourcesBuildPhase;
290 | buildActionMask = 2147483647;
291 | files = (
292 | A9E714CA28B7893100A273A7 /* DemoAppTests.swift in Sources */,
293 | );
294 | runOnlyForDeploymentPostprocessing = 0;
295 | };
296 | A9E714CB28B7893100A273A7 /* Sources */ = {
297 | isa = PBXSourcesBuildPhase;
298 | buildActionMask = 2147483647;
299 | files = (
300 | A9E714D428B7893100A273A7 /* DemoAppUITests.swift in Sources */,
301 | A9E714D628B7893100A273A7 /* DemoAppUITestsLaunchTests.swift in Sources */,
302 | );
303 | runOnlyForDeploymentPostprocessing = 0;
304 | };
305 | /* End PBXSourcesBuildPhase section */
306 |
307 | /* Begin PBXTargetDependency section */
308 | A9E714C728B7893100A273A7 /* PBXTargetDependency */ = {
309 | isa = PBXTargetDependency;
310 | target = A9E714B428B7892F00A273A7 /* DemoApp */;
311 | targetProxy = A9E714C628B7893100A273A7 /* PBXContainerItemProxy */;
312 | };
313 | A9E714D128B7893100A273A7 /* PBXTargetDependency */ = {
314 | isa = PBXTargetDependency;
315 | target = A9E714B428B7892F00A273A7 /* DemoApp */;
316 | targetProxy = A9E714D028B7893100A273A7 /* PBXContainerItemProxy */;
317 | };
318 | /* End PBXTargetDependency section */
319 |
320 | /* Begin XCBuildConfiguration section */
321 | A9E714D728B7893100A273A7 /* Debug */ = {
322 | isa = XCBuildConfiguration;
323 | buildSettings = {
324 | ALWAYS_SEARCH_USER_PATHS = NO;
325 | CLANG_ANALYZER_NONNULL = YES;
326 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
327 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
328 | CLANG_ENABLE_MODULES = YES;
329 | CLANG_ENABLE_OBJC_ARC = YES;
330 | CLANG_ENABLE_OBJC_WEAK = YES;
331 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
332 | CLANG_WARN_BOOL_CONVERSION = YES;
333 | CLANG_WARN_COMMA = YES;
334 | CLANG_WARN_CONSTANT_CONVERSION = YES;
335 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
336 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
337 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
338 | CLANG_WARN_EMPTY_BODY = YES;
339 | CLANG_WARN_ENUM_CONVERSION = YES;
340 | CLANG_WARN_INFINITE_RECURSION = YES;
341 | CLANG_WARN_INT_CONVERSION = YES;
342 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
343 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
344 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
345 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
346 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
347 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
348 | CLANG_WARN_STRICT_PROTOTYPES = YES;
349 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
350 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
351 | CLANG_WARN_UNREACHABLE_CODE = YES;
352 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
353 | COPY_PHASE_STRIP = NO;
354 | DEBUG_INFORMATION_FORMAT = dwarf;
355 | ENABLE_STRICT_OBJC_MSGSEND = YES;
356 | ENABLE_TESTABILITY = YES;
357 | GCC_C_LANGUAGE_STANDARD = gnu11;
358 | GCC_DYNAMIC_NO_PIC = NO;
359 | GCC_NO_COMMON_BLOCKS = YES;
360 | GCC_OPTIMIZATION_LEVEL = 0;
361 | GCC_PREPROCESSOR_DEFINITIONS = (
362 | "DEBUG=1",
363 | "$(inherited)",
364 | );
365 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
366 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
367 | GCC_WARN_UNDECLARED_SELECTOR = YES;
368 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
369 | GCC_WARN_UNUSED_FUNCTION = YES;
370 | GCC_WARN_UNUSED_VARIABLE = YES;
371 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
372 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
373 | MTL_FAST_MATH = YES;
374 | ONLY_ACTIVE_ARCH = YES;
375 | SDKROOT = iphoneos;
376 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
377 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
378 | };
379 | name = Debug;
380 | };
381 | A9E714D828B7893100A273A7 /* Release */ = {
382 | isa = XCBuildConfiguration;
383 | buildSettings = {
384 | ALWAYS_SEARCH_USER_PATHS = NO;
385 | CLANG_ANALYZER_NONNULL = YES;
386 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
387 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
388 | CLANG_ENABLE_MODULES = YES;
389 | CLANG_ENABLE_OBJC_ARC = YES;
390 | CLANG_ENABLE_OBJC_WEAK = YES;
391 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
392 | CLANG_WARN_BOOL_CONVERSION = YES;
393 | CLANG_WARN_COMMA = YES;
394 | CLANG_WARN_CONSTANT_CONVERSION = YES;
395 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
396 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
397 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
398 | CLANG_WARN_EMPTY_BODY = YES;
399 | CLANG_WARN_ENUM_CONVERSION = YES;
400 | CLANG_WARN_INFINITE_RECURSION = YES;
401 | CLANG_WARN_INT_CONVERSION = YES;
402 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
403 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
404 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
405 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
406 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
407 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
408 | CLANG_WARN_STRICT_PROTOTYPES = YES;
409 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
410 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
411 | CLANG_WARN_UNREACHABLE_CODE = YES;
412 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
413 | COPY_PHASE_STRIP = NO;
414 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
415 | ENABLE_NS_ASSERTIONS = NO;
416 | ENABLE_STRICT_OBJC_MSGSEND = YES;
417 | GCC_C_LANGUAGE_STANDARD = gnu11;
418 | GCC_NO_COMMON_BLOCKS = YES;
419 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
420 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
421 | GCC_WARN_UNDECLARED_SELECTOR = YES;
422 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
423 | GCC_WARN_UNUSED_FUNCTION = YES;
424 | GCC_WARN_UNUSED_VARIABLE = YES;
425 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
426 | MTL_ENABLE_DEBUG_INFO = NO;
427 | MTL_FAST_MATH = YES;
428 | SDKROOT = iphoneos;
429 | SWIFT_COMPILATION_MODE = wholemodule;
430 | SWIFT_OPTIMIZATION_LEVEL = "-O";
431 | VALIDATE_PRODUCT = YES;
432 | };
433 | name = Release;
434 | };
435 | A9E714DA28B7893100A273A7 /* Debug */ = {
436 | isa = XCBuildConfiguration;
437 | buildSettings = {
438 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
439 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
440 | CODE_SIGN_STYLE = Automatic;
441 | CURRENT_PROJECT_VERSION = 1;
442 | DEVELOPMENT_ASSET_PATHS = "\"DemoApp/Preview Content\"";
443 | DEVELOPMENT_TEAM = YAFLRP6JUR;
444 | ENABLE_PREVIEWS = YES;
445 | GENERATE_INFOPLIST_FILE = YES;
446 | INFOPLIST_KEY_NSCameraUsageDescription = "DemoApp needs to access your camera to take pictures.";
447 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
448 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
449 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
450 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
451 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
452 | LD_RUNPATH_SEARCH_PATHS = (
453 | "$(inherited)",
454 | "@executable_path/Frameworks",
455 | );
456 | MARKETING_VERSION = 1.0;
457 | PRODUCT_BUNDLE_IDENTIFIER = "com.rry.cemera-swiftui.demo";
458 | PRODUCT_NAME = "$(TARGET_NAME)";
459 | SWIFT_EMIT_LOC_STRINGS = YES;
460 | SWIFT_VERSION = 5.0;
461 | TARGETED_DEVICE_FAMILY = "1,2";
462 | };
463 | name = Debug;
464 | };
465 | A9E714DB28B7893100A273A7 /* Release */ = {
466 | isa = XCBuildConfiguration;
467 | buildSettings = {
468 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
469 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
470 | CODE_SIGN_STYLE = Automatic;
471 | CURRENT_PROJECT_VERSION = 1;
472 | DEVELOPMENT_ASSET_PATHS = "\"DemoApp/Preview Content\"";
473 | DEVELOPMENT_TEAM = YAFLRP6JUR;
474 | ENABLE_PREVIEWS = YES;
475 | GENERATE_INFOPLIST_FILE = YES;
476 | INFOPLIST_KEY_NSCameraUsageDescription = "DemoApp needs to access your camera to take pictures.";
477 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
478 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
479 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
480 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
481 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
482 | LD_RUNPATH_SEARCH_PATHS = (
483 | "$(inherited)",
484 | "@executable_path/Frameworks",
485 | );
486 | MARKETING_VERSION = 1.0;
487 | PRODUCT_BUNDLE_IDENTIFIER = "com.rry.cemera-swiftui.demo";
488 | PRODUCT_NAME = "$(TARGET_NAME)";
489 | SWIFT_EMIT_LOC_STRINGS = YES;
490 | SWIFT_VERSION = 5.0;
491 | TARGETED_DEVICE_FAMILY = "1,2";
492 | };
493 | name = Release;
494 | };
495 | A9E714DD28B7893100A273A7 /* Debug */ = {
496 | isa = XCBuildConfiguration;
497 | buildSettings = {
498 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
499 | BUNDLE_LOADER = "$(TEST_HOST)";
500 | CODE_SIGN_STYLE = Automatic;
501 | CURRENT_PROJECT_VERSION = 1;
502 | DEVELOPMENT_TEAM = YAFLRP6JUR;
503 | GENERATE_INFOPLIST_FILE = YES;
504 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
505 | MARKETING_VERSION = 1.0;
506 | PRODUCT_BUNDLE_IDENTIFIER = rry.com.DemoAppTests;
507 | PRODUCT_NAME = "$(TARGET_NAME)";
508 | SWIFT_EMIT_LOC_STRINGS = NO;
509 | SWIFT_VERSION = 5.0;
510 | TARGETED_DEVICE_FAMILY = "1,2";
511 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DemoApp.app/DemoApp";
512 | };
513 | name = Debug;
514 | };
515 | A9E714DE28B7893100A273A7 /* Release */ = {
516 | isa = XCBuildConfiguration;
517 | buildSettings = {
518 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
519 | BUNDLE_LOADER = "$(TEST_HOST)";
520 | CODE_SIGN_STYLE = Automatic;
521 | CURRENT_PROJECT_VERSION = 1;
522 | DEVELOPMENT_TEAM = YAFLRP6JUR;
523 | GENERATE_INFOPLIST_FILE = YES;
524 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
525 | MARKETING_VERSION = 1.0;
526 | PRODUCT_BUNDLE_IDENTIFIER = rry.com.DemoAppTests;
527 | PRODUCT_NAME = "$(TARGET_NAME)";
528 | SWIFT_EMIT_LOC_STRINGS = NO;
529 | SWIFT_VERSION = 5.0;
530 | TARGETED_DEVICE_FAMILY = "1,2";
531 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DemoApp.app/DemoApp";
532 | };
533 | name = Release;
534 | };
535 | A9E714E028B7893100A273A7 /* Debug */ = {
536 | isa = XCBuildConfiguration;
537 | buildSettings = {
538 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
539 | CODE_SIGN_STYLE = Automatic;
540 | CURRENT_PROJECT_VERSION = 1;
541 | DEVELOPMENT_TEAM = YAFLRP6JUR;
542 | GENERATE_INFOPLIST_FILE = YES;
543 | MARKETING_VERSION = 1.0;
544 | PRODUCT_BUNDLE_IDENTIFIER = rry.com.DemoAppUITests;
545 | PRODUCT_NAME = "$(TARGET_NAME)";
546 | SWIFT_EMIT_LOC_STRINGS = NO;
547 | SWIFT_VERSION = 5.0;
548 | TARGETED_DEVICE_FAMILY = "1,2";
549 | TEST_TARGET_NAME = DemoApp;
550 | };
551 | name = Debug;
552 | };
553 | A9E714E128B7893100A273A7 /* Release */ = {
554 | isa = XCBuildConfiguration;
555 | buildSettings = {
556 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
557 | CODE_SIGN_STYLE = Automatic;
558 | CURRENT_PROJECT_VERSION = 1;
559 | DEVELOPMENT_TEAM = YAFLRP6JUR;
560 | GENERATE_INFOPLIST_FILE = YES;
561 | MARKETING_VERSION = 1.0;
562 | PRODUCT_BUNDLE_IDENTIFIER = rry.com.DemoAppUITests;
563 | PRODUCT_NAME = "$(TARGET_NAME)";
564 | SWIFT_EMIT_LOC_STRINGS = NO;
565 | SWIFT_VERSION = 5.0;
566 | TARGETED_DEVICE_FAMILY = "1,2";
567 | TEST_TARGET_NAME = DemoApp;
568 | };
569 | name = Release;
570 | };
571 | /* End XCBuildConfiguration section */
572 |
573 | /* Begin XCConfigurationList section */
574 | A9E714B028B7892F00A273A7 /* Build configuration list for PBXProject "DemoApp" */ = {
575 | isa = XCConfigurationList;
576 | buildConfigurations = (
577 | A9E714D728B7893100A273A7 /* Debug */,
578 | A9E714D828B7893100A273A7 /* Release */,
579 | );
580 | defaultConfigurationIsVisible = 0;
581 | defaultConfigurationName = Release;
582 | };
583 | A9E714D928B7893100A273A7 /* Build configuration list for PBXNativeTarget "DemoApp" */ = {
584 | isa = XCConfigurationList;
585 | buildConfigurations = (
586 | A9E714DA28B7893100A273A7 /* Debug */,
587 | A9E714DB28B7893100A273A7 /* Release */,
588 | );
589 | defaultConfigurationIsVisible = 0;
590 | defaultConfigurationName = Release;
591 | };
592 | A9E714DC28B7893100A273A7 /* Build configuration list for PBXNativeTarget "DemoAppTests" */ = {
593 | isa = XCConfigurationList;
594 | buildConfigurations = (
595 | A9E714DD28B7893100A273A7 /* Debug */,
596 | A9E714DE28B7893100A273A7 /* Release */,
597 | );
598 | defaultConfigurationIsVisible = 0;
599 | defaultConfigurationName = Release;
600 | };
601 | A9E714DF28B7893100A273A7 /* Build configuration list for PBXNativeTarget "DemoAppUITests" */ = {
602 | isa = XCConfigurationList;
603 | buildConfigurations = (
604 | A9E714E028B7893100A273A7 /* Debug */,
605 | A9E714E128B7893100A273A7 /* Release */,
606 | );
607 | defaultConfigurationIsVisible = 0;
608 | defaultConfigurationName = Release;
609 | };
610 | /* End XCConfigurationList section */
611 |
612 | /* Begin XCSwiftPackageProductDependency section */
613 | A9E714E528B789A100A273A7 /* Camera-SwiftUI */ = {
614 | isa = XCSwiftPackageProductDependency;
615 | productName = "Camera-SwiftUI";
616 | };
617 | /* End XCSwiftPackageProductDependency section */
618 | };
619 | rootObject = A9E714AD28B7892F00A273A7 /* Project object */;
620 | }
621 |
--------------------------------------------------------------------------------
/Sources/Camera-SwiftUI/Service/CameraService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraService.swift
3 | // Campus
4 | //
5 | // Created by Rolando Rodriguez on 12/20/19.
6 | // Copyright © 2018 Rolando Rodriguez. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 | import AVFoundation
12 | import Photos
13 | import UIKit
14 |
15 | // MARK: Class Camera Service, handles setup of AVFoundation needed for a basic camera app.
16 | public struct Photo: Identifiable, Equatable {
17 | public var id: String
18 | public var originalData: Data
19 |
20 | public init(id: String = UUID().uuidString, originalData: Data) {
21 | self.id = id
22 | self.originalData = originalData
23 | }
24 | }
25 |
26 | public struct AlertError {
27 | public var title: String = ""
28 | public var message: String = ""
29 | public var primaryButtonTitle = "Accept"
30 | public var secondaryButtonTitle: String?
31 | public var primaryAction: (() -> ())?
32 | public var secondaryAction: (() -> ())?
33 |
34 | public init(title: String = "", message: String = "", primaryButtonTitle: String = "Accept", secondaryButtonTitle: String? = nil, primaryAction: (() -> ())? = nil, secondaryAction: (() -> ())? = nil) {
35 | self.title = title
36 | self.message = message
37 | self.primaryAction = primaryAction
38 | self.primaryButtonTitle = primaryButtonTitle
39 | self.secondaryAction = secondaryAction
40 | }
41 | }
42 |
43 | extension Photo {
44 | public var compressedData: Data? {
45 | ImageResizer(targetWidth: 800).resize(data: originalData)?.jpegData(compressionQuality: 0.5)
46 | }
47 | public var thumbnailData: Data? {
48 | ImageResizer(targetWidth: 100).resize(data: originalData)?.jpegData(compressionQuality: 0.5)
49 | }
50 | public var thumbnailImage: UIImage? {
51 | guard let data = thumbnailData else { return nil }
52 | return UIImage(data: data)
53 | }
54 | public var image: UIImage? {
55 | guard let data = compressedData else { return nil }
56 | return UIImage(data: data)
57 | }
58 | }
59 |
60 | public class CameraService: NSObject, Identifiable {
61 | typealias PhotoCaptureSessionID = String
62 |
63 | // MARK: Observed Properties UI must react to
64 |
65 | @Published public var flashMode: AVCaptureDevice.FlashMode = .off
66 | @Published public var shouldShowAlertView = false
67 | @Published public var shouldShowSpinner = false
68 |
69 | @Published public var willCapturePhoto = false
70 | @Published public var isCameraButtonDisabled = false
71 | @Published public var isCameraUnavailable = false
72 | @Published public var photo: Photo?
73 |
74 | // MARK: Alert properties
75 | public var alertError: AlertError = AlertError()
76 |
77 | // MARK: Session Management Properties
78 |
79 | public let session = AVCaptureSession()
80 |
81 | var isSessionRunning = false
82 | var isConfigured = false
83 | var setupResult: SessionSetupResult = .success
84 |
85 | // Communicate with the session and other session objects on this queue.
86 | let sessionQueue = DispatchQueue(label: "session queue")
87 |
88 | @objc dynamic var videoDeviceInput: AVCaptureDeviceInput!
89 |
90 | // MARK: Device Configuration Properties
91 | let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDualCamera, .builtInTrueDepthCamera], mediaType: .video, position: .unspecified)
92 |
93 | // MARK: Capturing Photos
94 |
95 | let photoOutput = AVCapturePhotoOutput()
96 |
97 | var inProgressPhotoCaptureDelegates = [Int64: PhotoCaptureProcessor]()
98 |
99 | // MARK: KVO and Notifications Properties
100 |
101 | var keyValueObservations = [NSKeyValueObservation]()
102 |
103 | // MARK: Init
104 |
105 | override public init() {
106 | super.init()
107 |
108 | // Disable the UI. Enable the UI later, if and only if the session starts running.
109 | DispatchQueue.main.async {
110 | self.isCameraButtonDisabled = true
111 | self.isCameraUnavailable = true
112 | }
113 | }
114 |
115 | public func configure() {
116 | if !self.isSessionRunning && !self.isConfigured {
117 | /*
118 | Setup the capture session.
119 | In general, it's not safe to mutate an AVCaptureSession or any of its
120 | inputs, outputs, or connections from multiple threads at the same time.
121 |
122 | Don't perform these tasks on the main queue because
123 | AVCaptureSession.startRunning() is a blocking call, which can
124 | take a long time. Dispatch session setup to the sessionQueue, so
125 | that the main queue isn't blocked, which keeps the UI responsive.
126 | */
127 | sessionQueue.async {
128 | self.configureSession()
129 | }
130 | }
131 | }
132 |
133 | // MARK: Checks for permisions, setup obeservers and starts running session
134 | public func checkForPermissions() {
135 | /*
136 | Check the video authorization status. Video access is required and audio
137 | access is optional. If the user denies audio access, AVCam won't
138 | record audio during movie recording.
139 | */
140 | switch AVCaptureDevice.authorizationStatus(for: .video) {
141 | case .authorized:
142 | // The user has previously granted access to the camera.
143 | break
144 | case .notDetermined:
145 | /*
146 | The user has not yet been presented with the option to grant
147 | video access. Suspend the session queue to delay session
148 | setup until the access request has completed.
149 |
150 | Note that audio access will be implicitly requested when we
151 | create an AVCaptureDeviceInput for audio during session setup.
152 | */
153 | sessionQueue.suspend()
154 | AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
155 | if !granted {
156 | self.setupResult = .notAuthorized
157 | }
158 | self.sessionQueue.resume()
159 | })
160 |
161 | default:
162 | // The user has previously denied access.
163 | setupResult = .notAuthorized
164 |
165 | DispatchQueue.main.async {
166 | self.alertError = AlertError(title: "Camera Access", message: "Campus no tiene permiso para usar la cámara, por favor cambia la configruación de privacidad", primaryButtonTitle: "Configuración", secondaryButtonTitle: nil, primaryAction: {
167 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!,
168 | options: [:], completionHandler: nil)
169 |
170 | }, secondaryAction: nil)
171 | self.shouldShowAlertView = true
172 | self.isCameraUnavailable = true
173 | self.isCameraButtonDisabled = true
174 | }
175 | }
176 | }
177 |
178 | // MARK: Session Managment
179 |
180 | // Call this on the session queue.
181 | /// - Tag: ConfigureSession
182 | private func configureSession() {
183 | if setupResult != .success {
184 | return
185 | }
186 |
187 | session.beginConfiguration()
188 |
189 | /*
190 | Do not create an AVCaptureMovieFileOutput when setting up the session because
191 | Live Photo is not supported when AVCaptureMovieFileOutput is added to the session.
192 | */
193 | session.sessionPreset = .photo
194 |
195 | // Add video input.
196 | do {
197 | var defaultVideoDevice: AVCaptureDevice?
198 |
199 | if let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
200 | // If a rear dual camera is not available, default to the rear wide angle camera.
201 | defaultVideoDevice = backCameraDevice
202 | } else if let frontCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) {
203 | // If the rear wide angle camera isn't available, default to the front wide angle camera.
204 | defaultVideoDevice = frontCameraDevice
205 | }
206 |
207 | guard let videoDevice = defaultVideoDevice else {
208 | print("Default video device is unavailable.")
209 | setupResult = .configurationFailed
210 | session.commitConfiguration()
211 | return
212 | }
213 |
214 | let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
215 |
216 | if session.canAddInput(videoDeviceInput) {
217 | session.addInput(videoDeviceInput)
218 | self.videoDeviceInput = videoDeviceInput
219 |
220 | } else {
221 | print("Couldn't add video device input to the session.")
222 | setupResult = .configurationFailed
223 | session.commitConfiguration()
224 | return
225 | }
226 | } catch {
227 | print("Couldn't create video device input: \(error)")
228 | setupResult = .configurationFailed
229 | session.commitConfiguration()
230 | return
231 | }
232 |
233 | // Add the photo output.
234 | if session.canAddOutput(photoOutput) {
235 | session.addOutput(photoOutput)
236 |
237 | photoOutput.isHighResolutionCaptureEnabled = true
238 | photoOutput.maxPhotoQualityPrioritization = .quality
239 |
240 | } else {
241 | print("Could not add photo output to the session")
242 | setupResult = .configurationFailed
243 | session.commitConfiguration()
244 | return
245 | }
246 |
247 | session.commitConfiguration()
248 | self.isConfigured = true
249 |
250 | self.start()
251 | }
252 |
253 | private func resumeInterruptedSession() {
254 | sessionQueue.async {
255 | /*
256 | The session might fail to start running, for example, if a phone or FaceTime call is still
257 | using audio or video. This failure is communicated by the session posting a
258 | runtime error notification. To avoid repeatedly failing to start the session,
259 | only try to restart the session in the error handler if you aren't
260 | trying to resume the session.
261 | */
262 | self.session.startRunning()
263 | self.isSessionRunning = self.session.isRunning
264 | if !self.session.isRunning {
265 | DispatchQueue.main.async {
266 | self.alertError = AlertError(title: "Camera Error", message: "Unable to resume camera", primaryButtonTitle: "Accept", secondaryButtonTitle: nil, primaryAction: nil, secondaryAction: nil)
267 | self.shouldShowAlertView = true
268 | self.isCameraUnavailable = true
269 | self.isCameraButtonDisabled = true
270 | }
271 | } else {
272 | DispatchQueue.main.async {
273 | self.isCameraUnavailable = false
274 | self.isCameraButtonDisabled = false
275 | }
276 | }
277 | }
278 | }
279 |
280 | // MARK: Device Configuration
281 |
282 | /// - Tag: ChangeCamera
283 | public func changeCamera() {
284 | // MARK: Here disable all camera operation related buttons due to configuration is due upon and must not be interrupted
285 | DispatchQueue.main.async {
286 | self.isCameraButtonDisabled = true
287 | }
288 | //
289 |
290 | sessionQueue.async {
291 | let currentVideoDevice = self.videoDeviceInput.device
292 | let currentPosition = currentVideoDevice.position
293 |
294 | let preferredPosition: AVCaptureDevice.Position
295 | let preferredDeviceType: AVCaptureDevice.DeviceType
296 |
297 | switch currentPosition {
298 | case .unspecified, .front:
299 | preferredPosition = .back
300 | preferredDeviceType = .builtInWideAngleCamera
301 |
302 | case .back:
303 | preferredPosition = .front
304 | preferredDeviceType = .builtInWideAngleCamera
305 |
306 | @unknown default:
307 | print("Unknown capture position. Defaulting to back, dual-camera.")
308 | preferredPosition = .back
309 | preferredDeviceType = .builtInWideAngleCamera
310 | }
311 | let devices = self.videoDeviceDiscoverySession.devices
312 | var newVideoDevice: AVCaptureDevice? = nil
313 |
314 | // First, seek a device with both the preferred position and device type. Otherwise, seek a device with only the preferred position.
315 | if let device = devices.first(where: { $0.position == preferredPosition && $0.deviceType == preferredDeviceType }) {
316 | newVideoDevice = device
317 | } else if let device = devices.first(where: { $0.position == preferredPosition }) {
318 | newVideoDevice = device
319 | }
320 |
321 | if let videoDevice = newVideoDevice {
322 | do {
323 | let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
324 |
325 | self.session.beginConfiguration()
326 |
327 | // Remove the existing device input first, because AVCaptureSession doesn't support
328 | // simultaneous use of the rear and front cameras.
329 | self.session.removeInput(self.videoDeviceInput)
330 |
331 | if self.session.canAddInput(videoDeviceInput) {
332 | NotificationCenter.default.removeObserver(self, name: .AVCaptureDeviceSubjectAreaDidChange, object: currentVideoDevice)
333 | NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaDidChange), name: .AVCaptureDeviceSubjectAreaDidChange, object: videoDeviceInput.device)
334 |
335 | self.session.addInput(videoDeviceInput)
336 | self.videoDeviceInput = videoDeviceInput
337 | } else {
338 | self.session.addInput(self.videoDeviceInput)
339 | }
340 |
341 | if let connection = self.photoOutput.connection(with: .video) {
342 | if connection.isVideoStabilizationSupported {
343 | connection.preferredVideoStabilizationMode = .auto
344 | }
345 | }
346 |
347 | self.photoOutput.maxPhotoQualityPrioritization = .quality
348 |
349 | self.session.commitConfiguration()
350 | } catch {
351 | print("Error occurred while creating video device input: \(error)")
352 | }
353 | }
354 |
355 | DispatchQueue.main.async {
356 | // MARK: Here enable all camera operation related buttons due to succesfull setup
357 | self.isCameraButtonDisabled = false
358 | }
359 | }
360 | }
361 |
362 | public func focus(with focusMode: AVCaptureDevice.FocusMode, exposureMode: AVCaptureDevice.ExposureMode, at devicePoint: CGPoint, monitorSubjectAreaChange: Bool) {
363 | sessionQueue.async {
364 | guard let device = self.videoDeviceInput?.device else { return }
365 | do {
366 | try device.lockForConfiguration()
367 |
368 | /*
369 | Setting (focus/exposure)PointOfInterest alone does not initiate a (focus/exposure) operation.
370 | Call set(Focus/Exposure)Mode() to apply the new point of interest.
371 | */
372 | if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(focusMode) {
373 | device.focusPointOfInterest = devicePoint
374 | device.focusMode = focusMode
375 | }
376 |
377 | if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(exposureMode) {
378 | device.exposurePointOfInterest = devicePoint
379 | device.exposureMode = exposureMode
380 | }
381 |
382 | device.isSubjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange
383 | device.unlockForConfiguration()
384 | } catch {
385 | print("Could not lock device for configuration: \(error)")
386 | }
387 | }
388 | }
389 |
390 |
391 | public func focus(at focusPoint: CGPoint){
392 | let device = self.videoDeviceInput.device
393 | do {
394 | try device.lockForConfiguration()
395 | if device.isFocusPointOfInterestSupported {
396 | device.focusPointOfInterest = focusPoint
397 | device.exposurePointOfInterest = focusPoint
398 | device.exposureMode = .continuousAutoExposure
399 | device.focusMode = .continuousAutoFocus
400 | device.unlockForConfiguration()
401 | }
402 | }
403 | catch {
404 | print(error.localizedDescription)
405 | }
406 | }
407 |
408 | @objc public func stop(completion: (() -> ())? = nil) {
409 | sessionQueue.async {
410 | if self.isSessionRunning {
411 | if self.setupResult == .success {
412 | self.session.stopRunning()
413 | self.isSessionRunning = self.session.isRunning
414 | print("CAMERA STOPPED")
415 | self.removeObservers()
416 |
417 | if !self.session.isRunning {
418 | DispatchQueue.main.async {
419 | self.isCameraButtonDisabled = true
420 | self.isCameraUnavailable = true
421 | completion?()
422 | }
423 | }
424 | }
425 | }
426 | }
427 | }
428 |
429 | @objc public func start() {
430 | sessionQueue.async {
431 | if !self.isSessionRunning && self.isConfigured {
432 | switch self.setupResult {
433 | case .success:
434 | // Only setup observers and start the session if setup succeeded.
435 | self.addObservers()
436 | self.session.startRunning()
437 | print("CAMERA RUNNING")
438 | self.isSessionRunning = self.session.isRunning
439 |
440 | if self.session.isRunning {
441 | DispatchQueue.main.async {
442 | self.isCameraButtonDisabled = false
443 | self.isCameraUnavailable = false
444 | }
445 | }
446 |
447 | case .notAuthorized:
448 | print("Application not authorized to use camera")
449 | DispatchQueue.main.async {
450 | self.isCameraButtonDisabled = true
451 | self.isCameraUnavailable = true
452 | }
453 |
454 | case .configurationFailed:
455 | DispatchQueue.main.async {
456 | self.alertError = AlertError(title: "Camera Error", message: "Camera configuration failed. Either your device camera is not available or other application is using it", primaryButtonTitle: "Accept", secondaryButtonTitle: nil, primaryAction: nil, secondaryAction: nil)
457 | self.shouldShowAlertView = true
458 | self.isCameraButtonDisabled = true
459 | self.isCameraUnavailable = true
460 | }
461 | }
462 | }
463 | }
464 | }
465 |
466 | public func set(zoom: CGFloat){
467 | let factor = zoom < 1 ? 1 : zoom
468 | let device = self.videoDeviceInput.device
469 |
470 | do {
471 | try device.lockForConfiguration()
472 | device.videoZoomFactor = factor
473 | device.unlockForConfiguration()
474 | }
475 | catch {
476 | print(error.localizedDescription)
477 | }
478 | }
479 |
480 | // MARK: Capture Photo
481 |
482 | /// - Tag: CapturePhoto
483 | public func capturePhoto() {
484 | /*
485 | Retrieve the video preview layer's video orientation on the main queue before
486 | entering the session queue. This to ensures that UI elements are accessed on
487 | the main thread and session configuration is done on the session queue.
488 | */
489 |
490 | if self.setupResult != .configurationFailed {
491 | let videoPreviewLayerOrientation: AVCaptureVideoOrientation = .portrait
492 | self.isCameraButtonDisabled = true
493 |
494 | sessionQueue.async {
495 | if let photoOutputConnection = self.photoOutput.connection(with: .video) {
496 | photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
497 | }
498 | var photoSettings = AVCapturePhotoSettings()
499 |
500 | // Capture HEIF photos when supported. Enable according to user settings and high-resolution photos.
501 | if self.photoOutput.availablePhotoCodecTypes.contains(.hevc) {
502 | photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc])
503 | }
504 |
505 | if self.videoDeviceInput.device.isFlashAvailable {
506 | photoSettings.flashMode = self.flashMode
507 | }
508 |
509 | photoSettings.isHighResolutionPhotoEnabled = true
510 | if !photoSettings.__availablePreviewPhotoPixelFormatTypes.isEmpty {
511 | photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: photoSettings.__availablePreviewPhotoPixelFormatTypes.first!]
512 | }
513 |
514 | photoSettings.photoQualityPrioritization = .speed
515 |
516 | let photoCaptureProcessor = PhotoCaptureProcessor(with: photoSettings, willCapturePhotoAnimation: {
517 | // Flash the screen to signal that AVCam took a photo.
518 | DispatchQueue.main.async {
519 | self.willCapturePhoto.toggle()
520 | self.willCapturePhoto.toggle()
521 | }
522 | }, completionHandler: { (photoCaptureProcessor) in
523 | // When the capture is complete, remove a reference to the photo capture delegate so it can be deallocated.
524 | if let data = photoCaptureProcessor.photoData {
525 | self.photo = Photo(originalData: data)
526 | print("passing photo")
527 | } else {
528 | print("No photo data")
529 | }
530 |
531 | self.isCameraButtonDisabled = false
532 |
533 | self.sessionQueue.async {
534 | self.inProgressPhotoCaptureDelegates[photoCaptureProcessor.requestedPhotoSettings.uniqueID] = nil
535 | }
536 | }, photoProcessingHandler: { animate in
537 | // Animates a spinner while photo is processing
538 | if animate {
539 | self.shouldShowSpinner = true
540 | } else {
541 | self.shouldShowSpinner = false
542 | }
543 | })
544 |
545 | // The photo output holds a weak reference to the photo capture delegate and stores it in an array to maintain a strong reference.
546 | self.inProgressPhotoCaptureDelegates[photoCaptureProcessor.requestedPhotoSettings.uniqueID] = photoCaptureProcessor
547 | self.photoOutput.capturePhoto(with: photoSettings, delegate: photoCaptureProcessor)
548 | }
549 | }
550 | }
551 |
552 |
553 | // MARK: KVO & Observers
554 |
555 | /// - Tag: ObserveInterruption
556 | private func addObservers() {
557 | let systemPressureStateObservation = observe(\.videoDeviceInput.device.systemPressureState, options: .new) { _, change in
558 | guard let systemPressureState = change.newValue else { return }
559 | self.setRecommendedFrameRateRangeForPressureState(systemPressureState: systemPressureState)
560 | }
561 | keyValueObservations.append(systemPressureStateObservation)
562 |
563 | // NotificationCenter.default.addObserver(self, selector: #selector(self.onOrientationChange), name: UIDevice.orientationDidChangeNotification, object: nil)
564 |
565 | NotificationCenter.default.addObserver(self,
566 | selector: #selector(subjectAreaDidChange),
567 | name: .AVCaptureDeviceSubjectAreaDidChange,
568 | object: videoDeviceInput.device)
569 |
570 | NotificationCenter.default.addObserver(self, selector: #selector(uiRequestedNewFocusArea), name: .init(rawValue: "UserDidRequestNewFocusPoint"), object: nil)
571 |
572 | NotificationCenter.default.addObserver(self,
573 | selector: #selector(sessionRuntimeError),
574 | name: .AVCaptureSessionRuntimeError,
575 | object: session)
576 |
577 | /*
578 | A session can only run when the app is full screen. It will be interrupted
579 | in a multi-app layout, introduced in iOS 9, see also the documentation of
580 | AVCaptureSessionInterruptionReason. Add observers to handle these session
581 | interruptions and show a preview is paused message. See the documentation
582 | of AVCaptureSessionWasInterruptedNotification for other interruption reasons.
583 | */
584 | NotificationCenter.default.addObserver(self,
585 | selector: #selector(sessionWasInterrupted),
586 | name: .AVCaptureSessionWasInterrupted,
587 | object: session)
588 | NotificationCenter.default.addObserver(self,
589 | selector: #selector(sessionInterruptionEnded),
590 | name: .AVCaptureSessionInterruptionEnded,
591 | object: session)
592 | }
593 |
594 | private func removeObservers() {
595 | NotificationCenter.default.removeObserver(self)
596 |
597 | for keyValueObservation in keyValueObservations {
598 | keyValueObservation.invalidate()
599 | }
600 | keyValueObservations.removeAll()
601 | }
602 |
603 | @objc private func uiRequestedNewFocusArea(notification: NSNotification) {
604 | guard let userInfo = notification.userInfo as? [String: Any], let devicePoint = userInfo["devicePoint"] as? CGPoint else { return }
605 | self.focus(at: devicePoint)
606 | }
607 |
608 | @objc
609 | private func subjectAreaDidChange(notification: NSNotification) {
610 | let devicePoint = CGPoint(x: 0.5, y: 0.5)
611 | focus(with: .continuousAutoFocus, exposureMode: .continuousAutoExposure, at: devicePoint, monitorSubjectAreaChange: false)
612 | }
613 |
614 | /// - Tag: HandleRuntimeError
615 | @objc
616 | private func sessionRuntimeError(notification: NSNotification) {
617 | guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else { return }
618 |
619 | print("Capture session runtime error: \(error)")
620 | // If media services were reset, and the last start succeeded, restart the session.
621 | if error.code == .mediaServicesWereReset {
622 | sessionQueue.async {
623 | if self.isSessionRunning {
624 | self.session.startRunning()
625 | self.isSessionRunning = self.session.isRunning
626 | } else {
627 | DispatchQueue.main.async {
628 | // self.resumeButton.isHidden = false
629 | }
630 | }
631 | }
632 | } else {
633 | // resumeButton.isHidden = false
634 | }
635 | }
636 |
637 | /// - Tag: HandleSystemPressure
638 | private func setRecommendedFrameRateRangeForPressureState(systemPressureState: AVCaptureDevice.SystemPressureState) {
639 | /*
640 | The frame rates used here are only for demonstration purposes.
641 | Your frame rate throttling may be different depending on your app's camera configuration.
642 | */
643 | let pressureLevel = systemPressureState.level
644 | if pressureLevel == .serious || pressureLevel == .critical {
645 | do {
646 | try self.videoDeviceInput.device.lockForConfiguration()
647 | print("WARNING: Reached elevated system pressure level: \(pressureLevel). Throttling frame rate.")
648 | self.videoDeviceInput.device.activeVideoMinFrameDuration = CMTime(value: 1, timescale: 20)
649 | self.videoDeviceInput.device.activeVideoMaxFrameDuration = CMTime(value: 1, timescale: 15)
650 | self.videoDeviceInput.device.unlockForConfiguration()
651 | } catch {
652 | print("Could not lock device for configuration: \(error)")
653 | }
654 | } else if pressureLevel == .shutdown {
655 | print("Session stopped running due to shutdown system pressure level.")
656 | }
657 | }
658 |
659 | /// - Tag: HandleInterruption
660 | @objc
661 | private func sessionWasInterrupted(notification: NSNotification) {
662 | /*
663 | In some scenarios you want to enable the user to resume the session.
664 | For example, if music playback is initiated from Control Center while
665 | using Campus, then the user can let Campus resume
666 | the session running, which will stop music playback. Note that stopping
667 | music playback in Control Center will not automatically resume the session.
668 | Also note that it's not always possible to resume, see `resumeInterruptedSession(_:)`.
669 | */
670 | DispatchQueue.main.async {
671 | self.isCameraUnavailable = true
672 | }
673 |
674 | if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?,
675 | let reasonIntegerValue = userInfoValue.integerValue,
676 | let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) {
677 | print("Capture session was interrupted with reason \(reason)")
678 |
679 | if reason == .audioDeviceInUseByAnotherClient || reason == .videoDeviceInUseByAnotherClient {
680 | print("Session stopped running due to video devies in use by another client.")
681 | } else if reason == .videoDeviceNotAvailableWithMultipleForegroundApps {
682 | // Fade-in a label to inform the user that the camera is unavailable.
683 | print("Session stopped running due to video devies is not available with multiple foreground apps.")
684 | } else if reason == .videoDeviceNotAvailableDueToSystemPressure {
685 | print("Session stopped running due to shutdown system pressure level.")
686 | }
687 | }
688 | }
689 |
690 | @objc
691 | private func sessionInterruptionEnded(notification: NSNotification) {
692 | print("Capture session interruption ended")
693 | DispatchQueue.main.async {
694 | self.isCameraUnavailable = false
695 | }
696 | }
697 | }
698 |
--------------------------------------------------------------------------------