Skip to content

Commit de6cfd7

Browse files
committed
Workaround for app termination issue
1 parent 1b901d7 commit de6cfd7

15 files changed

Lines changed: 486 additions & 72 deletions

LockedCameraCaptureExtensionDemo.xcodeproj/project.pbxproj

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
22722D522CE331180045DB78 /* Exceptions for "LockedCameraCaptureExtensionDemo" folder in "LockedWidgetExtension" target */ = {
9090
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
9191
membershipExceptions = (
92+
Camera/Misc/LaunchSourceTracker.swift,
9293
Intent/AppCaptureIntent.swift,
9394
);
9495
target = 22DE8E032C74B3E500FC6EEA /* LockedWidgetExtension */;
@@ -110,13 +111,20 @@
110111
22DE8E2E2C74B5FF00FC6EEA /* Exceptions for "LockedCameraCaptureExtensionDemo" folder in "LockedExtension" target */ = {
111112
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
112113
membershipExceptions = (
114+
Camera/AppEnvironmentValues.swift,
113115
Camera/AppStorageConfigProvider.swift,
116+
Camera/CameraSessionService.swift,
117+
Camera/Misc/AVCaptureSessionControlsDelegateImpl.swift,
118+
Camera/Misc/LaunchSourceTracker.swift,
114119
Camera/OpenMainAppAction.swift,
115120
Camera/View/CaptureInteractionView.swift,
116121
Camera/View/ContentView.swift,
122+
Camera/View/ProtectTerminatedView.swift,
123+
Camera/View/SettingsPage.swift,
117124
Camera/ViewModel/CamPreviewViewModel.swift,
118125
Camera/ViewModel/CaptureProcessor.swift,
119126
Camera/ViewModel/MainViewModel.swift,
127+
Camera/ViewModel/SettingsViewModel.swift,
120128
Intent/AppCaptureIntent.swift,
121129
Localizable.xcstrings,
122130
);
@@ -553,6 +561,7 @@
553561
ENABLE_HARDENED_RUNTIME = YES;
554562
ENABLE_PREVIEWS = YES;
555563
GENERATE_INFOPLIST_FILE = YES;
564+
INFOPLIST_KEY_CFBundleDisplayName = LockedCamDemo;
556565
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
557566
INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos.";
558567
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library.";
@@ -565,7 +574,7 @@
565574
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
566575
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
567576
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
568-
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
577+
IPHONEOS_DEPLOYMENT_TARGET = 17.2;
569578
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
570579
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
571580
MACOSX_DEPLOYMENT_TARGET = 14.6;
@@ -595,6 +604,7 @@
595604
ENABLE_HARDENED_RUNTIME = YES;
596605
ENABLE_PREVIEWS = YES;
597606
GENERATE_INFOPLIST_FILE = YES;
607+
INFOPLIST_KEY_CFBundleDisplayName = LockedCamDemo;
598608
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
599609
INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos.";
600610
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library.";
@@ -607,7 +617,7 @@
607617
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
608618
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
609619
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
610-
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
620+
IPHONEOS_DEPLOYMENT_TARGET = 17.2;
611621
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
612622
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
613623
MACOSX_DEPLOYMENT_TARGET = 14.6;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//
2+
// AppEnvironmentValues.swift
3+
// LockedCameraCaptureExtensionDemo
4+
//
5+
// Created by JuniperPhoton on 2024/11/24.
6+
//
7+
import SwiftUI
8+
9+
extension EnvironmentValues {
10+
@Entry var inCaptureExtension: Bool = false
11+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//
2+
// CameraSessionService.swift
3+
// LockedCameraCaptureExtensionDemo
4+
//
5+
// Created by JuniperPhoton on 2024/11/24.
6+
//
7+
import AVFoundation
8+
import Models
9+
10+
actor CameraSessionService: NSObject {
11+
private let controlQueue = DispatchQueue(label: "control_queue")
12+
private let controlsDelegate = AVCaptureSessionControlsDelegateImpl()
13+
14+
private(set) var session: AVCaptureSession? = nil
15+
private(set) var photoOutput: AVCapturePhotoOutput? = nil
16+
private(set) var videoOutput: AVCaptureVideoDataOutput? = nil
17+
18+
let name: String
19+
20+
init(name: String) {
21+
self.name = name
22+
}
23+
24+
func stop() {
25+
print("\(name): stop")
26+
session?.stopRunning()
27+
self.session = nil
28+
}
29+
30+
func capturePhoto(delegate: any AVCapturePhotoCaptureDelegate) -> Bool {
31+
guard let photoOutput else {
32+
print("\(name) can't find photo output in capturePhoto")
33+
return false
34+
}
35+
36+
let settings = AVCapturePhotoSettings()
37+
photoOutput.capturePhoto(with: settings, delegate: delegate)
38+
return true
39+
}
40+
41+
func setupCameraSession(position: CameraPosition) async -> Bool {
42+
do {
43+
print("\(name) start setting up camera")
44+
45+
let session = AVCaptureSession()
46+
session.beginConfiguration()
47+
session.sessionPreset = .photo
48+
49+
guard let device = AVCaptureDevice.default(
50+
.builtInWideAngleCamera,
51+
for: .video,
52+
position: position.avFoundationPosition
53+
) else {
54+
print("\(name) can't find AVCaptureDevice")
55+
return false
56+
}
57+
58+
session.addInput(try AVCaptureDeviceInput(device: device))
59+
60+
let videoOutput = AVCaptureVideoDataOutput()
61+
62+
session.addOutput(videoOutput)
63+
64+
let photoOutput = AVCapturePhotoOutput()
65+
session.addOutput(photoOutput)
66+
67+
if let connection = videoOutput.connection(with: .video) {
68+
if connection.isVideoRotationAngleSupported(90) {
69+
connection.videoRotationAngle = 90
70+
}
71+
if connection.isVideoMirroringSupported && position == .front {
72+
connection.isVideoMirrored = true
73+
}
74+
}
75+
76+
if #available(iOS 18.0, *), session.supportsControls {
77+
let slider = AVCaptureSystemZoomSlider(device: device)
78+
session.addControl(slider)
79+
session.setControlsDelegate(controlsDelegate, queue: controlQueue)
80+
}
81+
82+
session.commitConfiguration()
83+
session.startRunning()
84+
85+
self.session = session
86+
self.videoOutput = videoOutput
87+
self.photoOutput = photoOutput
88+
89+
print("\(name) finish setting up camera")
90+
91+
return true
92+
} catch {
93+
print("\(name) error while setting up camera \(error)")
94+
return false
95+
}
96+
}
97+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// AVCaptureSessionControlsDelegateImpl.swift
3+
// LockedCameraCaptureExtensionDemo
4+
//
5+
// Created by JuniperPhoton on 2024/11/24.
6+
//
7+
import AVFoundation
8+
9+
class AVCaptureSessionControlsDelegateImpl: NSObject, AVCaptureSessionControlsDelegate {
10+
func sessionControlsDidBecomeActive(_ session: AVCaptureSession) {
11+
12+
}
13+
14+
func sessionControlsWillEnterFullscreenAppearance(_ session: AVCaptureSession) {
15+
16+
}
17+
18+
func sessionControlsWillExitFullscreenAppearance(_ session: AVCaptureSession) {
19+
20+
}
21+
22+
func sessionControlsDidBecomeInactive(_ session: AVCaptureSession) {
23+
24+
}
25+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// LaunchSourceTracker.swift
3+
// LockedCameraCaptureExtensionDemo
4+
//
5+
// Created by JuniperPhoton on 2024/11/24.
6+
//
7+
8+
import AppIntents
9+
import SwiftUICore
10+
11+
extension EnvironmentValues {
12+
@Entry var launchSource: LaunchSourceTracker.LaunchSource = .main
13+
}
14+
15+
@MainActor
16+
class LaunchSourceTracker: ObservableObject {
17+
enum LaunchSource {
18+
case main
19+
case captureIntent
20+
case otherIntents
21+
}
22+
23+
static let shared = LaunchSourceTracker()
24+
25+
@Published private(set) var launchSource = LaunchSourceTracker.LaunchSource.main {
26+
didSet {
27+
print("set LaunchSource: \(launchSource)")
28+
}
29+
}
30+
31+
private init() {
32+
// empty
33+
}
34+
35+
func invalidate() {
36+
self.launchSource = .main
37+
}
38+
39+
func setLaunchFrom<T: AppIntent>(_ intent: T) {
40+
if #available(iOS 18.0, *) {
41+
if type(of: intent) == AppCaptureIntent.self {
42+
self.launchSource = .captureIntent
43+
} else {
44+
self.launchSource = .otherIntents
45+
}
46+
} else {
47+
self.launchSource = .otherIntents
48+
}
49+
}
50+
}

LockedCameraCaptureExtensionDemo/Camera/View/CaptureInteractionView.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ import AVFoundation
1010
import AVKit
1111

1212
extension View {
13+
@ViewBuilder
14+
func registerOnCameraCaptureEvent() -> some View {
15+
self.onPressCapture {
16+
// empty
17+
}
18+
}
19+
1320
@ViewBuilder
1421
func onPressCapture(action: @escaping () -> Void) -> some View {
1522
if #available(iOS 18.0, *) {
@@ -28,12 +35,10 @@ extension View {
2835
break
2936
}
3037
}
31-
} else if #available(iOS 17.2, *) {
38+
} else {
3239
self.background {
3340
CaptureInteractionView(action: action)
3441
}
35-
} else {
36-
self
3742
}
3843
}
3944
}

