├── .gitignore
├── Doc
├── hero.jpg
├── anatomy.jpg
├── debugger.png
└── custom_flags.png
├── LockedWidget
├── Assets.xcassets
│ ├── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── WidgetBackground.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Info.plist
└── LockedWidgetBundle.swift
├── LockedCameraCaptureExtensionDemo
├── Assets.xcassets
│ ├── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── LockedCameraCaptureExtensionDemo.entitlements
├── DemoApp.swift
├── Camera
│ ├── AppStorageConfigProvider.swift
│ ├── AppUserDefaultSettings.swift
│ ├── OpenMainAppAction.swift
│ ├── ViewModel
│ │ ├── CamPreviewViewModel.swift
│ │ ├── CaptureProcessor.swift
│ │ └── MainViewModel.swift
│ └── View
│ │ ├── CaptureInteractionView.swift
│ │ └── ContentView.swift
├── Intent
│ └── AppCaptureIntent.swift
└── Localizable.xcstrings
├── Packages
├── Models
│ ├── .gitignore
│ ├── Tests
│ │ └── ModelsTests
│ │ │ └── ModelsTests.swift
│ ├── Sources
│ │ └── Models
│ │ │ └── CameraPosition.swift
│ └── Package.swift
└── MetalLib
│ ├── .gitignore
│ ├── Tests
│ └── MetalLibTests
│ │ └── MetalLibTests.swift
│ ├── Package.swift
│ └── Sources
│ └── MetalLib
│ ├── ViewRepresentable.swift
│ ├── MetalView.swift
│ └── MetalRenderer.swift
├── LockedCameraCaptureExtensionDemo.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcuserdata
│ └── juniperphoton.xcuserdatad
│ │ ├── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
├── xcshareddata
│ └── xcschemes
│ │ ├── LockedCameraCaptureExtensionDemo.xcscheme
│ │ ├── LockedExtension.xcscheme
│ │ └── LockedWidgetExtension.xcscheme
└── project.pbxproj
├── LockedExtension
├── Info.plist
├── LockedExtension.swift
└── OpenMainAppButton.swift
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/ltex.dictionary.en-US.txt
2 |
--------------------------------------------------------------------------------
/Doc/hero.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JuniperPhoton/LockedCameraCaptureExtensionDemo/HEAD/Doc/hero.jpg
--------------------------------------------------------------------------------
/Doc/anatomy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JuniperPhoton/LockedCameraCaptureExtensionDemo/HEAD/Doc/anatomy.jpg
--------------------------------------------------------------------------------
/Doc/debugger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JuniperPhoton/LockedCameraCaptureExtensionDemo/HEAD/Doc/debugger.png
--------------------------------------------------------------------------------
/Doc/custom_flags.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JuniperPhoton/LockedCameraCaptureExtensionDemo/HEAD/Doc/custom_flags.png
--------------------------------------------------------------------------------
/LockedWidget/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Packages/Models/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/Packages/MetalLib/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/LockedWidget/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 |
--------------------------------------------------------------------------------
/LockedWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Packages/Models/Tests/ModelsTests/ModelsTests.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | @testable import Models
3 |
4 | @Test func example() async throws {
5 | // Write your test here and use APIs like `#expect(...)` to check expected conditions.
6 | }
7 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Packages/MetalLib/Tests/MetalLibTests/MetalLibTests.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | @testable import MetalLib
3 |
4 | @Test func example() async throws {
5 | // Write your test here and use APIs like `#expect(...)` to check expected conditions.
6 | }
7 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/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 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo.xcodeproj/xcuserdata/juniperphoton.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/LockedWidget/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.widgetkit-extension
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/LockedCameraCaptureExtensionDemo.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/DemoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoApp.swift
3 | // LockedCameraCaptureExtensionDemo
4 | //
5 | // Created by Photon Juniper on 2024/8/20.
6 | //
7 |
8 | import SwiftUI
9 | import AppIntents
10 |
11 | @main
12 | struct LockedCameraCaptureExtensionDemoApp: App {
13 | var body: some Scene {
14 | WindowGroup {
15 | ContentView(configProvider: AppStorageConfigProvider.standard)
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/LockedExtension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UISupportedInterfaceOrientations
6 |
7 | UIInterfaceOrientationPortrait
8 |
9 | EXAppExtensionAttributes
10 |
11 | EXExtensionPointIdentifier
12 | com.apple.securecapture
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Packages/Models/Sources/Models/CameraPosition.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraPosition.swift
3 | // Models
4 | //
5 | // Created by JuniperPhoton on 2024/11/12.
6 | //
7 | import AVFoundation
8 |
9 | public enum CameraPosition: String, Codable, Sendable {
10 | case front
11 | case back
12 |
13 | public var avFoundationPosition: AVCaptureDevice.Position {
14 | switch self {
15 | case .front:
16 | return .front
17 | case .back:
18 | return .back
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/LockedWidget/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "appearances" : [
10 | {
11 | "appearance" : "luminosity",
12 | "value" : "dark"
13 | }
14 | ],
15 | "idiom" : "universal",
16 | "platform" : "ios",
17 | "size" : "1024x1024"
18 | },
19 | {
20 | "appearances" : [
21 | {
22 | "appearance" : "luminosity",
23 | "value" : "tinted"
24 | }
25 | ],
26 | "idiom" : "universal",
27 | "platform" : "ios",
28 | "size" : "1024x1024"
29 | }
30 | ],
31 | "info" : {
32 | "author" : "xcode",
33 | "version" : 1
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Camera/AppStorageConfigProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppStorageConfigProvider.swift
3 | // LockedCameraCaptureExtensionDemo
4 | //
5 | // Created by Photon Juniper on 2024/8/21.
6 | //
7 | import Foundation
8 | import LockedCameraCapture
9 |
10 | struct AppStorageConfigProvider : Sendable {
11 | let rootURL: URL?
12 | }
13 |
14 | extension AppStorageConfigProvider {
15 | static let standard = AppStorageConfigProvider(
16 | rootURL: getStandardRootURL()
17 | )
18 | }
19 |
20 | @available(iOS 18, *)
21 | extension AppStorageConfigProvider {
22 | init(_ session: LockedCameraCaptureSession) {
23 | self.rootURL = session.sessionContentURL
24 | }
25 | }
26 |
27 | private func getStandardRootURL() -> URL? {
28 | try? FileManager.default.url(
29 | for: .documentDirectory,
30 | in: .userDomainMask,
31 | appropriateFor: nil,
32 | create: true
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/LockedWidget/LockedWidgetBundle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LockedWidgetBundle.swift
3 | // LockedWidget
4 | //
5 | // Created by Photon Juniper on 2024/8/20.
6 | //
7 |
8 | import WidgetKit
9 | import SwiftUI
10 |
11 | @main
12 | struct LockedWidgetBundle: WidgetBundle {
13 | var body: some Widget {
14 | if #available(iOS 18, *) {
15 | LockedWidgetControl()
16 | }
17 | }
18 | }
19 |
20 | @available(iOS 18, *)
21 | struct LockedWidgetControl: ControlWidget {
22 | var body: some ControlWidgetConfiguration {
23 | StaticControlConfiguration(
24 | kind: "com.juniperphoton.widget.control"
25 | ) {
26 | ControlWidgetButton(action: AppCaptureIntent()) {
27 | Label("Launch App", systemImage: "camera.shutter.button")
28 | }
29 | }
30 | .displayName("Launch Camera")
31 | .description("A an example control that launch camera.")
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Packages/Models/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "Models",
8 | platforms: [
9 | .iOS(.v16),
10 | .macOS(.v13)
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, making them visible to other packages.
14 | .library(
15 | name: "Models",
16 | targets: ["Models"]),
17 | ],
18 | targets: [
19 | // Targets are the basic building blocks of a package, defining a module or a test suite.
20 | // Targets can depend on other targets in this package and products from dependencies.
21 | .target(
22 | name: "Models"),
23 | .testTarget(
24 | name: "ModelsTests",
25 | dependencies: ["Models"]
26 | ),
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Camera/AppUserDefaultSettings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppUserDefaultSettings.swift
3 | // LockedCameraCaptureExtensionDemo
4 | //
5 | // Created by Photon Juniper on 2024/8/21.
6 | //
7 | import SwiftUI
8 | import Models
9 |
10 | class AppUserDefaultSettings {
11 | static let shared = AppUserDefaultSettings()
12 |
13 | enum Keys: String {
14 | case cameraPosition
15 | }
16 |
17 | var cameraPosition: CameraPosition {
18 | get {
19 | if let rawValue = UserDefaults.standard.string(forKey: AppUserDefaultSettings.Keys.cameraPosition.rawValue) {
20 | return CameraPosition(rawValue: rawValue) ?? .back
21 | } else {
22 | return .back
23 | }
24 | }
25 | set {
26 | UserDefaults.standard.setValue(newValue.rawValue, forKey: AppUserDefaultSettings.Keys.cameraPosition.rawValue)
27 | }
28 | }
29 |
30 | private init() {
31 | // empty
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LockedExtension/LockedExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LockedExtension.swift
3 | // LockedExtension
4 | //
5 | // Created by Photon Juniper on 2024/8/20.
6 | //
7 |
8 | import Foundation
9 | import LockedCameraCapture
10 | import SwiftUI
11 |
12 | @main
13 | struct LockedExtension: LockedCameraCaptureExtension {
14 | var body: some LockedCameraCaptureExtensionScene {
15 | LockedCameraCaptureUIScene { session in
16 | LockedCameraCaptureView(session: session)
17 | }
18 | }
19 | }
20 |
21 | struct LockedCameraCaptureView: View {
22 | let session: LockedCameraCaptureSession
23 |
24 | var body: some View {
25 | // In LockedCameraCaptureExtensionScene, scenePhase will not be active.
26 | // Thus we need to set the scenePhase to active manually.
27 | ContentView(configProvider: AppStorageConfigProvider(session))
28 | .environment(\.scenePhase, .active)
29 | .environment(\.openMainApp, OpenMainAppAction(session: session))
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Packages/MetalLib/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "MetalLib",
8 | platforms: [
9 | .iOS(.v16),
10 | .macOS(.v13)
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, making them visible to other packages.
14 | .library(
15 | name: "MetalLib",
16 | targets: ["MetalLib"]),
17 | ],
18 | targets: [
19 | // Targets are the basic building blocks of a package, defining a module or a test suite.
20 | // Targets can depend on other targets in this package and products from dependencies.
21 | .target(
22 | name: "MetalLib"),
23 | .testTarget(
24 | name: "MetalLibTests",
25 | dependencies: ["MetalLib"]
26 | ),
27 | ],
28 | swiftLanguageModes: [.v5]
29 | )
30 |
--------------------------------------------------------------------------------
/LockedExtension/OpenMainAppButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OpenMainAppButton.swift
3 | // LockedCameraCaptureExtensionDemo
4 | //
5 | // Created by Photon Juniper on 2024/8/21.
6 | //
7 | import LockedCameraCapture
8 | import SwiftUI
9 |
10 | @available(iOS 18, *)
11 | struct OpenMainAppButton: View {
12 | @Environment(\.openMainApp) private var openMainApp: OpenMainAppAction
13 |
14 | var body: some View {
15 | Button {
16 | // For unknown reasons, we can't use the NSUserActivityTypeLockedCameraCapture constant
17 | // if we don't set the minimum target to iOS 18.
18 | // Otherwise when the app runs on the previous version, it will crash.
19 | openMainApp(NSUserActivity(activityType: NSUserActivityTypeLockedCameraCapture))
20 | } label: {
21 | Label("OPEN MAIN APP", systemImage: "arrow.up.right")
22 | .padding(4)
23 | .foregroundStyle(.black)
24 | .font(.footnote.bold())
25 | .background(RoundedRectangle(cornerRadius: 4).fill(.yellow))
26 | }.buttonStyle(.plain)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Packages/MetalLib/Sources/MetalLib/ViewRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewRepresentable.swift
3 | // PhotonCam
4 | //
5 | // Created by Photon Juniper on 2023/10/27.
6 | //
7 | import SwiftUI
8 |
9 | #if os(iOS) || os(tvOS)
10 | public protocol ViewRepresentable: UIViewRepresentable {
11 | associatedtype ViewType = UIViewType
12 | func makeView(context: Context) -> ViewType
13 | func updateView(_ view: ViewType, context: Context)
14 | }
15 |
16 | extension ViewRepresentable {
17 | public func makeUIView(context: Context) -> ViewType {
18 | makeView(context: context)
19 | }
20 |
21 | public func updateUIView(_ uiView: ViewType, context: Context) {
22 | updateView(uiView, context: context)
23 | }
24 | }
25 | #elseif os(macOS)
26 | public protocol ViewRepresentable: NSViewRepresentable {
27 | associatedtype ViewType = NSViewType
28 | func makeView(context: Context) -> ViewType
29 | func updateView(_ view: ViewType, context: Context)
30 | }
31 |
32 | extension ViewRepresentable {
33 | public func makeNSView(context: Context) -> ViewType {
34 | makeView(context: context)
35 | }
36 |
37 | public func updateNSView(_ nsView: ViewType, context: Context) {
38 | updateView(nsView, context: context)
39 | }
40 | }
41 | #endif
42 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo.xcodeproj/xcuserdata/juniperphoton.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | IntentsLibFramework.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 3
11 |
12 | LockedCameraCaptureExtensionDemo.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 2
16 |
17 | LockedExtension.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 0
21 |
22 | LockedWidgetExtension.xcscheme_^#shared#^_
23 |
24 | orderHint
25 | 1
26 |
27 |
28 | SuppressBuildableAutocreation
29 |
30 | 22DE8DDA2C74B36800FC6EEA
31 |
32 | primary
33 |
34 |
35 | 22DE8DF02C74B3CA00FC6EEA
36 |
37 | primary
38 |
39 |
40 | 22DE8E032C74B3E500FC6EEA
41 |
42 | primary
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Camera/OpenMainAppAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OpenMainAppAction.swift
3 | // LockedCameraCaptureExtensionDemo
4 | //
5 | // Created by Photon Juniper on 2024/8/21.
6 | //
7 | import SwiftUI
8 | import LockedCameraCapture
9 |
10 | struct OpenMainAppAction {
11 | private var action: (NSUserActivity) -> Void
12 |
13 | init() {
14 | self.init { _ in
15 | // ignored
16 | }
17 | }
18 |
19 | @available(iOS 18, *)
20 | init(session: LockedCameraCaptureSession) {
21 | self.init { activity in
22 | Task {
23 | do {
24 | try await session.openApplication(for: activity)
25 | } catch {
26 | print("failed to open application \(error)")
27 | }
28 | }
29 | }
30 | }
31 |
32 | init(action: @escaping (NSUserActivity) -> Void) {
33 | self.action = action
34 | }
35 |
36 | func callAsFunction(_ activity: NSUserActivity) {
37 | self.action(activity)
38 | }
39 | }
40 |
41 | struct OpenMainAppActionKey: EnvironmentKey {
42 | static var defaultValue: OpenMainAppAction = OpenMainAppAction()
43 | }
44 |
45 | extension EnvironmentValues {
46 | var openMainApp: OpenMainAppAction {
47 | get { self[OpenMainAppActionKey.self] }
48 | set { self[OpenMainAppActionKey.self] = newValue }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Camera/ViewModel/CamPreviewViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CamPreviewViewModel.swift
3 | // LockedCameraCaptureExtensionDemo
4 | //
5 | // Created by Photon Juniper on 2024/8/21.
6 | //
7 | import SwiftUI
8 | import AVFoundation
9 | import MetalLib
10 |
11 | class CamPreviewViewModel: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDelegate {
12 | @Published private(set) var previewImage: CIImage? = nil
13 | @Published private(set) var renderer = MetalRenderer()
14 |
15 | private(set) var previewQueue = DispatchQueue(label: "preview_queue")
16 |
17 | func initializeRenderer() {
18 | renderer.initializeCIContext(colorSpace: nil, name: "preview")
19 | }
20 |
21 | @MainActor
22 | func updatePreviewImage(_ previewImage: CIImage?) {
23 | self.previewImage = previewImage
24 | self.renderer.requestChanged(displayedImage: previewImage)
25 | }
26 |
27 | func captureOutput(
28 | _ output: AVCaptureOutput,
29 | didOutput sampleBuffer: CMSampleBuffer,
30 | from connection: AVCaptureConnection
31 | ) {
32 | let image = getVideoOutputImage(output, didOutput: sampleBuffer)
33 | DispatchQueue.main.async {
34 | self.updatePreviewImage(image)
35 | }
36 | }
37 |
38 | private func getVideoOutputImage(
39 | _ output: AVCaptureOutput,
40 | didOutput sampleBuffer: CMSampleBuffer
41 | ) -> CIImage? {
42 | guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
43 | return nil
44 | }
45 | return CIImage(cvPixelBuffer: pixelBuffer)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Intent/AppCaptureIntent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MyAppCaptureIntent.swift
3 | // LockedCameraCaptureExtensionDemo
4 | //
5 | // Created by Photon Juniper on 2024/8/20.
6 | //
7 | import AppIntents
8 | import AVFoundation
9 | import Models
10 |
11 | /// The implementation of CameraCaptureIntent should always built
12 | /// against the App and the extension targets, otherwise it won't be discovered as expected.
13 | ///
14 | /// If you own your framework, it possible that you put this implementation in the framework
15 | /// and set the target membership to be included in the App and the extension targets, even if
16 | /// the source code is in the framework itself.
17 | ///
18 | /// Note: it looks like there is a specific API [AppIntentsPackage](https://developer.apple.com/documentation/appintents/appintentspackage)
19 | /// that should solve this discovery issue. But it won't work.
20 | ///
21 | /// There is a post about this issue:
22 | /// [AppIntentsPackage protocol with SPM package not working](https://forums.developer.apple.com/forums/thread/732535).
23 | @available(iOS 18, *)
24 | struct AppCaptureIntent: CameraCaptureIntent {
25 | struct MyAppContext: Codable, Sendable {
26 | public private(set) var cameraPosition: CameraPosition = .back
27 |
28 | public init(cameraPosition: CameraPosition) {
29 | self.cameraPosition = cameraPosition
30 | }
31 | }
32 |
33 | typealias AppContext = MyAppContext
34 |
35 | static let title: LocalizedStringResource = "AppCaptureIntent"
36 | static let description = IntentDescription("Capture photos with MyApp.")
37 |
38 | @MainActor
39 | func perform() async throws -> some IntentResult {
40 | return .result()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Localizable.xcstrings:
--------------------------------------------------------------------------------
1 | {
2 | "sourceLanguage" : "en",
3 | "strings" : {
4 | "AppCaptureIntent" : {
5 |
6 | },
7 | "Capture photos with MyApp." : {
8 |
9 | },
10 | "NoPermissionHint" : {
11 | "extractionState" : "manual",
12 | "localizations" : {
13 | "en" : {
14 | "stringUnit" : {
15 | "state" : "translated",
16 | "value" : "You have denied Photo Library or Camera permission, please grant them in the system's settings."
17 | }
18 | },
19 | "zh-Hans" : {
20 | "stringUnit" : {
21 | "state" : "translated",
22 | "value" : "你拒绝了相机权限。请到设置里授予该权限。"
23 | }
24 | },
25 | "zh-Hant" : {
26 | "stringUnit" : {
27 | "state" : "translated",
28 | "value" : "你拒絕了相機權限。請到設置里授予該權限。"
29 | }
30 | }
31 | }
32 | },
33 | "OPEN MAIN APP" : {
34 |
35 | },
36 | "TapToCaptureHint" : {
37 | "extractionState" : "manual",
38 | "localizations" : {
39 | "en" : {
40 | "stringUnit" : {
41 | "state" : "translated",
42 | "value" : "You can tap the button below to capture a photo or use Volume buttons or Camera Control to capture."
43 | }
44 | },
45 | "zh-Hans" : {
46 | "stringUnit" : {
47 | "state" : "translated",
48 | "value" : "你可以点击下方的拍照按钮或者使用音量键或相机控制来进行拍照。"
49 | }
50 | },
51 | "zh-Hant" : {
52 | "stringUnit" : {
53 | "state" : "translated",
54 | "value" : "你可以點擊下方的拍照按鈕或者使用音量鍵或相機控制來進行拍照。"
55 | }
56 | }
57 | }
58 | }
59 | },
60 | "version" : "1.0"
61 | }
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Camera/View/CaptureInteractionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InteractionView.swift
3 | // LockedCameraCaptureExtensionDemo
4 | //
5 | // Created by Photon Juniper on 2024/8/20.
6 | //
7 | import SwiftUI
8 | import Photos
9 | import AVFoundation
10 | import AVKit
11 |
12 | extension View {
13 | @ViewBuilder
14 | func onPressCapture(action: @escaping () -> Void) -> some View {
15 | if #available(iOS 18.0, *) {
16 | self.onCameraCaptureEvent { event in
17 | switch event.phase {
18 | case .ended:
19 | action()
20 | default:
21 | break
22 | }
23 | } secondaryAction: { event in
24 | switch event.phase {
25 | case .ended:
26 | action()
27 | default:
28 | break
29 | }
30 | }
31 | } else if #available(iOS 17.2, *) {
32 | self.background {
33 | CaptureInteractionView(action: action)
34 | }
35 | } else {
36 | self
37 | }
38 | }
39 | }
40 |
41 | @available(iOS 17.2, *)
42 | private struct CaptureInteractionView: UIViewRepresentable {
43 | var action: () -> Void
44 |
45 | func makeUIView(context: Context) -> some UIView {
46 | let uiView = UIView()
47 | let interaction = AVCaptureEventInteraction { event in
48 | if event.phase == .began {
49 | action()
50 | }
51 | }
52 |
53 | uiView.addInteraction(interaction)
54 | return uiView
55 | }
56 |
57 | func updateUIView(_ uiView: UIViewType, context: Context) {
58 | // ignored
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "appearances" : [
10 | {
11 | "appearance" : "luminosity",
12 | "value" : "dark"
13 | }
14 | ],
15 | "idiom" : "universal",
16 | "platform" : "ios",
17 | "size" : "1024x1024"
18 | },
19 | {
20 | "appearances" : [
21 | {
22 | "appearance" : "luminosity",
23 | "value" : "tinted"
24 | }
25 | ],
26 | "idiom" : "universal",
27 | "platform" : "ios",
28 | "size" : "1024x1024"
29 | },
30 | {
31 | "idiom" : "mac",
32 | "scale" : "1x",
33 | "size" : "16x16"
34 | },
35 | {
36 | "idiom" : "mac",
37 | "scale" : "2x",
38 | "size" : "16x16"
39 | },
40 | {
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "32x32"
44 | },
45 | {
46 | "idiom" : "mac",
47 | "scale" : "2x",
48 | "size" : "32x32"
49 | },
50 | {
51 | "idiom" : "mac",
52 | "scale" : "1x",
53 | "size" : "128x128"
54 | },
55 | {
56 | "idiom" : "mac",
57 | "scale" : "2x",
58 | "size" : "128x128"
59 | },
60 | {
61 | "idiom" : "mac",
62 | "scale" : "1x",
63 | "size" : "256x256"
64 | },
65 | {
66 | "idiom" : "mac",
67 | "scale" : "2x",
68 | "size" : "256x256"
69 | },
70 | {
71 | "idiom" : "mac",
72 | "scale" : "1x",
73 | "size" : "512x512"
74 | },
75 | {
76 | "idiom" : "mac",
77 | "scale" : "2x",
78 | "size" : "512x512"
79 | }
80 | ],
81 | "info" : {
82 | "author" : "xcode",
83 | "version" : 1
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Packages/MetalLib/Sources/MetalLib/MetalView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MetalView.swift
3 | // PhotonCam
4 | //
5 | // Created by Photon Juniper on 2023/10/27.
6 | //
7 | import SwiftUI
8 | import MetalKit
9 |
10 | private class BoundAwareMTKView: MTKView {
11 | private var currentBounds: CGRect = .zero
12 |
13 | var onBoundsChanged: ((CGRect) -> Void)? = nil
14 |
15 | #if canImport(UIKit)
16 | override func layoutSubviews() {
17 | super.layoutSubviews()
18 |
19 | if self.currentBounds != self.bounds {
20 | self.currentBounds = self.bounds
21 | onBoundsChanged?(self.currentBounds)
22 | }
23 | }
24 | #else
25 | override func layout() {
26 | super.layout()
27 |
28 | if self.currentBounds != self.bounds {
29 | self.currentBounds = self.bounds
30 | onBoundsChanged?(self.currentBounds)
31 | }
32 | }
33 | #endif
34 | }
35 |
36 | public struct MetalView: ViewRepresentable {
37 | @ObservedObject public var renderer: MetalRenderer
38 |
39 | public let enableSetNeedsDisplay: Bool
40 | public let isOpaque: Bool
41 |
42 | public init(renderer: MetalRenderer, enableSetNeedsDisplay: Bool, isOpaque: Bool = true) {
43 | self.renderer = renderer
44 | self.enableSetNeedsDisplay = enableSetNeedsDisplay
45 | self.isOpaque = isOpaque
46 | }
47 |
48 | /// - Tag: MakeView
49 | public func makeView(context: Context) -> MTKView {
50 | let view = BoundAwareMTKView(frame: .zero, device: renderer.device)
51 | view.onBoundsChanged = { [weak view] bounds in
52 | if enableSetNeedsDisplay {
53 | view?.setNeedsDisplay(bounds)
54 | }
55 | }
56 |
57 | if enableSetNeedsDisplay {
58 | view.enableSetNeedsDisplay = true
59 | view.isPaused = true
60 | } else {
61 | // Suggest to Core Animation, through MetalKit, how often to redraw the view.
62 | view.preferredFramesPerSecond = 30
63 | view.enableSetNeedsDisplay = false
64 | view.isPaused = false
65 | }
66 |
67 | // Allow Core Image to render to the view using the Metal compute pipeline.
68 | view.framebufferOnly = false
69 | view.delegate = renderer
70 |
71 | if let layer = view.layer as? CAMetalLayer {
72 | layer.isOpaque = isOpaque
73 | }
74 |
75 | return view
76 | }
77 |
78 | public func updateView(_ view: MTKView, context: Context) {
79 | configure(view: view, using: renderer)
80 | view.setNeedsDisplay(view.bounds)
81 | }
82 |
83 | private func configure(view: MTKView, using renderer: MetalRenderer) {
84 | view.delegate = renderer
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Camera/ViewModel/CaptureProcessor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureProcessor.swift
3 | // LockedCameraCaptureExtensionDemo
4 | //
5 | // Created by Photon Juniper on 2024/8/21.
6 | //
7 | import AVFoundation
8 | import Photos
9 |
10 | class CaptureProcessor: NSObject, ObservableObject, AVCapturePhotoCaptureDelegate {
11 | @Published var saveResultText: String = ""
12 |
13 | let configProvider: AppStorageConfigProvider
14 |
15 | init(configProvider: AppStorageConfigProvider) {
16 | self.configProvider = configProvider
17 | }
18 |
19 | func photoOutput(
20 | _ output: AVCapturePhotoOutput,
21 | didFinishProcessingPhoto photo: AVCapturePhoto,
22 | error: (any Error)?
23 | ) {
24 | if let error = error {
25 | print("photoOutput didFinishProcessingPhoto error \(error)")
26 | return
27 | }
28 |
29 | Task { @MainActor in
30 | await savePhotoToLibrary(photo)
31 | }
32 | }
33 |
34 | @MainActor
35 | private func savePhotoToLibrary(_ photo: AVCapturePhoto) async {
36 | let saved = await savePhotoToLibraryInternal(photo)
37 | if saved {
38 | saveResultText = "Saved to Photo Library"
39 | } else {
40 | saveResultText = "Failed to save, see the console for more details"
41 | }
42 |
43 | do {
44 | try await Task.sleep(for: .seconds(2))
45 | saveResultText = ""
46 | } catch {
47 | // ignored
48 | }
49 | }
50 |
51 | private nonisolated func savePhotoToLibraryInternal(_ photo: AVCapturePhoto) async -> Bool {
52 | guard let photoData = photo.fileDataRepresentation() else {
53 | print("can't get photoData")
54 | return false
55 | }
56 |
57 | // Saving the data to a file isn't strictly necessary since
58 | // PHAssetCreationRequest can accept Data directly.
59 | // However, this example demonstrates saving the photo data to a temporary file
60 | // using a container URL provided by the environment.
61 | guard let fileURL = configProvider.rootURL?.appendingPathComponent(
62 | UUID().uuidString,
63 | conformingTo: .heic
64 | ) else {
65 | print("can't get fileURL")
66 | return false
67 | }
68 |
69 | print("savePhotoToLibraryInternal, fileURL is \(String(describing: fileURL))")
70 |
71 | do {
72 | try photoData.write(to: fileURL)
73 | } catch {
74 | print("savePhotoToLibraryInternal, failed to write to file \(error)")
75 | return false
76 | }
77 |
78 | return await withCheckedContinuation { continuation in
79 | PHPhotoLibrary.shared().performChanges {
80 | let _ = PHAssetCreationRequest.creationRequestForAssetFromImage(atFileURL: fileURL)
81 | } completionHandler: { success, error in
82 | print("savePhotoToLibrary, success: \(success), error: \(String(describing: error))")
83 | continuation.resume(returning: success)
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo.xcodeproj/xcshareddata/xcschemes/LockedCameraCaptureExtensionDemo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo.xcodeproj/xcshareddata/xcschemes/LockedExtension.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
11 |
17 |
23 |
24 |
25 |
31 |
37 |
38 |
39 |
40 |
41 |
47 |
48 |
60 |
62 |
68 |
69 |
70 |
71 |
79 |
81 |
87 |
88 |
89 |
90 |
92 |
93 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo.xcodeproj/xcshareddata/xcschemes/LockedWidgetExtension.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
11 |
17 |
23 |
24 |
25 |
31 |
37 |
38 |
39 |
40 |
41 |
47 |
48 |
60 |
62 |
68 |
69 |
70 |
71 |
75 |
76 |
80 |
81 |
85 |
86 |
87 |
88 |
96 |
98 |
104 |
105 |
106 |
107 |
109 |
110 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Camera/View/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // LockedCameraCaptureExtensionDemo
4 | //
5 | // Created by Photon Juniper on 2024/8/20.
6 | //
7 |
8 | import SwiftUI
9 | import MetalLib
10 |
11 | struct ContentView: View {
12 | @Environment(\.scenePhase) private var scenePhase
13 |
14 | @StateObject private var previewViewModel: CamPreviewViewModel
15 | @StateObject private var viewModel: MainViewModel
16 | @StateObject private var captureProcessor: CaptureProcessor
17 |
18 | /// Construct ``ContentView`` given the instance of ``AppStorageConfigProvider``, which provides the information
19 | /// about the current environment.
20 | init(configProvider: AppStorageConfigProvider) {
21 | let previewViewModel = CamPreviewViewModel()
22 | let captureProcessor = CaptureProcessor(configProvider: configProvider)
23 | let mainViewModel = MainViewModel(
24 | camPreviewViewModel: previewViewModel,
25 | captureProcessor: captureProcessor
26 | )
27 |
28 | self._previewViewModel = StateObject(wrappedValue: previewViewModel)
29 | self._captureProcessor = StateObject(wrappedValue: captureProcessor)
30 | self._viewModel = StateObject(wrappedValue: mainViewModel)
31 | }
32 |
33 | var body: some View {
34 | VStack(spacing: 0) {
35 | if viewModel.showNoPermissionHint {
36 | Text("NoPermissionHint")
37 | } else {
38 | // This demo uses MTKView to render CIImage, showing the possibility to not use AVCaptureVideoPreviewLayer.
39 | MetalView(renderer: previewViewModel.renderer, enableSetNeedsDisplay: true)
40 | .overlay {
41 | if viewModel.showFlashScreen {
42 | Color.black.zIndex(1)
43 | }
44 | }
45 |
46 | VStack {
47 | #if CAPTURE_EXTENSION
48 | OpenMainAppButton()
49 | .padding(.bottom)
50 | #endif
51 | Text("TapToCaptureHint")
52 | .foregroundStyle(.white)
53 | .font(.footnote)
54 | .padding()
55 |
56 | ZStack {
57 | SwitchCameraPositionButton(viewModel: viewModel)
58 | .frame(maxWidth: .infinity, alignment: .trailing)
59 |
60 | CaptureButton(viewModel: viewModel)
61 | }
62 | }
63 | .padding()
64 | .disabled(viewModel.isSettingUpCamera)
65 | .opacity(viewModel.isSettingUpCamera ? 0.5 : 1.0)
66 | }
67 | }
68 | .overlay {
69 | if !captureProcessor.saveResultText.isEmpty {
70 | Text(captureProcessor.saveResultText)
71 | .padding(12)
72 | .foregroundStyle(.black)
73 | .background(RoundedRectangle(cornerRadius: 12).fill(.white))
74 | .frame(maxHeight: .infinity, alignment: .top)
75 | .padding(12)
76 | }
77 | }
78 | .background {
79 | Color.black.ignoresSafeArea()
80 | }
81 | .animation(.default, value: viewModel.isSettingUpCamera)
82 | .animation(.default, value: captureProcessor.saveResultText)
83 | .onPressCapture {
84 | Task {
85 | await viewModel.capturePhoto()
86 | }
87 | }
88 | .task(id: scenePhase) {
89 | switch scenePhase {
90 | case .background:
91 | await viewModel.stopCamera()
92 | case .active:
93 | await viewModel.updateFromAppContext()
94 | await viewModel.setup()
95 | default:
96 | break
97 | }
98 | }
99 | .task {
100 | previewViewModel.initializeRenderer()
101 | }
102 | }
103 | }
104 |
105 | private struct SwitchCameraPositionButton: View {
106 | @ObservedObject var viewModel: MainViewModel
107 |
108 | var body: some View {
109 | Button {
110 | Task {
111 | await viewModel.toggleCameraPositionSwitch()
112 | }
113 | } label: {
114 | Image(systemName: "arrow.triangle.2.circlepath.camera")
115 | .padding()
116 | .foregroundStyle(viewModel.cameraPosition == .back ? .white : .black)
117 | .background {
118 | Circle().fill(Color.white.opacity(viewModel.cameraPosition == .back ? 0.1 : 1.0))
119 | }
120 | }.buttonStyle(.plain)
121 | }
122 | }
123 |
124 | private struct CaptureButton: View {
125 | @ObservedObject var viewModel: MainViewModel
126 |
127 | var body: some View {
128 | Button {
129 | Task {
130 | await viewModel.capturePhoto()
131 | }
132 | } label: {
133 | Circle().fill(Color.white)
134 | }.buttonStyle(.plain)
135 | .frame(width: 80, height: 80)
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo/Camera/ViewModel/MainViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewModel.swift
3 | // LockedCameraCaptureExtensionDemo
4 | //
5 | // Created by Photon Juniper on 2024/8/20.
6 | //
7 | import SwiftUI
8 | import AVFoundation
9 | import Photos
10 | import LockedCameraCapture
11 | import Models
12 |
13 | class MainViewModel: ObservableObject {
14 | @Published var showNoPermissionHint: Bool = false
15 | @Published var cameraPosition: CameraPosition = .back {
16 | didSet {
17 | Task {
18 | await updateAppContext()
19 | }
20 | }
21 | }
22 | @Published var isSettingUpCamera = false
23 | @Published var showFlashScreen = false
24 |
25 | private var session: AVCaptureSession? = nil
26 | private var photoOutput: AVCapturePhotoOutput? = nil
27 |
28 | private let camPreviewViewModel: CamPreviewViewModel
29 | private let captureProcessor: CaptureProcessor
30 |
31 | init(camPreviewViewModel: CamPreviewViewModel, captureProcessor: CaptureProcessor) {
32 | self.camPreviewViewModel = camPreviewViewModel
33 | self.captureProcessor = captureProcessor
34 | }
35 |
36 | @MainActor
37 | func setup() async {
38 | guard await requestForPermission() else {
39 | showNoPermissionHint = true
40 | return
41 | }
42 |
43 | await setupInternal()
44 | }
45 |
46 | @MainActor
47 | func capturePhoto() async {
48 | guard let photoOutput = photoOutput else {
49 | print("can't find photo output")
50 | return
51 | }
52 |
53 | let settings = AVCapturePhotoSettings()
54 | photoOutput.capturePhoto(with: settings, delegate: captureProcessor)
55 |
56 | self.showFlashScreen = true
57 | try? await Task.sleep(for: .seconds(0.2))
58 | self.showFlashScreen = false
59 | }
60 |
61 | @MainActor
62 | func toggleCameraPositionSwitch() async {
63 | self.cameraPosition = self.cameraPosition == .back ? .front : .back
64 | await reconfigureCamera()
65 | }
66 |
67 | @MainActor
68 | func reconfigureCamera() async {
69 | await stopCamera()
70 | await setupInternal()
71 | }
72 |
73 | @MainActor
74 | func stopCamera() async {
75 | guard let cameraSession = self.session else {
76 | return
77 | }
78 |
79 | cameraSession.stopRunning()
80 | self.session = nil
81 | }
82 |
83 | @MainActor
84 | private func setupInternal() async {
85 | if isSettingUpCamera {
86 | print("isSettingUpCamera, skip")
87 | return
88 | }
89 |
90 | isSettingUpCamera = true
91 |
92 | defer {
93 | isSettingUpCamera = false
94 | }
95 |
96 | print("start setting up")
97 |
98 | guard let (cameraSession, photoOutput) = await setupCameraSession(position: cameraPosition) else {
99 | return
100 | }
101 |
102 | self.session = cameraSession
103 | self.photoOutput = photoOutput
104 | }
105 |
106 | private nonisolated func setupCameraSession(position: CameraPosition) async -> (AVCaptureSession, AVCapturePhotoOutput)? {
107 | do {
108 | let session = AVCaptureSession()
109 | session.beginConfiguration()
110 | session.sessionPreset = .photo
111 |
112 | guard let device = AVCaptureDevice.default(
113 | .builtInWideAngleCamera,
114 | for: .video,
115 | position: position.avFoundationPosition
116 | ) else {
117 | print("can't find AVCaptureDevice")
118 | return nil
119 | }
120 |
121 | session.addInput(try AVCaptureDeviceInput(device: device))
122 |
123 | let videoOutput = AVCaptureVideoDataOutput()
124 | videoOutput.setSampleBufferDelegate(camPreviewViewModel, queue: camPreviewViewModel.previewQueue)
125 |
126 | session.addOutput(videoOutput)
127 |
128 | let photoOutput = AVCapturePhotoOutput()
129 | session.addOutput(photoOutput)
130 |
131 | if let connection = videoOutput.connection(with: .video) {
132 | if connection.isVideoRotationAngleSupported(90) {
133 | connection.videoRotationAngle = 90
134 | }
135 | if connection.isVideoMirroringSupported && position == .front {
136 | connection.isVideoMirrored = true
137 | }
138 | }
139 |
140 | session.commitConfiguration()
141 | session.startRunning()
142 |
143 | return (session, photoOutput)
144 | } catch {
145 | print("error while setting up camera \(error)")
146 | return nil
147 | }
148 | }
149 |
150 | private func requestForPermission() async -> Bool {
151 | let photoLibraryPermissionResult = await PHPhotoLibrary.requestAuthorization(for: .addOnly)
152 | let cameraPermissionPermitted = await AVCaptureDevice.requestAccess(for: .video)
153 | return photoLibraryPermissionResult == .authorized && cameraPermissionPermitted
154 | }
155 |
156 | @MainActor
157 | private func updateAppContext() async {
158 | #if !CAPTURE_EXTENSION
159 | AppUserDefaultSettings.shared.cameraPosition = self.cameraPosition
160 | #endif
161 |
162 | if #available(iOS 18, *) {
163 | let appContext = AppCaptureIntent.AppContext(cameraPosition: cameraPosition)
164 |
165 | do {
166 | try await AppCaptureIntent.updateAppContext(appContext)
167 | print("app context updated")
168 | } catch {
169 | print("error on updating app context \(error)")
170 | }
171 | }
172 | }
173 |
174 | @MainActor
175 | func updateFromAppContext() async {
176 | #if !CAPTURE_EXTENSION
177 | // If it's in the main app, first read the value from the UserDefaults.
178 | self.cameraPosition = AppUserDefaultSettings.shared.cameraPosition
179 | #endif
180 |
181 | if #available(iOS 18, *) {
182 | do {
183 | // If `AppCaptureIntent.appContext` exists, then read from it.
184 | if let appContext = try await AppCaptureIntent.appContext {
185 | self.cameraPosition = appContext.cameraPosition
186 | print("updated from app context")
187 | } else {
188 | print("app context is nil")
189 | }
190 | } catch {
191 | print("error on getting app context")
192 | }
193 | }
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/Packages/MetalLib/Sources/MetalLib/MetalRenderer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Renderer.swift
3 | // PhotonCam
4 | //
5 | // Created by Photon Juniper on 2023/10/27.
6 | //
7 |
8 | import Foundation
9 | import Metal
10 | import MetalKit
11 | import CoreImage
12 |
13 | private let maxBuffersInFlight = 3
14 |
15 | public final class MetalRenderer: NSObject, MTKViewDelegate, ObservableObject {
16 | @Published private(set) var requestedDisplayedTime = CFAbsoluteTimeGetCurrent()
17 |
18 | public let device: MTLDevice
19 |
20 | private let commandQueue: MTLCommandQueue
21 | private var ciContext: CIContext? = nil
22 | private var opaqueBackground: CIImage
23 | private let startTime: CFAbsoluteTime
24 |
25 | private let inFlightSemaphore = DispatchSemaphore(value: maxBuffersInFlight)
26 | private(set) var scaleToFill: Bool = false
27 |
28 | private var displayedImage: CIImage? = nil
29 | private var queue: DispatchQueue? = nil
30 |
31 | private var clearDestination: Bool = false
32 |
33 | public override init() {
34 | let start = CFAbsoluteTimeGetCurrent()
35 | self.device = MTLCreateSystemDefaultDevice()!
36 | self.commandQueue = self.device.makeCommandQueue()!
37 | self.opaqueBackground = CIImage.black
38 |
39 | self.startTime = CFAbsoluteTimeGetCurrent()
40 |
41 | debugPrint("MetalRenderer init \(CFAbsoluteTimeGetCurrent() - start)s")
42 | super.init()
43 | }
44 |
45 | /// The the background color to be composited over with.
46 | /// If the color is not opaque, please remember to set ``isOpaque`` in ``MetalView``.
47 | public func setBackgroundColor(ciColor: CIColor) {
48 | setBackgroundImage(CIImage(color: ciColor))
49 | }
50 |
51 | /// The the background image to be composited over with.
52 | public func setBackgroundImage(_ image: CIImage) {
53 | self.opaqueBackground = image
54 | }
55 |
56 | public func setScaleToFill(scaleToFill: Bool) {
57 | self.scaleToFill = scaleToFill
58 | }
59 |
60 | /// Initialize the CIContext with a specified working ``CGColorSpace``.
61 | /// - parameter colorSpace: The working Color Space to use.
62 | /// - parameter queue: A dedicated queue to start the render task. Although the whole render process is run by GPU,
63 | /// however creating and submitting textures to GPU will introduce performance overhead, and it's recommended not to do it on main thread.
64 | public func initializeCIContext(
65 | colorSpace: CGColorSpace?,
66 | name: String,
67 | queue: DispatchQueue? = DispatchQueue(label: "metal_render_queue", qos: .userInitiated)
68 | ) {
69 | self.queue = queue
70 |
71 | let start = CFAbsoluteTimeGetCurrent()
72 |
73 | // Set up the Core Image context's options:
74 | // - Name the context to make CI_PRINT_TREE debugging easier.
75 | // - Disable caching because the image differs every frame.
76 | // - Allow the context to use the low-power GPU, if available.
77 | var options = [CIContextOption: Any]()
78 | options = [
79 | .name: name,
80 | .cacheIntermediates: false,
81 | .allowLowPower: true,
82 | ]
83 | if let colorSpace = colorSpace {
84 | options[.workingColorSpace] = colorSpace
85 | }
86 |
87 | self.ciContext = CIContext(
88 | mtlCommandQueue: self.commandQueue,
89 | options: options
90 | )
91 |
92 | print("MetalRenderer initializeCIContext \(CFAbsoluteTimeGetCurrent() - start)s, name: \(name) to color space: \(String(describing: colorSpace))")
93 | }
94 |
95 | /// Request update the image.
96 | /// - parameter displayedImage: The CIImage to be rendered.
97 | public func requestChanged(displayedImage: CIImage?) {
98 | self.displayedImage = displayedImage
99 | self.requestedDisplayedTime = CFAbsoluteTimeGetCurrent()
100 | }
101 |
102 | /// Request clear the destination.
103 | /// - parameter clearDestination: If the destination should be cleared first before starting a new task to render image.
104 | public func requestClearDestination(clearDestination: Bool) {
105 | self.clearDestination = clearDestination
106 | }
107 |
108 | // MARK: MTKViewDelegate
109 | public func draw(in view: MTKView) {
110 | drawInternal(view)
111 | }
112 |
113 | public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
114 | // Respond to drawable size or orientation changes.
115 | }
116 |
117 | /// Draw the content into the view's texture.
118 | private func drawInternal(_ view: MTKView) {
119 | guard let ciContext = self.ciContext else {
120 | print("CIContext is nil!")
121 | return
122 | }
123 |
124 | // Create a displayable image for the current time.
125 | guard var image = self.displayedImage else {
126 | return
127 | }
128 |
129 | _ = self.inFlightSemaphore.wait(timeout: DispatchTime.distantFuture)
130 | if let commandBuffer = self.commandQueue.makeCommandBuffer() {
131 |
132 | // Add a completion handler that signals `inFlightSemaphore` when Metal and the GPU have fully
133 | // finished processing the commands that the app encoded for this frame.
134 | // This completion indicates that Metal and the GPU no longer need the dynamic buffers that
135 | // Core Image writes to in this frame.
136 | // Therefore, the CPU can overwrite the buffer contents without corrupting any rendering operations.
137 | let semaphore = self.inFlightSemaphore
138 | commandBuffer.addCompletedHandler { (_ commandBuffer)-> Swift.Void in
139 | semaphore.signal()
140 | }
141 |
142 | if let drawable = view.currentDrawable {
143 | let dSize = view.drawableSize
144 |
145 | // Create a destination the Core Image context uses to render to the drawable's Metal texture.
146 | let destination = CIRenderDestination(
147 | width: Int(dSize.width),
148 | height: Int(dSize.height),
149 | pixelFormat: view.colorPixelFormat,
150 | commandBuffer: nil
151 | ) {
152 | return drawable.texture
153 | }
154 |
155 | let scaleW = CGFloat(dSize.width) / image.extent.width
156 | let scaleH = CGFloat(dSize.height) / image.extent.height
157 |
158 | // To perform scaledToFit, use min. Use max for scaledToFill effect.
159 | let scale: CGFloat
160 | if self.scaleToFill {
161 | scale = max(scaleW, scaleH)
162 | } else {
163 | scale = min(scaleW, scaleH)
164 | }
165 |
166 | let originalExtent = image.extent
167 | let scaledWidth = Int(originalExtent.width * scale)
168 | let scaledHeight = Int(originalExtent.height * scale)
169 | image = image.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
170 | image = image.cropped(to: CGRect(x: 0, y: 0, width: scaledWidth, height: scaledHeight))
171 |
172 | // Center the image in the view's visible area.
173 | let iRect = image.extent
174 | var backBounds = CGRect(x: 0, y: 0, width: dSize.width, height: dSize.height)
175 |
176 | image = image.cropped(to: backBounds)
177 |
178 | let shiftX: CGFloat
179 | let shiftY: CGFloat
180 |
181 | shiftX = round((backBounds.size.width + iRect.origin.x - iRect.size.width) * 0.5)
182 | shiftY = round((backBounds.size.height + iRect.origin.y - iRect.size.height) * 0.5)
183 |
184 | // Read the center port of the image.
185 | backBounds = backBounds.offsetBy(dx: -shiftX, dy: -shiftY)
186 |
187 | // Blend the image over an opaque background image.
188 | // This is needed if the image is smaller than the view, or if it has transparent pixels.
189 | image = image.composited(over: self.opaqueBackground)
190 |
191 | let block = {
192 | if self.clearDestination {
193 | _ = try? ciContext.startTask(toClear: destination)
194 | }
195 |
196 | // Start a task that renders to the texture destination.
197 | // To prevent showing clamped content outside of the image's extent, we just render the
198 | // original extent from the image.
199 | _ = try? ciContext.startTask(
200 | toRender: image,
201 | from: iRect,
202 | to: destination,
203 | at: CGPoint(x: shiftX, y: shiftY)
204 | )
205 |
206 | // Insert a command to present the drawable when the buffer has been scheduled for execution.
207 | commandBuffer.present(drawable)
208 |
209 | // Commit the command buffer so that the GPU executes the work that the Core Image Render Task issues.
210 | commandBuffer.commit()
211 | }
212 |
213 | if let queue = queue {
214 | queue.async {
215 | block()
216 | }
217 | } else {
218 | block()
219 | }
220 | }
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LockedCameraCapture Extension Demo & Explanation
2 |
3 | 
4 |
5 | Starting from iOS 18, we can integrate the `LockedCameraCapture` framework to our apps and let users launch 3rd-party camera apps directly on the Lock Screen by either Lock Screen Control or the Action button without unlocking the iPhone.
6 |
7 | Apple has provided documentation for this feature at the following link: [documentation](https://developer.apple.com/documentation/LockedCameraCapture/Creating-a-camera-experience-for-the-Lock-Screen). However, there is currently no official demo available, and there are some subtle details that developers should be aware of.
8 |
9 | Here we are. I created this demo while experimenting with the `LockedCameraCapture` framework and integrating it into my upcoming release of the [PhotonCam](https://apps.apple.com/us/app/photoncam-better-proraw-cam/id6470341051?l) app. It’s a ProRAW camera app that offers Pro-level control, extensive customization options like filters and frames, and a photo editing feature.
10 |
11 | Before reading the detailed explanation below, you can also build and run the demo app first.
12 |
13 | > Note that due to the issue of iOS 18 Beta, you may find that the Capture Extension in this demo may not launch from the lock screen. Try uninstalling the app and rebooting your device to solve this issue.
14 |
15 | The following sections will talk about some key implementation details and I highly suggest you reading the Apple [documentation](https://developer.apple.com/documentation/LockedCameraCapture/Creating-a-camera-experience-for-the-Lock-Screen) first.
16 |
17 | > You are welcome to subscribe my [Substack](https://juniperphoton.substack.com/p/lockedcameracapture-extension-demo) for free to view more articles.
18 |
19 | The content of this article:
20 |
21 | 1. The anatomy of your app
22 | 2. How to debug the Capture Extension
23 | 3. Prevent the extension from being killed
24 | 4. Code sharing between main app & Capture Extension
25 | 5. Scene Phase handling
26 | 6. Store temporary files in the Capture Extension
27 | 7. Lock device orientation
28 | 8. Custom symbol image for ControlWidget
29 | 9. Be aware of the bundle size
30 | 10. Additional Information
31 |
32 | ## The anatomy of your app
33 |
34 | The app that implements the `LockedCameraCapture` feature should at least contain 3 different parts:
35 |
36 | 1. Your main app that will launch when users click the app icon on the home screen.
37 | 2. The Widget Extension provides Control Widget, which was introduced in iOS 18 to let users launch your app via Control Center or Lock Screen.
38 | 3. The Capture Extension that conforms to the `LockedCameraCaptureExtension` protocol and provides the entry point for your extension app.
39 |
40 | Here is the illustration of the launching process:
41 |
42 | 
43 |
44 | The following sections talk about some key details that need to be aware of while implementing the Capture Extension.
45 |
46 | ## How to debug the Capture Extension
47 |
48 | For a regular iOS app, when launching from Xcode, you don't worry about how the debugger is attached. If your app crashes during the launch, Xcode will guide you to the code where the crash happens.
49 |
50 | In contrast, the automatic attaching process for the app extension is not enabled. If you launch your app from Xcode, the debugger will only be attached to the main app. Consequently, if your extension crashes during launch, it becomes challenging to identify the cause of the issue.
51 |
52 | Xcode offers a mechanism to defer the attachment of the debugger to a process.
53 |
54 | 
55 |
56 | Access the Debug menu and locate the option “Attach to Process by PID or Name.” Subsequently, input the name of your Capture Extension. **By executing this action, even if the process you intend to debug is not currently running, Xcode will establish a connection with it upon its subsequent activation.**
57 |
58 | If your Capture Extension is currently running, you can simply locate the Process under the “Attach to Process” submenu to debug your code.
59 |
60 | ## Prevent the extension from being killed
61 |
62 | The Capture Extension, as its name suggests, is specifically designed for capturing purposes. Apple has implemented certain restrictions to ensure that the extension is solely intended for capturing functions, such as:
63 |
64 | 1. You should start your camera session while the extension is running.
65 | 2. Add `AVCaptureEventInteraction` to the view, which enables the hardware to detect pressing events.
66 |
67 | If your Capture Extension doesn’t adhere to the limitations mentioned above, it will be terminated after approximately 10 seconds.
68 |
69 | Please be aware that when the users click the Control Widget, iOS will determine whether to launch the main app or the Capture Extension based on the current conditions, like whether your device is locked or not.
70 |
71 | > For testing purposes, you should lock your iPhone manually, turn your face around, and launch the app via the Action button or Control Widget on the Lock Screen.
72 |
73 | ### Implement `AVCaptureEventInteraction`
74 |
75 | > UPDATE: On iOS 18.0, you can use [`onCameraCaptureEvent(isEnabled:primaryAction:secondaryAction:)`](https://developer.apple.com/documentation/SwiftUI/View/onCameraCaptureEvent(isEnabled:primaryAction:secondaryAction:)) in SwiftUI to provide such functionality.
76 |
77 | The `AVCaptureEventInteraction` is a subclass of `UIInteraction` introduced in iOS 17.2 as part of the UIKit framework.
78 |
79 | > `UIInteraction`: The protocol that an interaction implements to access the view that owns it.
80 |
81 | `UIView` has an instance method called `addInteraction(_:)` to add a specific `UIInteraction`.
82 |
83 | However, since Apple is transitioning to SwiftUI, and `LockedCameraCaptureUIScene` expects a SwiftUI view, it seems weird for Apple to offer such an API that can't be directly used in SwiftUI.
84 |
85 | You might want to retrieve the `UIView` from the current `UIWindow`’s root `UIViewController`. However, for the iOS extension, there’s no way to access the current `UIScene` because `UIApplication.shared` isn’t available in the extension.
86 |
87 | Therefore, we can add our custom `UIView` to the hierarchy of SwiftUI views, as shown below:
88 |
89 | ```swift
90 | @available(iOS 17.2, *)
91 | private struct CaptureInteractionView: UIViewRepresentable {
92 | var action: () -> Void
93 |
94 | func makeUIView(context: Context) -> some UIView {
95 | let uiView = UIView()
96 | let interaction = AVCaptureEventInteraction { event in
97 | if event.phase == .began {
98 | action()
99 | }
100 | }
101 |
102 | uiView.addInteraction(interaction)
103 | return uiView
104 | }
105 |
106 | func updateUIView(_ uiView: UIViewType, context: Context) {
107 | // ignored
108 | }
109 | }
110 | ```
111 |
112 | We can also implement an extension method for the `View` class to handle interaction events.
113 |
114 | ```swift
115 | extension View {
116 | @ViewBuilder
117 | func onPressCapture(action: @escaping () -> Void) -> some View {
118 | if #available(iOS 17.2, *) {
119 | self.background {
120 | CaptureInteractionView(action: action)
121 | }
122 | } else {
123 | self
124 | }
125 | }
126 | }
127 | ```
128 |
129 | Then you can use the `onPressCapture(action:)` on your root view:
130 |
131 | ```swift
132 | YourView {
133 | // content
134 | }.onPressCapture {
135 | Task {
136 | await viewModel.capturePhoto()
137 | }
138 | }
139 | ```
140 |
141 | ## Code sharing between main app & Capture Extension
142 |
143 | To share code or assets between your main app and the Capture Extension, you can mark your code files as belonging to the Targets.
144 |
145 | When you add a file to your Xcode project, you can specify which targets should include that file. The “Target Membership” section in the File Inspector (on the right pane of Xcode) presents a checklist of all the targets in your project. You can check or uncheck targets to control whether the file is compiled into the corresponding products.
146 |
147 | Note that compiling all the files from the main app to the Capture Extension will result in a larger compiled bundle size. Consequently, users will require more time and data to download your app from the App Store.
148 |
149 | Also, not all the code you write in the main app can be compiled for the Capture Extension. For instance, the `UIApplication.shared` API is unavailable for all types of extensions. Therefore, if you rely on `UIApplication.shared` to obtain `UIScene`, `UIWindow`, `UIApplication.shared.alternateIconName`, or other related instances, you should consider using those methods to handle this situation.
150 |
151 | 1. Avoid using `UIApplication.shared` and explore alternative methods to obtain the desired functionality.
152 | 2. Ensure that the code using `UIApplication.shared` is included in the main app target and provide a stub version of it that is included in the Capture Extension target.
153 | 3. Add a Swift custom flag to the build settings for your extension target and use the `#if` directive to conditionally compile code.
154 |
155 | 
156 |
157 | > Note that your Capture Extension should prioritize providing a seamless capture experience for users. Additionally, you should prompt users to open the main app when they are accessing unrelated features.
158 |
159 | ## Scene Phase handling
160 |
161 | In the Capture Extension, it is safe to assume that the `ScenePhase` environment variable is always active. However, when retrieving its value from `EnvironmentValues`, its value will always be `.background`. Any logic relied on this value, for example, you might want to perform some tasks when the scene is in foreground, will be invalid.
162 |
163 | To address this issue, you don’t need to modify each component of your logic. Instead, you can simply introduce your own `ScenePhase` to the root view. However, if you’re not utilizing SwiftUI’s lifecycle, you’ll need to create your own version of `ScenePhase` and inject it into your code.
164 |
165 | ```swift
166 | struct LockedCameraCaptureView: View {
167 | var body: some View {
168 | ContentView()
169 | .environment(\.scenePhase, .active)
170 | }
171 | }
172 | ```
173 |
174 | ## Data sharing between the main app and the Capture Extension.
175 |
176 | The challenging aspect of implementing the `LockedCameraCapture` extension is its distinction from other iOS extensions, such as the widget extension, and other extensions in general. Unlike these extensions, which can share `UserDefaults` and files through the `AppGroup` mechanism, the `LockedCameraCapture` extension lacks this capability due to the privacy concern.
177 |
178 | For Capture Extension, the only way to share your configurations is via the `CameraCaptureIntent.appContext` API or the `LockedCameraCaptureManager` to access the directories containing captured content.
179 |
180 | ### Share your configurations
181 |
182 | Consider the code below:
183 |
184 | ```swift
185 | struct SwitchCameraButton: View {
186 | @AppStorage("UseFrontCamera")
187 | private var useFrontCamera: Bool = false
188 |
189 | var body: some View {
190 | Button {
191 | useFrontCamera.toggle()
192 | } label: {
193 | // label
194 | }
195 | }
196 | }
197 | ```
198 |
199 | You might have relied on SwiftUI’s `AppStorage` property wrapper and `UserDefaults` to store the configurations of your UI, such as the camera position, user-selected zooming scale, or the ProRAW switch. However, since the Capture Extension can’t access or modify the shared `UserDefaults`, the state wrapped with `AppStorage` won’t function as intended.
200 |
201 | If your app’s minimum target version is iOS 18, you can fully utilize the `CameraCaptureIntent.appContext` to store configurations that should be shared across the main app and the Capture Extension. However, there’s a size limitation of 4 KB for `appContext`. For better compatibility, you still need to store the configurations in both `UserDefaults` and `CameraCaptureIntent.appContext`.
202 |
203 | - When the value of your switch changes, it must be written to both `UserDefaults` and `CameraCaptureIntent.appContext` for the main app. However, it should avoid writing to `UserDefaults` in the Capture Extension.
204 | - You can't use the `AppStorage` property wrapper to let SwiftUI automatically update your view. You should declare the configurations in your view model, and when the values are updated, invoke `objectWillChange.send()` to trigger UI update.
205 | - The initial value of your configuration should be read in the order of `CameraCaptureIntent.appContext`, `UserDefaults`, and a fallback value.
206 |
207 | Here’s an example of code to read configurations from the `appContext` and `UserDefaults`.
208 |
209 | ```swift
210 | @MainActor
211 | func updateFromAppContext() async {
212 | #if !CAPTURE_EXTENSION
213 | // If it's in the main app, first read the value from the UserDefaults.
214 | self.cameraPosition = AppUserDefaultSettings.shared.cameraPosition
215 | #endif
216 |
217 | if #available(iOS 18, *) {
218 | do {
219 | // If `AppCaptureIntent.appContext` exists, then read from it.
220 | if let appContext = try await AppCaptureIntent.appContext {
221 | self.cameraPosition = appContext.cameraPosition
222 | }
223 | } catch {
224 | print("error on getting app context")
225 | }
226 | }
227 | }
228 | ```
229 |
230 | ### Share files
231 |
232 | Technically, the main app and the Capture Extension can't share files since what `LockedCameraCaptureManager` has provided is a one-way method: You use `LockedCameraCaptureManager` in your main app to listen for the updates of session contents, and you can then invalidate the URLs after processing them properly.
233 |
234 | ```swift
235 | for await update in LockedCameraCaptureManager.shared.sessionContentUpdates {
236 | switch update {
237 | case .initial(let urls):
238 | // Process captured content from existing session content directories.
239 | break
240 | case .added(let url):
241 | // Process captured content from a new session content directory.
242 | break
243 | case .removed(let url):
244 | // Process captured content from a removed session content directory.
245 | break
246 | default:
247 | // An unknown sessionContentUpdate was received.
248 | break
249 | }
250 | }
251 | ```
252 |
253 | > For some unknown reasons, no updates will be emited when I test this API in Xcode 16 Beta 6 with iOS 18 Beta 7. I have fired the issue to Apple via the Feedback app.
254 |
255 | For the files created in your main app, even those under the `AppGroup`, the Capture Extension doesn’t have access to them. This includes the SQLite files used by SwiftData or Core Data. If you’re using SwiftData or Core Data to store user data, you’ll need to provide an in-memory database and disable any features that require writing to the database.
256 |
257 | For my PhotonCam app, I store users’ customized filters, aspect ratios, and frames using Core Data. While in the Capture Extension, I’ll provide default presets of these features for users to use.
258 |
259 | ## Store temporary files in the Capture Extension
260 |
261 | You could have used those methods below to obtain the root directory and then append the path and names to create a file URL for storing the file.
262 |
263 | ```swift
264 | func getRootURL() -> URL? {
265 | try? FileManager.default.url(
266 | for: .documentDirectory,
267 | in: .userDomainMask,
268 | appropriateFor: nil,
269 | create: true
270 | )
271 |
272 | // Or URL.documentsDirectory
273 | }
274 |
275 | func getSharedRootURL() -> URL? {
276 | try? FileManager.default.containerURL(
277 | forSecurityApplicationGroupIdentifier: "com.juniperphoton.appgroup"
278 | )
279 | }
280 | ```
281 |
282 | In the Capture Extension, the URL we got here is not accessible. Actually, the `LockedCameraCaptureSession` provided in the `LockedCameraCaptureUIScene` has a `sessionContentURL` property for us to use as the root URL.
283 |
284 | Here is a machanism to help you setup the correct rootURL in the entry point of your app, and read it throughout the rest of your app.
285 |
286 | You can declare a `AppStorageConfigProvider` that provides the rootURL and a `standard` shared instance for the main app to use.
287 |
288 | ```swift
289 | struct AppStorageConfigProvider {
290 | let rootURL: URL?
291 | }
292 |
293 | extension AppStorageConfigProvider {
294 | static let standard = AppStorageConfigProvider(
295 | rootURL: getStandardRootURL()
296 | )
297 | }
298 | ```
299 |
300 | To help creating an instance of `AppStorageConfigProvider` from `LockedCameraCaptureSession`, you can have another initializer that takes `LockedCameraCaptureSession`:
301 |
302 | ```swift
303 | @available(iOS 18, *)
304 | extension AppStorageConfigProvider {
305 | init(_ session: LockedCameraCaptureSession) {
306 | self.rootURL = session.sessionContentURL
307 | }
308 | }
309 | ```
310 |
311 | In your main app's entry:
312 |
313 | ```swift
314 | @main
315 | struct LockedCameraCaptureExtensionDemoApp: App {
316 | var body: some Scene {
317 | WindowGroup {
318 | ContentView(configProvider: AppStorageConfigProvider.standard)
319 | }
320 | }
321 | }
322 | ```
323 |
324 | In your Capture Extension:
325 |
326 | ```swift
327 | @main
328 | struct LockedExtension: LockedCameraCaptureExtension {
329 | var body: some LockedCameraCaptureExtensionScene {
330 | LockedCameraCaptureUIScene { session in
331 | LockedCameraCaptureView(session: session)
332 | }
333 | }
334 | }
335 |
336 | struct LockedCameraCaptureView: View {
337 | let session: LockedCameraCaptureSession
338 |
339 | var body: some View {
340 | ContentView(configProvider: AppStorageConfigProvider(session))
341 | }
342 | }
343 | ```
344 |
345 | Well, there are numerous other ways to inject the `AppStorageConfigProvider` into the rest of your application, such as using SwiftUI’s `EnvironmentObject`, `EnvironmentValues`, or other Dependencies Injection frameworks. Dependencies management is crucial when building a robust app, and it won’t be discussed further in this section.
346 |
347 | ## Lock device orientation
348 |
349 | For the main target, there is a settings in Xcode to let you select the supported device orientations. For most camera app, you typically handle the orientation by rotating some UI elements like buttons or labels, instead of rotating the whole UI.
350 |
351 | For the Capture Extension, there is no such UI for you to configure. However, you can add the `UISupportedInterfaceOrientations` to the `Info.plist` in the Capture Extension.
352 |
353 | 
354 |
355 | ## Custom symbol image for ControlWidget
356 |
357 | According to the [documentation](https://developer.apple.com/documentation/swiftui/controlwidget), only some particular views can be correctly rendered in a ControlWidget.
358 |
359 | > Controls are defined using templates in order to ensure that they control will work at all sizes and in all system spaces in which they might be displayed. These templates define images (specifically, symbol images) and text using simple SwiftUI views like [`Label`](https://developer.apple.com/documentation/swiftui/label), [`Text`](https://developer.apple.com/documentation/swiftui/text), and [`Image`](https://developer.apple.com/documentation/swiftui/image); and tint colors using the [`tint(_:)`](https://developer.apple.com/documentation/swiftui/controlwidgettemplate/tint(_:)) modifier.
360 |
361 | Particularly, regarding to images, only symbol images can be rendered.
362 |
363 | If the current SFSymbol images are good enough for you, then you can use those images using Image(systemName:) or `Label(:systemImage:)` inside the ControlWidget. However if you want to use your custom images, simple rasterized images won’t work in this case.
364 |
365 | To work with your customized images, you have to make it a symbol. Thera are a few guides that help you create your own custom symbol image:
366 |
367 | https://developer.apple.com/documentation/uikit/uiimage/creating_custom_symbol_images_for_your_app
368 |
369 | https://developer.apple.com/wwdc21/10250
370 |
371 | ## Be aware of the bundle size
372 |
373 | Building and including all source codes and assets for all targets will increase the app bundle size. Here are some techniques that can help analyze and reduce the bundle size.
374 |
375 | https://juniperphoton.substack.com/p/reducing-bundle-size-my-approach?r=1ss9aj
376 |
377 | ## Additional Information
378 |
379 | ### CameraCaptureIntent implementation should be built against the app target
380 |
381 | The implementation of ``CameraCaptureIntent`` should always be built against the App and the extension targets; otherwise, it won't be discovered as expected.
382 |
383 | If you own your framework, it possible that you put this implementation in the framework and set the target membership to be included in the App and the extension targets, even if the source code is in the framework itself.
384 |
385 | > Note: it looks like there is a specific API [AppIntentsPackage](https://developer.apple.com/documentation/appintents/appintentspackage) that should solve this discovery issue. But it won't work.
386 |
387 | There is a post about this issue:
388 | [AppIntentsPackage protocol with SPM package not working](https://forums.developer.apple.com/forums/thread/732535).
389 |
--------------------------------------------------------------------------------
/LockedCameraCaptureExtensionDemo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 77;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 2206C5302CE31EF600744B0F /* MetalLib in Frameworks */ = {isa = PBXBuildFile; productRef = 2206C52F2CE31EF600744B0F /* MetalLib */; };
11 | 2206C5322CE31EFC00744B0F /* MetalLib in Frameworks */ = {isa = PBXBuildFile; productRef = 2206C5312CE31EFC00744B0F /* MetalLib */; };
12 | 22722BB92CE3258E0045DB78 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 22722BB82CE3258E0045DB78 /* Models */; };
13 | 22722BBD2CE325980045DB78 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 22722BBC2CE325980045DB78 /* Models */; };
14 | 22722BC32CE325FD0045DB78 /* MetalLib in Frameworks */ = {isa = PBXBuildFile; productRef = 22722BC22CE325FD0045DB78 /* MetalLib */; };
15 | 22722BC62CE326050045DB78 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 22722BC52CE326050045DB78 /* Models */; };
16 | 22722D542CE3314A0045DB78 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 22722D532CE3314A0045DB78 /* Models */; };
17 | 22DE8DFA2C74B3CA00FC6EEA /* LockedExtension.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 22DE8DF12C74B3CA00FC6EEA /* LockedExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
18 | 22DE8E072C74B3E500FC6EEA /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22DE8E062C74B3E500FC6EEA /* WidgetKit.framework */; };
19 | 22DE8E092C74B3E500FC6EEA /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22DE8E082C74B3E500FC6EEA /* SwiftUI.framework */; };
20 | 22DE8E142C74B3E600FC6EEA /* LockedWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 22DE8E042C74B3E500FC6EEA /* LockedWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
21 | 22DE8E462C74CC7C00FC6EEA /* LockedCameraCapture.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22DE8E452C74CC7C00FC6EEA /* LockedCameraCapture.framework */; };
22 | /* End PBXBuildFile section */
23 |
24 | /* Begin PBXContainerItemProxy section */
25 | 22DE8DF82C74B3CA00FC6EEA /* PBXContainerItemProxy */ = {
26 | isa = PBXContainerItemProxy;
27 | containerPortal = 22DE8DD32C74B36800FC6EEA /* Project object */;
28 | proxyType = 1;
29 | remoteGlobalIDString = 22DE8DF02C74B3CA00FC6EEA;
30 | remoteInfo = LockedExtension;
31 | };
32 | 22DE8E122C74B3E600FC6EEA /* PBXContainerItemProxy */ = {
33 | isa = PBXContainerItemProxy;
34 | containerPortal = 22DE8DD32C74B36800FC6EEA /* Project object */;
35 | proxyType = 1;
36 | remoteGlobalIDString = 22DE8E032C74B3E500FC6EEA;
37 | remoteInfo = LockedWidgetExtension;
38 | };
39 | /* End PBXContainerItemProxy section */
40 |
41 | /* Begin PBXCopyFilesBuildPhase section */
42 | 22722D4B2CE32FDA0045DB78 /* Embed Frameworks */ = {
43 | isa = PBXCopyFilesBuildPhase;
44 | buildActionMask = 2147483647;
45 | dstPath = "";
46 | dstSubfolderSpec = 10;
47 | files = (
48 | );
49 | name = "Embed Frameworks";
50 | runOnlyForDeploymentPostprocessing = 0;
51 | };
52 | 22DE8DFF2C74B3CA00FC6EEA /* Embed ExtensionKit Extensions */ = {
53 | isa = PBXCopyFilesBuildPhase;
54 | buildActionMask = 2147483647;
55 | dstPath = "$(EXTENSIONS_FOLDER_PATH)";
56 | dstSubfolderSpec = 16;
57 | files = (
58 | 22DE8DFA2C74B3CA00FC6EEA /* LockedExtension.appex in Embed ExtensionKit Extensions */,
59 | );
60 | name = "Embed ExtensionKit Extensions";
61 | runOnlyForDeploymentPostprocessing = 0;
62 | };
63 | 22DE8E192C74B3E600FC6EEA /* Embed Foundation Extensions */ = {
64 | isa = PBXCopyFilesBuildPhase;
65 | buildActionMask = 2147483647;
66 | dstPath = "";
67 | dstSubfolderSpec = 13;
68 | files = (
69 | 22DE8E142C74B3E600FC6EEA /* LockedWidgetExtension.appex in Embed Foundation Extensions */,
70 | );
71 | name = "Embed Foundation Extensions";
72 | runOnlyForDeploymentPostprocessing = 0;
73 | };
74 | /* End PBXCopyFilesBuildPhase section */
75 |
76 | /* Begin PBXFileReference section */
77 | 2206C52E2CE31E9100744B0F /* MetalLib */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MetalLib; path = Packages/MetalLib; sourceTree = ""; };
78 | 22722BB02CE324C80045DB78 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = ""; };
79 | 22722CE42CE32F0F0045DB78 /* IntentsLib.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = IntentsLib.xcodeproj; sourceTree = ""; };
80 | 22DE8DDB2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LockedCameraCaptureExtensionDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
81 | 22DE8DF12C74B3CA00FC6EEA /* LockedExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockedExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
82 | 22DE8E042C74B3E500FC6EEA /* LockedWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockedWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
83 | 22DE8E062C74B3E500FC6EEA /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
84 | 22DE8E082C74B3E500FC6EEA /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
85 | 22DE8E452C74CC7C00FC6EEA /* LockedCameraCapture.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LockedCameraCapture.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/LockedCameraCapture.framework; sourceTree = DEVELOPER_DIR; };
86 | /* End PBXFileReference section */
87 |
88 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
89 | 22722D522CE331180045DB78 /* Exceptions for "LockedCameraCaptureExtensionDemo" folder in "LockedWidgetExtension" target */ = {
90 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
91 | membershipExceptions = (
92 | Intent/AppCaptureIntent.swift,
93 | );
94 | target = 22DE8E032C74B3E500FC6EEA /* LockedWidgetExtension */;
95 | };
96 | 22DE8DFB2C74B3CA00FC6EEA /* Exceptions for "LockedExtension" folder in "LockedExtension" target */ = {
97 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
98 | membershipExceptions = (
99 | Info.plist,
100 | );
101 | target = 22DE8DF02C74B3CA00FC6EEA /* LockedExtension */;
102 | };
103 | 22DE8E152C74B3E600FC6EEA /* Exceptions for "LockedWidget" folder in "LockedWidgetExtension" target */ = {
104 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
105 | membershipExceptions = (
106 | Info.plist,
107 | );
108 | target = 22DE8E032C74B3E500FC6EEA /* LockedWidgetExtension */;
109 | };
110 | 22DE8E2E2C74B5FF00FC6EEA /* Exceptions for "LockedCameraCaptureExtensionDemo" folder in "LockedExtension" target */ = {
111 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
112 | membershipExceptions = (
113 | Camera/AppStorageConfigProvider.swift,
114 | Camera/OpenMainAppAction.swift,
115 | Camera/View/CaptureInteractionView.swift,
116 | Camera/View/ContentView.swift,
117 | Camera/ViewModel/CamPreviewViewModel.swift,
118 | Camera/ViewModel/CaptureProcessor.swift,
119 | Camera/ViewModel/MainViewModel.swift,
120 | Intent/AppCaptureIntent.swift,
121 | Localizable.xcstrings,
122 | );
123 | target = 22DE8DF02C74B3CA00FC6EEA /* LockedExtension */;
124 | };
125 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
126 |
127 | /* Begin PBXFileSystemSynchronizedRootGroup section */
128 | 22DE8DDD2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo */ = {
129 | isa = PBXFileSystemSynchronizedRootGroup;
130 | exceptions = (
131 | 22DE8E2E2C74B5FF00FC6EEA /* Exceptions for "LockedCameraCaptureExtensionDemo" folder in "LockedExtension" target */,
132 | 22722D522CE331180045DB78 /* Exceptions for "LockedCameraCaptureExtensionDemo" folder in "LockedWidgetExtension" target */,
133 | );
134 | path = LockedCameraCaptureExtensionDemo;
135 | sourceTree = "";
136 | };
137 | 22DE8DF22C74B3CA00FC6EEA /* LockedExtension */ = {
138 | isa = PBXFileSystemSynchronizedRootGroup;
139 | exceptions = (
140 | 22DE8DFB2C74B3CA00FC6EEA /* Exceptions for "LockedExtension" folder in "LockedExtension" target */,
141 | );
142 | path = LockedExtension;
143 | sourceTree = "";
144 | };
145 | 22DE8E0A2C74B3E500FC6EEA /* LockedWidget */ = {
146 | isa = PBXFileSystemSynchronizedRootGroup;
147 | exceptions = (
148 | 22DE8E152C74B3E600FC6EEA /* Exceptions for "LockedWidget" folder in "LockedWidgetExtension" target */,
149 | );
150 | path = LockedWidget;
151 | sourceTree = "";
152 | };
153 | /* End PBXFileSystemSynchronizedRootGroup section */
154 |
155 | /* Begin PBXFrameworksBuildPhase section */
156 | 22DE8DD82C74B36800FC6EEA /* Frameworks */ = {
157 | isa = PBXFrameworksBuildPhase;
158 | buildActionMask = 2147483647;
159 | files = (
160 | 22722BC62CE326050045DB78 /* Models in Frameworks */,
161 | 22722BC32CE325FD0045DB78 /* MetalLib in Frameworks */,
162 | 22722BB92CE3258E0045DB78 /* Models in Frameworks */,
163 | 2206C5302CE31EF600744B0F /* MetalLib in Frameworks */,
164 | );
165 | runOnlyForDeploymentPostprocessing = 0;
166 | };
167 | 22DE8DEE2C74B3CA00FC6EEA /* Frameworks */ = {
168 | isa = PBXFrameworksBuildPhase;
169 | buildActionMask = 2147483647;
170 | files = (
171 | 22722BBD2CE325980045DB78 /* Models in Frameworks */,
172 | 2206C5322CE31EFC00744B0F /* MetalLib in Frameworks */,
173 | );
174 | runOnlyForDeploymentPostprocessing = 0;
175 | };
176 | 22DE8E012C74B3E500FC6EEA /* Frameworks */ = {
177 | isa = PBXFrameworksBuildPhase;
178 | buildActionMask = 2147483647;
179 | files = (
180 | 22DE8E092C74B3E500FC6EEA /* SwiftUI.framework in Frameworks */,
181 | 22DE8E072C74B3E500FC6EEA /* WidgetKit.framework in Frameworks */,
182 | 22722D542CE3314A0045DB78 /* Models in Frameworks */,
183 | 22DE8E462C74CC7C00FC6EEA /* LockedCameraCapture.framework in Frameworks */,
184 | );
185 | runOnlyForDeploymentPostprocessing = 0;
186 | };
187 | /* End PBXFrameworksBuildPhase section */
188 |
189 | /* Begin PBXGroup section */
190 | 22722CE62CE32F0F0045DB78 /* Products */ = {
191 | isa = PBXGroup;
192 | children = (
193 | );
194 | name = Products;
195 | sourceTree = "";
196 | };
197 | 22DE8DD22C74B36800FC6EEA = {
198 | isa = PBXGroup;
199 | children = (
200 | 22722BB02CE324C80045DB78 /* Models */,
201 | 2206C52E2CE31E9100744B0F /* MetalLib */,
202 | 22DE8DDD2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo */,
203 | 22DE8DF22C74B3CA00FC6EEA /* LockedExtension */,
204 | 22DE8E0A2C74B3E500FC6EEA /* LockedWidget */,
205 | 22DE8E052C74B3E500FC6EEA /* Frameworks */,
206 | 22DE8DDC2C74B36800FC6EEA /* Products */,
207 | );
208 | sourceTree = "";
209 | };
210 | 22DE8DDC2C74B36800FC6EEA /* Products */ = {
211 | isa = PBXGroup;
212 | children = (
213 | 22DE8DDB2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo.app */,
214 | 22DE8DF12C74B3CA00FC6EEA /* LockedExtension.appex */,
215 | 22DE8E042C74B3E500FC6EEA /* LockedWidgetExtension.appex */,
216 | );
217 | name = Products;
218 | sourceTree = "";
219 | };
220 | 22DE8E052C74B3E500FC6EEA /* Frameworks */ = {
221 | isa = PBXGroup;
222 | children = (
223 | 22DE8E452C74CC7C00FC6EEA /* LockedCameraCapture.framework */,
224 | 22DE8E062C74B3E500FC6EEA /* WidgetKit.framework */,
225 | 22DE8E082C74B3E500FC6EEA /* SwiftUI.framework */,
226 | );
227 | name = Frameworks;
228 | sourceTree = "";
229 | };
230 | /* End PBXGroup section */
231 |
232 | /* Begin PBXNativeTarget section */
233 | 22DE8DDA2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo */ = {
234 | isa = PBXNativeTarget;
235 | buildConfigurationList = 22DE8DEA2C74B36900FC6EEA /* Build configuration list for PBXNativeTarget "LockedCameraCaptureExtensionDemo" */;
236 | buildPhases = (
237 | 22DE8DD72C74B36800FC6EEA /* Sources */,
238 | 22DE8DD82C74B36800FC6EEA /* Frameworks */,
239 | 22DE8DD92C74B36800FC6EEA /* Resources */,
240 | 22DE8DFF2C74B3CA00FC6EEA /* Embed ExtensionKit Extensions */,
241 | 22DE8E192C74B3E600FC6EEA /* Embed Foundation Extensions */,
242 | 22722D4B2CE32FDA0045DB78 /* Embed Frameworks */,
243 | );
244 | buildRules = (
245 | );
246 | dependencies = (
247 | 22DE8DF92C74B3CA00FC6EEA /* PBXTargetDependency */,
248 | 22DE8E132C74B3E600FC6EEA /* PBXTargetDependency */,
249 | );
250 | fileSystemSynchronizedGroups = (
251 | 22DE8DDD2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo */,
252 | );
253 | name = LockedCameraCaptureExtensionDemo;
254 | packageProductDependencies = (
255 | 2206C52F2CE31EF600744B0F /* MetalLib */,
256 | 22722BB82CE3258E0045DB78 /* Models */,
257 | 22722BC22CE325FD0045DB78 /* MetalLib */,
258 | 22722BC52CE326050045DB78 /* Models */,
259 | );
260 | productName = LockedCameraCaptureExtensionDemo;
261 | productReference = 22DE8DDB2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo.app */;
262 | productType = "com.apple.product-type.application";
263 | };
264 | 22DE8DF02C74B3CA00FC6EEA /* LockedExtension */ = {
265 | isa = PBXNativeTarget;
266 | buildConfigurationList = 22DE8DFC2C74B3CA00FC6EEA /* Build configuration list for PBXNativeTarget "LockedExtension" */;
267 | buildPhases = (
268 | 22DE8DED2C74B3CA00FC6EEA /* Sources */,
269 | 22DE8DEE2C74B3CA00FC6EEA /* Frameworks */,
270 | 22DE8DEF2C74B3CA00FC6EEA /* Resources */,
271 | );
272 | buildRules = (
273 | );
274 | dependencies = (
275 | );
276 | fileSystemSynchronizedGroups = (
277 | 22DE8DF22C74B3CA00FC6EEA /* LockedExtension */,
278 | );
279 | name = LockedExtension;
280 | packageProductDependencies = (
281 | 2206C5312CE31EFC00744B0F /* MetalLib */,
282 | 22722BBC2CE325980045DB78 /* Models */,
283 | );
284 | productName = LockedExtension;
285 | productReference = 22DE8DF12C74B3CA00FC6EEA /* LockedExtension.appex */;
286 | productType = "com.apple.product-type.extensionkit-extension";
287 | };
288 | 22DE8E032C74B3E500FC6EEA /* LockedWidgetExtension */ = {
289 | isa = PBXNativeTarget;
290 | buildConfigurationList = 22DE8E162C74B3E600FC6EEA /* Build configuration list for PBXNativeTarget "LockedWidgetExtension" */;
291 | buildPhases = (
292 | 22DE8E002C74B3E500FC6EEA /* Sources */,
293 | 22DE8E012C74B3E500FC6EEA /* Frameworks */,
294 | 22DE8E022C74B3E500FC6EEA /* Resources */,
295 | );
296 | buildRules = (
297 | );
298 | dependencies = (
299 | );
300 | fileSystemSynchronizedGroups = (
301 | 22DE8E0A2C74B3E500FC6EEA /* LockedWidget */,
302 | );
303 | name = LockedWidgetExtension;
304 | packageProductDependencies = (
305 | 22722D532CE3314A0045DB78 /* Models */,
306 | );
307 | productName = LockedWidgetExtension;
308 | productReference = 22DE8E042C74B3E500FC6EEA /* LockedWidgetExtension.appex */;
309 | productType = "com.apple.product-type.app-extension";
310 | };
311 | /* End PBXNativeTarget section */
312 |
313 | /* Begin PBXProject section */
314 | 22DE8DD32C74B36800FC6EEA /* Project object */ = {
315 | isa = PBXProject;
316 | attributes = {
317 | BuildIndependentTargetsInParallel = 1;
318 | LastSwiftUpdateCheck = 1600;
319 | LastUpgradeCheck = 1600;
320 | TargetAttributes = {
321 | 22DE8DDA2C74B36800FC6EEA = {
322 | CreatedOnToolsVersion = 16.0;
323 | };
324 | 22DE8DF02C74B3CA00FC6EEA = {
325 | CreatedOnToolsVersion = 16.0;
326 | };
327 | 22DE8E032C74B3E500FC6EEA = {
328 | CreatedOnToolsVersion = 16.0;
329 | };
330 | };
331 | };
332 | buildConfigurationList = 22DE8DD62C74B36800FC6EEA /* Build configuration list for PBXProject "LockedCameraCaptureExtensionDemo" */;
333 | developmentRegion = en;
334 | hasScannedForEncodings = 0;
335 | knownRegions = (
336 | en,
337 | Base,
338 | "zh-Hans",
339 | "zh-Hant",
340 | );
341 | mainGroup = 22DE8DD22C74B36800FC6EEA;
342 | minimizedProjectReferenceProxies = 1;
343 | packageReferences = (
344 | 22722BC12CE325FD0045DB78 /* XCLocalSwiftPackageReference "Packages/MetalLib" */,
345 | 22722BC42CE326050045DB78 /* XCLocalSwiftPackageReference "Packages/Models" */,
346 | );
347 | preferredProjectObjectVersion = 77;
348 | productRefGroup = 22DE8DDC2C74B36800FC6EEA /* Products */;
349 | projectDirPath = "";
350 | projectReferences = (
351 | {
352 | ProductGroup = 22722CE62CE32F0F0045DB78 /* Products */;
353 | ProjectRef = 22722CE42CE32F0F0045DB78 /* IntentsLib.xcodeproj */;
354 | },
355 | );
356 | projectRoot = "";
357 | targets = (
358 | 22DE8DDA2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo */,
359 | 22DE8DF02C74B3CA00FC6EEA /* LockedExtension */,
360 | 22DE8E032C74B3E500FC6EEA /* LockedWidgetExtension */,
361 | );
362 | };
363 | /* End PBXProject section */
364 |
365 | /* Begin PBXResourcesBuildPhase section */
366 | 22DE8DD92C74B36800FC6EEA /* Resources */ = {
367 | isa = PBXResourcesBuildPhase;
368 | buildActionMask = 2147483647;
369 | files = (
370 | );
371 | runOnlyForDeploymentPostprocessing = 0;
372 | };
373 | 22DE8DEF2C74B3CA00FC6EEA /* Resources */ = {
374 | isa = PBXResourcesBuildPhase;
375 | buildActionMask = 2147483647;
376 | files = (
377 | );
378 | runOnlyForDeploymentPostprocessing = 0;
379 | };
380 | 22DE8E022C74B3E500FC6EEA /* Resources */ = {
381 | isa = PBXResourcesBuildPhase;
382 | buildActionMask = 2147483647;
383 | files = (
384 | );
385 | runOnlyForDeploymentPostprocessing = 0;
386 | };
387 | /* End PBXResourcesBuildPhase section */
388 |
389 | /* Begin PBXSourcesBuildPhase section */
390 | 22DE8DD72C74B36800FC6EEA /* Sources */ = {
391 | isa = PBXSourcesBuildPhase;
392 | buildActionMask = 2147483647;
393 | files = (
394 | );
395 | runOnlyForDeploymentPostprocessing = 0;
396 | };
397 | 22DE8DED2C74B3CA00FC6EEA /* Sources */ = {
398 | isa = PBXSourcesBuildPhase;
399 | buildActionMask = 2147483647;
400 | files = (
401 | );
402 | runOnlyForDeploymentPostprocessing = 0;
403 | };
404 | 22DE8E002C74B3E500FC6EEA /* Sources */ = {
405 | isa = PBXSourcesBuildPhase;
406 | buildActionMask = 2147483647;
407 | files = (
408 | );
409 | runOnlyForDeploymentPostprocessing = 0;
410 | };
411 | /* End PBXSourcesBuildPhase section */
412 |
413 | /* Begin PBXTargetDependency section */
414 | 22DE8DF92C74B3CA00FC6EEA /* PBXTargetDependency */ = {
415 | isa = PBXTargetDependency;
416 | target = 22DE8DF02C74B3CA00FC6EEA /* LockedExtension */;
417 | targetProxy = 22DE8DF82C74B3CA00FC6EEA /* PBXContainerItemProxy */;
418 | };
419 | 22DE8E132C74B3E600FC6EEA /* PBXTargetDependency */ = {
420 | isa = PBXTargetDependency;
421 | target = 22DE8E032C74B3E500FC6EEA /* LockedWidgetExtension */;
422 | targetProxy = 22DE8E122C74B3E600FC6EEA /* PBXContainerItemProxy */;
423 | };
424 | /* End PBXTargetDependency section */
425 |
426 | /* Begin XCBuildConfiguration section */
427 | 22DE8DE82C74B36900FC6EEA /* Debug */ = {
428 | isa = XCBuildConfiguration;
429 | buildSettings = {
430 | ALWAYS_SEARCH_USER_PATHS = NO;
431 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
432 | CLANG_ANALYZER_NONNULL = YES;
433 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
434 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
435 | CLANG_ENABLE_MODULES = YES;
436 | CLANG_ENABLE_OBJC_ARC = YES;
437 | CLANG_ENABLE_OBJC_WEAK = YES;
438 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
439 | CLANG_WARN_BOOL_CONVERSION = YES;
440 | CLANG_WARN_COMMA = YES;
441 | CLANG_WARN_CONSTANT_CONVERSION = YES;
442 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
443 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
444 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
445 | CLANG_WARN_EMPTY_BODY = YES;
446 | CLANG_WARN_ENUM_CONVERSION = YES;
447 | CLANG_WARN_INFINITE_RECURSION = YES;
448 | CLANG_WARN_INT_CONVERSION = YES;
449 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
450 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
451 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
452 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
453 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
454 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
455 | CLANG_WARN_STRICT_PROTOTYPES = YES;
456 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
457 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
458 | CLANG_WARN_UNREACHABLE_CODE = YES;
459 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
460 | COPY_PHASE_STRIP = NO;
461 | DEBUG_INFORMATION_FORMAT = dwarf;
462 | ENABLE_STRICT_OBJC_MSGSEND = YES;
463 | ENABLE_TESTABILITY = YES;
464 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
465 | GCC_C_LANGUAGE_STANDARD = gnu17;
466 | GCC_DYNAMIC_NO_PIC = NO;
467 | GCC_NO_COMMON_BLOCKS = YES;
468 | GCC_OPTIMIZATION_LEVEL = 0;
469 | GCC_PREPROCESSOR_DEFINITIONS = (
470 | "DEBUG=1",
471 | "$(inherited)",
472 | );
473 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
474 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
475 | GCC_WARN_UNDECLARED_SELECTOR = YES;
476 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
477 | GCC_WARN_UNUSED_FUNCTION = YES;
478 | GCC_WARN_UNUSED_VARIABLE = YES;
479 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
480 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
481 | MTL_FAST_MATH = YES;
482 | ONLY_ACTIVE_ARCH = YES;
483 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
484 | SWIFT_EMIT_LOC_STRINGS = YES;
485 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
486 | };
487 | name = Debug;
488 | };
489 | 22DE8DE92C74B36900FC6EEA /* Release */ = {
490 | isa = XCBuildConfiguration;
491 | buildSettings = {
492 | ALWAYS_SEARCH_USER_PATHS = NO;
493 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
494 | CLANG_ANALYZER_NONNULL = YES;
495 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
496 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
497 | CLANG_ENABLE_MODULES = YES;
498 | CLANG_ENABLE_OBJC_ARC = YES;
499 | CLANG_ENABLE_OBJC_WEAK = YES;
500 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
501 | CLANG_WARN_BOOL_CONVERSION = YES;
502 | CLANG_WARN_COMMA = YES;
503 | CLANG_WARN_CONSTANT_CONVERSION = YES;
504 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
505 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
506 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
507 | CLANG_WARN_EMPTY_BODY = YES;
508 | CLANG_WARN_ENUM_CONVERSION = YES;
509 | CLANG_WARN_INFINITE_RECURSION = YES;
510 | CLANG_WARN_INT_CONVERSION = YES;
511 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
512 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
513 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
514 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
515 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
516 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
517 | CLANG_WARN_STRICT_PROTOTYPES = YES;
518 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
519 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
520 | CLANG_WARN_UNREACHABLE_CODE = YES;
521 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
522 | COPY_PHASE_STRIP = NO;
523 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
524 | ENABLE_NS_ASSERTIONS = NO;
525 | ENABLE_STRICT_OBJC_MSGSEND = YES;
526 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
527 | GCC_C_LANGUAGE_STANDARD = gnu17;
528 | GCC_NO_COMMON_BLOCKS = YES;
529 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
530 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
531 | GCC_WARN_UNDECLARED_SELECTOR = YES;
532 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
533 | GCC_WARN_UNUSED_FUNCTION = YES;
534 | GCC_WARN_UNUSED_VARIABLE = YES;
535 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
536 | MTL_ENABLE_DEBUG_INFO = NO;
537 | MTL_FAST_MATH = YES;
538 | SWIFT_COMPILATION_MODE = wholemodule;
539 | SWIFT_EMIT_LOC_STRINGS = YES;
540 | };
541 | name = Release;
542 | };
543 | 22DE8DEB2C74B36900FC6EEA /* Debug */ = {
544 | isa = XCBuildConfiguration;
545 | buildSettings = {
546 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
547 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
548 | CODE_SIGN_ENTITLEMENTS = LockedCameraCaptureExtensionDemo/LockedCameraCaptureExtensionDemo.entitlements;
549 | CODE_SIGN_STYLE = Automatic;
550 | CURRENT_PROJECT_VERSION = 1;
551 | DEVELOPMENT_ASSET_PATHS = "\"LockedCameraCaptureExtensionDemo/Preview Content\"";
552 | DEVELOPMENT_TEAM = 7GB9CHCB87;
553 | ENABLE_HARDENED_RUNTIME = YES;
554 | ENABLE_PREVIEWS = YES;
555 | GENERATE_INFOPLIST_FILE = YES;
556 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
557 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos.";
558 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library.";
559 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
560 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
561 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
562 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
563 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
564 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
565 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
566 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
567 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
568 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
569 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
570 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
571 | MACOSX_DEPLOYMENT_TARGET = 14.6;
572 | MARKETING_VERSION = 1.0;
573 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo;
574 | PRODUCT_NAME = "$(TARGET_NAME)";
575 | SDKROOT = auto;
576 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
577 | SUPPORTS_MACCATALYST = NO;
578 | SWIFT_EMIT_LOC_STRINGS = YES;
579 | SWIFT_VERSION = 5.0;
580 | TARGETED_DEVICE_FAMILY = "1,2";
581 | XROS_DEPLOYMENT_TARGET = 2.0;
582 | };
583 | name = Debug;
584 | };
585 | 22DE8DEC2C74B36900FC6EEA /* Release */ = {
586 | isa = XCBuildConfiguration;
587 | buildSettings = {
588 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
589 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
590 | CODE_SIGN_ENTITLEMENTS = LockedCameraCaptureExtensionDemo/LockedCameraCaptureExtensionDemo.entitlements;
591 | CODE_SIGN_STYLE = Automatic;
592 | CURRENT_PROJECT_VERSION = 1;
593 | DEVELOPMENT_ASSET_PATHS = "\"LockedCameraCaptureExtensionDemo/Preview Content\"";
594 | DEVELOPMENT_TEAM = 7GB9CHCB87;
595 | ENABLE_HARDENED_RUNTIME = YES;
596 | ENABLE_PREVIEWS = YES;
597 | GENERATE_INFOPLIST_FILE = YES;
598 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
599 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos.";
600 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library.";
601 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
602 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
603 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
604 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
605 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
606 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
607 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
608 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
609 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
610 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
611 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
612 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
613 | MACOSX_DEPLOYMENT_TARGET = 14.6;
614 | MARKETING_VERSION = 1.0;
615 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo;
616 | PRODUCT_NAME = "$(TARGET_NAME)";
617 | SDKROOT = auto;
618 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
619 | SUPPORTS_MACCATALYST = NO;
620 | SWIFT_EMIT_LOC_STRINGS = YES;
621 | SWIFT_VERSION = 5.0;
622 | TARGETED_DEVICE_FAMILY = "1,2";
623 | XROS_DEPLOYMENT_TARGET = 2.0;
624 | };
625 | name = Release;
626 | };
627 | 22DE8DFD2C74B3CA00FC6EEA /* Debug */ = {
628 | isa = XCBuildConfiguration;
629 | buildSettings = {
630 | CODE_SIGN_STYLE = Automatic;
631 | CURRENT_PROJECT_VERSION = 1;
632 | DEVELOPMENT_TEAM = 7GB9CHCB87;
633 | GENERATE_INFOPLIST_FILE = YES;
634 | INFOPLIST_FILE = LockedExtension/Info.plist;
635 | INFOPLIST_KEY_CFBundleDisplayName = LockedExtension;
636 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos.";
637 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
638 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library.";
639 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
640 | IPHONEOS_DEPLOYMENT_TARGET = 18.0;
641 | LD_RUNPATH_SEARCH_PATHS = (
642 | "$(inherited)",
643 | "@executable_path/Frameworks",
644 | "@executable_path/../../Frameworks",
645 | );
646 | MARKETING_VERSION = 1.0;
647 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo.LockedExtension;
648 | PRODUCT_NAME = "$(TARGET_NAME)";
649 | SDKROOT = iphoneos;
650 | SKIP_INSTALL = YES;
651 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
652 | SUPPORTS_MACCATALYST = NO;
653 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
654 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
655 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited) CAPTURE_EXTENSION";
656 | SWIFT_EMIT_LOC_STRINGS = YES;
657 | SWIFT_VERSION = 5.0;
658 | TARGETED_DEVICE_FAMILY = "1,2";
659 | };
660 | name = Debug;
661 | };
662 | 22DE8DFE2C74B3CA00FC6EEA /* Release */ = {
663 | isa = XCBuildConfiguration;
664 | buildSettings = {
665 | CODE_SIGN_STYLE = Automatic;
666 | CURRENT_PROJECT_VERSION = 1;
667 | DEVELOPMENT_TEAM = 7GB9CHCB87;
668 | GENERATE_INFOPLIST_FILE = YES;
669 | INFOPLIST_FILE = LockedExtension/Info.plist;
670 | INFOPLIST_KEY_CFBundleDisplayName = LockedExtension;
671 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos.";
672 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
673 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library.";
674 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
675 | IPHONEOS_DEPLOYMENT_TARGET = 18.0;
676 | LD_RUNPATH_SEARCH_PATHS = (
677 | "$(inherited)",
678 | "@executable_path/Frameworks",
679 | "@executable_path/../../Frameworks",
680 | );
681 | MARKETING_VERSION = 1.0;
682 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo.LockedExtension;
683 | PRODUCT_NAME = "$(TARGET_NAME)";
684 | SDKROOT = iphoneos;
685 | SKIP_INSTALL = YES;
686 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
687 | SUPPORTS_MACCATALYST = NO;
688 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
689 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
690 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = CAPTURE_EXTENSION;
691 | SWIFT_EMIT_LOC_STRINGS = YES;
692 | SWIFT_VERSION = 5.0;
693 | TARGETED_DEVICE_FAMILY = "1,2";
694 | VALIDATE_PRODUCT = YES;
695 | };
696 | name = Release;
697 | };
698 | 22DE8E172C74B3E600FC6EEA /* Debug */ = {
699 | isa = XCBuildConfiguration;
700 | buildSettings = {
701 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
702 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
703 | CODE_SIGN_STYLE = Automatic;
704 | CURRENT_PROJECT_VERSION = 1;
705 | DEVELOPMENT_TEAM = 7GB9CHCB87;
706 | GENERATE_INFOPLIST_FILE = YES;
707 | INFOPLIST_FILE = LockedWidget/Info.plist;
708 | INFOPLIST_KEY_CFBundleDisplayName = LockedWidget;
709 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos.";
710 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
711 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library.";
712 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
713 | LD_RUNPATH_SEARCH_PATHS = (
714 | "$(inherited)",
715 | "@executable_path/Frameworks",
716 | "@executable_path/../../Frameworks",
717 | );
718 | MARKETING_VERSION = 1.0;
719 | OTHER_SWIFT_FLAGS = "";
720 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo.LockedWidget;
721 | PRODUCT_NAME = "$(TARGET_NAME)";
722 | SDKROOT = iphoneos;
723 | SKIP_INSTALL = YES;
724 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
725 | SUPPORTS_MACCATALYST = NO;
726 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
727 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
728 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
729 | SWIFT_EMIT_LOC_STRINGS = YES;
730 | SWIFT_VERSION = 5.0;
731 | TARGETED_DEVICE_FAMILY = "1,2";
732 | };
733 | name = Debug;
734 | };
735 | 22DE8E182C74B3E600FC6EEA /* Release */ = {
736 | isa = XCBuildConfiguration;
737 | buildSettings = {
738 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
739 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
740 | CODE_SIGN_STYLE = Automatic;
741 | CURRENT_PROJECT_VERSION = 1;
742 | DEVELOPMENT_TEAM = 7GB9CHCB87;
743 | GENERATE_INFOPLIST_FILE = YES;
744 | INFOPLIST_FILE = LockedWidget/Info.plist;
745 | INFOPLIST_KEY_CFBundleDisplayName = LockedWidget;
746 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos.";
747 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
748 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library.";
749 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
750 | LD_RUNPATH_SEARCH_PATHS = (
751 | "$(inherited)",
752 | "@executable_path/Frameworks",
753 | "@executable_path/../../Frameworks",
754 | );
755 | MARKETING_VERSION = 1.0;
756 | OTHER_SWIFT_FLAGS = "";
757 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo.LockedWidget;
758 | PRODUCT_NAME = "$(TARGET_NAME)";
759 | SDKROOT = iphoneos;
760 | SKIP_INSTALL = YES;
761 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
762 | SUPPORTS_MACCATALYST = NO;
763 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
764 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
765 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
766 | SWIFT_EMIT_LOC_STRINGS = YES;
767 | SWIFT_VERSION = 5.0;
768 | TARGETED_DEVICE_FAMILY = "1,2";
769 | VALIDATE_PRODUCT = YES;
770 | };
771 | name = Release;
772 | };
773 | /* End XCBuildConfiguration section */
774 |
775 | /* Begin XCConfigurationList section */
776 | 22DE8DD62C74B36800FC6EEA /* Build configuration list for PBXProject "LockedCameraCaptureExtensionDemo" */ = {
777 | isa = XCConfigurationList;
778 | buildConfigurations = (
779 | 22DE8DE82C74B36900FC6EEA /* Debug */,
780 | 22DE8DE92C74B36900FC6EEA /* Release */,
781 | );
782 | defaultConfigurationIsVisible = 0;
783 | defaultConfigurationName = Release;
784 | };
785 | 22DE8DEA2C74B36900FC6EEA /* Build configuration list for PBXNativeTarget "LockedCameraCaptureExtensionDemo" */ = {
786 | isa = XCConfigurationList;
787 | buildConfigurations = (
788 | 22DE8DEB2C74B36900FC6EEA /* Debug */,
789 | 22DE8DEC2C74B36900FC6EEA /* Release */,
790 | );
791 | defaultConfigurationIsVisible = 0;
792 | defaultConfigurationName = Release;
793 | };
794 | 22DE8DFC2C74B3CA00FC6EEA /* Build configuration list for PBXNativeTarget "LockedExtension" */ = {
795 | isa = XCConfigurationList;
796 | buildConfigurations = (
797 | 22DE8DFD2C74B3CA00FC6EEA /* Debug */,
798 | 22DE8DFE2C74B3CA00FC6EEA /* Release */,
799 | );
800 | defaultConfigurationIsVisible = 0;
801 | defaultConfigurationName = Release;
802 | };
803 | 22DE8E162C74B3E600FC6EEA /* Build configuration list for PBXNativeTarget "LockedWidgetExtension" */ = {
804 | isa = XCConfigurationList;
805 | buildConfigurations = (
806 | 22DE8E172C74B3E600FC6EEA /* Debug */,
807 | 22DE8E182C74B3E600FC6EEA /* Release */,
808 | );
809 | defaultConfigurationIsVisible = 0;
810 | defaultConfigurationName = Release;
811 | };
812 | /* End XCConfigurationList section */
813 |
814 | /* Begin XCLocalSwiftPackageReference section */
815 | 22722BC12CE325FD0045DB78 /* XCLocalSwiftPackageReference "Packages/MetalLib" */ = {
816 | isa = XCLocalSwiftPackageReference;
817 | relativePath = Packages/MetalLib;
818 | };
819 | 22722BC42CE326050045DB78 /* XCLocalSwiftPackageReference "Packages/Models" */ = {
820 | isa = XCLocalSwiftPackageReference;
821 | relativePath = Packages/Models;
822 | };
823 | /* End XCLocalSwiftPackageReference section */
824 |
825 | /* Begin XCSwiftPackageProductDependency section */
826 | 2206C52F2CE31EF600744B0F /* MetalLib */ = {
827 | isa = XCSwiftPackageProductDependency;
828 | productName = MetalLib;
829 | };
830 | 2206C5312CE31EFC00744B0F /* MetalLib */ = {
831 | isa = XCSwiftPackageProductDependency;
832 | productName = MetalLib;
833 | };
834 | 22722BB82CE3258E0045DB78 /* Models */ = {
835 | isa = XCSwiftPackageProductDependency;
836 | productName = Models;
837 | };
838 | 22722BBC2CE325980045DB78 /* Models */ = {
839 | isa = XCSwiftPackageProductDependency;
840 | productName = Models;
841 | };
842 | 22722BC22CE325FD0045DB78 /* MetalLib */ = {
843 | isa = XCSwiftPackageProductDependency;
844 | productName = MetalLib;
845 | };
846 | 22722BC52CE326050045DB78 /* Models */ = {
847 | isa = XCSwiftPackageProductDependency;
848 | productName = Models;
849 | };
850 | 22722D532CE3314A0045DB78 /* Models */ = {
851 | isa = XCSwiftPackageProductDependency;
852 | productName = Models;
853 | };
854 | /* End XCSwiftPackageProductDependency section */
855 | };
856 | rootObject = 22DE8DD32C74B36800FC6EEA /* Project object */;
857 | }
858 |
--------------------------------------------------------------------------------