├── 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 | --------------------------------------------------------------------------------