From 8690051518da7721a13e1d3dda1ffca8739da06c Mon Sep 17 00:00:00 2001 From: Daniel Bauke Date: Wed, 13 Jun 2018 18:02:15 +0200 Subject: [PATCH 01/21] Added iOS target --- CloudKitCodable-iOS/CloudKitCodable_iOS.h | 19 ++ CloudKitCodable-iOS/Info.plist | 24 ++ .../CloudKitCodable_iOSTests.swift | 36 +++ CloudKitCodable-iOSTests/Info.plist | 22 ++ CloudKitCodable.xcodeproj/project.pbxproj | 269 ++++++++++++++++++ .../xcschemes/CloudKitCodable-iOS.xcscheme | 99 +++++++ 6 files changed, 469 insertions(+) create mode 100644 CloudKitCodable-iOS/CloudKitCodable_iOS.h create mode 100644 CloudKitCodable-iOS/Info.plist create mode 100644 CloudKitCodable-iOSTests/CloudKitCodable_iOSTests.swift create mode 100644 CloudKitCodable-iOSTests/Info.plist create mode 100644 CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable-iOS.xcscheme diff --git a/CloudKitCodable-iOS/CloudKitCodable_iOS.h b/CloudKitCodable-iOS/CloudKitCodable_iOS.h new file mode 100644 index 0000000..d07ad01 --- /dev/null +++ b/CloudKitCodable-iOS/CloudKitCodable_iOS.h @@ -0,0 +1,19 @@ +// +// CloudKitCodable_iOS.h +// CloudKitCodable-iOS +// +// Created by Daniel Bauke on 13.06.18. +// Copyright © 2018 Guilherme Rambo. All rights reserved. +// + +#import + +//! Project version number for CloudKitCodable_iOS. +FOUNDATION_EXPORT double CloudKitCodable_iOSVersionNumber; + +//! Project version string for CloudKitCodable_iOS. +FOUNDATION_EXPORT const unsigned char CloudKitCodable_iOSVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/CloudKitCodable-iOS/Info.plist b/CloudKitCodable-iOS/Info.plist new file mode 100644 index 0000000..1007fd9 --- /dev/null +++ b/CloudKitCodable-iOS/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/CloudKitCodable-iOSTests/CloudKitCodable_iOSTests.swift b/CloudKitCodable-iOSTests/CloudKitCodable_iOSTests.swift new file mode 100644 index 0000000..91e8f2e --- /dev/null +++ b/CloudKitCodable-iOSTests/CloudKitCodable_iOSTests.swift @@ -0,0 +1,36 @@ +// +// CloudKitCodable_iOSTests.swift +// CloudKitCodable-iOSTests +// +// Created by Daniel Bauke on 13.06.18. +// Copyright © 2018 Guilherme Rambo. All rights reserved. +// + +import XCTest +@testable import CloudKitCodable_iOS + +class CloudKitCodable_iOSTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/CloudKitCodable-iOSTests/Info.plist b/CloudKitCodable-iOSTests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/CloudKitCodable-iOSTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/CloudKitCodable.xcodeproj/project.pbxproj b/CloudKitCodable.xcodeproj/project.pbxproj index 53e6bf6..a821d7d 100644 --- a/CloudKitCodable.xcodeproj/project.pbxproj +++ b/CloudKitCodable.xcodeproj/project.pbxproj @@ -7,6 +7,17 @@ objects = { /* Begin PBXBuildFile section */ + 02DF660120D1772900A20812 /* CloudKitCodable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DF65F820D1772900A20812 /* CloudKitCodable.framework */; }; + 02DF660620D1772900A20812 /* CloudKitCodable_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DF660520D1772900A20812 /* CloudKitCodable_iOSTests.swift */; }; + 02DF660820D1772900A20812 /* CloudKitCodable_iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 02DF65FA20D1772900A20812 /* CloudKitCodable_iOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 02DF660F20D1774300A20812 /* Rambo.ckrecord in Resources */ = {isa = PBXBuildFile; fileRef = DD7FD01D20A72BE000F5778D /* Rambo.ckrecord */; }; + 02DF661020D1774300A20812 /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD637320A655E500CDB5B2 /* Person.swift */; }; + 02DF661120D1774300A20812 /* CloudKitRecordEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD636120A6532D00CDB5B2 /* CloudKitRecordEncoderTests.swift */; }; + 02DF661220D1774300A20812 /* CloudKitRecordDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFF920A725CF00F5778D /* CloudKitRecordDecoderTests.swift */; }; + 02DF661320D1774300A20812 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFFB20A7261D00F5778D /* TestUtils.swift */; }; + 02DF661420D1774D00A20812 /* CustomCloudKitEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD636E20A6533C00CDB5B2 /* CustomCloudKitEncodable.swift */; }; + 02DF661520D1774D00A20812 /* CloudKitRecordEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD637020A6536100CDB5B2 /* CloudKitRecordEncoder.swift */; }; + 02DF661620D1774D00A20812 /* CloudKitRecordDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFF720A7232F00F5778D /* CloudKitRecordDecoder.swift */; }; DD7FCFF820A7232F00F5778D /* CloudKitRecordDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFF720A7232F00F5778D /* CloudKitRecordDecoder.swift */; }; DD7FCFFA20A725CF00F5778D /* CloudKitRecordDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFF920A725CF00F5778D /* CloudKitRecordDecoderTests.swift */; }; DD7FCFFC20A7261D00F5778D /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFFB20A7261D00F5778D /* TestUtils.swift */; }; @@ -20,6 +31,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 02DF660220D1772900A20812 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DDCD634A20A6532D00CDB5B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 02DF65F720D1772900A20812; + remoteInfo = "CloudKitCodable-iOS"; + }; DDCD635E20A6532D00CDB5B2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DDCD634A20A6532D00CDB5B2 /* Project object */; @@ -30,6 +48,12 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 02DF65F820D1772900A20812 /* CloudKitCodable.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudKitCodable.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 02DF65FA20D1772900A20812 /* CloudKitCodable_iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CloudKitCodable_iOS.h; sourceTree = ""; }; + 02DF65FB20D1772900A20812 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 02DF660020D1772900A20812 /* CloudKitCodable-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CloudKitCodable-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 02DF660520D1772900A20812 /* CloudKitCodable_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitCodable_iOSTests.swift; sourceTree = ""; }; + 02DF660720D1772900A20812 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DD7FCFF720A7232F00F5778D /* CloudKitRecordDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitRecordDecoder.swift; sourceTree = ""; }; DD7FCFF920A725CF00F5778D /* CloudKitRecordDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitRecordDecoderTests.swift; sourceTree = ""; }; DD7FCFFB20A7261D00F5778D /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; @@ -46,6 +70,21 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 02DF65F420D1772900A20812 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 02DF65FD20D1772900A20812 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 02DF660120D1772900A20812 /* CloudKitCodable.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DDCD634F20A6532D00CDB5B2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -64,6 +103,24 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 02DF65F920D1772900A20812 /* CloudKitCodable-iOS */ = { + isa = PBXGroup; + children = ( + 02DF65FA20D1772900A20812 /* CloudKitCodable_iOS.h */, + 02DF65FB20D1772900A20812 /* Info.plist */, + ); + path = "CloudKitCodable-iOS"; + sourceTree = ""; + }; + 02DF660420D1772900A20812 /* CloudKitCodable-iOSTests */ = { + isa = PBXGroup; + children = ( + 02DF660520D1772900A20812 /* CloudKitCodable_iOSTests.swift */, + 02DF660720D1772900A20812 /* Info.plist */, + ); + path = "CloudKitCodable-iOSTests"; + sourceTree = ""; + }; DD7FD01C20A72BD700F5778D /* Fixtures */ = { isa = PBXGroup; children = ( @@ -77,6 +134,8 @@ children = ( DDCD635520A6532D00CDB5B2 /* CloudKitCodable */, DDCD636020A6532D00CDB5B2 /* CloudKitCodableTests */, + 02DF65F920D1772900A20812 /* CloudKitCodable-iOS */, + 02DF660420D1772900A20812 /* CloudKitCodable-iOSTests */, DDCD635420A6532D00CDB5B2 /* Products */, ); sourceTree = ""; @@ -86,6 +145,8 @@ children = ( DDCD635320A6532D00CDB5B2 /* CloudKitCodable.framework */, DDCD635C20A6532D00CDB5B2 /* CloudKitCodableTests.xctest */, + 02DF65F820D1772900A20812 /* CloudKitCodable.framework */, + 02DF660020D1772900A20812 /* CloudKitCodable-iOSTests.xctest */, ); name = Products; sourceTree = ""; @@ -134,6 +195,14 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + 02DF65F520D1772900A20812 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 02DF660820D1772900A20812 /* CloudKitCodable_iOS.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DDCD635020A6532D00CDB5B2 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -145,6 +214,42 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 02DF65F720D1772900A20812 /* CloudKitCodable-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 02DF660D20D1772900A20812 /* Build configuration list for PBXNativeTarget "CloudKitCodable-iOS" */; + buildPhases = ( + 02DF65F320D1772900A20812 /* Sources */, + 02DF65F420D1772900A20812 /* Frameworks */, + 02DF65F520D1772900A20812 /* Headers */, + 02DF65F620D1772900A20812 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "CloudKitCodable-iOS"; + productName = "CloudKitCodable-iOS"; + productReference = 02DF65F820D1772900A20812 /* CloudKitCodable.framework */; + productType = "com.apple.product-type.framework"; + }; + 02DF65FF20D1772900A20812 /* CloudKitCodable-iOSTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 02DF660E20D1772900A20812 /* Build configuration list for PBXNativeTarget "CloudKitCodable-iOSTests" */; + buildPhases = ( + 02DF65FC20D1772900A20812 /* Sources */, + 02DF65FD20D1772900A20812 /* Frameworks */, + 02DF65FE20D1772900A20812 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 02DF660320D1772900A20812 /* PBXTargetDependency */, + ); + name = "CloudKitCodable-iOSTests"; + productName = "CloudKitCodable-iOSTests"; + productReference = 02DF660020D1772900A20812 /* CloudKitCodable-iOSTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; DDCD635220A6532D00CDB5B2 /* CloudKitCodable */ = { isa = PBXNativeTarget; buildConfigurationList = DDCD636720A6532D00CDB5B2 /* Build configuration list for PBXNativeTarget "CloudKitCodable" */; @@ -191,6 +296,12 @@ LastUpgradeCheck = 0930; ORGANIZATIONNAME = "Guilherme Rambo"; TargetAttributes = { + 02DF65F720D1772900A20812 = { + CreatedOnToolsVersion = 9.3.1; + }; + 02DF65FF20D1772900A20812 = { + CreatedOnToolsVersion = 9.3.1; + }; DDCD635220A6532D00CDB5B2 = { CreatedOnToolsVersion = 9.3; LastSwiftMigration = 0930; @@ -214,11 +325,28 @@ targets = ( DDCD635220A6532D00CDB5B2 /* CloudKitCodable */, DDCD635B20A6532D00CDB5B2 /* CloudKitCodableTests */, + 02DF65F720D1772900A20812 /* CloudKitCodable-iOS */, + 02DF65FF20D1772900A20812 /* CloudKitCodable-iOSTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 02DF65F620D1772900A20812 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 02DF65FE20D1772900A20812 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 02DF660F20D1774300A20812 /* Rambo.ckrecord in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DDCD635120A6532D00CDB5B2 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -237,6 +365,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 02DF65F320D1772900A20812 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 02DF661620D1774D00A20812 /* CloudKitRecordDecoder.swift in Sources */, + 02DF661520D1774D00A20812 /* CloudKitRecordEncoder.swift in Sources */, + 02DF661420D1774D00A20812 /* CustomCloudKitEncodable.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 02DF65FC20D1772900A20812 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 02DF661220D1774300A20812 /* CloudKitRecordDecoderTests.swift in Sources */, + 02DF661020D1774300A20812 /* Person.swift in Sources */, + 02DF660620D1772900A20812 /* CloudKitCodable_iOSTests.swift in Sources */, + 02DF661320D1774300A20812 /* TestUtils.swift in Sources */, + 02DF661120D1774300A20812 /* CloudKitRecordEncoderTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DDCD634E20A6532D00CDB5B2 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -261,6 +411,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 02DF660320D1772900A20812 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 02DF65F720D1772900A20812 /* CloudKitCodable-iOS */; + targetProxy = 02DF660220D1772900A20812 /* PBXContainerItemProxy */; + }; DDCD635F20A6532D00CDB5B2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DDCD635220A6532D00CDB5B2 /* CloudKitCodable */; @@ -269,6 +424,102 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 02DF660920D1772900A20812 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "CloudKitCodable-iOS/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "br.com.guilhermerambo.CloudKitCodable-iOS"; + PRODUCT_NAME = CloudKitCodable; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 02DF660A20D1772900A20812 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "CloudKitCodable-iOS/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "br.com.guilhermerambo.CloudKitCodable-iOS"; + PRODUCT_NAME = CloudKitCodable; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 02DF660B20D1772900A20812 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "CloudKitCodable-iOSTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "br.com.guilhermerambo.CloudKitCodable-iOSTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 02DF660C20D1772900A20812 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "CloudKitCodable-iOSTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "br.com.guilhermerambo.CloudKitCodable-iOSTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; DDCD636520A6532D00CDB5B2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -492,6 +743,24 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 02DF660D20D1772900A20812 /* Build configuration list for PBXNativeTarget "CloudKitCodable-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 02DF660920D1772900A20812 /* Debug */, + 02DF660A20D1772900A20812 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 02DF660E20D1772900A20812 /* Build configuration list for PBXNativeTarget "CloudKitCodable-iOSTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 02DF660B20D1772900A20812 /* Debug */, + 02DF660C20D1772900A20812 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; DDCD634D20A6532D00CDB5B2 /* Build configuration list for PBXProject "CloudKitCodable" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable-iOS.xcscheme b/CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable-iOS.xcscheme new file mode 100644 index 0000000..1c9762a --- /dev/null +++ b/CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable-iOS.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c9242bf35e45cc9c7aecb782097e536989201cf1 Mon Sep 17 00:00:00 2001 From: Daniel Bauke Date: Wed, 13 Jun 2018 18:04:24 +0200 Subject: [PATCH 02/21] Dropped redundant CloudKitCodable_iOSTests.swift --- .../CloudKitCodable_iOSTests.swift | 36 ------------------- CloudKitCodable.xcodeproj/project.pbxproj | 4 --- 2 files changed, 40 deletions(-) delete mode 100644 CloudKitCodable-iOSTests/CloudKitCodable_iOSTests.swift diff --git a/CloudKitCodable-iOSTests/CloudKitCodable_iOSTests.swift b/CloudKitCodable-iOSTests/CloudKitCodable_iOSTests.swift deleted file mode 100644 index 91e8f2e..0000000 --- a/CloudKitCodable-iOSTests/CloudKitCodable_iOSTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// CloudKitCodable_iOSTests.swift -// CloudKitCodable-iOSTests -// -// Created by Daniel Bauke on 13.06.18. -// Copyright © 2018 Guilherme Rambo. All rights reserved. -// - -import XCTest -@testable import CloudKitCodable_iOS - -class CloudKitCodable_iOSTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/CloudKitCodable.xcodeproj/project.pbxproj b/CloudKitCodable.xcodeproj/project.pbxproj index a821d7d..38c2d0a 100644 --- a/CloudKitCodable.xcodeproj/project.pbxproj +++ b/CloudKitCodable.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 02DF660120D1772900A20812 /* CloudKitCodable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DF65F820D1772900A20812 /* CloudKitCodable.framework */; }; - 02DF660620D1772900A20812 /* CloudKitCodable_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DF660520D1772900A20812 /* CloudKitCodable_iOSTests.swift */; }; 02DF660820D1772900A20812 /* CloudKitCodable_iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 02DF65FA20D1772900A20812 /* CloudKitCodable_iOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; 02DF660F20D1774300A20812 /* Rambo.ckrecord in Resources */ = {isa = PBXBuildFile; fileRef = DD7FD01D20A72BE000F5778D /* Rambo.ckrecord */; }; 02DF661020D1774300A20812 /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD637320A655E500CDB5B2 /* Person.swift */; }; @@ -52,7 +51,6 @@ 02DF65FA20D1772900A20812 /* CloudKitCodable_iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CloudKitCodable_iOS.h; sourceTree = ""; }; 02DF65FB20D1772900A20812 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 02DF660020D1772900A20812 /* CloudKitCodable-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CloudKitCodable-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 02DF660520D1772900A20812 /* CloudKitCodable_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitCodable_iOSTests.swift; sourceTree = ""; }; 02DF660720D1772900A20812 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DD7FCFF720A7232F00F5778D /* CloudKitRecordDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitRecordDecoder.swift; sourceTree = ""; }; DD7FCFF920A725CF00F5778D /* CloudKitRecordDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitRecordDecoderTests.swift; sourceTree = ""; }; @@ -115,7 +113,6 @@ 02DF660420D1772900A20812 /* CloudKitCodable-iOSTests */ = { isa = PBXGroup; children = ( - 02DF660520D1772900A20812 /* CloudKitCodable_iOSTests.swift */, 02DF660720D1772900A20812 /* Info.plist */, ); path = "CloudKitCodable-iOSTests"; @@ -381,7 +378,6 @@ files = ( 02DF661220D1774300A20812 /* CloudKitRecordDecoderTests.swift in Sources */, 02DF661020D1774300A20812 /* Person.swift in Sources */, - 02DF660620D1772900A20812 /* CloudKitCodable_iOSTests.swift in Sources */, 02DF661320D1774300A20812 /* TestUtils.swift in Sources */, 02DF661120D1774300A20812 /* CloudKitRecordEncoderTests.swift in Sources */, ); From 288ea5d9e8de1f59e9280782c974c1a70d4f9793 Mon Sep 17 00:00:00 2001 From: Daniel Bauke Date: Wed, 13 Jun 2018 18:06:42 +0200 Subject: [PATCH 03/21] Added travis build for iOS --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5bd2e07..3d6703c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,5 @@ os: osx osx_image: xcode9.3 script: - xcodebuild -version - - set -o pipefail && env "NSUnbufferedIO=YES" xcodebuild test -scheme CloudKitCodable -project CloudKitCodable.xcodeproj | xcpretty -f `xcpretty-travis-formatter` \ No newline at end of file + - set -o pipefail && env "NSUnbufferedIO=YES" xcodebuild test -scheme CloudKitCodable -project CloudKitCodable.xcodeproj | xcpretty -f `xcpretty-travis-formatter` + - set -o pipefail && env "NSUnbufferedIO=YES" xcodebuild test -scheme CloudKitCodable-iOS -project CloudKitCodable.xcodeproj | xcpretty -f `xcpretty-travis-formatter` \ No newline at end of file From 9fea5281d27091b7887aff76d16a58cd7a2a1038 Mon Sep 17 00:00:00 2001 From: Daniel Bauke Date: Wed, 13 Jun 2018 19:08:36 +0200 Subject: [PATCH 04/21] Added simulator to travis iOS build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3d6703c..ab19534 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,4 @@ osx_image: xcode9.3 script: - xcodebuild -version - set -o pipefail && env "NSUnbufferedIO=YES" xcodebuild test -scheme CloudKitCodable -project CloudKitCodable.xcodeproj | xcpretty -f `xcpretty-travis-formatter` - - set -o pipefail && env "NSUnbufferedIO=YES" xcodebuild test -scheme CloudKitCodable-iOS -project CloudKitCodable.xcodeproj | xcpretty -f `xcpretty-travis-formatter` \ No newline at end of file + - set -o pipefail && env "NSUnbufferedIO=YES" xcodebuild test -scheme CloudKitCodable-iOS -project CloudKitCodable.xcodeproj -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=11.3,name=iPhone 6' | xcpretty -f `xcpretty-travis-formatter` \ No newline at end of file From 3c96239ecb5ec598c76e0a39f8428af1c470ad8f Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Sun, 10 Jul 2022 22:56:25 +1000 Subject: [PATCH 05/21] Convert the project to be a Swift package --- CloudKitCodable-iOS/CloudKitCodable_iOS.h | 19 - CloudKitCodable-iOS/Info.plist | 24 - CloudKitCodable-iOSTests/Info.plist | 22 - CloudKitCodable.xcodeproj/project.pbxproj | 790 ------------------ .../xcschemes/CloudKitCodable-iOS.xcscheme | 99 --- .../xcschemes/CloudKitCodable.xcscheme | 99 --- CloudKitCodable/CloudKitCodable.h | 19 - CloudKitCodable/Info.plist | 26 - CloudKitCodableTests/Info.plist | 22 - Package.swift | 24 + README.MD | 30 +- Sources/CloudKitCodable/CloudKitCodable.swift | 6 + .../CloudKitRecordDecoder.swift | 19 +- .../CloudKitRecordEncoder.swift | 34 +- .../CustomCloudKitEncodable.swift | 0 .../CloudKitRecordDecoderTests.swift | 4 +- .../CloudKitRecordEncoderTests.swift | 10 +- .../Fixtures/Rambo.ckrecord | Bin .../TestTypes/Person.swift | 0 .../CloudKitCodableTests}/TestUtils.swift | 36 +- 20 files changed, 110 insertions(+), 1173 deletions(-) delete mode 100644 CloudKitCodable-iOS/CloudKitCodable_iOS.h delete mode 100644 CloudKitCodable-iOS/Info.plist delete mode 100644 CloudKitCodable-iOSTests/Info.plist delete mode 100644 CloudKitCodable.xcodeproj/project.pbxproj delete mode 100644 CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable-iOS.xcscheme delete mode 100644 CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable.xcscheme delete mode 100644 CloudKitCodable/CloudKitCodable.h delete mode 100644 CloudKitCodable/Info.plist delete mode 100644 CloudKitCodableTests/Info.plist create mode 100644 Package.swift create mode 100644 Sources/CloudKitCodable/CloudKitCodable.swift rename {CloudKitCodable/Source => Sources/CloudKitCodable}/CloudKitRecordDecoder.swift (92%) rename {CloudKitCodable/Source => Sources/CloudKitCodable}/CloudKitRecordEncoder.swift (87%) rename {CloudKitCodable/Source => Sources/CloudKitCodable}/CustomCloudKitEncodable.swift (100%) rename {CloudKitCodableTests => Tests/CloudKitCodableTests}/CloudKitRecordDecoderTests.swift (91%) rename {CloudKitCodableTests => Tests/CloudKitCodableTests}/CloudKitRecordEncoderTests.swift (81%) rename {CloudKitCodableTests => Tests/CloudKitCodableTests}/Fixtures/Rambo.ckrecord (100%) rename {CloudKitCodableTests => Tests/CloudKitCodableTests}/TestTypes/Person.swift (100%) rename {CloudKitCodableTests => Tests/CloudKitCodableTests}/TestUtils.swift (69%) diff --git a/CloudKitCodable-iOS/CloudKitCodable_iOS.h b/CloudKitCodable-iOS/CloudKitCodable_iOS.h deleted file mode 100644 index d07ad01..0000000 --- a/CloudKitCodable-iOS/CloudKitCodable_iOS.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// CloudKitCodable_iOS.h -// CloudKitCodable-iOS -// -// Created by Daniel Bauke on 13.06.18. -// Copyright © 2018 Guilherme Rambo. All rights reserved. -// - -#import - -//! Project version number for CloudKitCodable_iOS. -FOUNDATION_EXPORT double CloudKitCodable_iOSVersionNumber; - -//! Project version string for CloudKitCodable_iOS. -FOUNDATION_EXPORT const unsigned char CloudKitCodable_iOSVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/CloudKitCodable-iOS/Info.plist b/CloudKitCodable-iOS/Info.plist deleted file mode 100644 index 1007fd9..0000000 --- a/CloudKitCodable-iOS/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/CloudKitCodable-iOSTests/Info.plist b/CloudKitCodable-iOSTests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/CloudKitCodable-iOSTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/CloudKitCodable.xcodeproj/project.pbxproj b/CloudKitCodable.xcodeproj/project.pbxproj deleted file mode 100644 index 38c2d0a..0000000 --- a/CloudKitCodable.xcodeproj/project.pbxproj +++ /dev/null @@ -1,790 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 50; - objects = { - -/* Begin PBXBuildFile section */ - 02DF660120D1772900A20812 /* CloudKitCodable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DF65F820D1772900A20812 /* CloudKitCodable.framework */; }; - 02DF660820D1772900A20812 /* CloudKitCodable_iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 02DF65FA20D1772900A20812 /* CloudKitCodable_iOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 02DF660F20D1774300A20812 /* Rambo.ckrecord in Resources */ = {isa = PBXBuildFile; fileRef = DD7FD01D20A72BE000F5778D /* Rambo.ckrecord */; }; - 02DF661020D1774300A20812 /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD637320A655E500CDB5B2 /* Person.swift */; }; - 02DF661120D1774300A20812 /* CloudKitRecordEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD636120A6532D00CDB5B2 /* CloudKitRecordEncoderTests.swift */; }; - 02DF661220D1774300A20812 /* CloudKitRecordDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFF920A725CF00F5778D /* CloudKitRecordDecoderTests.swift */; }; - 02DF661320D1774300A20812 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFFB20A7261D00F5778D /* TestUtils.swift */; }; - 02DF661420D1774D00A20812 /* CustomCloudKitEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD636E20A6533C00CDB5B2 /* CustomCloudKitEncodable.swift */; }; - 02DF661520D1774D00A20812 /* CloudKitRecordEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD637020A6536100CDB5B2 /* CloudKitRecordEncoder.swift */; }; - 02DF661620D1774D00A20812 /* CloudKitRecordDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFF720A7232F00F5778D /* CloudKitRecordDecoder.swift */; }; - DD7FCFF820A7232F00F5778D /* CloudKitRecordDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFF720A7232F00F5778D /* CloudKitRecordDecoder.swift */; }; - DD7FCFFA20A725CF00F5778D /* CloudKitRecordDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFF920A725CF00F5778D /* CloudKitRecordDecoderTests.swift */; }; - DD7FCFFC20A7261D00F5778D /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FCFFB20A7261D00F5778D /* TestUtils.swift */; }; - DD7FD01E20A72BE000F5778D /* Rambo.ckrecord in Resources */ = {isa = PBXBuildFile; fileRef = DD7FD01D20A72BE000F5778D /* Rambo.ckrecord */; }; - DDCD635D20A6532D00CDB5B2 /* CloudKitCodable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDCD635320A6532D00CDB5B2 /* CloudKitCodable.framework */; }; - DDCD636220A6532D00CDB5B2 /* CloudKitRecordEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD636120A6532D00CDB5B2 /* CloudKitRecordEncoderTests.swift */; }; - DDCD636420A6532D00CDB5B2 /* CloudKitCodable.h in Headers */ = {isa = PBXBuildFile; fileRef = DDCD635620A6532D00CDB5B2 /* CloudKitCodable.h */; settings = {ATTRIBUTES = (Public, ); }; }; - DDCD636F20A6533C00CDB5B2 /* CustomCloudKitEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD636E20A6533C00CDB5B2 /* CustomCloudKitEncodable.swift */; }; - DDCD637120A6536100CDB5B2 /* CloudKitRecordEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD637020A6536100CDB5B2 /* CloudKitRecordEncoder.swift */; }; - DDCD637420A655E500CDB5B2 /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCD637320A655E500CDB5B2 /* Person.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 02DF660220D1772900A20812 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DDCD634A20A6532D00CDB5B2 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 02DF65F720D1772900A20812; - remoteInfo = "CloudKitCodable-iOS"; - }; - DDCD635E20A6532D00CDB5B2 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DDCD634A20A6532D00CDB5B2 /* Project object */; - proxyType = 1; - remoteGlobalIDString = DDCD635220A6532D00CDB5B2; - remoteInfo = CloudKitCodable; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 02DF65F820D1772900A20812 /* CloudKitCodable.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudKitCodable.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 02DF65FA20D1772900A20812 /* CloudKitCodable_iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CloudKitCodable_iOS.h; sourceTree = ""; }; - 02DF65FB20D1772900A20812 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 02DF660020D1772900A20812 /* CloudKitCodable-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CloudKitCodable-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 02DF660720D1772900A20812 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DD7FCFF720A7232F00F5778D /* CloudKitRecordDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitRecordDecoder.swift; sourceTree = ""; }; - DD7FCFF920A725CF00F5778D /* CloudKitRecordDecoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitRecordDecoderTests.swift; sourceTree = ""; }; - DD7FCFFB20A7261D00F5778D /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; - DD7FD01D20A72BE000F5778D /* Rambo.ckrecord */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = Rambo.ckrecord; sourceTree = ""; }; - DDCD635320A6532D00CDB5B2 /* CloudKitCodable.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudKitCodable.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DDCD635620A6532D00CDB5B2 /* CloudKitCodable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CloudKitCodable.h; sourceTree = ""; }; - DDCD635720A6532D00CDB5B2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DDCD635C20A6532D00CDB5B2 /* CloudKitCodableTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CloudKitCodableTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DDCD636120A6532D00CDB5B2 /* CloudKitRecordEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitRecordEncoderTests.swift; sourceTree = ""; }; - DDCD636320A6532D00CDB5B2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DDCD636E20A6533C00CDB5B2 /* CustomCloudKitEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCloudKitEncodable.swift; sourceTree = ""; }; - DDCD637020A6536100CDB5B2 /* CloudKitRecordEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitRecordEncoder.swift; sourceTree = ""; }; - DDCD637320A655E500CDB5B2 /* Person.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Person.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 02DF65F420D1772900A20812 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 02DF65FD20D1772900A20812 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 02DF660120D1772900A20812 /* CloudKitCodable.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DDCD634F20A6532D00CDB5B2 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DDCD635920A6532D00CDB5B2 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - DDCD635D20A6532D00CDB5B2 /* CloudKitCodable.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 02DF65F920D1772900A20812 /* CloudKitCodable-iOS */ = { - isa = PBXGroup; - children = ( - 02DF65FA20D1772900A20812 /* CloudKitCodable_iOS.h */, - 02DF65FB20D1772900A20812 /* Info.plist */, - ); - path = "CloudKitCodable-iOS"; - sourceTree = ""; - }; - 02DF660420D1772900A20812 /* CloudKitCodable-iOSTests */ = { - isa = PBXGroup; - children = ( - 02DF660720D1772900A20812 /* Info.plist */, - ); - path = "CloudKitCodable-iOSTests"; - sourceTree = ""; - }; - DD7FD01C20A72BD700F5778D /* Fixtures */ = { - isa = PBXGroup; - children = ( - DD7FD01D20A72BE000F5778D /* Rambo.ckrecord */, - ); - path = Fixtures; - sourceTree = ""; - }; - DDCD634920A6532D00CDB5B2 = { - isa = PBXGroup; - children = ( - DDCD635520A6532D00CDB5B2 /* CloudKitCodable */, - DDCD636020A6532D00CDB5B2 /* CloudKitCodableTests */, - 02DF65F920D1772900A20812 /* CloudKitCodable-iOS */, - 02DF660420D1772900A20812 /* CloudKitCodable-iOSTests */, - DDCD635420A6532D00CDB5B2 /* Products */, - ); - sourceTree = ""; - }; - DDCD635420A6532D00CDB5B2 /* Products */ = { - isa = PBXGroup; - children = ( - DDCD635320A6532D00CDB5B2 /* CloudKitCodable.framework */, - DDCD635C20A6532D00CDB5B2 /* CloudKitCodableTests.xctest */, - 02DF65F820D1772900A20812 /* CloudKitCodable.framework */, - 02DF660020D1772900A20812 /* CloudKitCodable-iOSTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - DDCD635520A6532D00CDB5B2 /* CloudKitCodable */ = { - isa = PBXGroup; - children = ( - DDCD636D20A6533200CDB5B2 /* Source */, - DDCD635620A6532D00CDB5B2 /* CloudKitCodable.h */, - DDCD635720A6532D00CDB5B2 /* Info.plist */, - ); - path = CloudKitCodable; - sourceTree = ""; - }; - DDCD636020A6532D00CDB5B2 /* CloudKitCodableTests */ = { - isa = PBXGroup; - children = ( - DD7FD01C20A72BD700F5778D /* Fixtures */, - DDCD637220A655D500CDB5B2 /* TestTypes */, - DDCD636120A6532D00CDB5B2 /* CloudKitRecordEncoderTests.swift */, - DD7FCFF920A725CF00F5778D /* CloudKitRecordDecoderTests.swift */, - DD7FCFFB20A7261D00F5778D /* TestUtils.swift */, - DDCD636320A6532D00CDB5B2 /* Info.plist */, - ); - path = CloudKitCodableTests; - sourceTree = ""; - }; - DDCD636D20A6533200CDB5B2 /* Source */ = { - isa = PBXGroup; - children = ( - DDCD636E20A6533C00CDB5B2 /* CustomCloudKitEncodable.swift */, - DDCD637020A6536100CDB5B2 /* CloudKitRecordEncoder.swift */, - DD7FCFF720A7232F00F5778D /* CloudKitRecordDecoder.swift */, - ); - path = Source; - sourceTree = ""; - }; - DDCD637220A655D500CDB5B2 /* TestTypes */ = { - isa = PBXGroup; - children = ( - DDCD637320A655E500CDB5B2 /* Person.swift */, - ); - path = TestTypes; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 02DF65F520D1772900A20812 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 02DF660820D1772900A20812 /* CloudKitCodable_iOS.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DDCD635020A6532D00CDB5B2 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - DDCD636420A6532D00CDB5B2 /* CloudKitCodable.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 02DF65F720D1772900A20812 /* CloudKitCodable-iOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 02DF660D20D1772900A20812 /* Build configuration list for PBXNativeTarget "CloudKitCodable-iOS" */; - buildPhases = ( - 02DF65F320D1772900A20812 /* Sources */, - 02DF65F420D1772900A20812 /* Frameworks */, - 02DF65F520D1772900A20812 /* Headers */, - 02DF65F620D1772900A20812 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "CloudKitCodable-iOS"; - productName = "CloudKitCodable-iOS"; - productReference = 02DF65F820D1772900A20812 /* CloudKitCodable.framework */; - productType = "com.apple.product-type.framework"; - }; - 02DF65FF20D1772900A20812 /* CloudKitCodable-iOSTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 02DF660E20D1772900A20812 /* Build configuration list for PBXNativeTarget "CloudKitCodable-iOSTests" */; - buildPhases = ( - 02DF65FC20D1772900A20812 /* Sources */, - 02DF65FD20D1772900A20812 /* Frameworks */, - 02DF65FE20D1772900A20812 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 02DF660320D1772900A20812 /* PBXTargetDependency */, - ); - name = "CloudKitCodable-iOSTests"; - productName = "CloudKitCodable-iOSTests"; - productReference = 02DF660020D1772900A20812 /* CloudKitCodable-iOSTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - DDCD635220A6532D00CDB5B2 /* CloudKitCodable */ = { - isa = PBXNativeTarget; - buildConfigurationList = DDCD636720A6532D00CDB5B2 /* Build configuration list for PBXNativeTarget "CloudKitCodable" */; - buildPhases = ( - DDCD634E20A6532D00CDB5B2 /* Sources */, - DDCD634F20A6532D00CDB5B2 /* Frameworks */, - DDCD635020A6532D00CDB5B2 /* Headers */, - DDCD635120A6532D00CDB5B2 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = CloudKitCodable; - productName = CloudKitCodable; - productReference = DDCD635320A6532D00CDB5B2 /* CloudKitCodable.framework */; - productType = "com.apple.product-type.framework"; - }; - DDCD635B20A6532D00CDB5B2 /* CloudKitCodableTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = DDCD636A20A6532D00CDB5B2 /* Build configuration list for PBXNativeTarget "CloudKitCodableTests" */; - buildPhases = ( - DDCD635820A6532D00CDB5B2 /* Sources */, - DDCD635920A6532D00CDB5B2 /* Frameworks */, - DDCD635A20A6532D00CDB5B2 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - DDCD635F20A6532D00CDB5B2 /* PBXTargetDependency */, - ); - name = CloudKitCodableTests; - productName = CloudKitCodableTests; - productReference = DDCD635C20A6532D00CDB5B2 /* CloudKitCodableTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - DDCD634A20A6532D00CDB5B2 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0930; - LastUpgradeCheck = 0930; - ORGANIZATIONNAME = "Guilherme Rambo"; - TargetAttributes = { - 02DF65F720D1772900A20812 = { - CreatedOnToolsVersion = 9.3.1; - }; - 02DF65FF20D1772900A20812 = { - CreatedOnToolsVersion = 9.3.1; - }; - DDCD635220A6532D00CDB5B2 = { - CreatedOnToolsVersion = 9.3; - LastSwiftMigration = 0930; - }; - DDCD635B20A6532D00CDB5B2 = { - CreatedOnToolsVersion = 9.3; - }; - }; - }; - buildConfigurationList = DDCD634D20A6532D00CDB5B2 /* Build configuration list for PBXProject "CloudKitCodable" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - ); - mainGroup = DDCD634920A6532D00CDB5B2; - productRefGroup = DDCD635420A6532D00CDB5B2 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - DDCD635220A6532D00CDB5B2 /* CloudKitCodable */, - DDCD635B20A6532D00CDB5B2 /* CloudKitCodableTests */, - 02DF65F720D1772900A20812 /* CloudKitCodable-iOS */, - 02DF65FF20D1772900A20812 /* CloudKitCodable-iOSTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 02DF65F620D1772900A20812 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 02DF65FE20D1772900A20812 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 02DF660F20D1774300A20812 /* Rambo.ckrecord in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DDCD635120A6532D00CDB5B2 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DDCD635A20A6532D00CDB5B2 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DD7FD01E20A72BE000F5778D /* Rambo.ckrecord in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 02DF65F320D1772900A20812 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 02DF661620D1774D00A20812 /* CloudKitRecordDecoder.swift in Sources */, - 02DF661520D1774D00A20812 /* CloudKitRecordEncoder.swift in Sources */, - 02DF661420D1774D00A20812 /* CustomCloudKitEncodable.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 02DF65FC20D1772900A20812 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 02DF661220D1774300A20812 /* CloudKitRecordDecoderTests.swift in Sources */, - 02DF661020D1774300A20812 /* Person.swift in Sources */, - 02DF661320D1774300A20812 /* TestUtils.swift in Sources */, - 02DF661120D1774300A20812 /* CloudKitRecordEncoderTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DDCD634E20A6532D00CDB5B2 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DD7FCFF820A7232F00F5778D /* CloudKitRecordDecoder.swift in Sources */, - DDCD637120A6536100CDB5B2 /* CloudKitRecordEncoder.swift in Sources */, - DDCD636F20A6533C00CDB5B2 /* CustomCloudKitEncodable.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DDCD635820A6532D00CDB5B2 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DD7FCFFA20A725CF00F5778D /* CloudKitRecordDecoderTests.swift in Sources */, - DDCD637420A655E500CDB5B2 /* Person.swift in Sources */, - DD7FCFFC20A7261D00F5778D /* TestUtils.swift in Sources */, - DDCD636220A6532D00CDB5B2 /* CloudKitRecordEncoderTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 02DF660320D1772900A20812 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 02DF65F720D1772900A20812 /* CloudKitCodable-iOS */; - targetProxy = 02DF660220D1772900A20812 /* PBXContainerItemProxy */; - }; - DDCD635F20A6532D00CDB5B2 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DDCD635220A6532D00CDB5B2 /* CloudKitCodable */; - targetProxy = DDCD635E20A6532D00CDB5B2 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - 02DF660920D1772900A20812 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "CloudKitCodable-iOS/Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "br.com.guilhermerambo.CloudKitCodable-iOS"; - PRODUCT_NAME = CloudKitCodable; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 02DF660A20D1772900A20812 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "CloudKitCodable-iOS/Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "br.com.guilhermerambo.CloudKitCodable-iOS"; - PRODUCT_NAME = CloudKitCodable; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 02DF660B20D1772900A20812 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "CloudKitCodable-iOSTests/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "br.com.guilhermerambo.CloudKitCodable-iOSTests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 02DF660C20D1772900A20812 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "CloudKitCodable-iOSTests/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "br.com.guilhermerambo.CloudKitCodable-iOSTests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - DDCD636520A6532D00CDB5B2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = ""; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - DDCD636620A6532D00CDB5B2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = ""; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - DDCD636820A6532D00CDB5B2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = CloudKitCodable/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.12; - PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.CloudKitCodable; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - }; - name = Debug; - }; - DDCD636920A6532D00CDB5B2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = CloudKitCodable/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.12; - PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.CloudKitCodable; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; - }; - name = Release; - }; - DDCD636B20A6532D00CDB5B2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = CloudKitCodableTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.CloudKitCodableTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 4.0; - }; - name = Debug; - }; - DDCD636C20A6532D00CDB5B2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = CloudKitCodableTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = br.com.guilhermerambo.CloudKitCodableTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 4.0; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 02DF660D20D1772900A20812 /* Build configuration list for PBXNativeTarget "CloudKitCodable-iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 02DF660920D1772900A20812 /* Debug */, - 02DF660A20D1772900A20812 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 02DF660E20D1772900A20812 /* Build configuration list for PBXNativeTarget "CloudKitCodable-iOSTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 02DF660B20D1772900A20812 /* Debug */, - 02DF660C20D1772900A20812 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - DDCD634D20A6532D00CDB5B2 /* Build configuration list for PBXProject "CloudKitCodable" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DDCD636520A6532D00CDB5B2 /* Debug */, - DDCD636620A6532D00CDB5B2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - DDCD636720A6532D00CDB5B2 /* Build configuration list for PBXNativeTarget "CloudKitCodable" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DDCD636820A6532D00CDB5B2 /* Debug */, - DDCD636920A6532D00CDB5B2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - DDCD636A20A6532D00CDB5B2 /* Build configuration list for PBXNativeTarget "CloudKitCodableTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DDCD636B20A6532D00CDB5B2 /* Debug */, - DDCD636C20A6532D00CDB5B2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = DDCD634A20A6532D00CDB5B2 /* Project object */; -} diff --git a/CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable-iOS.xcscheme b/CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable-iOS.xcscheme deleted file mode 100644 index 1c9762a..0000000 --- a/CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable-iOS.xcscheme +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable.xcscheme b/CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable.xcscheme deleted file mode 100644 index 631d299..0000000 --- a/CloudKitCodable.xcodeproj/xcshareddata/xcschemes/CloudKitCodable.xcscheme +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CloudKitCodable/CloudKitCodable.h b/CloudKitCodable/CloudKitCodable.h deleted file mode 100644 index 7f1296c..0000000 --- a/CloudKitCodable/CloudKitCodable.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// CloudKitCodable.h -// CloudKitCodable -// -// Created by Guilherme Rambo on 11/05/18. -// Copyright © 2018 Guilherme Rambo. All rights reserved. -// - -#import - -//! Project version number for CloudKitCodable. -FOUNDATION_EXPORT double CloudKitCodableVersionNumber; - -//! Project version string for CloudKitCodable. -FOUNDATION_EXPORT const unsigned char CloudKitCodableVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/CloudKitCodable/Info.plist b/CloudKitCodable/Info.plist deleted file mode 100644 index 249c28a..0000000 --- a/CloudKitCodable/Info.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSHumanReadableCopyright - Copyright © 2018 Guilherme Rambo. All rights reserved. - NSPrincipalClass - - - diff --git a/CloudKitCodableTests/Info.plist b/CloudKitCodableTests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/CloudKitCodableTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..fb43ee8 --- /dev/null +++ b/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 5.7 +import PackageDescription + +let package = Package( + name: "CloudKitCodable", + platforms: [ + .macOS(.v11), + .iOS(.v13), + .tvOS(.v13) + ], + products: [ + .library(name: "CloudKitCodable", targets: ["CloudKitCodable"]) + ], + targets: [ + .target(name: "CloudKitCodable"), + .testTarget( + name: "CloudKitCodableTests", + dependencies: ["CloudKitCodable"], + resources: [ + .copy("Fixtures/Rambo.ckrecord") + ] + ) + ] +) diff --git a/README.MD b/README.MD index 0a2bba1..65c7dc3 100644 --- a/README.MD +++ b/README.MD @@ -81,4 +81,32 @@ do { } catch { // something went wrong } -``` \ No newline at end of file +``` + +--- + +### Requirements + +- iOS 13.0+ +- macOS 11.0+ +- Xcode 13.2+ + +### Installation + +#### Swift Package Manager + +The [Swift Package Manager](https://www.swift.org/package-manager) is a tool for automating the distribution of Swift code and is integrated into the Swift build system. + +Once you have your Swift package set up, adding CloudKitCodable as a dependency is as easy as adding it to the dependencies value of your `Package.swift`. + +```swift +dependencies: [ + .package(url: "https://github.com/insidegui/CloudKitCodable.git", .upToNextMajor(from: "0.2.0")) +] +``` + +#### Manually + +If you prefer not to use SPM, you can integrate CloudKitCodable into your project manually by copying the files in. + +--- diff --git a/Sources/CloudKitCodable/CloudKitCodable.swift b/Sources/CloudKitCodable/CloudKitCodable.swift new file mode 100644 index 0000000..822c639 --- /dev/null +++ b/Sources/CloudKitCodable/CloudKitCodable.swift @@ -0,0 +1,6 @@ +public struct CloudKitCodable { + public private(set) var text = "Hello, World!" + + public init() { + } +} diff --git a/CloudKitCodable/Source/CloudKitRecordDecoder.swift b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift similarity index 92% rename from CloudKitCodable/Source/CloudKitRecordDecoder.swift rename to Sources/CloudKitCodable/CloudKitRecordDecoder.swift index d7d5cb7..ede4dba 100644 --- a/CloudKitCodable/Source/CloudKitRecordDecoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift @@ -54,7 +54,7 @@ extension _CloudKitRecordDecoder: Decoder { } } -protocol CloudKitRecordDecodingContainer: class { +protocol CloudKitRecordDecodingContainer: AnyObject { var codingPath: [CodingKey] { get set } var userInfo: [CodingUserInfoKey : Any] { get } @@ -145,7 +145,7 @@ extension _CloudKitRecordDecoder.KeyedContainer: KeyedDecodingContainerProtocol private func decodeURL(forKey key: Key) throws -> URL { if let asset = record[key.stringValue] as? CKAsset { - return decodeURL(from: asset) + return try decodeURL(from: asset) } guard let str = record[key.stringValue] as? String else { @@ -161,8 +161,13 @@ extension _CloudKitRecordDecoder.KeyedContainer: KeyedDecodingContainerProtocol return url } - private func decodeURL(from asset: CKAsset) -> URL { - return asset.fileURL + private func decodeURL(from asset: CKAsset) throws -> URL { + guard let url = asset.fileURL else { + let context = DecodingError.Context(codingPath: codingPath, debugDescription: "URL value not found") + throw DecodingError.valueNotFound(URL.self, context) + } + + return url } private func decodeBool(forKey key: Key) throws -> Bool { @@ -175,13 +180,11 @@ extension _CloudKitRecordDecoder.KeyedContainer: KeyedDecodingContainerProtocol } private func decodeSystemFields() -> Data { - let data = NSMutableData() - let coder = NSKeyedArchiver.init(forWritingWith: data) - coder.requiresSecureCoding = true + let coder = NSKeyedArchiver.init(requiringSecureCoding: true) record.encodeSystemFields(with: coder) coder.finishEncoding() - return data as Data + return coder.encodedData } func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { diff --git a/CloudKitCodable/Source/CloudKitRecordEncoder.swift b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift similarity index 87% rename from CloudKitCodable/Source/CloudKitRecordEncoder.swift rename to Sources/CloudKitCodable/CloudKitRecordEncoder.swift index c75d74c..cbb7304 100644 --- a/CloudKitCodable/Source/CloudKitRecordEncoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift @@ -31,7 +31,7 @@ public enum CloudKitRecordEncodingError: Error { } public class CloudKitRecordEncoder { - public var zoneID: CKRecordZoneID? + public var zoneID: CKRecordZone.ID? public func encode(_ value: Encodable) throws -> CKRecord { let type = recordTypeName(for: value) @@ -60,17 +60,17 @@ public class CloudKitRecordEncoder { } } - public init(zoneID: CKRecordZoneID? = nil) { + public init(zoneID: CKRecordZone.ID? = nil) { self.zoneID = zoneID } } final class _CloudKitRecordEncoder { - let zoneID: CKRecordZoneID? + let zoneID: CKRecordZone.ID? let recordTypeName: String let recordName: String - init(recordTypeName: String, zoneID: CKRecordZoneID?, recordName: String) { + init(recordTypeName: String, zoneID: CKRecordZone.ID?, recordName: String) { self.recordTypeName = recordTypeName self.zoneID = zoneID self.recordName = recordName @@ -91,8 +91,8 @@ extension _CloudKitRecordEncoder: Encoder { var record: CKRecord { if let existingRecord = container?.record { return existingRecord } - let zid = zoneID ?? CKRecordZoneID(zoneName: CKRecordZoneDefaultName, ownerName: CKCurrentUserDefaultName) - let rid = CKRecordID(recordName: recordName, zoneID: zid) + let zid = zoneID ?? CKRecordZone.ID(zoneName: CKRecordZone.ID.defaultZoneName, ownerName: CKCurrentUserDefaultName) + let rid = CKRecord.ID(recordName: recordName, zoneID: zid) return CKRecord(recordType: recordTypeName, recordID: rid) } @@ -123,14 +123,14 @@ extension _CloudKitRecordEncoder: Encoder { } } -protocol CloudKitRecordEncodingContainer: class { +protocol CloudKitRecordEncodingContainer: AnyObject { var record: CKRecord? { get } } extension _CloudKitRecordEncoder { final class KeyedContainer where Key: CodingKey { let recordTypeName: String - let zoneID: CKRecordZoneID? + let zoneID: CKRecordZone.ID? let recordName: String var metaRecord: CKRecord? var codingPath: [CodingKey] @@ -139,7 +139,7 @@ extension _CloudKitRecordEncoder { fileprivate var storage: [String: CKRecordValue] = [:] init(recordTypeName: String, - zoneID: CKRecordZoneID?, + zoneID: CKRecordZone.ID?, recordName: String, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) @@ -164,7 +164,7 @@ extension _CloudKitRecordEncoder.KeyedContainer: KeyedEncodingContainerProtocol throw CloudKitRecordEncodingError.systemFieldsDecode("\(_CKSystemFieldsKeyName) property must be of type Data") } - prepareMetaRecord(with: systemFields) + try prepareMetaRecord(with: systemFields) return } @@ -186,14 +186,14 @@ extension _CloudKitRecordEncoder.KeyedContainer: KeyedEncodingContainerProtocol } } - private func produceReference(for value: CustomCloudKitEncodable) throws -> CKReference { + private func produceReference(for value: CustomCloudKitEncodable) throws -> CKRecord.Reference { let childRecord = try CloudKitRecordEncoder().encode(value) - return CKReference(record: childRecord, action: .deleteSelf) + return CKRecord.Reference(record: childRecord, action: .deleteSelf) } - private func prepareMetaRecord(with systemFields: Data) { - let coder = NSKeyedUnarchiver(forReadingWith: systemFields) + private func prepareMetaRecord(with systemFields: Data) throws { + let coder = try NSKeyedUnarchiver(forReadingFrom: systemFields) coder.requiresSecureCoding = true metaRecord = CKRecord(coder: coder) coder.finishDecoding() @@ -226,9 +226,9 @@ extension _CloudKitRecordEncoder.KeyedContainer: KeyedEncodingContainerProtocol extension _CloudKitRecordEncoder.KeyedContainer: CloudKitRecordEncodingContainer { - var recordID: CKRecordID { - let zid = zoneID ?? CKRecordZoneID(zoneName: CKRecordZoneDefaultName, ownerName: CKCurrentUserDefaultName) - return CKRecordID(recordName: recordName, zoneID: zid) + var recordID: CKRecord.ID { + let zid = zoneID ?? CKRecordZone.ID(zoneName: CKRecordZone.ID.defaultZoneName, ownerName: CKCurrentUserDefaultName) + return CKRecord.ID(recordName: recordName, zoneID: zid) } var record: CKRecord? { diff --git a/CloudKitCodable/Source/CustomCloudKitEncodable.swift b/Sources/CloudKitCodable/CustomCloudKitEncodable.swift similarity index 100% rename from CloudKitCodable/Source/CustomCloudKitEncodable.swift rename to Sources/CloudKitCodable/CustomCloudKitEncodable.swift diff --git a/CloudKitCodableTests/CloudKitRecordDecoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift similarity index 91% rename from CloudKitCodableTests/CloudKitRecordDecoderTests.swift rename to Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift index 1935761..a7cf856 100644 --- a/CloudKitCodableTests/CloudKitRecordDecoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift @@ -31,7 +31,7 @@ final class CloudKitRecordDecoderTests: XCTestCase { } func testRoundTripWithCustomZoneID() throws { - let zoneID = CKRecordZoneID(zoneName: "ABCDE", ownerName: CKCurrentUserDefaultName) + let zoneID = CKRecordZone.ID(zoneName: "ABCDE", ownerName: CKCurrentUserDefaultName) let encodedPerson = try CloudKitRecordEncoder(zoneID: zoneID).encode(Person.rambo) let samePersonDecoded = try CloudKitRecordDecoder().decode(Person.self, from: encodedPerson) let samePersonReencoded = try CloudKitRecordEncoder().encode(samePersonDecoded) @@ -42,7 +42,7 @@ final class CloudKitRecordDecoderTests: XCTestCase { } func testCustomRecordIdentifierRoundTrip() throws { - let zoneID = CKRecordZoneID(zoneName: "ABCDE", ownerName: CKCurrentUserDefaultName) + let zoneID = CKRecordZone.ID(zoneName: "ABCDE", ownerName: CKCurrentUserDefaultName) let record = try CloudKitRecordEncoder(zoneID: zoneID).encode(PersonWithCustomIdentifier.rambo) diff --git a/CloudKitCodableTests/CloudKitRecordEncoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift similarity index 81% rename from CloudKitCodableTests/CloudKitRecordEncoderTests.swift rename to Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift index c545c7d..588ed19 100644 --- a/CloudKitCodableTests/CloudKitRecordEncoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift @@ -15,14 +15,14 @@ final class CloudKitRecordEncoderTests: XCTestCase { func testComplexPersonStructEncoding() throws { let record = try CloudKitRecordEncoder().encode(Person.rambo) - _validateRamboFields(in: record) + try _validateRamboFields(in: record) } func testCustomZoneIDEncoding() throws { - let zoneID = CKRecordZoneID(zoneName: "ABCDE", ownerName: CKCurrentUserDefaultName) + let zoneID = CKRecordZone.ID(zoneName: "ABCDE", ownerName: CKCurrentUserDefaultName) let record = try CloudKitRecordEncoder(zoneID: zoneID).encode(Person.rambo) - _validateRamboFields(in: record) + try _validateRamboFields(in: record) XCTAssert(record.recordID.zoneID == zoneID) } @@ -38,11 +38,11 @@ final class CloudKitRecordEncoderTests: XCTestCase { XCTAssertEqual(record.recordID.zoneID.zoneName, "ZoneABCD") XCTAssertEqual(record.recordID.zoneID.ownerName, "OwnerABCD") - _validateRamboFields(in: record) + try _validateRamboFields(in: record) } func testCustomRecordIdentifierEncoding() throws { - let zoneID = CKRecordZoneID(zoneName: "ABCDE", ownerName: CKCurrentUserDefaultName) + let zoneID = CKRecordZone.ID(zoneName: "ABCDE", ownerName: CKCurrentUserDefaultName) let record = try CloudKitRecordEncoder(zoneID: zoneID).encode(PersonWithCustomIdentifier.rambo) diff --git a/CloudKitCodableTests/Fixtures/Rambo.ckrecord b/Tests/CloudKitCodableTests/Fixtures/Rambo.ckrecord similarity index 100% rename from CloudKitCodableTests/Fixtures/Rambo.ckrecord rename to Tests/CloudKitCodableTests/Fixtures/Rambo.ckrecord diff --git a/CloudKitCodableTests/TestTypes/Person.swift b/Tests/CloudKitCodableTests/TestTypes/Person.swift similarity index 100% rename from CloudKitCodableTests/TestTypes/Person.swift rename to Tests/CloudKitCodableTests/TestTypes/Person.swift diff --git a/CloudKitCodableTests/TestUtils.swift b/Tests/CloudKitCodableTests/TestUtils.swift similarity index 69% rename from CloudKitCodableTests/TestUtils.swift rename to Tests/CloudKitCodableTests/TestUtils.swift index 157506d..852b494 100644 --- a/CloudKitCodableTests/TestUtils.swift +++ b/Tests/CloudKitCodableTests/TestUtils.swift @@ -10,43 +10,38 @@ import XCTest import CloudKit @testable import CloudKitCodable -private final class _TestBundleClass {} - -extension Bundle { - static var testBundle: Bundle { - return Bundle(for: _TestBundleClass.self) - } -} - extension CKRecord { /// Creates a temporary record to simulate what would happen when encoding a CKRecord /// from a value that was previosly encoded to a CKRecord and had its system fields set static var systemFieldsDataForTesting: Data { - let zoneID = CKRecordZoneID(zoneName: "ZoneABCD", ownerName: "OwnerABCD") - let recordID = CKRecordID(recordName: "RecordABCD", zoneID: zoneID) + let zoneID = CKRecordZone.ID(zoneName: "ZoneABCD", ownerName: "OwnerABCD") + let recordID = CKRecord.ID(recordName: "RecordABCD", zoneID: zoneID) let testRecord = CKRecord(recordType: "Person", recordID: recordID) - let data = NSMutableData() - let coder = NSKeyedArchiver.init(forWritingWith: data) - coder.requiresSecureCoding = true + let coder = NSKeyedArchiver(requiringSecureCoding: true) testRecord.encodeSystemFields(with: coder) coder.finishEncoding() - return data as Data + return coder.encodedData } static var testRecord: CKRecord { - guard let url = Bundle.testBundle.url(forResource: "Rambo", withExtension: "ckrecord") else { - fatalError("Required test asset Rambo.ckrecord not found") - } + get throws { + guard let url = Bundle.module.url(forResource: "Rambo", withExtension: "ckrecord") else { + fatalError("Required test asset Rambo.ckrecord not found") + } + + let data = try Data(contentsOf: url) + let record = try NSKeyedUnarchiver.unarchivedObject(ofClass: CKRecord.self, from: data) - return NSKeyedUnarchiver.unarchiveObject(withFile: url.path) as! CKRecord + return try XCTUnwrap(record) + } } } /// Validates that all fields in `record` match the expectations of encoding the test `Person` struct to a `CKRecord` /// /// - Parameter record: A record generated by encoding `Person.rambo` with `CloudKitRecordEncoder` -func _validateRamboFields(in record: CKRecord) { +func _validateRamboFields(in record: CKRecord) throws { XCTAssertEqual(record.recordType, "Person") XCTAssertEqual(record["name"] as? String, "Guilherme Rambo") XCTAssertEqual(record["age"] as? Int, 26) @@ -58,7 +53,8 @@ func _validateRamboFields(in record: CKRecord) { return } - XCTAssertEqual(asset.fileURL.path, "/Users/inside/Library/Containers/br.com.guilhermerambo.CloudKitRoundTrip/Data/Library/Caches/CloudKit/aa007d03cf247aebef55372fa57c05d0dc3d8682/Assets/7644AD10-A5A5-4191-B4FF-EF412CC08A52.01ec4e7f3a4fe140bcc758ae2c4a30c7bbb04de8db") + let filePath = try XCTUnwrap(asset.fileURL?.path) + XCTAssertEqual(filePath, "/Users/inside/Library/Containers/br.com.guilhermerambo.CloudKitRoundTrip/Data/Library/Caches/CloudKit/aa007d03cf247aebef55372fa57c05d0dc3d8682/Assets/7644AD10-A5A5-4191-B4FF-EF412CC08A52.01ec4e7f3a4fe140bcc758ae2c4a30c7bbb04de8db") XCTAssertNil(record[_CKSystemFieldsKeyName], "\(_CKSystemFieldsKeyName) should NOT be encoded to the record directly") } From 9ca4a79325a4b0d2f68e59d9ab4ef3dde6c3cf43 Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Sun, 10 Jul 2022 23:04:25 +1000 Subject: [PATCH 06/21] Remove TravisCI --- .travis.yml | 6 ------ README.MD | 2 -- 2 files changed, 8 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ab19534..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -os: osx -osx_image: xcode9.3 -script: - - xcodebuild -version - - set -o pipefail && env "NSUnbufferedIO=YES" xcodebuild test -scheme CloudKitCodable -project CloudKitCodable.xcodeproj | xcpretty -f `xcpretty-travis-formatter` - - set -o pipefail && env "NSUnbufferedIO=YES" xcodebuild test -scheme CloudKitCodable-iOS -project CloudKitCodable.xcodeproj -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=11.3,name=iPhone 6' | xcpretty -f `xcpretty-travis-formatter` \ No newline at end of file diff --git a/README.MD b/README.MD index 65c7dc3..d817f74 100644 --- a/README.MD +++ b/README.MD @@ -1,5 +1,3 @@ -[![Build Status](https://travis-ci.org/insidegui/CloudKitCodable.svg?branch=master)](https://travis-ci.org/insidegui/CloudKitCodable) - # CloudKit + Codable = ❤️ This project implements a `CloudKitRecordEncoder` and a `CloudKitRecordDecoder` so you can easily convert your custom data structure to a `CKRecord` and convert your `CKRecord` back to your custom data structure. From a3e1d6b6907df50bf7d8396909a43695e2cec7ba Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Sun, 10 Jul 2022 23:07:15 +1000 Subject: [PATCH 07/21] Ignore standard Swift Package Manager directories --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6a59638..fe5e072 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ generatechangelog.sh Pods/ Carthage Provisioning -Crashlytics.sh \ No newline at end of file +Crashlytics.sh +.swiftpm +.build From 43b7b69c14146e1de8bbee040e51b7d187f1d687 Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Sun, 10 Jul 2022 23:07:53 +1000 Subject: [PATCH 08/21] Add workflow to test the package --- .github/workflows/swift-package.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/swift-package.yml diff --git a/.github/workflows/swift-package.yml b/.github/workflows/swift-package.yml new file mode 100644 index 0000000..bb5fc02 --- /dev/null +++ b/.github/workflows/swift-package.yml @@ -0,0 +1,19 @@ +name: Swift Package + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: macos-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: swift build -v + - name: Run tests + run: swift test -v From 7dcde6c05b6c0409353bf59cdf861e49d90824be Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Tue, 12 Jul 2022 09:31:18 +1000 Subject: [PATCH 09/21] Restore a build status badge to the readme --- README.MD | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.MD b/README.MD index d817f74..dc6562a 100644 --- a/README.MD +++ b/README.MD @@ -1,5 +1,7 @@ # CloudKit + Codable = ❤️ +[![Badge showing the current build status](https://github.com/insidegui/CloudKitCodable/workflows/Swift%20Package/badge.svg)](https://github.com/insidegui/CloudKitCodable/actions) + This project implements a `CloudKitRecordEncoder` and a `CloudKitRecordDecoder` so you can easily convert your custom data structure to a `CKRecord` and convert your `CKRecord` back to your custom data structure. **Be aware this is an initial implementation that's not being used in production (yet) and it doesn't support nesting. Nested values would have to be encoded as `CKReference` and I haven't implemented that yet (feel free to open a PR 🤓).** From 9cd4dc580bf1ab262d92b729ebac28e369888b3d Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Tue, 12 Jul 2022 09:35:46 +1000 Subject: [PATCH 10/21] A few minor formatting and wording fixes to the readme --- README.MD | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/README.MD b/README.MD index dc6562a..262e37e 100644 --- a/README.MD +++ b/README.MD @@ -8,7 +8,7 @@ This project implements a `CloudKitRecordEncoder` and a `CloudKitRecordDecoder` ## Usage -## `CustomCloudKitCodable` +### `CustomCloudKitCodable` The types you want to convert to/from `CKRecord` must implement the `CustomCloudKitCodable` protocol. This is necessary because unlike most implementations of encoders/decoders, we are not converting to/from `Data`, but to/from `CKRecord`, which has some special requirements. @@ -28,12 +28,12 @@ var cloudKitRecordType: String { get } This property should return the record type for your custom type. It's implemented automatically to return the name of the type, you only need to implement this if you need to customize the record type. -## URLs +### URLs There's special handling for URLs because of the way CloudKit works with files. If you have a property that's a remote `URL` (i.e. a website), it's encoded as a `String` (CloudKit doesn't support URLs natively) and decoded back as a `URL`. If your property is a `URL` and it contains a `URL` to a local file, it is encoded as a `CKAsset`, the file will be automatically uploaded to CloudKit when you save the containing record and downloaded when you get the record from the cloud. The decoded `URL` will contain the `URL` for the location on disk where CloudKit has downloaded the file. -## Example +### Example Let's say you have a `Person` model you want to sync to CloudKit. This is what the model would look like: @@ -83,30 +83,26 @@ do { } ``` ---- - -### Requirements +## Requirements - iOS 13.0+ - macOS 11.0+ - Xcode 13.2+ -### Installation +## Installation -#### Swift Package Manager +### Swift Package Manager -The [Swift Package Manager](https://www.swift.org/package-manager) is a tool for automating the distribution of Swift code and is integrated into the Swift build system. +[Swift Package Manager](https://www.swift.org/package-manager) is a tool for automating the distribution of Swift code and is integrated into the Swift build system. Once you have your Swift package set up, adding CloudKitCodable as a dependency is as easy as adding it to the dependencies value of your `Package.swift`. ```swift dependencies: [ - .package(url: "https://github.com/insidegui/CloudKitCodable.git", .upToNextMajor(from: "0.2.0")) + .package(url: "https://github.com/insidegui/CloudKitCodable.git", from: "0.2.0") ] ``` -#### Manually - -If you prefer not to use SPM, you can integrate CloudKitCodable into your project manually by copying the files in. +### Manually ---- +If you prefer not to use Swift Package Manager, you can integrate CloudKitCodable into your project manually by copying the files in. From b831aca5ed840ff346307ca85243ae037cd89f3b Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Wed, 20 Jul 2022 10:10:59 +1000 Subject: [PATCH 11/21] Reduce the Swift tools version to 5.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That’s what the project is being tested on, but I’m open to pushing this back further. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index fb43ee8..5585259 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.5 import PackageDescription let package = Package( From 25ac9ada3ffb8eaf70c88c49d495facc45deb498 Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Wed, 20 Jul 2022 10:13:46 +1000 Subject: [PATCH 12/21] Fix badge in the README --- README.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.MD b/README.MD index 262e37e..2ae2db8 100644 --- a/README.MD +++ b/README.MD @@ -1,6 +1,6 @@ # CloudKit + Codable = ❤️ -[![Badge showing the current build status](https://github.com/insidegui/CloudKitCodable/workflows/Swift%20Package/badge.svg)](https://github.com/insidegui/CloudKitCodable/actions) +[![Badge showing the current build status](https://github.com/insidegui/CloudKitCodable/actions/workflows/swift-package.yml/badge.svg)](https://github.com/insidegui/CloudKitCodable/actions/workflows/swift-package.yml) This project implements a `CloudKitRecordEncoder` and a `CloudKitRecordDecoder` so you can easily convert your custom data structure to a `CKRecord` and convert your `CKRecord` back to your custom data structure. From 6d8c36efca8a4d702746072fe761f9543581599d Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Wed, 20 Jul 2022 10:21:06 +1000 Subject: [PATCH 13/21] Restrict watchOS to SDK version 4.0 or later This addresses #1 --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 5585259..488af86 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,8 @@ let package = Package( platforms: [ .macOS(.v11), .iOS(.v13), - .tvOS(.v13) + .tvOS(.v13), + .watchOS(.v4) ], products: [ .library(name: "CloudKitCodable", targets: ["CloudKitCodable"]) From 85d19d698415189158d919282edc754d02925acd Mon Sep 17 00:00:00 2001 From: Gui Rambo Date: Sat, 14 Oct 2023 16:07:18 -0300 Subject: [PATCH 14/21] Add support for encoding/decoding String and Int enum properties --- Sources/CloudKitCodable/CloudKitCodable.swift | 6 --- .../CloudKitRecordDecoder.swift | 35 +++++++++++++++++- .../CloudKitRecordEncoder.swift | 4 ++ .../CustomCloudKitEncodable.swift | 12 ++++++ .../CloudKitRecordDecoderTests.swift | 11 ++++++ .../CloudKitRecordEncoderTests.swift | 24 +++++++++++- .../TestTypes/TestModelWithEnum.swift | 37 +++++++++++++++++++ 7 files changed, 120 insertions(+), 9 deletions(-) delete mode 100644 Sources/CloudKitCodable/CloudKitCodable.swift create mode 100644 Tests/CloudKitCodableTests/TestTypes/TestModelWithEnum.swift diff --git a/Sources/CloudKitCodable/CloudKitCodable.swift b/Sources/CloudKitCodable/CloudKitCodable.swift deleted file mode 100644 index 822c639..0000000 --- a/Sources/CloudKitCodable/CloudKitCodable.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct CloudKitCodable { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/Sources/CloudKitCodable/CloudKitRecordDecoder.swift b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift index ede4dba..f23d683 100644 --- a/Sources/CloudKitCodable/CloudKitRecordDecoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift @@ -135,9 +135,40 @@ extension _CloudKitRecordDecoder.KeyedContainer: KeyedDecodingContainerProtocol return try decodeURL(forKey: key) as! T } + func typeMismatch(_ message: String) -> DecodingError { + let context = DecodingError.Context( + codingPath: codingPath, + debugDescription: message + ) + return DecodingError.typeMismatch(type, context) + } + + if let stringEnumType = T.self as? any CloudKitStringEnum.Type { + guard let stringValue = record[key.stringValue] as? String else { + throw typeMismatch("Expected to decode a rawValue String for \"\(String(describing: type))\"") + } + guard let enumValue = stringEnumType.init(rawValue: stringValue) ?? stringEnumType.cloudKitFallbackCase else { + #if DEBUG + throw typeMismatch("Failed to construct enum \"\(String(describing: type))\" from String \"\(stringValue)\"") + #else + throw typeMismatch("Failed to construct enum \"\(String(describing: type))\" from String value") + #endif + } + return enumValue as! T + } + + if let intEnumType = T.self as? any CloudKitIntEnum.Type { + guard let intValue = record[key.stringValue] as? Int else { + throw typeMismatch("Expected to decode a rawValue Int for \"\(String(describing: type))\"") + } + guard let enumValue = intEnumType.init(rawValue: intValue) ?? intEnumType.cloudKitFallbackCase else { + throw typeMismatch("Failed to construct enum \"\(String(describing: type))\" from value \"\(intValue)\"") + } + return enumValue as! T + } + guard let value = record[key.stringValue] as? T else { - let context = DecodingError.Context(codingPath: codingPath, debugDescription: "CKRecordValue couldn't be converted to \(String(describing: type))'") - throw DecodingError.typeMismatch(type, context) + throw typeMismatch("CKRecordValue couldn't be converted to \"\(String(describing: type))\"") } return value diff --git a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift index cbb7304..9ff9f19 100644 --- a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift @@ -181,6 +181,10 @@ extension _CloudKitRecordEncoder.KeyedContainer: KeyedEncodingContainerProtocol throw CloudKitRecordEncodingError.referencesNotSupported(key.stringValue) } else if let ckValue = value as? CKRecordValue { return ckValue + } else if let stringValue = (value as? any CloudKitStringEnum)?.rawValue { + return stringValue as NSString + } else if let intValue = (value as? any CloudKitIntEnum)?.rawValue { + return NSNumber(value: Int(intValue)) } else { throw CloudKitRecordEncodingError.unsupportedValueForKey(key.stringValue) } diff --git a/Sources/CloudKitCodable/CustomCloudKitEncodable.swift b/Sources/CloudKitCodable/CustomCloudKitEncodable.swift index 2296a8f..e35660c 100644 --- a/Sources/CloudKitCodable/CustomCloudKitEncodable.swift +++ b/Sources/CloudKitCodable/CustomCloudKitEncodable.swift @@ -37,3 +37,15 @@ public protocol CustomCloudKitDecodable: CloudKitRecordRepresentable & Decodable } public protocol CustomCloudKitCodable: CustomCloudKitEncodable & CustomCloudKitDecodable { } + +public protocol CloudKitEnum { + static var cloudKitFallbackCase: Self? { get } +} + +public extension CloudKitEnum where Self: CaseIterable { + static var cloudKitFallbackCase: Self? { allCases.first } +} + +public protocol CloudKitStringEnum: Codable, RawRepresentable, CloudKitEnum where RawValue == String { } + +public protocol CloudKitIntEnum: Codable, RawRepresentable, CloudKitEnum where RawValue == Int { } diff --git a/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift index a7cf856..58f05db 100644 --- a/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift @@ -53,4 +53,15 @@ final class CloudKitRecordDecoderTests: XCTestCase { XCTAssert(samePersonDecoded.cloudKitIdentifier == "MY-ID") } + func testEnumRoundtrip() throws { + let model = TestModelWithEnum.allEnumsPopulated + + let record = try CloudKitRecordEncoder().encode(model) + + var sameModelDecoded = try CloudKitRecordDecoder().decode(TestModelWithEnum.self, from: record) + sameModelDecoded.cloudKitSystemFields = nil + + XCTAssertEqual(sameModelDecoded, model) + } + } diff --git a/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift index 588ed19..532bd98 100644 --- a/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift @@ -49,5 +49,27 @@ final class CloudKitRecordEncoderTests: XCTestCase { XCTAssert(record.recordID.zoneID == zoneID) XCTAssert(record.recordID.recordName == "MY-ID") } - + + func testEnumEncoding() throws { + let model = TestModelWithEnum.allEnumsPopulated + + let record = try CloudKitRecordEncoder().encode(model) + + XCTAssertEqual(record["enumProperty"], "enumCase3") + XCTAssertEqual(record["optionalEnumProperty"], "enumCase2") + XCTAssertEqual(record["intEnumProperty"], 1) + XCTAssertEqual(record["optionalIntEnumProperty"], 2) + } + + func testEnumEncodingNilValue() throws { + let model = TestModelWithEnum.optionalEnumNil + + let record = try CloudKitRecordEncoder().encode(model) + + XCTAssertEqual(record["enumProperty"], "enumCase3") + XCTAssertNil(record["optionalEnumProperty"]) + XCTAssertEqual(record["intEnumProperty"], 1) + XCTAssertNil(record["optionalIntEnumProperty"]) + } + } diff --git a/Tests/CloudKitCodableTests/TestTypes/TestModelWithEnum.swift b/Tests/CloudKitCodableTests/TestTypes/TestModelWithEnum.swift new file mode 100644 index 0000000..ced8a24 --- /dev/null +++ b/Tests/CloudKitCodableTests/TestTypes/TestModelWithEnum.swift @@ -0,0 +1,37 @@ +import Foundation +import CloudKitCodable + +struct TestModelWithEnum: CustomCloudKitCodable, Hashable { + enum MyStringEnum: String, CloudKitStringEnum, CaseIterable { + case enumCase0 + case enumCase1 + case enumCase2 + case enumCase3 + } + enum MyIntEnum: Int, CloudKitIntEnum, CaseIterable { + case enumCase0 + case enumCase1 + case enumCase2 + case enumCase3 + } + var cloudKitSystemFields: Data? + var enumProperty: MyStringEnum + var optionalEnumProperty: MyStringEnum? + var intEnumProperty: MyIntEnum + var optionalIntEnumProperty: MyIntEnum? +} + +extension TestModelWithEnum { + static let allEnumsPopulated = TestModelWithEnum( + enumProperty: .enumCase3, + optionalEnumProperty: .enumCase2, + intEnumProperty: .enumCase1, + optionalIntEnumProperty: .enumCase2 + ) + static let optionalEnumNil = TestModelWithEnum( + enumProperty: .enumCase3, + optionalEnumProperty: nil, + intEnumProperty: .enumCase1, + optionalIntEnumProperty: nil + ) +} From 358c28423620cc2c80ff10f37eeb3a63c477e02c Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Sat, 13 Apr 2024 12:16:59 -0300 Subject: [PATCH 15/21] Implemented support for nested Codable values --- .../CloudKitRecordDecoder.swift | 22 +++++-- .../CloudKitRecordEncoder.swift | 44 +++++++++++--- .../CloudKitRecordDecoderTests.swift | 21 +++++++ .../CloudKitRecordEncoderTests.swift | 30 ++++++++++ .../TestTypes/TestNestedModel.swift | 58 +++++++++++++++++++ 5 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 Tests/CloudKitCodableTests/TestTypes/TestNestedModel.swift diff --git a/Sources/CloudKitCodable/CloudKitRecordDecoder.swift b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift index f23d683..75172bd 100644 --- a/Sources/CloudKitCodable/CloudKitRecordDecoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift @@ -167,11 +167,21 @@ extension _CloudKitRecordDecoder.KeyedContainer: KeyedDecodingContainerProtocol return enumValue as! T } - guard let value = record[key.stringValue] as? T else { - throw typeMismatch("CKRecordValue couldn't be converted to \"\(String(describing: type))\"") - } + /// This will attempt to JSON-decode child values for `Data` fields, but it's important to check that the type of the field + /// is not `Data`, otherwise we'd be trying to decode JSON from any data field, even those that do not contain JSON-encoded children. + if T.self != Data.self, + let nestedData = record[key.stringValue] as? Data + { + let value = try JSONDecoder.nestedCloudKitValue.decode(T.self, from: nestedData) + + return value + } else { + guard let value = record[key.stringValue] as? T else { + throw typeMismatch("CKRecordValue couldn't be converted to \"\(String(describing: type))\"") + } - return value + return value + } } private func decodeURL(forKey key: Key) throws -> URL { @@ -239,3 +249,7 @@ extension _CloudKitRecordDecoder.KeyedContainer: KeyedDecodingContainerProtocol } extension _CloudKitRecordDecoder.KeyedContainer: CloudKitRecordDecodingContainer {} + +private extension JSONDecoder { + static let nestedCloudKitValue = JSONDecoder() +} diff --git a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift index 9ff9f19..4fb373a 100644 --- a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift @@ -13,6 +13,7 @@ public enum CloudKitRecordEncodingError: Error { case unsupportedValueForKey(String) case systemFieldsDecode(String) case referencesNotSupported(String) + case dataFieldTooLarge(key: String, size: Int) public var localizedDescription: String { switch self { @@ -26,6 +27,8 @@ public enum CloudKitRecordEncodingError: Error { return "Failed to process \(_CKSystemFieldsKeyName): \(info)" case .referencesNotSupported(let key): return "References are not supported by CloudKitRecordEncoder yet. Key: \(key)." + case .dataFieldTooLarge(let key, let size): + return "Value for child data \"\(key)\" of \(size) bytes exceeds maximum of \(CKRecord.maxDataSize) bytes" } } } @@ -175,10 +178,16 @@ extension _CloudKitRecordEncoder.KeyedContainer: KeyedEncodingContainerProtocol private func produceCloudKitValue(for value: T, withKey key: Key) throws -> CKRecordValue where T : Encodable { if let urlValue = value as? URL { return produceCloudKitValue(for: urlValue) - } else if value is CustomCloudKitEncodable { - throw CloudKitRecordEncodingError.referencesNotSupported(key.stringValue) - } else if value is [CustomCloudKitEncodable] { - throw CloudKitRecordEncodingError.referencesNotSupported(key.stringValue) + } else if let collection = value as? [Any] { + /// The `value as? CKRecordValue` cast in the next `else if` will always succeed for arrays, + /// so here we check that the value is actually an array where the elements conform to `CKRecordValue`, + /// then return it as an `NSArray`. Otherwise, this is an array with arbitrary `Encodable` elements, + /// in which case they'll be stored as a single data field with the JSON-encoded representation. + if let ckValueArray = collection as? [CKRecordValue] { + return ckValueArray as NSArray + } else { + return try encodedChildValue(for: value, withKey: key) + } } else if let ckValue = value as? CKRecordValue { return ckValue } else if let stringValue = (value as? any CloudKitStringEnum)?.rawValue { @@ -186,14 +195,18 @@ extension _CloudKitRecordEncoder.KeyedContainer: KeyedEncodingContainerProtocol } else if let intValue = (value as? any CloudKitIntEnum)?.rawValue { return NSNumber(value: Int(intValue)) } else { - throw CloudKitRecordEncodingError.unsupportedValueForKey(key.stringValue) + return try encodedChildValue(for: value, withKey: key) } } - private func produceReference(for value: CustomCloudKitEncodable) throws -> CKRecord.Reference { - let childRecord = try CloudKitRecordEncoder().encode(value) + private func encodedChildValue(for value: T, withKey key: Key) throws -> CKRecordValue where T : Encodable { + let encodedChild = try JSONEncoder.nestedCloudKitValue.encode(value) + + guard encodedChild.count < CKRecord.maxDataSize else { + throw CloudKitRecordEncodingError.dataFieldTooLarge(key: key.stringValue, size: encodedChild.count) + } - return CKRecord.Reference(record: childRecord, action: .deleteSelf) + return encodedChild as NSData } private func prepareMetaRecord(with systemFields: Data) throws { @@ -262,3 +275,18 @@ extension _CloudKitRecordEncoder.KeyedContainer: CloudKitRecordEncodingContainer } } + +private extension JSONEncoder { + static let nestedCloudKitValue: JSONEncoder = { + let e = JSONEncoder() + e.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + return e + }() +} + +private extension CKRecord { + /// The entire `CKRecord` can't exceed 1MB, but since we don't really know how large the whole + /// record is, we just check data fields to ensure that they fit within the limit. This doesn't prevent + /// the record from exceeding the 1MB limit, but at least catches the most egregious attempts. + static let maxDataSize = 1_000_000 +} diff --git a/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift index 58f05db..86fee1a 100644 --- a/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift @@ -64,4 +64,25 @@ final class CloudKitRecordDecoderTests: XCTestCase { XCTAssertEqual(sameModelDecoded, model) } + func testNestedRoundtrip() throws { + let model = TestParent.test + + let record = try CloudKitRecordEncoder().encode(model) + + var sameModelDecoded = try CloudKitRecordDecoder().decode(TestParent.self, from: record) + sameModelDecoded.cloudKitSystemFields = nil + + XCTAssertEqual(sameModelDecoded, model) + } + + func testNestedRoundtripCollection() throws { + let model = TestParentCollection.test + + let record = try CloudKitRecordEncoder().encode(model) + + var sameModelDecoded = try CloudKitRecordDecoder().decode(TestParentCollection.self, from: record) + sameModelDecoded.cloudKitSystemFields = nil + + XCTAssertEqual(sameModelDecoded, model) + } } diff --git a/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift index 532bd98..89a76a7 100644 --- a/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift @@ -72,4 +72,34 @@ final class CloudKitRecordEncoderTests: XCTestCase { XCTAssertNil(record["optionalIntEnumProperty"]) } + func testNestedEncoding() throws { + let model = TestParent.test + + let record = try CloudKitRecordEncoder().encode(model) + + let encodedChild = """ + {"name":"Hello Child Name","value":"Hello Child Value"} + """.UTF8Data() + + XCTAssertEqual(record["parentName"], "Hello Parent") + XCTAssertEqual(record["child"], encodedChild) + } + + func testNestedEncodingCollection() throws { + let model = TestParentCollection.test + + let record = try CloudKitRecordEncoder().encode(model) + + let encodedChildren = """ + [{"name":"0 - Hello Child Name","value":"0 - Hello Child Value"},{"name":"1 - Hello Child Name","value":"1 - Hello Child Value"},{"name":"2 - Hello Child Name","value":"2 - Hello Child Value"}] + """.UTF8Data() + + XCTAssertEqual(record["parentName"], "Hello Parent Collection") + XCTAssertEqual(record["children"], encodedChildren) + } + +} + +extension String { + func UTF8Data() -> Data { Data(utf8) } } diff --git a/Tests/CloudKitCodableTests/TestTypes/TestNestedModel.swift b/Tests/CloudKitCodableTests/TestTypes/TestNestedModel.swift new file mode 100644 index 0000000..25e5242 --- /dev/null +++ b/Tests/CloudKitCodableTests/TestTypes/TestNestedModel.swift @@ -0,0 +1,58 @@ +import Foundation +import CloudKitCodable + +struct TestParent: CustomCloudKitCodable, Hashable { + struct TestChild: Codable, Hashable { + var name: String + var value: String + } + var cloudKitSystemFields: Data? + var parentName: String + var child: TestChild + var dataProperty: Data +} + +struct TestParentCollection: CustomCloudKitCodable, Hashable { + struct TestCollectionChild: Codable, Hashable { + var name: String + var value: String + } + var cloudKitSystemFields: Data? + var parentName: String + var children: [TestCollectionChild] + /// This data property is used to ensure that the special handling of `Data` for JSON-encoded children + /// does not break encoding/decoding of regular data fields. + var dataProperty: Data +} + +extension TestParent { + static let test = TestParent( + parentName: "Hello Parent", + child: .init( + name: "Hello Child Name", + value: "Hello Child Value" + ), + dataProperty: Data([0xFF]) + ) +} + +extension TestParentCollection { + static let test = TestParentCollection( + parentName: "Hello Parent Collection", + children: [ + .init( + name: "0 - Hello Child Name", + value: "0 - Hello Child Value" + ), + .init( + name: "1 - Hello Child Name", + value: "1 - Hello Child Value" + ), + .init( + name: "2 - Hello Child Name", + value: "2 - Hello Child Value" + ), + ], + dataProperty: Data([0xFF]) + ) +} From faaf199e83ee0feafbc1bb3c908f342b6276ee58 Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Sat, 13 Apr 2024 12:29:40 -0300 Subject: [PATCH 16/21] Added tests for optional nested values, just in case --- .../CloudKitRecordDecoderTests.swift | 22 +++++++++++++++ .../CloudKitRecordEncoderTests.swift | 22 +++++++++++++++ .../TestTypes/TestNestedModel.swift | 27 +++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift index 86fee1a..715a434 100644 --- a/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift @@ -75,6 +75,28 @@ final class CloudKitRecordDecoderTests: XCTestCase { XCTAssertEqual(sameModelDecoded, model) } + func testNestedRoundtripOptionalChild() throws { + let model = TestParentOptionalChild.test + + let record = try CloudKitRecordEncoder().encode(model) + + var sameModelDecoded = try CloudKitRecordDecoder().decode(TestParentOptionalChild.self, from: record) + sameModelDecoded.cloudKitSystemFields = nil + + XCTAssertEqual(sameModelDecoded, model) + } + + func testNestedRoundtripOptionalChildNil() throws { + let model = TestParentOptionalChild.testNilChild + + let record = try CloudKitRecordEncoder().encode(model) + + var sameModelDecoded = try CloudKitRecordDecoder().decode(TestParentOptionalChild.self, from: record) + sameModelDecoded.cloudKitSystemFields = nil + + XCTAssertEqual(sameModelDecoded, model) + } + func testNestedRoundtripCollection() throws { let model = TestParentCollection.test diff --git a/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift index 89a76a7..fbce777 100644 --- a/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift @@ -85,6 +85,28 @@ final class CloudKitRecordEncoderTests: XCTestCase { XCTAssertEqual(record["child"], encodedChild) } + func testNestedEncodingOptional() throws { + let model = TestParentOptionalChild.test + + let record = try CloudKitRecordEncoder().encode(model) + + let encodedChild = """ + {"name":"Hello Optional Child Name","value":"Hello Optional Child Value"} + """.UTF8Data() + + XCTAssertEqual(record["parentName"], "Hello Parent") + XCTAssertEqual(record["child"], encodedChild) + } + + func testNestedEncodingOptionalNil() throws { + let model = TestParentOptionalChild.testNilChild + + let record = try CloudKitRecordEncoder().encode(model) + + XCTAssertEqual(record["parentName"], "Hello Parent") + XCTAssertNil(record["child"]) + } + func testNestedEncodingCollection() throws { let model = TestParentCollection.test diff --git a/Tests/CloudKitCodableTests/TestTypes/TestNestedModel.swift b/Tests/CloudKitCodableTests/TestTypes/TestNestedModel.swift index 25e5242..bbcc792 100644 --- a/Tests/CloudKitCodableTests/TestTypes/TestNestedModel.swift +++ b/Tests/CloudKitCodableTests/TestTypes/TestNestedModel.swift @@ -12,6 +12,17 @@ struct TestParent: CustomCloudKitCodable, Hashable { var dataProperty: Data } +struct TestParentOptionalChild: CustomCloudKitCodable, Hashable { + struct TestOptionalChild: Codable, Hashable { + var name: String + var value: String + } + var cloudKitSystemFields: Data? + var parentName: String + var child: TestOptionalChild? + var dataProperty: Data +} + struct TestParentCollection: CustomCloudKitCodable, Hashable { struct TestCollectionChild: Codable, Hashable { var name: String @@ -36,6 +47,22 @@ extension TestParent { ) } +extension TestParentOptionalChild { + static let test = TestParentOptionalChild( + parentName: "Hello Parent", + child: .init( + name: "Hello Optional Child Name", + value: "Hello Optional Child Value" + ), + dataProperty: Data([0xFF]) + ) + static let testNilChild = TestParentOptionalChild( + parentName: "Hello Parent", + child: nil, + dataProperty: Data([0xFF]) + ) +} + extension TestParentCollection { static let test = TestParentCollection( parentName: "Hello Parent Collection", From 7c36d442fdbcc247c0b122e868b19e93c3f54489 Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Sat, 13 Apr 2024 13:12:08 -0300 Subject: [PATCH 17/21] Implemented support for custom asset types --- .../CloudKitCodable/CloudKitAssetValue.swift | 84 +++++++++++++++++++ .../CloudKitRecordDecoder.swift | 26 +++++- .../CloudKitRecordEncoder.swift | 46 ++++++++-- .../CloudKitRecordDecoderTests.swift | 11 +++ .../CloudKitRecordEncoderTests.swift | 24 ++++++ .../TestTypes/TestModelCustomAsset.swift | 28 +++++++ 6 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 Sources/CloudKitCodable/CloudKitAssetValue.swift create mode 100644 Tests/CloudKitCodableTests/TestTypes/TestModelCustomAsset.swift diff --git a/Sources/CloudKitCodable/CloudKitAssetValue.swift b/Sources/CloudKitCodable/CloudKitAssetValue.swift new file mode 100644 index 0000000..65690bb --- /dev/null +++ b/Sources/CloudKitCodable/CloudKitAssetValue.swift @@ -0,0 +1,84 @@ +import Foundation +import CloudKit +import UniformTypeIdentifiers + +/// Adopted by `Codable` types that can be nested in ``CustomCloudKitCodable`` types, represented as `CKAsset` in records. +/// +/// The corresponding `CKRecord` field is encoded as a `CKAsset` with a file containing the encoded representation of the value. +/// +/// Implementers can customize the encoding/decoding, file type, and asset file name, but there are default implementations for all of this protocol's requirements. +public protocol CloudKitAssetValue: Codable { + + /// The default content type for `CKAsset` files representing values of this type. + /// + /// There's a default implementation that returns `.json`, so by default ``CloudKitAssetValue`` types are encoded as JSON. + static var preferredContentType: UTType { get } + + /// The preferred filename for this value when being encoded as a `CKAsset`. + /// + /// There's a default implementation for a filename with the format `-.(json/plist)`, + /// and a default implementation for `Identifiable` types that uses the `id` property instead of a random UUID. + var filename: String { get } + + /// Encodes this value as data. + /// + /// There's a default implementation that uses `JSONEncoder`/`PropertyListDecoder` according to the ``preferredContentType`` property. + func encoded() throws -> Data + + /// Decodes an instance of this type from encoded data. + /// - Parameters: + /// - data: The encoded value data. + /// - type: Determines the type of decoder to be used. + /// - Returns: The instance of the type. + /// + /// There is a default implementation supporting JSON and PLIST types that uses `JSONDecoder`/`PropertyListDecoder`. + static func decoded(from data: Data, type: UTType) throws -> Self +} + +// MARK: - Default Implementations + +public extension CloudKitAssetValue { + /// Default implementation that returns `.json`, so the value is encoded as JSON data. + static var preferredContentType: UTType { .json } +} + +public extension CloudKitAssetValue { + /// The file extension (including the leading `.`), computed according to ``preferredContentType``. + static var filenameSuffix: String { Self.preferredContentType.preferredFilenameExtension.flatMap { ".\($0)" } ?? "" } + + /// The file name (including extension) for this value when encoded into a `CKAsset`. + var filename: String { [String(describing: Self.self), UUID().uuidString].joined(separator: "-") + Self.filenameSuffix } +} + +public extension CloudKitAssetValue where Self: Identifiable { + /// The file name (including extension) for this value when encoded into a `CKAsset`. + /// Uses the `id` property from `Identifiable` conformance. + var filename: String { [String(describing: Self.self), String(describing: id)].joined(separator: "-") + Self.filenameSuffix } +} + +public extension CloudKitAssetValue { + func encoded() throws -> Data { + let type = Self.preferredContentType + if type.conforms(to: .json) { + return try JSONEncoder.nestedCloudKitValue.encode(self) + } else if type.conforms(to: .xmlPropertyList) { + return try PropertyListEncoder.nestedCloudKitValueXML.encode(self) + } else if type.conforms(to: .binaryPropertyList) { + return try PropertyListEncoder.nestedCloudKitValueBinary.encode(self) + } else if type.conforms(to: .propertyList) { + return try PropertyListEncoder.nestedCloudKitValueXML.encode(self) + } else { + throw EncodingError.invalidValue(self, .init(codingPath: [], debugDescription: "Unsupported content type \"\(type.identifier)\": the default implementation only supports JSON and PLIST")) + } + } + + static func decoded(from data: Data, type: UTType) throws -> Self { + if type.conforms(to: .json) { + return try JSONDecoder.nestedCloudKitValue.decode(Self.self, from: data) + } else if type.conforms(to: .propertyList) { + return try PropertyListDecoder.nestedCloudKitValue.decode(Self.self, from: data) + } else { + throw DecodingError.typeMismatch(Self.self, .init(codingPath: [], debugDescription: "Unsupported content type \"\(type.identifier)\": the default implementation only supports JSON and PLIST")) + } + } +} diff --git a/Sources/CloudKitCodable/CloudKitRecordDecoder.swift b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift index 75172bd..b603cae 100644 --- a/Sources/CloudKitCodable/CloudKitRecordDecoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift @@ -175,6 +175,14 @@ extension _CloudKitRecordDecoder.KeyedContainer: KeyedDecodingContainerProtocol let value = try JSONDecoder.nestedCloudKitValue.decode(T.self, from: nestedData) return value + } else if let customAssetType = type as? CloudKitAssetValue.Type { + guard let ckAsset = record[key.stringValue] as? CKAsset else { + throw typeMismatch("CKRecord value for CloudKitAssetValue field must be a CKAsset") + } + + let value = try decodeCustomAsset(customAssetType, from: ckAsset, key: key) + + return value as! T } else { guard let value = record[key.stringValue] as? T else { throw typeMismatch("CKRecordValue couldn't be converted to \"\(String(describing: type))\"") @@ -202,6 +210,18 @@ extension _CloudKitRecordDecoder.KeyedContainer: KeyedDecodingContainerProtocol return url } + private func decodeCustomAsset(_ type: T.Type, from asset: CKAsset, key: Key) throws -> T { + guard let url = asset.fileURL else { + throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "CKAsset has no fileURL") + } + + let contentType = (try url.resourceValues(forKeys: [.contentTypeKey])).contentType ?? T.preferredContentType + + let data = try Data(contentsOf: url) + + return try T.decoded(from: data, type: contentType) + } + private func decodeURL(from asset: CKAsset) throws -> URL { guard let url = asset.fileURL else { let context = DecodingError.Context(codingPath: codingPath, debugDescription: "URL value not found") @@ -250,6 +270,10 @@ extension _CloudKitRecordDecoder.KeyedContainer: KeyedDecodingContainerProtocol extension _CloudKitRecordDecoder.KeyedContainer: CloudKitRecordDecodingContainer {} -private extension JSONDecoder { +extension JSONDecoder { static let nestedCloudKitValue = JSONDecoder() } + +extension PropertyListDecoder { + static let nestedCloudKitValue = PropertyListDecoder() +} diff --git a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift index 4fb373a..3040924 100644 --- a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift @@ -200,13 +200,18 @@ extension _CloudKitRecordEncoder.KeyedContainer: KeyedEncodingContainerProtocol } private func encodedChildValue(for value: T, withKey key: Key) throws -> CKRecordValue where T : Encodable { - let encodedChild = try JSONEncoder.nestedCloudKitValue.encode(value) + if let customAssetValue = value as? CloudKitAssetValue { + let asset = try customAssetValue.createAsset() + return asset + } else { + let encodedChild = try JSONEncoder.nestedCloudKitValue.encode(value) - guard encodedChild.count < CKRecord.maxDataSize else { - throw CloudKitRecordEncodingError.dataFieldTooLarge(key: key.stringValue, size: encodedChild.count) - } + guard encodedChild.count < CKRecord.maxDataSize else { + throw CloudKitRecordEncodingError.dataFieldTooLarge(key: key.stringValue, size: encodedChild.count) + } - return encodedChild as NSData + return encodedChild as NSData + } } private func prepareMetaRecord(with systemFields: Data) throws { @@ -276,7 +281,7 @@ extension _CloudKitRecordEncoder.KeyedContainer: CloudKitRecordEncodingContainer } -private extension JSONEncoder { +extension JSONEncoder { static let nestedCloudKitValue: JSONEncoder = { let e = JSONEncoder() e.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] @@ -284,9 +289,38 @@ private extension JSONEncoder { }() } +extension PropertyListEncoder { + static let nestedCloudKitValueBinary: PropertyListEncoder = { + let e = PropertyListEncoder() + e.outputFormat = .binary + return e + }() + + static let nestedCloudKitValueXML: PropertyListEncoder = { + let e = PropertyListEncoder() + e.outputFormat = .xml + return e + }() +} + private extension CKRecord { /// The entire `CKRecord` can't exceed 1MB, but since we don't really know how large the whole /// record is, we just check data fields to ensure that they fit within the limit. This doesn't prevent /// the record from exceeding the 1MB limit, but at least catches the most egregious attempts. static let maxDataSize = 1_000_000 } + +// MARK: - CloudKitAssetValue Support + +private extension CloudKitAssetValue { + func createAsset() throws -> CKAsset { + let data = try encoded() + + let tempURL = FileManager.default.temporaryDirectory + .appendingPathComponent(filename) + + try data.write(to: tempURL) + + return CKAsset(fileURL: tempURL) + } +} diff --git a/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift index 715a434..ed2b29a 100644 --- a/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift @@ -107,4 +107,15 @@ final class CloudKitRecordDecoderTests: XCTestCase { XCTAssertEqual(sameModelDecoded, model) } + + func testCustomAssetRoundtrip() throws { + let model = TestModelCustomAsset.test + + let record = try CloudKitRecordEncoder().encode(model) + + var sameModelDecoded = try CloudKitRecordDecoder().decode(TestModelCustomAsset.self, from: record) + sameModelDecoded.cloudKitSystemFields = nil + + XCTAssertEqual(sameModelDecoded, model) + } } diff --git a/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift index fbce777..40ca500 100644 --- a/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift @@ -120,6 +120,30 @@ final class CloudKitRecordEncoderTests: XCTestCase { XCTAssertEqual(record["children"], encodedChildren) } + func testCustomAssetEncoding() throws { + let model = TestModelCustomAsset.test + + let record = try CloudKitRecordEncoder().encode(model) + + XCTAssertEqual(record["title"], model.title) + guard let asset = record["contents"] as? CKAsset else { + XCTFail("Expected CloudKitAssetValue to be encoded as CKAsset") + return + } + + let url = asset.fileURL! + + XCTAssertEqual(url.lastPathComponent, "Contents-MyID.json") + + let encodedAsset = """ + {"contentProperty1":"Prop1","contentProperty2":"Prop2","contentProperty3":"Prop3","contentProperty4":"Prop4","id":"MyID"} + """.UTF8Data() + + let assetData = try Data(contentsOf: url) + + XCTAssertEqual(assetData, encodedAsset) + } + } extension String { diff --git a/Tests/CloudKitCodableTests/TestTypes/TestModelCustomAsset.swift b/Tests/CloudKitCodableTests/TestTypes/TestModelCustomAsset.swift new file mode 100644 index 0000000..9bf3977 --- /dev/null +++ b/Tests/CloudKitCodableTests/TestTypes/TestModelCustomAsset.swift @@ -0,0 +1,28 @@ +import Foundation +import CloudKitCodable + +struct TestModelCustomAsset: Hashable, CustomCloudKitCodable { + struct Contents: Identifiable, Hashable, CloudKitAssetValue { + var id: String + var contentProperty1: String + var contentProperty2: String + var contentProperty3: String + var contentProperty4: String + } + var cloudKitSystemFields: Data? + var title: String + var contents: Contents +} + +extension TestModelCustomAsset { + static let test = TestModelCustomAsset( + title: "Hello Title", + contents: .init( + id: "MyID", + contentProperty1: "Prop1", + contentProperty2: "Prop2", + contentProperty3: "Prop3", + contentProperty4: "Prop4" + ) + ) +} From d2c7b437ad3e242d340d756dd5f5b3b6565680c5 Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Sun, 14 Apr 2024 11:58:07 -0300 Subject: [PATCH 18/21] Raised deployment target --- Package.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 488af86..c5a8e6a 100644 --- a/Package.swift +++ b/Package.swift @@ -5,9 +5,9 @@ let package = Package( name: "CloudKitCodable", platforms: [ .macOS(.v11), - .iOS(.v13), - .tvOS(.v13), - .watchOS(.v4) + .iOS(.v14), + .tvOS(.v14), + .watchOS(.v5) ], products: [ .library(name: "CloudKitCodable", targets: ["CloudKitCodable"]) From 27591535c8437fd4ca82e6d7f9c8ca1bcf1ac470 Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Mon, 15 Apr 2024 12:12:13 -0300 Subject: [PATCH 19/21] DocC documentation --- .spi.yml | 4 + README.MD | 30 ++++-- .../CloudKitCodable/CloudKitAssetValue.swift | 35 ++++++- Sources/CloudKitCodable/CloudKitEnum.swift | 32 +++++++ .../CustomCloudKitEncodable.swift | 92 ++++++++++++++----- .../Documentation.docc/CloudKitCodable.md | 77 ++++++++++++++++ 6 files changed, 234 insertions(+), 36 deletions(-) create mode 100644 .spi.yml create mode 100644 Sources/CloudKitCodable/CloudKitEnum.swift create mode 100644 Sources/CloudKitCodable/Documentation.docc/CloudKitCodable.md diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..a11143a --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [CloudKitCodable] diff --git a/README.MD b/README.MD index 2ae2db8..480af39 100644 --- a/README.MD +++ b/README.MD @@ -4,8 +4,6 @@ This project implements a `CloudKitRecordEncoder` and a `CloudKitRecordDecoder` so you can easily convert your custom data structure to a `CKRecord` and convert your `CKRecord` back to your custom data structure. -**Be aware this is an initial implementation that's not being used in production (yet) and it doesn't support nesting. Nested values would have to be encoded as `CKReference` and I haven't implemented that yet (feel free to open a PR 🤓).** - ## Usage ### `CustomCloudKitCodable` @@ -28,7 +26,19 @@ var cloudKitRecordType: String { get } This property should return the record type for your custom type. It's implemented automatically to return the name of the type, you only need to implement this if you need to customize the record type. -### URLs +### Data Types + +CloudKit imposes some limits on the types of data that can be stored as part of a `CKRecord`. CloudKitCodable attempts to use the best supported representation for any given property, but you should be aware of the limitations. + +#### Enumerations + +CloudKitCodable can encode and decode `enum` properties that are backed by either a `String` or `Int` raw value. + +In order for the enum encoding to work, your enum must conform to `CloudKitStringEnum` or `CloudKitIntEnum`, the only requirement being a static `cloudKitFallbackCase` property that determines the default value for the property in case the `CKRecord` contains a raw value that can't initialize the enum. + +This is important because if you add new cases to the enum, then it's possible for an older version of your app to get an enum case that it doesn't know about. + +#### URLs There's special handling for URLs because of the way CloudKit works with files. If you have a property that's a remote `URL` (i.e. a website), it's encoded as a `String` (CloudKit doesn't support URLs natively) and decoded back as a `URL`. If your property is a `URL` and it contains a `URL` to a local file, it is encoded as a `CKAsset`, the file will be automatically uploaded to CloudKit when you save the containing record and downloaded when you get the record from the cloud. The decoded `URL` will contain the `URL` for the location on disk where CloudKit has downloaded the file. @@ -83,19 +93,19 @@ do { } ``` -## Requirements +## Minimum Deployment Targets -- iOS 13.0+ -- macOS 11.0+ -- Xcode 13.2+ +- iOS 14 +- tvOS 14 +- watchOS 5 +- macOS 11 +- Xcode 15 (recommended) ## Installation ### Swift Package Manager -[Swift Package Manager](https://www.swift.org/package-manager) is a tool for automating the distribution of Swift code and is integrated into the Swift build system. - -Once you have your Swift package set up, adding CloudKitCodable as a dependency is as easy as adding it to the dependencies value of your `Package.swift`. +Add CloudKitCodable to your `Package.swift`: ```swift dependencies: [ diff --git a/Sources/CloudKitCodable/CloudKitAssetValue.swift b/Sources/CloudKitCodable/CloudKitAssetValue.swift index 65690bb..25bf1c3 100644 --- a/Sources/CloudKitCodable/CloudKitAssetValue.swift +++ b/Sources/CloudKitCodable/CloudKitAssetValue.swift @@ -4,17 +4,30 @@ import UniformTypeIdentifiers /// Adopted by `Codable` types that can be nested in ``CustomCloudKitCodable`` types, represented as `CKAsset` in records. /// -/// The corresponding `CKRecord` field is encoded as a `CKAsset` with a file containing the encoded representation of the value. +/// You implement `CloudKitAssetValue` for `Codable` types that can be used as properties of a type conforming to ``CustomCloudKitCodable``. +/// +/// This allows ``CloudKitRecordEncoder`` to encode the nested type as a `CKAsset` containing a (by default) JSON-encoded representation of the value. +/// When decoding with ``CloudKitRecordDecoder``, the local file downloaded from CloudKit is then read and decoded back as the corresponding value. /// -/// Implementers can customize the encoding/decoding, file type, and asset file name, but there are default implementations for all of this protocol's requirements. +/// Implementations can customize the encoding/decoding, file type, and asset file name, but there are default implementations for all of this protocol's requirements. public protocol CloudKitAssetValue: Codable { /// The default content type for `CKAsset` files representing values of this type. /// + /// When using the default implementations of ``CloudKitAssetValue/encoded()`` and ``CloudKitAssetValue/decoded(from:type:)``, + /// the preferred content type determines which encoder/decoder is used for the value: + /// + /// - `.json`: uses `JSONEncoder` and `JSONDecoder` + /// - `.xmlPropertyList`: uses `PropertyListEncoder` (XML) and `PropertyListDecoder` + /// - `.binaryPropertyList`: uses `PropertyListEncoder` (binary) and `PropertyListDecoder` + /// /// There's a default implementation that returns `.json`, so by default ``CloudKitAssetValue`` types are encoded as JSON. + /// + /// - Important: Changing the content type after you ship a version of your app to production is not recommended, but if you do, ``CloudKitRecordDecoder`` tries to determine the content type + /// based on the asset downloaded from CloudKit, using the declared type as a fallback. static var preferredContentType: UTType { get } - /// The preferred filename for this value when being encoded as a `CKAsset`. + /// The file name for this value when being encoded as a `CKAsset`. /// /// There's a default implementation for a filename with the format `-.(json/plist)`, /// and a default implementation for `Identifiable` types that uses the `id` property instead of a random UUID. @@ -31,7 +44,8 @@ public protocol CloudKitAssetValue: Codable { /// - type: Determines the type of decoder to be used. /// - Returns: The instance of the type. /// - /// There is a default implementation supporting JSON and PLIST types that uses `JSONDecoder`/`PropertyListDecoder`. + /// The default implementation uses `JSONDecoder`/`PropertyListDecoder` depending upon the `type`. + /// For more details, see the documentation for ``preferredContentType-8zbfl``. static func decoded(from data: Data, type: UTType) throws -> Self } @@ -57,6 +71,12 @@ public extension CloudKitAssetValue where Self: Identifiable { } public extension CloudKitAssetValue { + + /// Encodes the nested value. + /// - Returns: The encoded data. + /// + /// This default implementation uses ``preferredContentType-8zbfl`` in order to determine which encoder to use. + /// For more details, see the documentation for ``preferredContentType-8zbfl``. func encoded() throws -> Data { let type = Self.preferredContentType if type.conforms(to: .json) { @@ -71,7 +91,12 @@ public extension CloudKitAssetValue { throw EncodingError.invalidValue(self, .init(codingPath: [], debugDescription: "Unsupported content type \"\(type.identifier)\": the default implementation only supports JSON and PLIST")) } } - + + /// Decodes the nested value using data fetched from CloudKit. + /// - Parameters: + /// - data: The encoded data fetched from CloudKit. + /// - type: The `UTType` of the data. + /// - Returns: A decoded instance of the type. static func decoded(from data: Data, type: UTType) throws -> Self { if type.conforms(to: .json) { return try JSONDecoder.nestedCloudKitValue.decode(Self.self, from: data) diff --git a/Sources/CloudKitCodable/CloudKitEnum.swift b/Sources/CloudKitCodable/CloudKitEnum.swift new file mode 100644 index 0000000..7722df3 --- /dev/null +++ b/Sources/CloudKitCodable/CloudKitEnum.swift @@ -0,0 +1,32 @@ +import Foundation + +/// Base protocol for `enum` types that can be used as properties of types conforming to ``CloudKitRecordRepresentable``. +public protocol CloudKitEnum { + + /// The fallback `enum` `case` to be used when decoding from `CKRecord` encounters an unknown `case`. + /// + /// You implement this in custom enums that can be properties of ``CloudKitRecordRepresentable`` types in order to provide + /// a fallback value when ``CloudKitRecordDecoder`` encounters a raw value that's unknown. + /// + /// This can happen if for example you add more cases to your enum type in an app update. If a user has different versions of your app installed, + /// then it's possible for data on CloudKit to contain raw values that can't be decoded by an older version of the app. + /// + /// - Tip: if you'd like to have the model decoding fail completely if one of its `enum` properties has an unknown raw value, + /// then just return `nil` from your implementation. + static var cloudKitFallbackCase: Self? { get } +} + +public extension CloudKitEnum where Self: CaseIterable { + /// Uses the first `enum` case as the fallback when decoding from `CKRecord` encounters an unknown `case`. + static var cloudKitFallbackCase: Self? { allCases.first } +} + +/// Implemented by `enum` types with `String` raw value that can be used as properties of types conforming to ``CloudKitRecordRepresentable``. +/// +/// See ``CloudKitEnum`` for more details. +public protocol CloudKitStringEnum: Codable, RawRepresentable, CloudKitEnum where RawValue == String { } + +/// Implemented by `enum` types with `Int` raw value that can be used as properties of types conforming to ``CloudKitRecordRepresentable``. +/// +/// See ``CloudKitEnum`` for more details. +public protocol CloudKitIntEnum: Codable, RawRepresentable, CloudKitEnum where RawValue == Int { } diff --git a/Sources/CloudKitCodable/CustomCloudKitEncodable.swift b/Sources/CloudKitCodable/CustomCloudKitEncodable.swift index e35660c..c8bf393 100644 --- a/Sources/CloudKitCodable/CustomCloudKitEncodable.swift +++ b/Sources/CloudKitCodable/CustomCloudKitEncodable.swift @@ -12,40 +12,90 @@ import CloudKit internal let _CKSystemFieldsKeyName = "cloudKitSystemFields" internal let _CKIdentifierKeyName = "cloudKitIdentifier" +/// Base protocol for types that can be represented as a `CKRecord`. +/// +/// This protocol is the base for both ``CustomCloudKitEncodable`` and ``CustomCloudKitDecodable``. +/// +/// Its only requirement that doesn't have a default implementation is ``CloudKitRecordRepresentable/cloudKitSystemFields``, which is a required +/// property for all types that can be encoded as a `CKRecord` and decoded from a `CKRecord`. +/// +/// The ``CloudKitRecordRepresentable/cloudKitRecordType`` and ``CloudKitRecordRepresentable/cloudKitIdentifier`` +/// allow you to customize the CloudKit record type and record name. +/// +/// - note: You probably don't want to conform to `CloudKitRecordRepresentable` by itself. +/// Declare conformance to ``CustomCloudKitCodable`` instead, which will include the requirements from this protocol, +/// as well as support for encoding/decoding. public protocol CloudKitRecordRepresentable { + + /// Stores metadata about the `CKRecord` for this value. + /// + /// After a `CKRecord` is uploaded to CloudKit, or when a `CKRecord` is initially downloaded from CloudKit, + /// decoding it with ``CloudKitRecordDecoder`` will populate this property with metadata about the `CKRecord`. + /// + /// If you're using CloudKit's sync functionality, then you want to keep this metadata around so that new instances of `CKRecord` + /// created from the same value of your custom data type are recognized by CloudKit as being the same "instance" of that record type. + /// + /// - Important: If you're using CloudKit to keep local and remote data in sync between devices, then it's extremely important that you + /// store this data with your model, be it on a database, the filesystem, or wherever you're storing local data. Doing this ensures that CloudKit's sync + /// functionality will recognize the model across devices and allow for conflict resolution, preventing issues with duplicated records or data getting out of sync. + /// If you're just storing/retrieving data on the public database or just not using CloudKit's advanced sync capabilities, then it's less important to keep this metadata around. + /// Think of whether you're going to be uploading the same "instance" of your model to CloudKit multiple times, for example to update some of its properties. + /// If that's the case, then you should make sure that this metadata is present when encoding your updated model prior to uploading it to CloudKit again. var cloudKitSystemFields: Data? { get } + + /// The `recordType` for this type when encoded as a `CKRecord`. + /// + /// When you encode a custom data type into a `CKRecord` with ``CloudKitRecordEncoder``, + /// the encoder uses the value of this property when constructing the `CKRecord`, passing it as the record's `recordType`. + /// + /// **Default implementation**: ``cloudKitRecordType-79t3x`` var cloudKitRecordType: String { get } + + /// The `recordName` for this type when encoded as a `CKRecord`. + /// + /// When you encode a custom data type into a `CKRecord` with ``CloudKitRecordEncoder``, + /// the encoder uses the value of this property for its `recordName`, which is the canonical identifier for a record on CloudKit. + /// + /// If you already have an identifier for your model, then you'll probably want to implement this and return the value for that identifier, + /// so that it's easier to match between local values and their corresponding `CKRecord` on CloudKit. var cloudKitIdentifier: String { get } } -extension CloudKitRecordRepresentable { - public var cloudKitRecordType: String { +public extension CloudKitRecordRepresentable { + + /// The `recordType` using the type's name. + /// + /// This default implementation uses the name of your type as the `recordType` when encoding it as a `CKRecord`. + /// + /// For example, if you have a `Person` type, then the `recordType` of a `CKRecord` representing an instance of `Person` + /// will be — you guessed it — `Person`. + var cloudKitRecordType: String { return String(describing: type(of: self)) } - public var cloudKitIdentifier: String { + /// A random `UUID` to be used as the `recordName`. + /// + /// This default implementation generates a random `UUID` that's used as the `recordName` of a `CKRecord` when encoding your type. + /// + /// - note: If you already have a unique identifier for your data type, then you probably want to implement this property, returning your existing identifier. + /// + /// **Default implementation**: ``cloudKitIdentifier-uk1q`` + var cloudKitIdentifier: String { return UUID().uuidString } } -public protocol CustomCloudKitEncodable: CloudKitRecordRepresentable & Encodable { - -} - -public protocol CustomCloudKitDecodable: CloudKitRecordRepresentable & Decodable { +/// Implemented by types that can be encoded into `CKRecord` with ``CloudKitRecordEncoder``. +/// +/// See ``CloudKitRecordRepresentable`` for details. +public protocol CustomCloudKitEncodable: CloudKitRecordRepresentable & Encodable { } -} +/// Implemented by types that can be decoded from a `CKRecord` with ``CloudKitRecordDecoder``. +/// +/// See ``CloudKitRecordRepresentable`` for details. +public protocol CustomCloudKitDecodable: CloudKitRecordRepresentable & Decodable { } +/// Implemented by types that can be encoded and decoded to/from `CKRecord` with ``CloudKitRecordEncoder`` and ``CloudKitRecordDecoder``. +/// +/// See ``CloudKitRecordRepresentable`` for details. public protocol CustomCloudKitCodable: CustomCloudKitEncodable & CustomCloudKitDecodable { } - -public protocol CloudKitEnum { - static var cloudKitFallbackCase: Self? { get } -} - -public extension CloudKitEnum where Self: CaseIterable { - static var cloudKitFallbackCase: Self? { allCases.first } -} - -public protocol CloudKitStringEnum: Codable, RawRepresentable, CloudKitEnum where RawValue == String { } - -public protocol CloudKitIntEnum: Codable, RawRepresentable, CloudKitEnum where RawValue == Int { } diff --git a/Sources/CloudKitCodable/Documentation.docc/CloudKitCodable.md b/Sources/CloudKitCodable/Documentation.docc/CloudKitCodable.md new file mode 100644 index 0000000..a4a8707 --- /dev/null +++ b/Sources/CloudKitCodable/Documentation.docc/CloudKitCodable.md @@ -0,0 +1,77 @@ +# ``CloudKitCodable`` + +This library provides encoding and decoding of custom value types to and from `CKRecord`, making it a lot easier to transfer custom data types between your app and CloudKit. + +## Overview + +To make a type `CKRecord`-compatible, you implement the ``CustomCloudKitCodable`` protocol, which is composed of the ``CustomCloudKitEncodable`` and ``CustomCloudKitDecodable`` protocols. + +Encoding and decoding uses the same mechanism as `Codable`, but instead of using something like `JSONEncoder` and `JSONDecoder`, you use ``CloudKitRecordEncoder`` and ``CloudKitRecordDecoder``. +The encoder takes your custom data type as input and produces a corresponding `CKRecord`, and the decoder takes an existing `CKRecord` and produces an instance of your custom type. + +## Example + +Here's an example of a custom data type that implements ``CustomCloudKitCodable``: + +```swift +struct Person: CustomCloudKitCodable { + var cloudKitSystemFields: Data? + let name: String + let age: Int + let website: URL + let avatar: URL + let isDeveloper: Bool +} +``` + +The only requirement I had to implement in this example was the ``CloudKitRecordRepresentable/cloudKitSystemFields`` property, which is used to store metadata from CloudKit that's fetched alongside the `CKRecord`. + +If you plan on using your model as a way to sync user data with CloudKit, then you're probably storing it locally using something like a database. If that's the case, then it's important that you also store the value of `cloudKitSystemFields` after fetching a record from CloudKit, or after uploading the model for the first time. That way CloudKit can keep track of the data and allow you to address issues such as sync conflicts. + +Let's say I want to upload a `Person` record to CloudKit, this is how I would do it: + +```swift +let rambo = Person( + cloudKitSystemFields: nil, + name: "Guilherme Rambo", + age: 32, + website: URL(string:"https://rambo.codes")!, + avatar: URL(fileURLWithPath: "/Users/inside/Pictures/avatar.png"), + isDeveloper: true +) + +do { + let record = try CloudKitRecordEncoder().encode(rambo) + // record is now a CKRecord you can upload to CloudKit +} catch { + // something went wrong +} +``` + +This is how I would decode a `CKRecord` representing a `Person`: + +```swift +let record = // record obtained from CloudKit +do { + let person = try CloudKitRecordDecoder().decode(Person.self, from: record) +} catch { + // something went wrong +} +``` + +## Topics + +### Implementing Support for `CKRecord` in Your Models + +- ``CustomCloudKitCodable`` + +### Encoding and Decoding Records + +- ``CloudKitRecordEncoder`` +- ``CloudKitRecordDecoder`` + +### Supporting Custom Types and Assets + +- ``CloudKitStringEnum`` +- ``CloudKitIntEnum`` +- ``CloudKitAssetValue`` From 742a5c482b9190d4c8b7ac49641d748d7043dc58 Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Mon, 15 Apr 2024 13:26:38 -0300 Subject: [PATCH 20/21] Documented CloudKitRecordEncoder/Decoder --- .../CloudKitRecordDecoder.swift | 16 ++++++++ .../CloudKitRecordEncoder.swift | 38 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Sources/CloudKitCodable/CloudKitRecordDecoder.swift b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift index b603cae..029384c 100644 --- a/Sources/CloudKitCodable/CloudKitRecordDecoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift @@ -9,12 +9,28 @@ import Foundation import CloudKit +/// A decoder that takes a `CKRecord` and produces a value conforming to ``CustomCloudKitDecodable``. +/// +/// You use an instance of ``CloudKitRecordDecoder`` in order to transform a `CKRecord` downloaded from CloudKit into a value of your custom data type. final public class CloudKitRecordDecoder { + + /// Decodes a value conforming to ``CustomCloudKitDecodable`` from a `CKRecord` fetched from CloudKit. + /// - Parameters: + /// - type: The type of value. + /// - record: The record that was fetched from CloudKit. + /// - Returns: The decoded value with its properties matching those of the `CKRecord`. + /// + /// Once decoded from a `CKRecord`, your value will have its ``CloudKitRecordRepresentable/cloudKitSystemFields`` set to the corresponding + /// metadata from the `CKRecord`. When encoding the same value again, such as when updating a record, ``CloudKitRecordEncoder`` will use this encoded metadata + /// to produce a record that CloudKit will recognize as being the same "instance". public func decode(_ type: T.Type, from record: CKRecord) throws -> T where T : Decodable { let decoder = _CloudKitRecordDecoder(record: record) return try T(from: decoder) } + /// Creates a new instance of the decoder. + /// + /// - Tip: You may safely reuse an instance of ``CloudKitRecordDecoder`` for multiple operations. public init() { } } diff --git a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift index 3040924..a681c1c 100644 --- a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift @@ -9,12 +9,22 @@ import Foundation import CloudKit +/// Errors that can occur when encoding a custom type to `CKRecord`. public enum CloudKitRecordEncodingError: Error { + /// A given model property contains a value that can't be encoded into the `CKRecord`. case unsupportedValueForKey(String) + /// The existing value in ``CloudKitRecordRepresentable/cloudKitSystemFields`` couldn't be decoded + /// for constructing an updated version of the corresponding `CKRecord`. case systemFieldsDecode(String) + @available(*, deprecated, message: "This is no longer thrown and is kept for source compatibility.") case referencesNotSupported(String) + /// A `Data` property or a nested `Codable` value ended up being too large for encoding into a `CKRecord`. + /// + /// - Tip: If you're experiencing this error when encoding a model that has a nested `Codable` property, + /// consider adopting ``CloudKitAssetValue`` so that the property can be encoded as a `CKAsset` instead. case dataFieldTooLarge(key: String, size: Int) + /// A description of the encoding error. public var localizedDescription: String { switch self { case .unsupportedValueForKey(let key): @@ -33,9 +43,28 @@ public enum CloudKitRecordEncodingError: Error { } } +/// An encoder that takes a value conforming to ``CustomCloudKitEncodable`` and produces a `CKRecord`. +/// +/// You use an instance of ``CloudKitRecordEncoder`` in order to transform your custom data type into a `CKRecord` before uploading it to CloudKit. public class CloudKitRecordEncoder { - public var zoneID: CKRecordZone.ID? + /// The CloudKit zone identifier that will be associated with the record created by this encoder. + /// + /// - Note: This property is ignored when encoding a value with its ``CloudKitRecordRepresentable/cloudKitSystemFields`` property set. + /// When that's the case, the zone ID is read from the record metadata encoded in the system fields. + public var zoneID: CKRecordZone.ID? + + /// Encodes a value conforming to ``CustomCloudKitEncodable``, turning it into a `CKRecord`. + /// - Parameter value: The value to be encoded. + /// - Returns: A `CKRecord` representing the value. + /// + /// Your custom data type that conforms to ``CustomCloudKitEncodable`` is turned into a `CKRecord` where each record field + /// represents a property of your type, according to its `CodingKeys`. + /// + /// If the encoder is initialized with a ``zoneID``, then the ID of the `CKRecord` will include that zone ID. + /// + /// When encoding a value that's already been through the CloudKit servers, its ``CloudKitRecordRepresentable/cloudKitSystemFields`` should be available, + /// in which case the encoder will construct a `CKRecord` with the metadata corresponding to the record on the server. public func encode(_ value: Encodable) throws -> CKRecord { let type = recordTypeName(for: value) let name = recordName(for: value) @@ -62,7 +91,12 @@ public class CloudKitRecordEncoder { return UUID().uuidString } } - + + /// Initializes the encoder. + /// - Parameter zoneID: If provided, the `CKRecord` produced will have its record ID + /// set to the specified zone. Uses the default CloudKit zone if the zone is not specified. + /// + /// - Tip: You may safely reuse an instance of ``CloudKitRecordEncoder`` for multiple operations. public init(zoneID: CKRecordZone.ID? = nil) { self.zoneID = zoneID } From 9b56bf80b4a3b48e88daf317a0aef9249bbe16c5 Mon Sep 17 00:00:00 2001 From: Guilherme Rambo Date: Mon, 15 Apr 2024 14:05:22 -0300 Subject: [PATCH 21/21] Article detailing data type support, simplified readme --- README.MD | 60 +++---------------- .../CloudKitRecordEncoder.swift | 2 + .../Documentation.docc/CloudKitCodable.md | 50 +--------------- .../Documentation.docc/DataTypes.md | 41 +++++++++++++ .../Documentation.docc/Example.md | 51 ++++++++++++++++ 5 files changed, 104 insertions(+), 100 deletions(-) create mode 100644 Sources/CloudKitCodable/Documentation.docc/DataTypes.md create mode 100644 Sources/CloudKitCodable/Documentation.docc/Example.md diff --git a/README.MD b/README.MD index 480af39..1c3b3d3 100644 --- a/README.MD +++ b/README.MD @@ -2,50 +2,15 @@ [![Badge showing the current build status](https://github.com/insidegui/CloudKitCodable/actions/workflows/swift-package.yml/badge.svg)](https://github.com/insidegui/CloudKitCodable/actions/workflows/swift-package.yml) -This project implements a `CloudKitRecordEncoder` and a `CloudKitRecordDecoder` so you can easily convert your custom data structure to a `CKRecord` and convert your `CKRecord` back to your custom data structure. +This project implements `CloudKitRecordEncoder` and `CloudKitRecordDecoder`, allowing for custom data types to be converted to/from `CKRecord` automatically. ## Usage -### `CustomCloudKitCodable` - -The types you want to convert to/from `CKRecord` must implement the `CustomCloudKitCodable` protocol. This is necessary because unlike most implementations of encoders/decoders, we are not converting to/from `Data`, but to/from `CKRecord`, which has some special requirements. - -There are also two other protocols: `CustomCloudKitEncodable` and `CustomCloudKitDecodable`. You can use those if you only need either encoding or decoding respectively. - -The protocol requires two properties on the type you want to convert to/from `CKRecord`: - -```swift -var cloudKitSystemFields: Data? { get } -``` - -This will be used to store the system fields for the `CKRecord` when decoding. The system fields contain metadata for the record such as its unique identifier and they're very important when syncing. - -```swift -var cloudKitRecordType: String { get } -``` - -This property should return the record type for your custom type. It's implemented automatically to return the name of the type, you only need to implement this if you need to customize the record type. - -### Data Types - -CloudKit imposes some limits on the types of data that can be stored as part of a `CKRecord`. CloudKitCodable attempts to use the best supported representation for any given property, but you should be aware of the limitations. - -#### Enumerations - -CloudKitCodable can encode and decode `enum` properties that are backed by either a `String` or `Int` raw value. - -In order for the enum encoding to work, your enum must conform to `CloudKitStringEnum` or `CloudKitIntEnum`, the only requirement being a static `cloudKitFallbackCase` property that determines the default value for the property in case the `CKRecord` contains a raw value that can't initialize the enum. - -This is important because if you add new cases to the enum, then it's possible for an older version of your app to get an enum case that it doesn't know about. - -#### URLs -There's special handling for URLs because of the way CloudKit works with files. If you have a property that's a remote `URL` (i.e. a website), it's encoded as a `String` (CloudKit doesn't support URLs natively) and decoded back as a `URL`. - -If your property is a `URL` and it contains a `URL` to a local file, it is encoded as a `CKAsset`, the file will be automatically uploaded to CloudKit when you save the containing record and downloaded when you get the record from the cloud. The decoded `URL` will contain the `URL` for the location on disk where CloudKit has downloaded the file. +For details on how to use CloudKitCodable, please check the included documentation. ### Example -Let's say you have a `Person` model you want to sync to CloudKit. This is what the model would look like: +Declaring a model that can be encoded as a `CKRecord`: ```swift struct Person: CustomCloudKitCodable { @@ -58,19 +23,10 @@ struct Person: CustomCloudKitCodable { } ``` -Notice I didn't implement `cloudKitRecordType`, in that case, the `CKRecord` type for this model will be `Person` (the name of the type itself). - -Now, before saving the record to CloudKit, we encode it: +Creating a `CKRecord` from a `CustomCloudKitCodable` type: ```swift -let rambo = Person( - cloudKitSystemFields: nil, - name: "Guilherme Rambo", - age: 26, - website: URL(string:"https://guilhermerambo.me")!, - avatar: URL(fileURLWithPath: "/Users/inside/Pictures/avatar.png"), - isDeveloper: true -) +let rambo = Person(...) do { let record = try CloudKitRecordEncoder().encode(rambo) @@ -80,9 +36,7 @@ do { } ``` -Since `avatar` points to a local file, the corresponding file will be uploaded as a `CKAsset` when the record is saved to CloudKit and downloaded back when the record is retrieved. - -To decode the record: +Decoding a `CustomCloudKitCodable` type from a `CKRecord`: ```swift let record = // record obtained from CloudKit @@ -109,7 +63,7 @@ Add CloudKitCodable to your `Package.swift`: ```swift dependencies: [ - .package(url: "https://github.com/insidegui/CloudKitCodable.git", from: "0.2.0") + .package(url: "https://github.com/insidegui/CloudKitCodable.git", from: "0.3.0") ] ``` diff --git a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift index a681c1c..c778de1 100644 --- a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift @@ -12,6 +12,8 @@ import CloudKit /// Errors that can occur when encoding a custom type to `CKRecord`. public enum CloudKitRecordEncodingError: Error { /// A given model property contains a value that can't be encoded into the `CKRecord`. + /// + /// To learn more about supported data types and limitations, see . case unsupportedValueForKey(String) /// The existing value in ``CloudKitRecordRepresentable/cloudKitSystemFields`` couldn't be decoded /// for constructing an updated version of the corresponding `CKRecord`. diff --git a/Sources/CloudKitCodable/Documentation.docc/CloudKitCodable.md b/Sources/CloudKitCodable/Documentation.docc/CloudKitCodable.md index a4a8707..c03f300 100644 --- a/Sources/CloudKitCodable/Documentation.docc/CloudKitCodable.md +++ b/Sources/CloudKitCodable/Documentation.docc/CloudKitCodable.md @@ -9,55 +9,11 @@ To make a type `CKRecord`-compatible, you implement the ``CustomCloudKitCodable` Encoding and decoding uses the same mechanism as `Codable`, but instead of using something like `JSONEncoder` and `JSONDecoder`, you use ``CloudKitRecordEncoder`` and ``CloudKitRecordDecoder``. The encoder takes your custom data type as input and produces a corresponding `CKRecord`, and the decoder takes an existing `CKRecord` and produces an instance of your custom type. -## Example +## Quick Start -Here's an example of a custom data type that implements ``CustomCloudKitCodable``: +For a simple example, see . -```swift -struct Person: CustomCloudKitCodable { - var cloudKitSystemFields: Data? - let name: String - let age: Int - let website: URL - let avatar: URL - let isDeveloper: Bool -} -``` - -The only requirement I had to implement in this example was the ``CloudKitRecordRepresentable/cloudKitSystemFields`` property, which is used to store metadata from CloudKit that's fetched alongside the `CKRecord`. - -If you plan on using your model as a way to sync user data with CloudKit, then you're probably storing it locally using something like a database. If that's the case, then it's important that you also store the value of `cloudKitSystemFields` after fetching a record from CloudKit, or after uploading the model for the first time. That way CloudKit can keep track of the data and allow you to address issues such as sync conflicts. - -Let's say I want to upload a `Person` record to CloudKit, this is how I would do it: - -```swift -let rambo = Person( - cloudKitSystemFields: nil, - name: "Guilherme Rambo", - age: 32, - website: URL(string:"https://rambo.codes")!, - avatar: URL(fileURLWithPath: "/Users/inside/Pictures/avatar.png"), - isDeveloper: true -) - -do { - let record = try CloudKitRecordEncoder().encode(rambo) - // record is now a CKRecord you can upload to CloudKit -} catch { - // something went wrong -} -``` - -This is how I would decode a `CKRecord` representing a `Person`: - -```swift -let record = // record obtained from CloudKit -do { - let person = try CloudKitRecordDecoder().decode(Person.self, from: record) -} catch { - // something went wrong -} -``` +To familiarize yourself with how different data types are encoded and decoded, see . ## Topics diff --git a/Sources/CloudKitCodable/Documentation.docc/DataTypes.md b/Sources/CloudKitCodable/Documentation.docc/DataTypes.md new file mode 100644 index 0000000..45e1ead --- /dev/null +++ b/Sources/CloudKitCodable/Documentation.docc/DataTypes.md @@ -0,0 +1,41 @@ +# Data Types + +How CloudKitCodable handles different data types when dealing with `CKRecord` encoding/decoding. + +CloudKit [imposes some limits](https://developer.apple.com/documentation/cloudkit/ckrecord) on what data types can be stored in a `CKRecord`, as well as the maximum size for all data associated with an individual record. + +CloudKitCodable tries to handle most of these limitations automatically, but if you're looking to encode and decode complex types to/from `CKRecord`, then there's additional work you can do to make sure that everything works correctly. + +## Primitive Types + +Simple types such as `String` and `Int` work as you would expect: they're simply stored in the `CKRecord` as-is. + +## URL + +There's no native support for `URL` in `CKRecord`, and CloudKitCodable handles URLs differently depending upon whether the `URL` is a local file URL, or a remote web URL. + +### Local File URLs + +When ``CloudKitRecordEncoder`` encounters a property with a `URL` pointing to a local file, the corresponding property on the `CKRecord` will be encoded as a [CKAsset](https://developer.apple.com/documentation/cloudkit/ckasset). + +So in order to upload a file to CloudKit, you can have the corresponding property be a `URL` pointing to the local file, and it will be uploaded when saving the record. + +The opposite occurs when decoding a record downloaded from CloudKit: ``CloudKitRecordDecoder`` will find the `CKAsset` and set the `URL` property to point to the local file URL downloaded by CloudKit. + +### Remote Web URLs + +When the `URL` property being encoded has a web URL such as `https://apple.com`, it will be encoded into the `CKRecord` as a `String` containing its absolute string. Upon decoding, the string is then parsed as a `URL`. + +### Custom Enumerations + +CloudKitCodable can encode and decode `enum` properties that are backed by either a `String` or `Int` raw value. + +In order for the enum encoding to work, your enum must conform to ``CloudKitStringEnum`` or ``CloudKitIntEnum``, the only requirement being a static ``CloudKitEnum/cloudKitFallbackCase`` property that determines the default value for the property in case the `CKRecord` contains a raw value that can't initialize the enum. + +### Nested Codable Values + +Sometimes models have properties that use small value types with a few of their own properties, and you might want to store such models on CloudKit as well. + +To enable this, CloudKitCodable will detect properties that have a custom `Codable` type and set the corresponding `CKRecord` field to be a `Data` value with the JSON-encoded representation of the value. + +> Important: If your model has a property with a `Codable` type that can potentially become large when encoded, or if your model has more than a couple of properties with `Codable` types, then you should adopt ``CloudKitAssetValue`` instead so that the properties can be represented as a `CKAsset`, which doesn't run the risk of bumping into the 1MB per-record size limit. diff --git a/Sources/CloudKitCodable/Documentation.docc/Example.md b/Sources/CloudKitCodable/Documentation.docc/Example.md new file mode 100644 index 0000000..7fadd59 --- /dev/null +++ b/Sources/CloudKitCodable/Documentation.docc/Example.md @@ -0,0 +1,51 @@ +# Example + +Simple example of how to implement support for `CKRecord` in a custom data type. + +Here's an example of a custom data type that implements ``CustomCloudKitCodable``: + +```swift +struct Person: CustomCloudKitCodable { + var cloudKitSystemFields: Data? + let name: String + let age: Int + let website: URL + let avatar: URL + let isDeveloper: Bool +} +``` + +The only requirement I had to implement in this example was the ``CloudKitRecordRepresentable/cloudKitSystemFields`` property, which is used to store metadata from CloudKit that's fetched alongside the `CKRecord`. + +If you plan on using your model as a way to sync user data with CloudKit, then you're probably storing it locally using something like a database. If that's the case, then it's important that you also store the value of `cloudKitSystemFields` after fetching a record from CloudKit, or after uploading the model for the first time. That way CloudKit can keep track of the data and allow you to address issues such as sync conflicts. + +Let's say I want to upload a `Person` record to CloudKit, this is how I would do it: + +```swift +let rambo = Person( + cloudKitSystemFields: nil, + name: "Guilherme Rambo", + age: 32, + website: URL(string:"https://rambo.codes")!, + avatar: URL(fileURLWithPath: "/Users/inside/Pictures/avatar.png"), + isDeveloper: true +) + +do { + let record = try CloudKitRecordEncoder().encode(rambo) + // record is now a CKRecord you can upload to CloudKit +} catch { + // something went wrong +} +``` + +This is how I would decode a `CKRecord` representing a `Person`: + +```swift +let record = // record obtained from CloudKit +do { + let person = try CloudKitRecordDecoder().decode(Person.self, from: record) +} catch { + // something went wrong +} +```