├── .gitignore ├── Encryption.xcodeproj └── project.pbxproj ├── Encryption ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Config.swift ├── Encryption.entitlements ├── EncryptionApp.swift ├── Info.plist ├── Models │ └── Contact.swift ├── ViewModels │ └── ViewModel.swift └── Views │ ├── AddContactView.swift │ └── ContentView.swift ├── EncryptionTests ├── EncryptionTests.swift └── Info.plist ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Xcode 5 | **/*.xcodeproj/project.xcworkspace/* 6 | .build/ 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | *.xcuserdatad 24 | *.dSYM* 25 | -------------------------------------------------------------------------------- /Encryption.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F413045C2613982000F6CCA4 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = F413045B2613982000F6CCA4 /* Config.swift */; }; 11 | F4A2CBE5261294CB00B734BB /* EncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A2CBE4261294CB00B734BB /* EncryptionTests.swift */; }; 12 | F4B773B125EE9E5E0056D6AE /* EncryptionApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B773B025EE9E5E0056D6AE /* EncryptionApp.swift */; }; 13 | F4B773B325EE9E5E0056D6AE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B773B225EE9E5E0056D6AE /* ContentView.swift */; }; 14 | F4B773B525EE9E5F0056D6AE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4B773B425EE9E5F0056D6AE /* Assets.xcassets */; }; 15 | F4B773E025EEAB910056D6AE /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4B773DF25EEAB910056D6AE /* CloudKit.framework */; }; 16 | F4D52D9325FAB07300E4FF7D /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D52D9225FAB07300E4FF7D /* AddContactView.swift */; }; 17 | F4D52D9825FAB23E00E4FF7D /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4D52D9725FAB23E00E4FF7D /* Contact.swift */; }; 18 | F4D52DA025FAB3AF00E4FF7D /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B773DB25EE9E760056D6AE /* ViewModel.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | F4A2CBE7261294CB00B734BB /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = F4B773A525EE9E5E0056D6AE /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = F4B773AC25EE9E5E0056D6AE; 27 | remoteInfo = Encryption; 28 | }; 29 | /* End PBXContainerItemProxy section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | F413045B2613982000F6CCA4 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 33 | F4A2CBE2261294CB00B734BB /* EncryptionTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EncryptionTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | F4A2CBE4261294CB00B734BB /* EncryptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionTests.swift; sourceTree = ""; }; 35 | F4A2CBE6261294CB00B734BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 36 | F4B773AD25EE9E5E0056D6AE /* Encryption.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Encryption.app; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | F4B773B025EE9E5E0056D6AE /* EncryptionApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionApp.swift; sourceTree = ""; }; 38 | F4B773B225EE9E5E0056D6AE /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 39 | F4B773B425EE9E5F0056D6AE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 40 | F4B773B925EE9E5F0056D6AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | F4B773DB25EE9E760056D6AE /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; 42 | F4B773DD25EEAB4C0056D6AE /* Encryption.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Encryption.entitlements; sourceTree = ""; }; 43 | F4B773DF25EEAB910056D6AE /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 44 | F4D52D9225FAB07300E4FF7D /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; }; 45 | F4D52D9725FAB23E00E4FF7D /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; 46 | /* End PBXFileReference section */ 47 | 48 | /* Begin PBXFrameworksBuildPhase section */ 49 | F4A2CBDF261294CB00B734BB /* Frameworks */ = { 50 | isa = PBXFrameworksBuildPhase; 51 | buildActionMask = 2147483647; 52 | files = ( 53 | ); 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | F4B773AA25EE9E5E0056D6AE /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | F4B773E025EEAB910056D6AE /* CloudKit.framework in Frameworks */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | F4A2CBE3261294CB00B734BB /* EncryptionTests */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | F4A2CBE4261294CB00B734BB /* EncryptionTests.swift */, 71 | F4A2CBE6261294CB00B734BB /* Info.plist */, 72 | ); 73 | path = EncryptionTests; 74 | sourceTree = ""; 75 | }; 76 | F4B773A425EE9E5E0056D6AE = { 77 | isa = PBXGroup; 78 | children = ( 79 | F4B773AF25EE9E5E0056D6AE /* Encryption */, 80 | F4A2CBE3261294CB00B734BB /* EncryptionTests */, 81 | F4B773AE25EE9E5E0056D6AE /* Products */, 82 | F4B773DE25EEAB910056D6AE /* Frameworks */, 83 | ); 84 | sourceTree = ""; 85 | }; 86 | F4B773AE25EE9E5E0056D6AE /* Products */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | F4B773AD25EE9E5E0056D6AE /* Encryption.app */, 90 | F4A2CBE2261294CB00B734BB /* EncryptionTests.xctest */, 91 | ); 92 | name = Products; 93 | sourceTree = ""; 94 | }; 95 | F4B773AF25EE9E5E0056D6AE /* Encryption */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | F4D52D9625FAB23500E4FF7D /* Models */, 99 | F4D52D8F25FAAF9E00E4FF7D /* Views */, 100 | F4D52D9B25FAB2DC00E4FF7D /* ViewModels */, 101 | F413045B2613982000F6CCA4 /* Config.swift */, 102 | F4B773B025EE9E5E0056D6AE /* EncryptionApp.swift */, 103 | F4B773B425EE9E5F0056D6AE /* Assets.xcassets */, 104 | F4B773B925EE9E5F0056D6AE /* Info.plist */, 105 | F4B773DD25EEAB4C0056D6AE /* Encryption.entitlements */, 106 | ); 107 | path = Encryption; 108 | sourceTree = ""; 109 | }; 110 | F4B773DE25EEAB910056D6AE /* Frameworks */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | F4B773DF25EEAB910056D6AE /* CloudKit.framework */, 114 | ); 115 | name = Frameworks; 116 | sourceTree = ""; 117 | }; 118 | F4D52D8F25FAAF9E00E4FF7D /* Views */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | F4B773B225EE9E5E0056D6AE /* ContentView.swift */, 122 | F4D52D9225FAB07300E4FF7D /* AddContactView.swift */, 123 | ); 124 | path = Views; 125 | sourceTree = ""; 126 | }; 127 | F4D52D9625FAB23500E4FF7D /* Models */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | F4D52D9725FAB23E00E4FF7D /* Contact.swift */, 131 | ); 132 | path = Models; 133 | sourceTree = ""; 134 | }; 135 | F4D52D9B25FAB2DC00E4FF7D /* ViewModels */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | F4B773DB25EE9E760056D6AE /* ViewModel.swift */, 139 | ); 140 | path = ViewModels; 141 | sourceTree = ""; 142 | }; 143 | /* End PBXGroup section */ 144 | 145 | /* Begin PBXNativeTarget section */ 146 | F4A2CBE1261294CB00B734BB /* EncryptionTests */ = { 147 | isa = PBXNativeTarget; 148 | buildConfigurationList = F4A2CBEB261294CB00B734BB /* Build configuration list for PBXNativeTarget "EncryptionTests" */; 149 | buildPhases = ( 150 | F4A2CBDE261294CB00B734BB /* Sources */, 151 | F4A2CBDF261294CB00B734BB /* Frameworks */, 152 | F4A2CBE0261294CB00B734BB /* Resources */, 153 | ); 154 | buildRules = ( 155 | ); 156 | dependencies = ( 157 | F4A2CBE8261294CB00B734BB /* PBXTargetDependency */, 158 | ); 159 | name = EncryptionTests; 160 | productName = EncryptionTests; 161 | productReference = F4A2CBE2261294CB00B734BB /* EncryptionTests.xctest */; 162 | productType = "com.apple.product-type.bundle.unit-test"; 163 | }; 164 | F4B773AC25EE9E5E0056D6AE /* Encryption */ = { 165 | isa = PBXNativeTarget; 166 | buildConfigurationList = F4B773D225EE9E5F0056D6AE /* Build configuration list for PBXNativeTarget "Encryption" */; 167 | buildPhases = ( 168 | F4B773A925EE9E5E0056D6AE /* Sources */, 169 | F4B773AA25EE9E5E0056D6AE /* Frameworks */, 170 | F4B773AB25EE9E5E0056D6AE /* Resources */, 171 | ); 172 | buildRules = ( 173 | ); 174 | dependencies = ( 175 | ); 176 | name = Encryption; 177 | productName = Encryption; 178 | productReference = F4B773AD25EE9E5E0056D6AE /* Encryption.app */; 179 | productType = "com.apple.product-type.application"; 180 | }; 181 | /* End PBXNativeTarget section */ 182 | 183 | /* Begin PBXProject section */ 184 | F4B773A525EE9E5E0056D6AE /* Project object */ = { 185 | isa = PBXProject; 186 | attributes = { 187 | LastSwiftUpdateCheck = 1250; 188 | LastUpgradeCheck = 1250; 189 | TargetAttributes = { 190 | F4A2CBE1261294CB00B734BB = { 191 | CreatedOnToolsVersion = 12.5; 192 | TestTargetID = F4B773AC25EE9E5E0056D6AE; 193 | }; 194 | F4B773AC25EE9E5E0056D6AE = { 195 | CreatedOnToolsVersion = 12.5; 196 | }; 197 | }; 198 | }; 199 | buildConfigurationList = F4B773A825EE9E5E0056D6AE /* Build configuration list for PBXProject "Encryption" */; 200 | compatibilityVersion = "Xcode 9.3"; 201 | developmentRegion = en; 202 | hasScannedForEncodings = 0; 203 | knownRegions = ( 204 | en, 205 | Base, 206 | ); 207 | mainGroup = F4B773A425EE9E5E0056D6AE; 208 | productRefGroup = F4B773AE25EE9E5E0056D6AE /* Products */; 209 | projectDirPath = ""; 210 | projectRoot = ""; 211 | targets = ( 212 | F4B773AC25EE9E5E0056D6AE /* Encryption */, 213 | F4A2CBE1261294CB00B734BB /* EncryptionTests */, 214 | ); 215 | }; 216 | /* End PBXProject section */ 217 | 218 | /* Begin PBXResourcesBuildPhase section */ 219 | F4A2CBE0261294CB00B734BB /* Resources */ = { 220 | isa = PBXResourcesBuildPhase; 221 | buildActionMask = 2147483647; 222 | files = ( 223 | ); 224 | runOnlyForDeploymentPostprocessing = 0; 225 | }; 226 | F4B773AB25EE9E5E0056D6AE /* Resources */ = { 227 | isa = PBXResourcesBuildPhase; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | F4B773B525EE9E5F0056D6AE /* Assets.xcassets in Resources */, 231 | ); 232 | runOnlyForDeploymentPostprocessing = 0; 233 | }; 234 | /* End PBXResourcesBuildPhase section */ 235 | 236 | /* Begin PBXSourcesBuildPhase section */ 237 | F4A2CBDE261294CB00B734BB /* Sources */ = { 238 | isa = PBXSourcesBuildPhase; 239 | buildActionMask = 2147483647; 240 | files = ( 241 | F4A2CBE5261294CB00B734BB /* EncryptionTests.swift in Sources */, 242 | ); 243 | runOnlyForDeploymentPostprocessing = 0; 244 | }; 245 | F4B773A925EE9E5E0056D6AE /* Sources */ = { 246 | isa = PBXSourcesBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | F4D52DA025FAB3AF00E4FF7D /* ViewModel.swift in Sources */, 250 | F4B773B325EE9E5E0056D6AE /* ContentView.swift in Sources */, 251 | F4D52D9825FAB23E00E4FF7D /* Contact.swift in Sources */, 252 | F4B773B125EE9E5E0056D6AE /* EncryptionApp.swift in Sources */, 253 | F4D52D9325FAB07300E4FF7D /* AddContactView.swift in Sources */, 254 | F413045C2613982000F6CCA4 /* Config.swift in Sources */, 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | /* End PBXSourcesBuildPhase section */ 259 | 260 | /* Begin PBXTargetDependency section */ 261 | F4A2CBE8261294CB00B734BB /* PBXTargetDependency */ = { 262 | isa = PBXTargetDependency; 263 | target = F4B773AC25EE9E5E0056D6AE /* Encryption */; 264 | targetProxy = F4A2CBE7261294CB00B734BB /* PBXContainerItemProxy */; 265 | }; 266 | /* End PBXTargetDependency section */ 267 | 268 | /* Begin XCBuildConfiguration section */ 269 | F4A2CBE9261294CB00B734BB /* Debug */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | BUNDLE_LOADER = "$(TEST_HOST)"; 273 | CODE_SIGN_STYLE = Automatic; 274 | INFOPLIST_FILE = EncryptionTests/Info.plist; 275 | LD_RUNPATH_SEARCH_PATHS = ( 276 | "$(inherited)", 277 | "@executable_path/Frameworks", 278 | "@loader_path/Frameworks", 279 | ); 280 | PRODUCT_BUNDLE_IDENTIFIER = com.apple.samples.cloudkit.EncryptionTests; 281 | PRODUCT_NAME = "$(TARGET_NAME)"; 282 | SWIFT_VERSION = 5.0; 283 | TARGETED_DEVICE_FAMILY = "1,2"; 284 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Encryption.app/Encryption"; 285 | }; 286 | name = Debug; 287 | }; 288 | F4A2CBEA261294CB00B734BB /* Release */ = { 289 | isa = XCBuildConfiguration; 290 | buildSettings = { 291 | BUNDLE_LOADER = "$(TEST_HOST)"; 292 | CODE_SIGN_STYLE = Automatic; 293 | INFOPLIST_FILE = EncryptionTests/Info.plist; 294 | LD_RUNPATH_SEARCH_PATHS = ( 295 | "$(inherited)", 296 | "@executable_path/Frameworks", 297 | "@loader_path/Frameworks", 298 | ); 299 | PRODUCT_BUNDLE_IDENTIFIER = com.apple.samples.cloudkit.EncryptionTests; 300 | PRODUCT_NAME = "$(TARGET_NAME)"; 301 | SWIFT_VERSION = 5.0; 302 | TARGETED_DEVICE_FAMILY = "1,2"; 303 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Encryption.app/Encryption"; 304 | }; 305 | name = Release; 306 | }; 307 | F4B773D025EE9E5F0056D6AE /* Debug */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ALWAYS_SEARCH_USER_PATHS = NO; 311 | CLANG_ANALYZER_NONNULL = YES; 312 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 313 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 314 | CLANG_CXX_LIBRARY = "libc++"; 315 | CLANG_ENABLE_MODULES = YES; 316 | CLANG_ENABLE_OBJC_ARC = YES; 317 | CLANG_ENABLE_OBJC_WEAK = YES; 318 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 319 | CLANG_WARN_BOOL_CONVERSION = YES; 320 | CLANG_WARN_COMMA = YES; 321 | CLANG_WARN_CONSTANT_CONVERSION = YES; 322 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 323 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 324 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 325 | CLANG_WARN_EMPTY_BODY = YES; 326 | CLANG_WARN_ENUM_CONVERSION = YES; 327 | CLANG_WARN_INFINITE_RECURSION = YES; 328 | CLANG_WARN_INT_CONVERSION = YES; 329 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 331 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 332 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 333 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 334 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 335 | CLANG_WARN_STRICT_PROTOTYPES = YES; 336 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 337 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 338 | CLANG_WARN_UNREACHABLE_CODE = YES; 339 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 340 | COPY_PHASE_STRIP = NO; 341 | DEBUG_INFORMATION_FORMAT = dwarf; 342 | ENABLE_STRICT_OBJC_MSGSEND = YES; 343 | ENABLE_TESTABILITY = YES; 344 | GCC_C_LANGUAGE_STANDARD = gnu11; 345 | GCC_DYNAMIC_NO_PIC = NO; 346 | GCC_NO_COMMON_BLOCKS = YES; 347 | GCC_OPTIMIZATION_LEVEL = 0; 348 | GCC_PREPROCESSOR_DEFINITIONS = ( 349 | "DEBUG=1", 350 | "$(inherited)", 351 | ); 352 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 353 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 354 | GCC_WARN_UNDECLARED_SELECTOR = YES; 355 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 356 | GCC_WARN_UNUSED_FUNCTION = YES; 357 | GCC_WARN_UNUSED_VARIABLE = YES; 358 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 359 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 360 | MTL_FAST_MATH = YES; 361 | ONLY_ACTIVE_ARCH = YES; 362 | SDKROOT = iphoneos; 363 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 364 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 365 | }; 366 | name = Debug; 367 | }; 368 | F4B773D125EE9E5F0056D6AE /* Release */ = { 369 | isa = XCBuildConfiguration; 370 | buildSettings = { 371 | ALWAYS_SEARCH_USER_PATHS = NO; 372 | CLANG_ANALYZER_NONNULL = YES; 373 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 374 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 375 | CLANG_CXX_LIBRARY = "libc++"; 376 | CLANG_ENABLE_MODULES = YES; 377 | CLANG_ENABLE_OBJC_ARC = YES; 378 | CLANG_ENABLE_OBJC_WEAK = YES; 379 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 380 | CLANG_WARN_BOOL_CONVERSION = YES; 381 | CLANG_WARN_COMMA = YES; 382 | CLANG_WARN_CONSTANT_CONVERSION = YES; 383 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 384 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 385 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 386 | CLANG_WARN_EMPTY_BODY = YES; 387 | CLANG_WARN_ENUM_CONVERSION = YES; 388 | CLANG_WARN_INFINITE_RECURSION = YES; 389 | CLANG_WARN_INT_CONVERSION = YES; 390 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 391 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 392 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 393 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 394 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 395 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 396 | CLANG_WARN_STRICT_PROTOTYPES = YES; 397 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 398 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 399 | CLANG_WARN_UNREACHABLE_CODE = YES; 400 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 401 | COPY_PHASE_STRIP = NO; 402 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 403 | ENABLE_NS_ASSERTIONS = NO; 404 | ENABLE_STRICT_OBJC_MSGSEND = YES; 405 | GCC_C_LANGUAGE_STANDARD = gnu11; 406 | GCC_NO_COMMON_BLOCKS = YES; 407 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 408 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 409 | GCC_WARN_UNDECLARED_SELECTOR = YES; 410 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 411 | GCC_WARN_UNUSED_FUNCTION = YES; 412 | GCC_WARN_UNUSED_VARIABLE = YES; 413 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 414 | MTL_ENABLE_DEBUG_INFO = NO; 415 | MTL_FAST_MATH = YES; 416 | SDKROOT = iphoneos; 417 | SWIFT_COMPILATION_MODE = wholemodule; 418 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 419 | VALIDATE_PRODUCT = YES; 420 | }; 421 | name = Release; 422 | }; 423 | F4B773D325EE9E5F0056D6AE /* Debug */ = { 424 | isa = XCBuildConfiguration; 425 | buildSettings = { 426 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 427 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 428 | CODE_SIGN_ENTITLEMENTS = Encryption/Encryption.entitlements; 429 | CODE_SIGN_STYLE = Automatic; 430 | DEVELOPMENT_TEAM = ""; 431 | ENABLE_PREVIEWS = YES; 432 | INFOPLIST_FILE = Encryption/Info.plist; 433 | LD_RUNPATH_SEARCH_PATHS = ( 434 | "$(inherited)", 435 | "@executable_path/Frameworks", 436 | ); 437 | PRODUCT_BUNDLE_IDENTIFIER = com.apple.samples.cloudkit.encryption; 438 | PRODUCT_NAME = "$(TARGET_NAME)"; 439 | SWIFT_VERSION = 5.0; 440 | TARGETED_DEVICE_FAMILY = "1,2"; 441 | }; 442 | name = Debug; 443 | }; 444 | F4B773D425EE9E5F0056D6AE /* Release */ = { 445 | isa = XCBuildConfiguration; 446 | buildSettings = { 447 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 448 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 449 | CODE_SIGN_ENTITLEMENTS = Encryption/Encryption.entitlements; 450 | CODE_SIGN_STYLE = Automatic; 451 | DEVELOPMENT_TEAM = ""; 452 | ENABLE_PREVIEWS = YES; 453 | INFOPLIST_FILE = Encryption/Info.plist; 454 | LD_RUNPATH_SEARCH_PATHS = ( 455 | "$(inherited)", 456 | "@executable_path/Frameworks", 457 | ); 458 | PRODUCT_BUNDLE_IDENTIFIER = com.apple.samples.cloudkit.encryption; 459 | PRODUCT_NAME = "$(TARGET_NAME)"; 460 | SWIFT_VERSION = 5.0; 461 | TARGETED_DEVICE_FAMILY = "1,2"; 462 | }; 463 | name = Release; 464 | }; 465 | /* End XCBuildConfiguration section */ 466 | 467 | /* Begin XCConfigurationList section */ 468 | F4A2CBEB261294CB00B734BB /* Build configuration list for PBXNativeTarget "EncryptionTests" */ = { 469 | isa = XCConfigurationList; 470 | buildConfigurations = ( 471 | F4A2CBE9261294CB00B734BB /* Debug */, 472 | F4A2CBEA261294CB00B734BB /* Release */, 473 | ); 474 | defaultConfigurationIsVisible = 0; 475 | defaultConfigurationName = Release; 476 | }; 477 | F4B773A825EE9E5E0056D6AE /* Build configuration list for PBXProject "Encryption" */ = { 478 | isa = XCConfigurationList; 479 | buildConfigurations = ( 480 | F4B773D025EE9E5F0056D6AE /* Debug */, 481 | F4B773D125EE9E5F0056D6AE /* Release */, 482 | ); 483 | defaultConfigurationIsVisible = 0; 484 | defaultConfigurationName = Release; 485 | }; 486 | F4B773D225EE9E5F0056D6AE /* Build configuration list for PBXNativeTarget "Encryption" */ = { 487 | isa = XCConfigurationList; 488 | buildConfigurations = ( 489 | F4B773D325EE9E5F0056D6AE /* Debug */, 490 | F4B773D425EE9E5F0056D6AE /* Release */, 491 | ); 492 | defaultConfigurationIsVisible = 0; 493 | defaultConfigurationName = Release; 494 | }; 495 | /* End XCConfigurationList section */ 496 | }; 497 | rootObject = F4B773A525EE9E5E0056D6AE /* Project object */; 498 | } 499 | -------------------------------------------------------------------------------- /Encryption/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 | -------------------------------------------------------------------------------- /Encryption/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Encryption/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Encryption/Config.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Config.swift 3 | // Encryption 4 | // 5 | 6 | enum Config { 7 | /// iCloud container identifier. 8 | /// Update this if you wish to use your own iCloud container. 9 | static let containerIdentifier = "iCloud.com.apple.samples.cloudkit.encryption" 10 | } 11 | -------------------------------------------------------------------------------- /Encryption/Encryption.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.icloud-container-identifiers 8 | 9 | iCloud.com.apple.samples.cloudkit.encryption 10 | 11 | com.apple.developer.icloud-services 12 | 13 | CloudKit 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Encryption/EncryptionApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EncryptionApp.swift 3 | // (cloudkit-samples) Encryption 4 | // 5 | 6 | import SwiftUI 7 | 8 | @main 9 | struct EncryptionApp: App { 10 | var body: some Scene { 11 | WindowGroup { 12 | ContentView().environmentObject(ViewModel()) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Encryption/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Encryption/Models/Contact.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Contact.swift 3 | // (cloudkit-samples) Encryption 4 | // 5 | 6 | import Foundation 7 | import CloudKit 8 | 9 | struct Contact: Identifiable { 10 | let id: String 11 | let name: String 12 | let phoneNumber: String 13 | let associatedRecord: CKRecord 14 | } 15 | 16 | extension Contact { 17 | /// Initializes a `Contact` object from a CloudKit record. 18 | /// - Parameter record: CloudKit record to pull values from. 19 | init?(record: CKRecord) { 20 | guard let name = record["name"] as? String, 21 | let phoneNumber = record.encryptedValues["phoneNumber"] as? String else { 22 | return nil 23 | } 24 | 25 | self.id = record.recordID.recordName 26 | self.name = name 27 | self.phoneNumber = phoneNumber 28 | self.associatedRecord = record 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Encryption/ViewModels/ViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModel.swift 3 | // (cloudkit-samples) Encryption 4 | // 5 | 6 | import Foundation 7 | import CloudKit 8 | import OSLog 9 | 10 | @MainActor 11 | final class ViewModel: ObservableObject { 12 | 13 | // MARK: - State 14 | 15 | enum State { 16 | case idle 17 | case loading 18 | case loaded(contacts: [Contact]) 19 | case error(Error) 20 | } 21 | 22 | // MARK: - Properties 23 | 24 | /// State directly observable by our view. 25 | @Published private(set) var state = State.idle 26 | /// Use the specified iCloud container ID, which should also be present in the entitlements file. 27 | private lazy var container = CKContainer(identifier: Config.containerIdentifier) 28 | /// This project uses the user's private database. 29 | private lazy var database = container.privateCloudDatabase 30 | /// This project uses custom record zone. 31 | let recordZone = CKRecordZone(zoneName: "EncryptedContacts") 32 | 33 | // MARK: - API 34 | 35 | nonisolated init() {} 36 | 37 | /// Initializes the ViewModel, preparing for CloudKit interaction. 38 | func initialize() async throws { 39 | state = .loading 40 | 41 | do { 42 | try await createZoneIfNeeded() 43 | } catch { 44 | state = .error(error) 45 | } 46 | } 47 | 48 | /// Fetches contacts from the database and updates local state. 49 | func refresh() async throws { 50 | state = .loading 51 | 52 | do { 53 | let contacts = try await fetchContacts() 54 | state = .loaded(contacts: contacts) 55 | } catch { 56 | state = .error(error) 57 | } 58 | } 59 | 60 | /// Fetches records from iCloud Database and returns converted Contacts. 61 | func fetchContacts() async throws -> [Contact] { 62 | let changes = try await database.recordZoneChanges(inZoneWith: recordZone.zoneID, since: nil) 63 | 64 | /// Map new/changed records to `Contact` objects. 65 | let contacts = changes.modificationResultsByID.values 66 | .compactMap { try? $0.get().record } 67 | .compactMap { Contact(record: $0) } 68 | 69 | return contacts 70 | } 71 | 72 | /// Adds a new Contact to the database, using `encryptedValues` to encrypt the Contact's phone number. 73 | /// - Parameters: 74 | /// - name: Name of the Contact. 75 | /// - phoneNumber: Phone number of the contact which will be stored in an encrypted field. 76 | /// - Returns: The newly created Contact. 77 | func addContact(name: String, phoneNumber: String) async throws -> Contact? { 78 | let record = CKRecord(recordType: "Contact", recordID: CKRecord.ID(zoneID: recordZone.zoneID)) 79 | record["name"] = name 80 | record.encryptedValues["phoneNumber"] = phoneNumber 81 | 82 | do { 83 | let savedRecord = try await database.save(record) 84 | return Contact(record: savedRecord) 85 | } catch { 86 | handleError(error) 87 | throw error 88 | } 89 | } 90 | 91 | /// Deletes a given list of Contacts from the database. 92 | /// - Parameters: 93 | /// - contacts: Contacts to delete. 94 | func deleteContacts(_ contacts: [Contact]) async throws { 95 | let recordIDs = contacts.map { $0.associatedRecord.recordID } 96 | guard !recordIDs.isEmpty else { 97 | debugPrint("Attempted to delete empty array of Contacts. Skipping.") 98 | return 99 | } 100 | 101 | do { 102 | _ = try await database.modifyRecords(saving: [], deleting: recordIDs) 103 | } catch { 104 | handleError(error) 105 | } 106 | } 107 | 108 | /// Creates the custom zone in use if needed. 109 | private func createZoneIfNeeded() async throws { 110 | // Avoid the operation if this has already been done. 111 | guard !UserDefaults.standard.bool(forKey: "isZoneCreated") else { 112 | return 113 | } 114 | 115 | do { 116 | _ = try await database.modifyRecordZones(saving: [recordZone], deleting: []) 117 | } catch { 118 | print("ERROR: Failed to create custom zone: \(error.localizedDescription)") 119 | throw error 120 | } 121 | 122 | UserDefaults.standard.setValue(true, forKey: "isZoneCreated") 123 | } 124 | 125 | private func handleError(_ error: Error) { 126 | guard let ckerror = error as? CKError else { 127 | os_log("Not a CKError: \(error.localizedDescription)") 128 | return 129 | } 130 | 131 | switch ckerror.code { 132 | case .zoneNotFound: 133 | if ckerror.userInfo[CKErrorUserDidResetEncryptedDataKey] != nil { 134 | // CloudKit is unable to decrypt previously encrypted data. This occurs when a user 135 | // resets their iCloud Keychain and thus deletes the key material previously used 136 | // to encrypt and decrypt their encrypted fields stored via CloudKit. 137 | // In this case, it is recommended to delete the associated zone and re-upload any 138 | // locally cached data, which will be encrypted with the new key. 139 | os_log("Encryption key has been reset by user.") 140 | } 141 | 142 | case .partialFailure: 143 | // Iterate through error(s) in partial failure and report each one. 144 | let dict = ckerror.userInfo[CKPartialErrorsByItemIDKey] as? [NSObject: CKError] 145 | if let errorDictionary = dict { 146 | for (_, error) in errorDictionary { 147 | os_log("An error occurred: \(error.localizedDescription)") 148 | } 149 | } 150 | 151 | default: 152 | os_log("CKError: Code \(ckerror.code.rawValue): \(ckerror.localizedDescription)") 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Encryption/Views/AddContactView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddContactView.swift 3 | // (cloudkit-samples) Encryption 4 | // 5 | 6 | import Foundation 7 | import SwiftUI 8 | 9 | /// View for adding new contacts. 10 | struct AddContactView: View { 11 | @State private var nameInput: String = "" 12 | @State private var phoneInput: String = "" 13 | 14 | /// Callback after user selects to add contact with given name and image. 15 | let onAdd: ((String, String) async throws -> Void)? 16 | /// Callback after user cancels. 17 | let onCancel: (() -> Void)? 18 | 19 | var body: some View { 20 | NavigationView { 21 | VStack { 22 | TextField("Full Name", text: $nameInput) 23 | .textContentType(.name) 24 | TextField("Phone Number", text: $phoneInput) 25 | .textContentType(.telephoneNumber) 26 | Spacer() 27 | } 28 | .padding() 29 | .navigationTitle("Add Contact") 30 | .toolbar { 31 | ToolbarItem(placement: .cancellationAction) { 32 | Button("Cancel", action: { onCancel?() }) 33 | } 34 | ToolbarItem(placement: .confirmationAction) { 35 | Button("Save", action: { Task { try await onAdd?(nameInput, phoneInput) } }) 36 | .disabled(nameInput.isEmpty || phoneInput.isEmpty) 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Encryption/Views/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // (cloudkit-samples) Encryption 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct ContentView: View { 9 | @EnvironmentObject var vm: ViewModel 10 | 11 | @State var nameInput: String = "" 12 | @State var isAddingContact: Bool = false 13 | 14 | var body: some View { 15 | NavigationView { 16 | contentView.sheet(isPresented: $isAddingContact) { 17 | AddContactView(onAdd: addContact, onCancel: { isAddingContact = false }) 18 | } 19 | .navigationTitle("Encrypted Contacts") 20 | .toolbar { 21 | ToolbarItem(placement: .navigationBarLeading) { 22 | Button { Task { try await vm.refresh() } } label: { Image(systemName: "arrow.clockwise") } 23 | } 24 | ToolbarItem(placement: .navigationBarTrailing) { 25 | Button { isAddingContact = true } label: { Image(systemName: "plus") } 26 | } 27 | } 28 | }.onAppear { 29 | Task { 30 | try await vm.initialize() 31 | try await vm.refresh() 32 | } 33 | } 34 | } 35 | 36 | private var contentView: some View { 37 | let listView = List { 38 | switch vm.state { 39 | case .loaded(contacts: let contacts): 40 | ForEach(contacts) { contact in 41 | VStack(alignment: .leading) { 42 | Text(contact.name) 43 | Text(contact.phoneNumber) 44 | .textContentType(.telephoneNumber) 45 | .font(.footnote) 46 | } 47 | }.onDelete(perform: deleteContacts(at:)) 48 | 49 | case .loading: 50 | ProgressView() 51 | 52 | case .error(let error): 53 | Text("An error occurred: \(error.localizedDescription)") 54 | 55 | case .idle: 56 | EmptyView() 57 | } 58 | } 59 | 60 | return AnyView(listView) 61 | } 62 | 63 | private func addContact(name: String, phoneNumber: String) async throws { 64 | isAddingContact = false 65 | 66 | _ = try await vm.addContact(name: name, phoneNumber: phoneNumber) 67 | try await vm.refresh() 68 | } 69 | 70 | private func deleteContacts(at indexSet: IndexSet) { 71 | guard case .loaded(let contacts) = vm.state else { 72 | debugPrint("Tried to delete contacts without loaded VM state.") 73 | return 74 | } 75 | 76 | // Get set of Contacts based on indexSet argument. 77 | let contactsToDelete = contacts.enumerated() 78 | .filter { index, _ in indexSet.contains(index) } 79 | .map { _, contact in contact } 80 | 81 | Task { 82 | try await vm.deleteContacts(contactsToDelete) 83 | try await vm.refresh() 84 | } 85 | } 86 | } 87 | 88 | struct ContentView_Previews: PreviewProvider { 89 | static var previews: some View { 90 | ContentView().environmentObject(ViewModel()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /EncryptionTests/EncryptionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EncryptionTests.swift 3 | // EncryptionTests 4 | // 5 | 6 | import XCTest 7 | import CloudKit 8 | @testable import Encryption 9 | 10 | class EncryptionTests: XCTestCase { 11 | 12 | // MARK: - Properties 13 | 14 | let viewModel = ViewModel() 15 | 16 | var contactsToDelete: [Contact] = [] 17 | 18 | // MARK: - Setup & Tear-down 19 | 20 | override func setUp() { 21 | let expectation = self.expectation(description: "Expect initialization completed") 22 | 23 | Task { 24 | try await viewModel.initialize() 25 | expectation.fulfill() 26 | } 27 | 28 | waitForExpectations(timeout: 10) 29 | } 30 | 31 | override func tearDown() { 32 | guard !contactsToDelete.isEmpty else { 33 | return 34 | } 35 | 36 | let deleteExpectation = expectation(description: "Expect Contacts created during tests to delete.") 37 | 38 | Task { 39 | try await viewModel.deleteContacts(contactsToDelete) 40 | deleteExpectation.fulfill() 41 | } 42 | 43 | waitForExpectations(timeout: 10) 44 | } 45 | 46 | // MARK: - Tests 47 | 48 | func test_CloudKitReadiness() async throws { 49 | // Fetch zones from the Private Database of the CKContainer for the current user to test for valid/ready state 50 | let container = CKContainer(identifier: Config.containerIdentifier) 51 | let database = container.privateCloudDatabase 52 | 53 | do { 54 | _ = try await database.allRecordZones() 55 | } catch let error as CKError { 56 | switch error.code { 57 | case .badContainer, .badDatabase: 58 | XCTFail("Create or select a CloudKit container in this app target's Signing & Capabilities in Xcode") 59 | 60 | case .permissionFailure, .notAuthenticated: 61 | XCTFail("Simulator or device running this app needs a signed-in iCloud account") 62 | 63 | default: 64 | XCTFail("CKError: \(error)") 65 | } 66 | } 67 | } 68 | 69 | func testCreatingAndFetchingContact() async throws { 70 | do { 71 | let contact = try await viewModel.addContact(name: "TestContact-\(UUID().uuidString)", phoneNumber: "555-123-4567") 72 | 73 | guard let contact = contact else { 74 | XCTFail("Contact returned from addContact was nil.") 75 | return 76 | } 77 | 78 | contactsToDelete.append(contact) 79 | 80 | let contacts = try await viewModel.fetchContacts() 81 | guard let foundContact = contacts.first(where: { $0.id == contact.id }) else { 82 | XCTFail("Created contact not found in subsequent fetch.") 83 | return 84 | } 85 | 86 | XCTAssert(foundContact.phoneNumber == contact.phoneNumber, 87 | "Fetched Contact number does not match created Contact number.") 88 | } catch { 89 | XCTFail("Failed to create new contact: \(error)") 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /EncryptionTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2022 Apple Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudKit Samples: Encryption 2 | 3 | ### Goals 4 | 5 | This project demonstrates using encrypted values with CloudKit and iCloud containers. CloudKit encrypts data with key material stored in a customer’s iCloud Keychain. If a customer loses access to their iCloud Keychain, CloudKit cannot access the key material previously used to encrypt data stored in the cloud, meaning that data can no longer be decrypted and accessed by the customer. More information about this is covered in the “Error Handling” section below. 6 | 7 | ### Prerequisites 8 | 9 | * A Mac with [Xcode 12](https://developer.apple.com/xcode/) (or later) installed is required to build and test this project. 10 | * An active [Apple Developer Program membership](https://developer.apple.com/support/compare-memberships/) is needed to create a CloudKit container. 11 | 12 | ### Setup Instructions 13 | 14 | * Ensure the simulator or device you run the project on is signed in to an Apple ID account with iCloud enabled. This can be done in the Settings app. 15 | * If you wish to run the app on a device, ensure the correct developer team is selected in the “Signing & Capabilities” tab of the Encryption app target, and a valid iCloud container is selected under the “iCloud” section. 16 | 17 | #### Using Your Own iCloud Container 18 | 19 | * Create a new iCloud container through Xcode’s “Signing & Capabilities” tab of the Queries app target. 20 | * Update the `containerIdentifier` property in [Config.swift](Encryption/Config.swift) with your new iCloud container ID. 21 | 22 | ### How it Works 23 | 24 | This project only differs very slightly from other samples, in that it uses the `encryptedValues` property of [`CKRecord`](https://developer.apple.com/documentation/cloudkit/ckrecord) in two places. 25 | 26 | Setting the `phoneNumber` value in ViewModel.swift `addContact`: 27 | ```swift 28 | contactRecord.encryptedValues["phoneNumber"] = phoneNumber 29 | ``` 30 | 31 | …and retrieving the `phoneNumber` value (in Contact.swift `Contact.init(record:)`): 32 | ```swift 33 | let phoneNumber = record.encryptedValues["phoneNumber"] as? String 34 | ``` 35 | 36 | You can confirm that the value is encrypted by viewing the schema in [CloudKit Dashboard](https://icloud.developer.apple.com) and confirming that the `phoneNumber` custom field under the Contact type shows “Encrypted Bytes” for its “Field Type”. 37 | 38 | ### Notes on Encrypted Fields 39 | 40 | * Encrypted fields cannot have indexes. 41 | * Existing fields in a CloudKit schema are not eligible for encryption. 42 | * `CKReference` fields cannot be encrypted. 43 | * `CKAsset` fields are encrypted by default, and therefore should not be set as `encryptedValues` fields. 44 | * `CKRecordID`, `CKRecordZoneID` or any other data types that is not one of `NSString`, `NSNumber`, `NSDate`, `NSData`, `CLLocation` and `NSArray` cannot be set as `encryptedValues` fields. 45 | 46 | ### Error Handling 47 | 48 | * As described above, CloudKit encrypts data with key material store in a customer’s iCloud Keychain. If this key material is lost, for example by a customer resetting their iCloud Keychain, CloudKit is unable to decrypt previously encrypted data and returns a specific error code. 49 | * This is demonstrated in the `handleError` function, where a `CKError` with a `zoneNotFound` code may have a `CKErrorUserDidResetEncryptedDataKey` `NSNumber` value in the `userInfo` dictionary. 50 | * It is outside the scope of this sample, but it is recommended when encountering this error to first delete the relevant zone(s), re-create them, and then re-upload locally-cached data from the device to those zones. This new data is encrypted using the new key material from the user’s iCloud Keychain. 51 | 52 | ### Things To Learn 53 | 54 | * Creating, fetching from, and saving to a custom zone. 55 | * Saving and retrieving encrypted values in a record in the remote Private Database. 56 | * Handling errors specifically related to using Encrypted Fields. 57 | * Using XCTest to asynchronously test creating new temporary records, fetching records, and cleaning up records created during tests with `tearDown` functions. 58 | 59 | ### Note on Swift Concurrency 60 | 61 | This project uses Swift concurrency APIs. A prior `completionHandler`-based implementation has been tagged [`pre-async`](https://github.com/apple/cloudkit-sample-encryption/tree/pre-async). 62 | 63 | ### Further Reading 64 | 65 | * [Encrypting User Data](https://developer.apple.com/documentation/cloudkit/encrypting_user_data) 66 | --------------------------------------------------------------------------------