├── RawCamera
├── Assets.xcassets
│ ├── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── RawCamera.entitlements
├── Info.plist
├── RawCameraApp.swift
└── ContentView.swift
└── RawCamera.xcodeproj
├── project.xcworkspace
└── contents.xcworkspacedata
├── xcuserdata
└── pandadev.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
└── project.pbxproj
/RawCamera/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/RawCamera.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/RawCamera/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 |
--------------------------------------------------------------------------------
/RawCamera/RawCamera.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 |
--------------------------------------------------------------------------------
/RawCamera/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSCameraUsageDescription
6 | This app needs camera access to capture RAW photos
7 | NSPhotoLibraryUsageDescription
8 | This app needs photo library access to save RAW photos
9 |
10 |
--------------------------------------------------------------------------------
/RawCamera.xcodeproj/xcuserdata/pandadev.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | RawCamera.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/RawCamera/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 |
--------------------------------------------------------------------------------
/RawCamera.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 77;
7 | objects = {
8 |
9 | /* Begin PBXFileReference section */
10 | 153166022DC8FC0E00079F56 /* RawCamera.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RawCamera.app; sourceTree = BUILT_PRODUCTS_DIR; };
11 | /* End PBXFileReference section */
12 |
13 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
14 | 1531663A2DC8FEE800079F56 /* Exceptions for "RawCamera" folder in "RawCamera" target */ = {
15 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
16 | membershipExceptions = (
17 | Info.plist,
18 | );
19 | target = 153166012DC8FC0E00079F56 /* RawCamera */;
20 | };
21 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
22 |
23 | /* Begin PBXFileSystemSynchronizedRootGroup section */
24 | 153166042DC8FC0E00079F56 /* RawCamera */ = {
25 | isa = PBXFileSystemSynchronizedRootGroup;
26 | exceptions = (
27 | 1531663A2DC8FEE800079F56 /* Exceptions for "RawCamera" folder in "RawCamera" target */,
28 | );
29 | path = RawCamera;
30 | sourceTree = "";
31 | };
32 | /* End PBXFileSystemSynchronizedRootGroup section */
33 |
34 | /* Begin PBXFrameworksBuildPhase section */
35 | 153165FF2DC8FC0E00079F56 /* Frameworks */ = {
36 | isa = PBXFrameworksBuildPhase;
37 | buildActionMask = 2147483647;
38 | files = (
39 | );
40 | runOnlyForDeploymentPostprocessing = 0;
41 | };
42 | /* End PBXFrameworksBuildPhase section */
43 |
44 | /* Begin PBXGroup section */
45 | 153165F92DC8FC0E00079F56 = {
46 | isa = PBXGroup;
47 | children = (
48 | 153166042DC8FC0E00079F56 /* RawCamera */,
49 | 153166032DC8FC0E00079F56 /* Products */,
50 | );
51 | sourceTree = "";
52 | };
53 | 153166032DC8FC0E00079F56 /* Products */ = {
54 | isa = PBXGroup;
55 | children = (
56 | 153166022DC8FC0E00079F56 /* RawCamera.app */,
57 | );
58 | name = Products;
59 | sourceTree = "";
60 | };
61 | /* End PBXGroup section */
62 |
63 | /* Begin PBXNativeTarget section */
64 | 153166012DC8FC0E00079F56 /* RawCamera */ = {
65 | isa = PBXNativeTarget;
66 | buildConfigurationList = 153166272DC8FC0F00079F56 /* Build configuration list for PBXNativeTarget "RawCamera" */;
67 | buildPhases = (
68 | 153165FE2DC8FC0E00079F56 /* Sources */,
69 | 153165FF2DC8FC0E00079F56 /* Frameworks */,
70 | 153166002DC8FC0E00079F56 /* Resources */,
71 | );
72 | buildRules = (
73 | );
74 | dependencies = (
75 | );
76 | fileSystemSynchronizedGroups = (
77 | 153166042DC8FC0E00079F56 /* RawCamera */,
78 | );
79 | name = RawCamera;
80 | packageProductDependencies = (
81 | );
82 | productName = RawCamera;
83 | productReference = 153166022DC8FC0E00079F56 /* RawCamera.app */;
84 | productType = "com.apple.product-type.application";
85 | };
86 | /* End PBXNativeTarget section */
87 |
88 | /* Begin PBXProject section */
89 | 153165FA2DC8FC0E00079F56 /* Project object */ = {
90 | isa = PBXProject;
91 | attributes = {
92 | BuildIndependentTargetsInParallel = 1;
93 | LastSwiftUpdateCheck = 1610;
94 | LastUpgradeCheck = 1610;
95 | TargetAttributes = {
96 | 153166012DC8FC0E00079F56 = {
97 | CreatedOnToolsVersion = 16.1;
98 | };
99 | };
100 | };
101 | buildConfigurationList = 153165FD2DC8FC0E00079F56 /* Build configuration list for PBXProject "RawCamera" */;
102 | developmentRegion = en;
103 | hasScannedForEncodings = 0;
104 | knownRegions = (
105 | en,
106 | Base,
107 | );
108 | mainGroup = 153165F92DC8FC0E00079F56;
109 | minimizedProjectReferenceProxies = 1;
110 | preferredProjectObjectVersion = 77;
111 | productRefGroup = 153166032DC8FC0E00079F56 /* Products */;
112 | projectDirPath = "";
113 | projectRoot = "";
114 | targets = (
115 | 153166012DC8FC0E00079F56 /* RawCamera */,
116 | );
117 | };
118 | /* End PBXProject section */
119 |
120 | /* Begin PBXResourcesBuildPhase section */
121 | 153166002DC8FC0E00079F56 /* Resources */ = {
122 | isa = PBXResourcesBuildPhase;
123 | buildActionMask = 2147483647;
124 | files = (
125 | );
126 | runOnlyForDeploymentPostprocessing = 0;
127 | };
128 | /* End PBXResourcesBuildPhase section */
129 |
130 | /* Begin PBXSourcesBuildPhase section */
131 | 153165FE2DC8FC0E00079F56 /* Sources */ = {
132 | isa = PBXSourcesBuildPhase;
133 | buildActionMask = 2147483647;
134 | files = (
135 | );
136 | runOnlyForDeploymentPostprocessing = 0;
137 | };
138 | /* End PBXSourcesBuildPhase section */
139 |
140 | /* Begin XCBuildConfiguration section */
141 | 153166252DC8FC0F00079F56 /* Debug */ = {
142 | isa = XCBuildConfiguration;
143 | buildSettings = {
144 | ALWAYS_SEARCH_USER_PATHS = NO;
145 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
146 | CLANG_ANALYZER_NONNULL = YES;
147 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
148 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
149 | CLANG_ENABLE_MODULES = YES;
150 | CLANG_ENABLE_OBJC_ARC = YES;
151 | CLANG_ENABLE_OBJC_WEAK = YES;
152 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
153 | CLANG_WARN_BOOL_CONVERSION = YES;
154 | CLANG_WARN_COMMA = YES;
155 | CLANG_WARN_CONSTANT_CONVERSION = YES;
156 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
157 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
158 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
159 | CLANG_WARN_EMPTY_BODY = YES;
160 | CLANG_WARN_ENUM_CONVERSION = YES;
161 | CLANG_WARN_INFINITE_RECURSION = YES;
162 | CLANG_WARN_INT_CONVERSION = YES;
163 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
164 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
165 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
166 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
167 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
168 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
169 | CLANG_WARN_STRICT_PROTOTYPES = YES;
170 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
171 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
172 | CLANG_WARN_UNREACHABLE_CODE = YES;
173 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
174 | COPY_PHASE_STRIP = NO;
175 | DEBUG_INFORMATION_FORMAT = dwarf;
176 | ENABLE_STRICT_OBJC_MSGSEND = YES;
177 | ENABLE_TESTABILITY = YES;
178 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
179 | GCC_C_LANGUAGE_STANDARD = gnu17;
180 | GCC_DYNAMIC_NO_PIC = NO;
181 | GCC_NO_COMMON_BLOCKS = YES;
182 | GCC_OPTIMIZATION_LEVEL = 0;
183 | GCC_PREPROCESSOR_DEFINITIONS = (
184 | "DEBUG=1",
185 | "$(inherited)",
186 | );
187 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
188 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
189 | GCC_WARN_UNDECLARED_SELECTOR = YES;
190 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
191 | GCC_WARN_UNUSED_FUNCTION = YES;
192 | GCC_WARN_UNUSED_VARIABLE = YES;
193 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
194 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
195 | MTL_FAST_MATH = YES;
196 | ONLY_ACTIVE_ARCH = YES;
197 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
198 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
199 | };
200 | name = Debug;
201 | };
202 | 153166262DC8FC0F00079F56 /* Release */ = {
203 | isa = XCBuildConfiguration;
204 | buildSettings = {
205 | ALWAYS_SEARCH_USER_PATHS = NO;
206 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
207 | CLANG_ANALYZER_NONNULL = YES;
208 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
209 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
210 | CLANG_ENABLE_MODULES = YES;
211 | CLANG_ENABLE_OBJC_ARC = YES;
212 | CLANG_ENABLE_OBJC_WEAK = YES;
213 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
214 | CLANG_WARN_BOOL_CONVERSION = YES;
215 | CLANG_WARN_COMMA = YES;
216 | CLANG_WARN_CONSTANT_CONVERSION = YES;
217 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
218 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
219 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
220 | CLANG_WARN_EMPTY_BODY = YES;
221 | CLANG_WARN_ENUM_CONVERSION = YES;
222 | CLANG_WARN_INFINITE_RECURSION = YES;
223 | CLANG_WARN_INT_CONVERSION = YES;
224 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
225 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
226 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
227 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
228 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
229 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
230 | CLANG_WARN_STRICT_PROTOTYPES = YES;
231 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
232 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
233 | CLANG_WARN_UNREACHABLE_CODE = YES;
234 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
235 | COPY_PHASE_STRIP = NO;
236 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
237 | ENABLE_NS_ASSERTIONS = NO;
238 | ENABLE_STRICT_OBJC_MSGSEND = YES;
239 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
240 | GCC_C_LANGUAGE_STANDARD = gnu17;
241 | GCC_NO_COMMON_BLOCKS = YES;
242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
244 | GCC_WARN_UNDECLARED_SELECTOR = YES;
245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
246 | GCC_WARN_UNUSED_FUNCTION = YES;
247 | GCC_WARN_UNUSED_VARIABLE = YES;
248 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
249 | MTL_ENABLE_DEBUG_INFO = NO;
250 | MTL_FAST_MATH = YES;
251 | SWIFT_COMPILATION_MODE = wholemodule;
252 | };
253 | name = Release;
254 | };
255 | 153166282DC8FC0F00079F56 /* Debug */ = {
256 | isa = XCBuildConfiguration;
257 | buildSettings = {
258 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
259 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
260 | CODE_SIGN_ENTITLEMENTS = RawCamera/RawCamera.entitlements;
261 | CODE_SIGN_STYLE = Automatic;
262 | CURRENT_PROJECT_VERSION = 1;
263 | DEVELOPMENT_TEAM = V943WJ84RH;
264 | ENABLE_HARDENED_RUNTIME = YES;
265 | ENABLE_PREVIEWS = YES;
266 | GENERATE_INFOPLIST_FILE = YES;
267 | INFOPLIST_KEY_CFBundleDisplayName = RawCamera;
268 | INFOPLIST_KEY_NSCameraUsageDescription = "This app needs camera access to capture RAW photos";
269 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app needs photo library access to save RAW photos";
270 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
271 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
272 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
273 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
274 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
275 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
276 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
277 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
278 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
279 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
280 | IPHONEOS_DEPLOYMENT_TARGET = 18.1;
281 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
282 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
283 | MACOSX_DEPLOYMENT_TARGET = 15.1;
284 | MARKETING_VERSION = 1.0;
285 | PRODUCT_BUNDLE_IDENTIFIER = net.pandadev.RawCamera;
286 | PRODUCT_NAME = "$(TARGET_NAME)";
287 | SDKROOT = auto;
288 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
289 | SUPPORTS_MACCATALYST = NO;
290 | SWIFT_EMIT_LOC_STRINGS = YES;
291 | SWIFT_VERSION = 5.0;
292 | TARGETED_DEVICE_FAMILY = 1;
293 | XROS_DEPLOYMENT_TARGET = 2.1;
294 | };
295 | name = Debug;
296 | };
297 | 153166292DC8FC0F00079F56 /* Release */ = {
298 | isa = XCBuildConfiguration;
299 | buildSettings = {
300 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
301 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
302 | CODE_SIGN_ENTITLEMENTS = RawCamera/RawCamera.entitlements;
303 | CODE_SIGN_STYLE = Automatic;
304 | CURRENT_PROJECT_VERSION = 1;
305 | DEVELOPMENT_TEAM = V943WJ84RH;
306 | ENABLE_HARDENED_RUNTIME = YES;
307 | ENABLE_PREVIEWS = YES;
308 | GENERATE_INFOPLIST_FILE = YES;
309 | INFOPLIST_KEY_CFBundleDisplayName = RawCamera;
310 | INFOPLIST_KEY_NSCameraUsageDescription = "This app needs camera access to capture RAW photos";
311 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app needs photo library access to save RAW photos";
312 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
313 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
314 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
315 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
316 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
317 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
318 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
319 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
320 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
321 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
322 | IPHONEOS_DEPLOYMENT_TARGET = 18.1;
323 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
324 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
325 | MACOSX_DEPLOYMENT_TARGET = 15.1;
326 | MARKETING_VERSION = 1.0;
327 | PRODUCT_BUNDLE_IDENTIFIER = net.pandadev.RawCamera;
328 | PRODUCT_NAME = "$(TARGET_NAME)";
329 | SDKROOT = auto;
330 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
331 | SUPPORTS_MACCATALYST = NO;
332 | SWIFT_EMIT_LOC_STRINGS = YES;
333 | SWIFT_VERSION = 5.0;
334 | TARGETED_DEVICE_FAMILY = 1;
335 | XROS_DEPLOYMENT_TARGET = 2.1;
336 | };
337 | name = Release;
338 | };
339 | /* End XCBuildConfiguration section */
340 |
341 | /* Begin XCConfigurationList section */
342 | 153165FD2DC8FC0E00079F56 /* Build configuration list for PBXProject "RawCamera" */ = {
343 | isa = XCConfigurationList;
344 | buildConfigurations = (
345 | 153166252DC8FC0F00079F56 /* Debug */,
346 | 153166262DC8FC0F00079F56 /* Release */,
347 | );
348 | defaultConfigurationIsVisible = 0;
349 | defaultConfigurationName = Release;
350 | };
351 | 153166272DC8FC0F00079F56 /* Build configuration list for PBXNativeTarget "RawCamera" */ = {
352 | isa = XCConfigurationList;
353 | buildConfigurations = (
354 | 153166282DC8FC0F00079F56 /* Debug */,
355 | 153166292DC8FC0F00079F56 /* Release */,
356 | );
357 | defaultConfigurationIsVisible = 0;
358 | defaultConfigurationName = Release;
359 | };
360 | /* End XCConfigurationList section */
361 | };
362 | rootObject = 153165FA2DC8FC0E00079F56 /* Project object */;
363 | }
364 |
--------------------------------------------------------------------------------
/RawCamera/RawCameraApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RawCameraApp.swift
3 | // RawCamera
4 | //
5 | // Created by Nils on 05.05.2025.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct RawCameraApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
19 | // MARK: - Camera Controls
20 |
21 | // Helper struct to reduce main ContentView size
22 | enum CameraControls {
23 | static func sliderRange(for name: String) -> ClosedRange {
24 | switch name {
25 | case "EV": return -2.0 ... 2.0
26 | case "WB": return 2000 ... 9000
27 | case "ISO": return 50 ... 1600
28 | case "Shutter": return 30 ... 1000
29 | default: return 0 ... 1
30 | }
31 | }
32 |
33 | static func sliderStep(for name: String) -> Float {
34 | switch name {
35 | case "EV": return 0.1
36 | case "WB": return 100
37 | case "ISO": return 50
38 | case "Shutter": return 10
39 | default: return 0.1
40 | }
41 | }
42 |
43 | static func formatValue(name: String, value: Float) -> String {
44 | switch name {
45 | case "EV": return String(format: "%.1f", value)
46 | case "WB": return "\(Int(value))K"
47 | case "ISO": return "\(Int(value))"
48 | case "Shutter": return "1/\(Int(value))"
49 | default: return ""
50 | }
51 | }
52 | }
53 |
54 | struct ContentView: View {
55 | @StateObject private var cameraManager = CameraManager()
56 | @State private var focusPoint: CGPoint?
57 | @State private var activeSlider: String? = nil
58 |
59 | // Animation properties
60 | @Namespace private var buttonAnimation
61 |
62 | var body: some View {
63 | ZStack {
64 | Color.black.ignoresSafeArea()
65 |
66 | if cameraManager.isAuthorized {
67 | VStack(spacing: 15) {
68 | // Camera Preview with explicit clipShape
69 | ZStack {
70 | CameraPreview(cameraManager: cameraManager, focusPoint: $focusPoint)
71 | .aspectRatio(3 / 4, contentMode: .fit)
72 | .clipShape(RoundedRectangle(cornerRadius: 20))
73 | .overlay(
74 | RoundedRectangle(cornerRadius: 20)
75 | .stroke(Color.white.opacity(0.3), lineWidth: 1)
76 | )
77 |
78 | // Focus point overlay in separate ZStack to avoid layout changes
79 | if let point = focusPoint {
80 | Circle()
81 | .stroke(Color.yellow, lineWidth: 2)
82 | .frame(width: 50, height: 50)
83 | .position(point)
84 | .transition(.opacity)
85 | }
86 | }
87 | .padding(.horizontal, 20)
88 | .padding(.top, 20)
89 |
90 | // Focus slider - full width between preview and controls
91 | focusSlider
92 | .padding(.horizontal, 20)
93 |
94 | Spacer()
95 |
96 | // Controls
97 | if let active = activeSlider {
98 | // Show only the active slider taking full width
99 | getSliderFor(name: active)
100 | .frame(height: 90)
101 | .padding(.horizontal, 20)
102 | .padding(.bottom, 10)
103 | .transition(.asymmetric(
104 | insertion: .opacity.combined(with: .move(edge: .trailing)),
105 | removal: .opacity.combined(with: .move(edge: .leading))
106 | ))
107 | } else {
108 | // Show all buttons in horizontal layout
109 | HStack(spacing: 8) {
110 | buttonFor("EV")
111 | buttonFor("WB")
112 | buttonFor("ISO")
113 | buttonFor("Shutter")
114 | }
115 | .padding(.horizontal, 20)
116 | .padding(.bottom, 10)
117 | .transition(.opacity)
118 | }
119 |
120 | // Capture Button
121 | Button(action: {
122 | withAnimation(.spring()) {
123 | cameraManager.capturePhoto()
124 | }
125 | }) {
126 | Circle()
127 | .fill(Color.white.opacity(0.9))
128 | .frame(width: 70, height: 70)
129 | .overlay(
130 | Circle()
131 | .stroke(Color.gray, lineWidth: 3)
132 | )
133 | .contentShape(Circle())
134 | }
135 | .padding(.bottom, 20)
136 | .scaleEffect(activeSlider == nil ? 1 : 0.8)
137 | .animation(.spring(), value: activeSlider)
138 | }
139 | } else {
140 | Text("Camera access not granted")
141 | .foregroundColor(.red)
142 | .font(.title)
143 | }
144 | }
145 | .animation(.easeInOut, value: cameraManager.isAuthorized)
146 | }
147 |
148 | // Focus Slider
149 | private var focusSlider: some View {
150 | VStack(spacing: 8) {
151 | HStack {
152 | Text("Focus")
153 | .foregroundColor(.white)
154 | .font(.system(size: 14, weight: .bold))
155 |
156 | Spacer()
157 |
158 | // Auto/Manual toggle for focus
159 | Button(action: {
160 | withAnimation(.spring()) {
161 | cameraManager.toggleAutoMode(for: "Focus")
162 | }
163 | }) {
164 | Text(cameraManager.isFocusAuto ? "Auto" : "Manual")
165 | .foregroundColor(cameraManager.isFocusAuto ? .green : .orange)
166 | .font(.system(size: 12, weight: .bold))
167 | .padding(.horizontal, 8)
168 | .padding(.vertical, 4)
169 | .background(
170 | Capsule()
171 | .fill(Color.black)
172 | .overlay(
173 | Capsule()
174 | .stroke(cameraManager.isFocusAuto ? Color.green : Color.orange, lineWidth: 1)
175 | )
176 | )
177 | }
178 | }
179 |
180 | HStack(spacing: 12) {
181 | Image(systemName: "mountain.2")
182 | .foregroundColor(.gray)
183 | .font(.system(size: 12))
184 |
185 | Slider(value: $cameraManager.currentFocus, in: 0 ... 1, step: 0.01)
186 | .accentColor(.yellow)
187 | .onChange(of: cameraManager.currentFocus) { _, newValue in
188 | cameraManager.setFocusManually(newValue)
189 | }
190 | .disabled(cameraManager.isFocusAuto)
191 | .opacity(cameraManager.isFocusAuto ? 0.5 : 1.0)
192 |
193 | Image(systemName: "person.bust")
194 | .foregroundColor(.gray)
195 | .font(.system(size: 12))
196 | }
197 | }
198 | .padding(.vertical, 8)
199 | .padding(.horizontal, 12)
200 | .background(
201 | RoundedRectangle(cornerRadius: 10)
202 | .fill(Color.black.opacity(0.7))
203 | .overlay(
204 | RoundedRectangle(cornerRadius: 10)
205 | .stroke(Color.gray.opacity(0.3), lineWidth: 1)
206 | )
207 | )
208 | }
209 |
210 | @ViewBuilder
211 | private func buttonFor(_ name: String) -> some View {
212 | let isAuto = isSettingAuto(for: name)
213 | let value = buttonValueText(for: name)
214 |
215 | Button(action: {
216 | withAnimation(.spring()) {
217 | activeSlider = name
218 | }
219 | }) {
220 | VStack(spacing: 4) {
221 | HStack {
222 | Text(name)
223 | .foregroundColor(.white)
224 | .font(.system(size: 14, weight: .bold))
225 |
226 | Spacer()
227 |
228 | // Auto/Manual indicator
229 | Text(isAuto ? "A" : "M")
230 | .foregroundColor(isAuto ? .green : .orange)
231 | .font(.system(size: 10, weight: .bold))
232 | .frame(width: 16, height: 16)
233 | .background(
234 | Circle()
235 | .fill(Color.black)
236 | .overlay(
237 | Circle()
238 | .stroke(isAuto ? Color.green : Color.orange, lineWidth: 1)
239 | )
240 | )
241 | }
242 |
243 | Text(value)
244 | .foregroundColor(.gray)
245 | .font(.system(size: 12))
246 | .animation(.easeInOut(duration: 0.2), value: value) // Animate value changes
247 | }
248 | .frame(maxWidth: .infinity)
249 | .frame(height: 50)
250 | .padding(.horizontal, 10)
251 | }
252 | .background(
253 | RoundedRectangle(cornerRadius: 10)
254 | .fill(Color.black.opacity(0.7))
255 | .overlay(
256 | RoundedRectangle(cornerRadius: 10)
257 | .stroke(Color.gray.opacity(0.3), lineWidth: 1)
258 | )
259 | )
260 | }
261 |
262 | @ViewBuilder
263 | private func getSliderFor(name: String) -> some View {
264 | VStack(spacing: 8) {
265 | HStack {
266 | Button(action: {
267 | withAnimation(.spring()) {
268 | activeSlider = nil
269 | }
270 | }) {
271 | HStack {
272 | Image(systemName: "chevron.left")
273 | .font(.system(size: 14))
274 |
275 | Text(name)
276 | .font(.system(size: 14, weight: .bold))
277 | }
278 | .foregroundColor(.yellow)
279 | }
280 |
281 | Spacer()
282 |
283 | Text(sliderValueText(for: name))
284 | .foregroundColor(.white)
285 | .font(.system(size: 14, weight: .medium))
286 | .animation(.easeInOut(duration: 0.2), value: sliderValueText(for: name)) // Animate value changes
287 |
288 | // Auto/Manual toggle
289 | Button(action: {
290 | withAnimation(.spring()) {
291 | cameraManager.toggleAutoMode(for: name)
292 | }
293 | }) {
294 | Text(isSettingAuto(for: name) ? "Auto" : "Manual")
295 | .foregroundColor(isSettingAuto(for: name) ? .green : .orange)
296 | .font(.system(size: 12, weight: .bold))
297 | .padding(.horizontal, 8)
298 | .padding(.vertical, 4)
299 | .background(
300 | Capsule()
301 | .fill(Color.black)
302 | .overlay(
303 | Capsule()
304 | .stroke(isSettingAuto(for: name) ? Color.green : Color.orange, lineWidth: 1)
305 | )
306 | )
307 | }
308 | }
309 | .padding(.horizontal, 10)
310 |
311 | Slider(value: sliderBinding(for: name), in: CameraControls.sliderRange(for: name), step: CameraControls.sliderStep(for: name))
312 | .accentColor(.yellow)
313 | .onChange(of: sliderValue(for: name)) { _, newValue in
314 | updateCamera(for: name, value: newValue)
315 | }
316 | .padding(.horizontal, 10)
317 | .disabled(isSettingAuto(for: name))
318 | .opacity(isSettingAuto(for: name) ? 0.5 : 1.0)
319 | }
320 | .padding(.vertical, 10)
321 | .background(
322 | RoundedRectangle(cornerRadius: 10)
323 | .fill(Color.black.opacity(0.7))
324 | .overlay(
325 | RoundedRectangle(cornerRadius: 10)
326 | .stroke(Color.yellow.opacity(0.5), lineWidth: 1)
327 | )
328 | )
329 | }
330 |
331 | private func buttonValueText(for name: String) -> String {
332 | switch name {
333 | case "EV":
334 | return CameraControls.formatValue(name: name, value: cameraManager.currentEV)
335 | case "WB":
336 | return CameraControls.formatValue(name: name, value: cameraManager.currentWhiteBalance)
337 | case "ISO":
338 | return CameraControls.formatValue(name: name, value: cameraManager.currentISO)
339 | case "Shutter":
340 | return CameraControls.formatValue(name: name, value: cameraManager.currentShutterSpeed)
341 | default:
342 | return ""
343 | }
344 | }
345 |
346 | private func isSettingAuto(for name: String) -> Bool {
347 | switch name {
348 | case "EV":
349 | return cameraManager.isEVAuto
350 | case "WB":
351 | return cameraManager.isWhiteBalanceAuto
352 | case "ISO":
353 | return cameraManager.isISOAuto
354 | case "Shutter":
355 | return cameraManager.isShutterAuto
356 | default:
357 | return true
358 | }
359 | }
360 |
361 | private func sliderBinding(for name: String) -> Binding {
362 | switch name {
363 | case "EV":
364 | return Binding(
365 | get: { self.cameraManager.currentEV },
366 | set: { self.cameraManager.currentEV = $0 }
367 | )
368 | case "WB":
369 | return Binding(
370 | get: { self.cameraManager.currentWhiteBalance },
371 | set: { self.cameraManager.currentWhiteBalance = $0 }
372 | )
373 | case "ISO":
374 | return Binding(
375 | get: { self.cameraManager.currentISO },
376 | set: { self.cameraManager.currentISO = $0 }
377 | )
378 | case "Shutter":
379 | return Binding(
380 | get: { self.cameraManager.currentShutterSpeed },
381 | set: { self.cameraManager.currentShutterSpeed = $0 }
382 | )
383 | default:
384 | return Binding(
385 | get: { 0 },
386 | set: { _ in }
387 | )
388 | }
389 | }
390 |
391 | private func sliderValue(for name: String) -> Float {
392 | switch name {
393 | case "EV":
394 | return cameraManager.currentEV
395 | case "WB":
396 | return cameraManager.currentWhiteBalance
397 | case "ISO":
398 | return cameraManager.currentISO
399 | case "Shutter":
400 | return cameraManager.currentShutterSpeed
401 | default:
402 | return 0
403 | }
404 | }
405 |
406 | private func sliderValueText(for name: String) -> String {
407 | return CameraControls.formatValue(name: name, value: sliderValue(for: name))
408 | }
409 |
410 | private func updateCamera(for name: String, value: Float) {
411 | switch name {
412 | case "EV":
413 | cameraManager.setExposureCompensation(value)
414 | case "WB":
415 | cameraManager.setWhiteBalance(value)
416 | case "ISO":
417 | cameraManager.setISO(value)
418 | case "Shutter":
419 | cameraManager.setShutterSpeed(value)
420 | default:
421 | break
422 | }
423 | }
424 | }
425 |
--------------------------------------------------------------------------------
/RawCamera/ContentView.swift:
--------------------------------------------------------------------------------
1 | import AVFoundation
2 | import Photos
3 | import SwiftUI
4 |
5 | class CameraManager: NSObject, ObservableObject, AVCapturePhotoCaptureDelegate {
6 | @Published var isAuthorized = false
7 | @Published var capturedImage: UIImage?
8 | @Published var error: Error?
9 | @Published var previewLayer: AVCaptureVideoPreviewLayer?
10 |
11 | // Published camera settings
12 | @Published var currentShutterSpeed: Float = 125
13 | @Published var currentISO: Float = 100
14 | @Published var currentWhiteBalance: Float = 5500
15 | @Published var currentEV: Float = 0.0
16 | @Published var currentFocus: Float = 0.5
17 |
18 | // Auto/Manual mode for each setting
19 | @Published var isShutterAuto = true
20 | @Published var isISOAuto = true
21 | @Published var isWhiteBalanceAuto = true
22 | @Published var isEVAuto = true
23 | @Published var isFocusAuto = true
24 |
25 | let session = AVCaptureSession()
26 | var photoOutput = AVCapturePhotoOutput()
27 | private var currentDevice: AVCaptureDevice?
28 | private var isObservingSettings = false
29 | private var observationTimer: Timer?
30 |
31 | override init() {
32 | super.init()
33 | checkPermissions()
34 | }
35 |
36 | deinit {
37 | stopObservingCameraSettings()
38 | }
39 |
40 | private func startObservingCameraSettings() {
41 | guard !isObservingSettings else { return }
42 | isObservingSettings = true
43 |
44 | // Update initial values
45 | updateCurrentCameraSettings()
46 |
47 | // Set up timer to update settings continuously
48 | observationTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
49 | self?.updateCurrentCameraSettings()
50 | }
51 | }
52 |
53 | private func stopObservingCameraSettings() {
54 | isObservingSettings = false
55 | observationTimer?.invalidate()
56 | observationTimer = nil
57 | }
58 |
59 | private func updateCurrentCameraSettings() {
60 | guard let device = currentDevice else { return }
61 |
62 | DispatchQueue.main.async { [weak self] in
63 | guard let self = self else { return }
64 |
65 | // Update settings that are in auto mode
66 | if self.isShutterAuto {
67 | let duration = CMTimeGetSeconds(device.exposureDuration)
68 | if duration > 0 {
69 | self.currentShutterSpeed = Float(1.0 / duration)
70 | }
71 | }
72 |
73 | if self.isISOAuto {
74 | self.currentISO = Float(device.iso)
75 | }
76 |
77 | if self.isWhiteBalanceAuto {
78 | // White balance is more complex, approximate from gains if possible
79 | let gains = device.deviceWhiteBalanceGains
80 | // This is a rough approximation
81 | let averageGain = (gains.redGain + gains.greenGain + gains.blueGain) / 3.0
82 | let estimatedTemp = 3000 + (7000 * (averageGain - 1.0) / (device.maxWhiteBalanceGain - 1.0))
83 | self.currentWhiteBalance = Float(max(2000, min(9000, estimatedTemp)))
84 | }
85 |
86 | if self.isEVAuto {
87 | self.currentEV = Float(device.exposureTargetBias)
88 | }
89 |
90 | if self.isFocusAuto && device.isAdjustingFocus == false {
91 | // Update focus position estimate (0.0 = far, 1.0 = near)
92 | // This is a rough approximation as iOS doesn't expose exact lens position
93 | self.currentFocus = Float(device.lensPosition)
94 | }
95 | }
96 | }
97 |
98 | func checkPermissions() {
99 | switch AVCaptureDevice.authorizationStatus(for: .video) {
100 | case .authorized:
101 | isAuthorized = true
102 | setupCamera()
103 | case .notDetermined:
104 | AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
105 | DispatchQueue.main.async {
106 | self?.isAuthorized = granted
107 | if granted {
108 | self?.setupCamera()
109 | }
110 | }
111 | }
112 | default:
113 | isAuthorized = false
114 | }
115 | }
116 |
117 | func setupCamera() {
118 | guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
119 | print("No wide-angle camera available")
120 | return
121 | }
122 |
123 | currentDevice = device
124 |
125 | do {
126 | session.beginConfiguration()
127 | session.sessionPreset = .photo
128 |
129 | let input = try AVCaptureDeviceInput(device: device)
130 | if session.canAddInput(input) {
131 | session.addInput(input)
132 | }
133 |
134 | if session.canAddOutput(photoOutput) {
135 | session.addOutput(photoOutput)
136 | }
137 |
138 | let formats = device.formats.filter { format in
139 | let dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription)
140 | print("Format dimensions: \(dimensions.width)x\(dimensions.height)")
141 | return dimensions.width >= 8000
142 | }
143 |
144 | if let bestPhotoFormat = formats.max(by: { format1, format2 in
145 | let dim1 = CMVideoFormatDescriptionGetDimensions(format1.formatDescription)
146 | let dim2 = CMVideoFormatDescriptionGetDimensions(format2.formatDescription)
147 | return dim1.width * dim1.height < dim2.width * dim2.height
148 | }) {
149 | let selectedDim = CMVideoFormatDescriptionGetDimensions(bestPhotoFormat.formatDescription)
150 | print("Selected format dimensions: \(selectedDim.width)x\(selectedDim.height)")
151 | try device.lockForConfiguration()
152 | device.activeFormat = bestPhotoFormat
153 | device.unlockForConfiguration()
154 | photoOutput.maxPhotoDimensions = selectedDim
155 | } else {
156 | print("No format found with required resolution")
157 | }
158 |
159 | let rawFormats = photoOutput.availableRawPhotoPixelFormatTypes
160 | print("Available RAW formats: \(rawFormats)")
161 | if rawFormats.isEmpty {
162 | print("Warning: No RAW formats available. Check device format and photo output configuration. This may be temporary or due to settings.")
163 | } else {
164 | print("RAW formats detected: \(rawFormats.count) formats available.")
165 | }
166 |
167 | session.commitConfiguration()
168 |
169 | let layer = AVCaptureVideoPreviewLayer(session: session)
170 | layer.videoGravity = .resizeAspectFill
171 | DispatchQueue.main.async { [weak self] in
172 | self?.previewLayer = layer
173 | }
174 |
175 | // Initially set the device to auto mode for all settings
176 | try device.lockForConfiguration()
177 | device.exposureMode = .continuousAutoExposure
178 | device.whiteBalanceMode = .continuousAutoWhiteBalance
179 | device.focusMode = .continuousAutoFocus
180 | device.unlockForConfiguration()
181 |
182 | DispatchQueue.global(qos: .userInitiated).async { [weak self] in
183 | self?.session.startRunning()
184 | self?.startObservingCameraSettings()
185 | }
186 | } catch {
187 | self.error = error
188 | print("Setup error: \(error)")
189 | }
190 | }
191 |
192 | func toggleAutoMode(for setting: String) {
193 | guard let device = currentDevice else { return }
194 |
195 | do {
196 | try device.lockForConfiguration()
197 |
198 | switch setting {
199 | case "Shutter":
200 | isShutterAuto.toggle()
201 | if isShutterAuto {
202 | if isISOAuto {
203 | // If both shutter and ISO are auto, set exposure mode to continuousAuto
204 | device.exposureMode = .continuousAutoExposure
205 | }
206 | } else {
207 | // If shutter is manual, need to use custom exposure mode
208 | device.exposureMode = .custom
209 | // Apply current value
210 | setShutterSpeed(currentShutterSpeed)
211 | }
212 |
213 | case "ISO":
214 | isISOAuto.toggle()
215 | if isISOAuto {
216 | if isShutterAuto {
217 | // If both shutter and ISO are auto, set exposure mode to continuousAuto
218 | device.exposureMode = .continuousAutoExposure
219 | }
220 | } else {
221 | // If ISO is manual, need to use custom exposure mode
222 | device.exposureMode = .custom
223 | // Apply current value
224 | setISO(currentISO)
225 | }
226 |
227 | case "WB":
228 | isWhiteBalanceAuto.toggle()
229 | if isWhiteBalanceAuto {
230 | device.whiteBalanceMode = .continuousAutoWhiteBalance
231 | } else {
232 | device.whiteBalanceMode = .locked
233 | // Apply current value
234 | setWhiteBalance(currentWhiteBalance)
235 | }
236 |
237 | case "EV":
238 | isEVAuto.toggle()
239 | if !isEVAuto {
240 | // Apply current value
241 | setExposureCompensation(currentEV)
242 | }
243 |
244 | case "Focus":
245 | isFocusAuto.toggle()
246 | if isFocusAuto {
247 | device.focusMode = .continuousAutoFocus
248 | } else {
249 | device.focusMode = .locked
250 | // Apply current value
251 | setFocusManually(currentFocus)
252 | }
253 |
254 | default:
255 | break
256 | }
257 |
258 | device.unlockForConfiguration()
259 | } catch {
260 | print("Failed to toggle auto mode for \(setting): \(error)")
261 | }
262 | }
263 |
264 | func setFocusPoint(_ point: CGPoint, in layer: AVCaptureVideoPreviewLayer) {
265 | guard let device = currentDevice else { return }
266 |
267 | do {
268 | try device.lockForConfiguration()
269 | if device.isFocusPointOfInterestSupported {
270 | let focusPoint = layer.captureDevicePointConverted(fromLayerPoint: point)
271 | device.focusPointOfInterest = focusPoint
272 | device.focusMode = .autoFocus
273 |
274 | // Switch to auto focus mode when user taps to focus
275 | isFocusAuto = true
276 | }
277 | device.unlockForConfiguration()
278 | } catch {
279 | print("Failed to set focus point: \(error)")
280 | }
281 | }
282 |
283 | func setFocusManually(_ position: Float) {
284 | guard let device = currentDevice, !isFocusAuto else { return }
285 | do {
286 | try device.lockForConfiguration()
287 | if device.isFocusModeSupported(.locked) {
288 | // Clamp focus position between 0 and 1
289 | let clampedPosition = min(max(position, 0), 1)
290 | device.setFocusModeLocked(lensPosition: clampedPosition, completionHandler: nil)
291 | }
292 | device.unlockForConfiguration()
293 | currentFocus = position
294 | } catch {
295 | print("Failed to set manual focus: \(error)")
296 | }
297 | }
298 |
299 | func setShutterSpeed(_ shutterSpeed: Float) {
300 | guard let device = currentDevice, !isShutterAuto else { return }
301 | do {
302 | try device.lockForConfiguration()
303 | let minDuration = device.activeFormat.minExposureDuration
304 | let maxDuration = device.activeFormat.maxExposureDuration
305 | let durationSeconds = 1.0 / Double(shutterSpeed)
306 | let duration = CMTimeMakeWithSeconds(durationSeconds, preferredTimescale: 1_000_000)
307 | let clampedDuration = CMTimeClampToRange(duration, range: CMTimeRange(start: minDuration, end: maxDuration))
308 |
309 | // When setting shutter speed, preserve current ISO if it's in manual mode
310 | let isoValue = isISOAuto ? AVCaptureDevice.currentISO : currentISO
311 | device.setExposureModeCustom(duration: clampedDuration, iso: isoValue, completionHandler: nil)
312 |
313 | device.unlockForConfiguration()
314 | currentShutterSpeed = shutterSpeed
315 | } catch {
316 | print("Failed to set shutter speed: \(error)")
317 | }
318 | }
319 |
320 | func setISO(_ iso: Float) {
321 | guard let device = currentDevice, !isISOAuto else { return }
322 | do {
323 | try device.lockForConfiguration()
324 | let minISO = device.activeFormat.minISO
325 | let maxISO = device.activeFormat.maxISO
326 | let clampedISO = min(max(iso, minISO), maxISO)
327 |
328 | // When setting ISO, preserve current shutter speed if it's in manual mode
329 | let duration = isShutterAuto ? AVCaptureDevice.currentExposureDuration :
330 | CMTimeMakeWithSeconds(1.0 / Double(currentShutterSpeed), preferredTimescale: 1_000_000)
331 |
332 | device.setExposureModeCustom(duration: duration, iso: clampedISO, completionHandler: nil)
333 | device.unlockForConfiguration()
334 | currentISO = iso
335 | } catch {
336 | print("Failed to set ISO: \(error)")
337 | }
338 | }
339 |
340 | func setWhiteBalance(_ temperature: Float) {
341 | guard let device = currentDevice, !isWhiteBalanceAuto else { return }
342 | do {
343 | try device.lockForConfiguration()
344 | if device.isWhiteBalanceModeSupported(.locked) {
345 | let wbValues = AVCaptureDevice.WhiteBalanceTemperatureAndTintValues(temperature: temperature, tint: 0)
346 | var gains = device.deviceWhiteBalanceGains(for: wbValues)
347 |
348 | // Clamp gains to the supported range
349 | let maxGain = device.maxWhiteBalanceGain
350 | gains.redGain = min(max(gains.redGain, 1.0), maxGain)
351 | gains.greenGain = min(max(gains.greenGain, 1.0), maxGain)
352 | gains.blueGain = min(max(gains.blueGain, 1.0), maxGain)
353 |
354 | device.setWhiteBalanceModeLocked(with: gains, completionHandler: nil)
355 | }
356 | device.unlockForConfiguration()
357 | currentWhiteBalance = temperature
358 | } catch {
359 | print("Failed to set white balance: \(error)")
360 | }
361 | }
362 |
363 | func setExposureCompensation(_ ev: Float) {
364 | guard let device = currentDevice, !isEVAuto else { return }
365 | do {
366 | try device.lockForConfiguration()
367 | device.setExposureTargetBias(ev, completionHandler: nil)
368 | device.unlockForConfiguration()
369 | currentEV = ev
370 | } catch {
371 | print("Failed to set exposure compensation: \(error)")
372 | }
373 | }
374 |
375 | func capturePhoto() {
376 | let rawFormats = photoOutput.availableRawPhotoPixelFormatTypes
377 | let settings: AVCapturePhotoSettings
378 |
379 | print("Checking RAW formats before capture: \(rawFormats)")
380 | if !rawFormats.isEmpty {
381 | let preferredRawFormat = rawFormats.first { format in
382 | format == kCVPixelFormatType_14Bayer_RGGB ||
383 | format == kCVPixelFormatType_14Bayer_BGGR ||
384 | format == kCVPixelFormatType_14Bayer_GRBG ||
385 | format == kCVPixelFormatType_14Bayer_GBRG
386 | } ?? rawFormats[0]
387 | settings = AVCapturePhotoSettings(rawPixelFormatType: preferredRawFormat)
388 | settings.maxPhotoDimensions = photoOutput.maxPhotoDimensions
389 | print("Capturing with RAW format: \(preferredRawFormat)")
390 | } else {
391 | print("No RAW formats available at capture time, falling back to standard format at 12 MP")
392 | settings = AVCapturePhotoSettings()
393 | settings.maxPhotoDimensions = CMVideoDimensions(width: 4032, height: 3024)
394 | }
395 |
396 | photoOutput.capturePhoto(with: settings, delegate: self)
397 | }
398 |
399 | func photoOutput(_: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
400 | if let error = error {
401 | self.error = error
402 | print("Capture error: \(error)")
403 | return
404 | }
405 |
406 | guard let data = photo.fileDataRepresentation() else {
407 | print("Failed to get photo data")
408 | return
409 | }
410 |
411 | if let metadata = photo.metadata as NSDictionary? {
412 | if let width = metadata[kCGImagePropertyPixelWidth as String] as? Int,
413 | let height = metadata[kCGImagePropertyPixelHeight as String] as? Int
414 | {
415 | print("Captured photo resolution: \(width)x\(height)")
416 | }
417 | }
418 |
419 | PHPhotoLibrary.shared().performChanges({
420 | let request = PHAssetCreationRequest.forAsset()
421 | request.addResource(with: .photo, data: data, options: nil)
422 | }) { success, error in
423 | DispatchQueue.main.async {
424 | if let error = error {
425 | self.error = error
426 | print("Save error: \(error)")
427 | } else if success {
428 | print("Photo saved successfully")
429 | }
430 | }
431 | }
432 | }
433 | }
434 |
435 | struct CameraPreview: UIViewControllerRepresentable {
436 | @ObservedObject var cameraManager: CameraManager
437 | @Binding var focusPoint: CGPoint?
438 |
439 | func makeUIViewController(context _: Context) -> UIViewController {
440 | let viewController = UIViewController()
441 | return viewController
442 | }
443 |
444 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
445 | if let layer = cameraManager.previewLayer {
446 | // Remove existing layer if present
447 | uiViewController.view.layer.sublayers?.forEach { sublayer in
448 | if sublayer is AVCaptureVideoPreviewLayer {
449 | sublayer.removeFromSuperlayer()
450 | }
451 | }
452 |
453 | // Add new layer
454 | layer.frame = uiViewController.view.bounds
455 | layer.videoGravity = .resizeAspectFill
456 | uiViewController.view.layer.addSublayer(layer)
457 |
458 | // Configure tap gesture
459 | if uiViewController.view.gestureRecognizers?.isEmpty ?? true {
460 | let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:)))
461 | uiViewController.view.addGestureRecognizer(tapGesture)
462 | }
463 | }
464 | }
465 |
466 | func makeCoordinator() -> Coordinator {
467 | Coordinator(self)
468 | }
469 |
470 | class Coordinator: NSObject {
471 | var parent: CameraPreview
472 |
473 | init(_ parent: CameraPreview) {
474 | self.parent = parent
475 | }
476 |
477 | @objc func handleTap(_ gesture: UITapGestureRecognizer) {
478 | let location = gesture.location(in: gesture.view)
479 | parent.focusPoint = location
480 |
481 | // Clear focus point after a delay
482 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [self] in
483 | self.parent.focusPoint = nil
484 | }
485 |
486 | if let layer = parent.cameraManager.previewLayer {
487 | parent.cameraManager.setFocusPoint(location, in: layer)
488 | }
489 | }
490 | }
491 | }
492 |
--------------------------------------------------------------------------------