LockedCameraCaptureExtensionDemo/Camera/View/ContentView.swift

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import MetalLib
1010

1111
struct ContentView: View {
1212
@Environment(\.scenePhase) private var scenePhase
13+
@Environment(\.inCaptureExtension) private var inCaptureExtension
1314

1415
@StateObject private var previewViewModel: CamPreviewViewModel
1516
@StateObject private var viewModel: MainViewModel
@@ -54,10 +55,15 @@ struct ContentView: View {
5455
.padding()
5556

5657
ZStack {
57-
SwitchCameraPositionButton(viewModel: viewModel)
58-
.frame(maxWidth: .infinity, alignment: .trailing)
58+
if !inCaptureExtension {
59+
SettingsButton(viewModel: viewModel)
60+
.frame(maxWidth: .infinity, alignment: .leading)
61+
}
5962

6063
CaptureButton(viewModel: viewModel)
64+
65+
SwitchCameraPositionButton(viewModel: viewModel)
66+
.frame(maxWidth: .infinity, alignment: .trailing)
6167
}
6268
}
6369
.padding()
@@ -78,27 +84,42 @@ struct ContentView: View {
7884
.background {
7985
Color.black.ignoresSafeArea()
8086
}
81-
.animation(.default, value: viewModel.isSettingUpCamera)
82-
.animation(.default, value: captureProcessor.saveResultText)
8387
.onPressCapture {
8488
Task {
8589
await viewModel.capturePhoto()
8690
}
8791
}
92+
.animation(.default, value: viewModel.isSettingUpCamera)
93+
.animation(.default, value: captureProcessor.saveResultText)
8894
.task(id: scenePhase) {
8995
switch scenePhase {
9096
case .background:
9197
await viewModel.stopCamera()
9298
case .active:
9399
await viewModel.updateFromAppContext()
94-
await viewModel.setup()
100+
101+
if !viewModel.showSettings {
102+
await viewModel.setup()
103+
}
95104
default:
96105
break
97106
}
98107
}
108+
.onChange(of: viewModel.showSettings) {
109+
Task {
110+
if viewModel.showSettings {
111+
await viewModel.stopCamera()
112+
} else {
113+
await viewModel.setup()
114+
}
115+
}
116+
}
99117
.task {
100118
previewViewModel.initializeRenderer()
101119
}
120+
.fullScreenCover(isPresented: $viewModel.showSettings) {
121+
SettingsPage()
122+
}
102123
}
103124
}
104125

@@ -121,6 +142,23 @@ private struct SwitchCameraPositionButton: View {
121142
}
122143
}
123144

145+
private struct SettingsButton: View {
146+
@ObservedObject var viewModel: MainViewModel
147+
148+
var body: some View {
149+
Button {
150+
viewModel.showSettings = true
151+
} label: {
152+
Image(systemName: "gear")
153+
.padding()
154+
.foregroundStyle(.white)
155+
.background {
156+
Circle().fill(Color.white.opacity(0.1))
157+
}
158+
}.buttonStyle(.plain)
159+
}
160+
}
161+
124162
private struct CaptureButton: View {
125163
@ObservedObject var viewModel: MainViewModel
126164

0 commit comments

Comments
 (0)