├── .gitignore ├── Cartfile ├── LICENSE ├── README.md ├── SyncEngine.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── SyncEngine.xcscheme └── xcuserdata │ └── purkylinking.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── SyncEngine ├── Core │ ├── CKDatabase+Subscription.swift │ ├── CKError+Detail.swift │ ├── Database.swift │ ├── DefaultsKey.swift │ ├── HandleCloudKitError.swift │ ├── KeyStore.swift │ ├── LocalCache.swift │ ├── SyncBaseModel.swift │ └── SyncEngine.swift ├── Info.plist └── SyncEngine.h └── SyncEngineDemo ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj ├── LaunchScreen.storyboard └── Main.storyboard ├── EditViewController.swift ├── Info.plist ├── InfoViewController.swift ├── Note.swift ├── SyncEngineDemo.entitlements └── ViewController.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "realm/realm-cocoa" == 3.7.4 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Purkylin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SyncEngine 2 | Sync between iCloud and Realm 3 | 4 | ## Feature 5 | 6 | - [x] Sync betweens different device 7 | - [x] Share 8 | - [x] Offline 9 | - [x] Resolve conflict 10 | - [x] Multi table 11 | - [ ] CKAsset 12 | - [ ] Stable API 13 | - [ ] Documentation 14 | - [ ] Same device different iCloud account 15 | - [ ] Background long task 16 | 17 | ## Requirement 18 | * iOS 10.0 19 | * swift 4.0 20 | * Xcode 9.0 21 | 22 | ## Usage 23 | 1. model 24 | ```swift 25 | @objc(SimpleNote) 26 | class SimpleNote: SyncBaseModel { 27 | @objc dynamic var title: String = "" 28 | } 29 | ``` 30 | 31 | 2. AppDelegate 32 | ```swift 33 | application.registerForRemoteNotifications() 34 | 35 | syncEngine.register(models: [SimpleNote.self]) 36 | syncEngine.start() 37 | ``` 38 | 39 | 3. Sync 40 | ``` 41 | syncEngine.sync() 42 | ``` 43 | 44 | ## Carthage 45 | `github "purkylin/SyncEngine"` 46 | -------------------------------------------------------------------------------- /SyncEngine.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0D24605020DD03D20091F4EC /* CKError+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D24604F20DD03D20091F4EC /* CKError+Detail.swift */; }; 11 | 0DC903D220C7CA040072B3D4 /* SyncEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC903D020C7CA040072B3D4 /* SyncEngine.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | 0DC903DE20C7CA780072B3D4 /* HandleCloudKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC903D920C7CA770072B3D4 /* HandleCloudKitError.swift */; }; 13 | 0DC903DF20C7CA780072B3D4 /* CKDatabase+Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC903DA20C7CA770072B3D4 /* CKDatabase+Subscription.swift */; }; 14 | 0DC903E020C7CA780072B3D4 /* SyncEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC903DB20C7CA770072B3D4 /* SyncEngine.swift */; }; 15 | 0DC903E120C7CA780072B3D4 /* LocalCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC903DC20C7CA770072B3D4 /* LocalCache.swift */; }; 16 | 0DC903E220C7CA780072B3D4 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC903DD20C7CA770072B3D4 /* Database.swift */; }; 17 | 0DC903E420C7CAA70072B3D4 /* KeyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC903E320C7CAA70072B3D4 /* KeyStore.swift */; }; 18 | 0DC903EC20C7CCFE0072B3D4 /* SyncBaseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC903EB20C7CCFD0072B3D4 /* SyncBaseModel.swift */; }; 19 | 0DC903EE20C7CD8E0072B3D4 /* DefaultsKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC903ED20C7CD8E0072B3D4 /* DefaultsKey.swift */; }; 20 | 0DC903F120C7CF470072B3D4 /* Realm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0DC903EF20C7CF470072B3D4 /* Realm.framework */; }; 21 | 0DC903F220C7CF470072B3D4 /* RealmSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0DC903F020C7CF470072B3D4 /* RealmSwift.framework */; }; 22 | 0DC903FA20C8F0C60072B3D4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC903F920C8F0C60072B3D4 /* AppDelegate.swift */; }; 23 | 0DC903FC20C8F0C60072B3D4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC903FB20C8F0C60072B3D4 /* ViewController.swift */; }; 24 | 0DC903FF20C8F0C60072B3D4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0DC903FD20C8F0C60072B3D4 /* Main.storyboard */; }; 25 | 0DC9040120C8F0C70072B3D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0DC9040020C8F0C70072B3D4 /* Assets.xcassets */; }; 26 | 0DC9040420C8F0C70072B3D4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0DC9040220C8F0C70072B3D4 /* LaunchScreen.storyboard */; }; 27 | 0DC9041120C8F4B30072B3D4 /* EditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC9041020C8F4B30072B3D4 /* EditViewController.swift */; }; 28 | 0DC9041320C8F59D0072B3D4 /* Note.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC9041220C8F59D0072B3D4 /* Note.swift */; }; 29 | 0DC9041620C8F83A0072B3D4 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0DC9041520C8F83A0072B3D4 /* CloudKit.framework */; }; 30 | 0DC9041920C8F89D0072B3D4 /* SyncEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0DC903CD20C7CA040072B3D4 /* SyncEngine.framework */; }; 31 | 0DC9041A20C8F89D0072B3D4 /* SyncEngine.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0DC903CD20C7CA040072B3D4 /* SyncEngine.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 32 | 0DC9041B20C8F8D20072B3D4 /* Realm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0DC903EF20C7CF470072B3D4 /* Realm.framework */; }; 33 | 0DC9041C20C8F8D20072B3D4 /* Realm.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0DC903EF20C7CF470072B3D4 /* Realm.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 34 | 0DC9041D20C8F8D20072B3D4 /* RealmSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0DC903F020C7CF470072B3D4 /* RealmSwift.framework */; }; 35 | 0DC9041E20C8F8D20072B3D4 /* RealmSwift.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0DC903F020C7CF470072B3D4 /* RealmSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 36 | 0DC9042020C9029E0072B3D4 /* InfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC9041F20C9029E0072B3D4 /* InfoViewController.swift */; }; 37 | /* End PBXBuildFile section */ 38 | 39 | /* Begin PBXContainerItemProxy section */ 40 | 0DC9041720C8F8780072B3D4 /* PBXContainerItemProxy */ = { 41 | isa = PBXContainerItemProxy; 42 | containerPortal = 0DC903C420C7CA040072B3D4 /* Project object */; 43 | proxyType = 1; 44 | remoteGlobalIDString = 0DC903CC20C7CA040072B3D4; 45 | remoteInfo = SyncEngine; 46 | }; 47 | /* End PBXContainerItemProxy section */ 48 | 49 | /* Begin PBXCopyFilesBuildPhase section */ 50 | 0DC9040F20C8F1060072B3D4 /* Embed Frameworks */ = { 51 | isa = PBXCopyFilesBuildPhase; 52 | buildActionMask = 2147483647; 53 | dstPath = ""; 54 | dstSubfolderSpec = 10; 55 | files = ( 56 | 0DC9041C20C8F8D20072B3D4 /* Realm.framework in Embed Frameworks */, 57 | 0DC9041A20C8F89D0072B3D4 /* SyncEngine.framework in Embed Frameworks */, 58 | 0DC9041E20C8F8D20072B3D4 /* RealmSwift.framework in Embed Frameworks */, 59 | ); 60 | name = "Embed Frameworks"; 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | /* End PBXCopyFilesBuildPhase section */ 64 | 65 | /* Begin PBXFileReference section */ 66 | 0D24604F20DD03D20091F4EC /* CKError+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKError+Detail.swift"; sourceTree = ""; }; 67 | 0DC903CD20C7CA040072B3D4 /* SyncEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SyncEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | 0DC903D020C7CA040072B3D4 /* SyncEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SyncEngine.h; sourceTree = ""; }; 69 | 0DC903D120C7CA040072B3D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 70 | 0DC903D920C7CA770072B3D4 /* HandleCloudKitError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HandleCloudKitError.swift; sourceTree = ""; }; 71 | 0DC903DA20C7CA770072B3D4 /* CKDatabase+Subscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CKDatabase+Subscription.swift"; sourceTree = ""; }; 72 | 0DC903DB20C7CA770072B3D4 /* SyncEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncEngine.swift; sourceTree = ""; }; 73 | 0DC903DC20C7CA770072B3D4 /* LocalCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalCache.swift; sourceTree = ""; }; 74 | 0DC903DD20C7CA770072B3D4 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; 75 | 0DC903E320C7CAA70072B3D4 /* KeyStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyStore.swift; sourceTree = ""; }; 76 | 0DC903E720C7CC690072B3D4 /* RealmSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RealmSwift.framework; path = SyncEngine/Carthage/Checkouts/Build/iOS/RealmSwift.framework; sourceTree = ""; }; 77 | 0DC903E820C7CC690072B3D4 /* Realm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Realm.framework; path = SyncEngine/Carthage/Checkouts/Build/iOS/Realm.framework; sourceTree = ""; }; 78 | 0DC903EB20C7CCFD0072B3D4 /* SyncBaseModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncBaseModel.swift; sourceTree = ""; }; 79 | 0DC903ED20C7CD8E0072B3D4 /* DefaultsKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsKey.swift; sourceTree = ""; }; 80 | 0DC903EF20C7CF470072B3D4 /* Realm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Realm.framework; path = Carthage/Build/iOS/Realm.framework; sourceTree = ""; }; 81 | 0DC903F020C7CF470072B3D4 /* RealmSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RealmSwift.framework; path = Carthage/Build/iOS/RealmSwift.framework; sourceTree = ""; }; 82 | 0DC903F720C8F0C60072B3D4 /* SyncEngineDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SyncEngineDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 83 | 0DC903F920C8F0C60072B3D4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 84 | 0DC903FB20C8F0C60072B3D4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 85 | 0DC903FE20C8F0C60072B3D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 86 | 0DC9040020C8F0C70072B3D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 87 | 0DC9040320C8F0C70072B3D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 88 | 0DC9040520C8F0C70072B3D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 89 | 0DC9041020C8F4B30072B3D4 /* EditViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditViewController.swift; sourceTree = ""; }; 90 | 0DC9041220C8F59D0072B3D4 /* Note.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Note.swift; sourceTree = ""; }; 91 | 0DC9041420C8F8370072B3D4 /* SyncEngineDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SyncEngineDemo.entitlements; sourceTree = ""; }; 92 | 0DC9041520C8F83A0072B3D4 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 93 | 0DC9041F20C9029E0072B3D4 /* InfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoViewController.swift; sourceTree = ""; }; 94 | /* End PBXFileReference section */ 95 | 96 | /* Begin PBXFrameworksBuildPhase section */ 97 | 0DC903C920C7CA040072B3D4 /* Frameworks */ = { 98 | isa = PBXFrameworksBuildPhase; 99 | buildActionMask = 2147483647; 100 | files = ( 101 | 0DC903F120C7CF470072B3D4 /* Realm.framework in Frameworks */, 102 | 0DC903F220C7CF470072B3D4 /* RealmSwift.framework in Frameworks */, 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | 0DC903F420C8F0C60072B3D4 /* Frameworks */ = { 107 | isa = PBXFrameworksBuildPhase; 108 | buildActionMask = 2147483647; 109 | files = ( 110 | 0DC9041B20C8F8D20072B3D4 /* Realm.framework in Frameworks */, 111 | 0DC9041620C8F83A0072B3D4 /* CloudKit.framework in Frameworks */, 112 | 0DC9041D20C8F8D20072B3D4 /* RealmSwift.framework in Frameworks */, 113 | 0DC9041920C8F89D0072B3D4 /* SyncEngine.framework in Frameworks */, 114 | ); 115 | runOnlyForDeploymentPostprocessing = 0; 116 | }; 117 | /* End PBXFrameworksBuildPhase section */ 118 | 119 | /* Begin PBXGroup section */ 120 | 0DC903C320C7CA040072B3D4 = { 121 | isa = PBXGroup; 122 | children = ( 123 | 0DC903CF20C7CA040072B3D4 /* SyncEngine */, 124 | 0DC903F820C8F0C60072B3D4 /* SyncEngineDemo */, 125 | 0DC903CE20C7CA040072B3D4 /* Products */, 126 | 0DC903E620C7CC690072B3D4 /* Frameworks */, 127 | ); 128 | sourceTree = ""; 129 | }; 130 | 0DC903CE20C7CA040072B3D4 /* Products */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 0DC903CD20C7CA040072B3D4 /* SyncEngine.framework */, 134 | 0DC903F720C8F0C60072B3D4 /* SyncEngineDemo.app */, 135 | ); 136 | name = Products; 137 | sourceTree = ""; 138 | }; 139 | 0DC903CF20C7CA040072B3D4 /* SyncEngine */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 0DC903D820C7CA770072B3D4 /* Core */, 143 | 0DC903D020C7CA040072B3D4 /* SyncEngine.h */, 144 | 0DC903D120C7CA040072B3D4 /* Info.plist */, 145 | ); 146 | path = SyncEngine; 147 | sourceTree = ""; 148 | }; 149 | 0DC903D820C7CA770072B3D4 /* Core */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 0DC903E320C7CAA70072B3D4 /* KeyStore.swift */, 153 | 0DC903D920C7CA770072B3D4 /* HandleCloudKitError.swift */, 154 | 0DC903EB20C7CCFD0072B3D4 /* SyncBaseModel.swift */, 155 | 0DC903DA20C7CA770072B3D4 /* CKDatabase+Subscription.swift */, 156 | 0DC903DB20C7CA770072B3D4 /* SyncEngine.swift */, 157 | 0DC903DC20C7CA770072B3D4 /* LocalCache.swift */, 158 | 0DC903DD20C7CA770072B3D4 /* Database.swift */, 159 | 0DC903ED20C7CD8E0072B3D4 /* DefaultsKey.swift */, 160 | 0D24604F20DD03D20091F4EC /* CKError+Detail.swift */, 161 | ); 162 | path = Core; 163 | sourceTree = ""; 164 | }; 165 | 0DC903E620C7CC690072B3D4 /* Frameworks */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | 0DC9041520C8F83A0072B3D4 /* CloudKit.framework */, 169 | 0DC903EF20C7CF470072B3D4 /* Realm.framework */, 170 | 0DC903F020C7CF470072B3D4 /* RealmSwift.framework */, 171 | 0DC903E820C7CC690072B3D4 /* Realm.framework */, 172 | 0DC903E720C7CC690072B3D4 /* RealmSwift.framework */, 173 | ); 174 | name = Frameworks; 175 | sourceTree = ""; 176 | }; 177 | 0DC903F820C8F0C60072B3D4 /* SyncEngineDemo */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 0DC9041420C8F8370072B3D4 /* SyncEngineDemo.entitlements */, 181 | 0DC903F920C8F0C60072B3D4 /* AppDelegate.swift */, 182 | 0DC903FB20C8F0C60072B3D4 /* ViewController.swift */, 183 | 0DC9041F20C9029E0072B3D4 /* InfoViewController.swift */, 184 | 0DC9041220C8F59D0072B3D4 /* Note.swift */, 185 | 0DC9041020C8F4B30072B3D4 /* EditViewController.swift */, 186 | 0DC903FD20C8F0C60072B3D4 /* Main.storyboard */, 187 | 0DC9040020C8F0C70072B3D4 /* Assets.xcassets */, 188 | 0DC9040220C8F0C70072B3D4 /* LaunchScreen.storyboard */, 189 | 0DC9040520C8F0C70072B3D4 /* Info.plist */, 190 | ); 191 | path = SyncEngineDemo; 192 | sourceTree = ""; 193 | }; 194 | /* End PBXGroup section */ 195 | 196 | /* Begin PBXHeadersBuildPhase section */ 197 | 0DC903CA20C7CA040072B3D4 /* Headers */ = { 198 | isa = PBXHeadersBuildPhase; 199 | buildActionMask = 2147483647; 200 | files = ( 201 | 0DC903D220C7CA040072B3D4 /* SyncEngine.h in Headers */, 202 | ); 203 | runOnlyForDeploymentPostprocessing = 0; 204 | }; 205 | /* End PBXHeadersBuildPhase section */ 206 | 207 | /* Begin PBXNativeTarget section */ 208 | 0DC903CC20C7CA040072B3D4 /* SyncEngine */ = { 209 | isa = PBXNativeTarget; 210 | buildConfigurationList = 0DC903D520C7CA040072B3D4 /* Build configuration list for PBXNativeTarget "SyncEngine" */; 211 | buildPhases = ( 212 | 0DC903C820C7CA040072B3D4 /* Sources */, 213 | 0DC903C920C7CA040072B3D4 /* Frameworks */, 214 | 0DC903CA20C7CA040072B3D4 /* Headers */, 215 | 0DC903CB20C7CA040072B3D4 /* Resources */, 216 | 0DC903E520C7CBDC0072B3D4 /* ShellScript */, 217 | ); 218 | buildRules = ( 219 | ); 220 | dependencies = ( 221 | ); 222 | name = SyncEngine; 223 | productName = SyncEngine; 224 | productReference = 0DC903CD20C7CA040072B3D4 /* SyncEngine.framework */; 225 | productType = "com.apple.product-type.framework"; 226 | }; 227 | 0DC903F620C8F0C60072B3D4 /* SyncEngineDemo */ = { 228 | isa = PBXNativeTarget; 229 | buildConfigurationList = 0DC9040620C8F0C70072B3D4 /* Build configuration list for PBXNativeTarget "SyncEngineDemo" */; 230 | buildPhases = ( 231 | 0DC903F320C8F0C60072B3D4 /* Sources */, 232 | 0DC903F420C8F0C60072B3D4 /* Frameworks */, 233 | 0DC903F520C8F0C60072B3D4 /* Resources */, 234 | 0DC9040F20C8F1060072B3D4 /* Embed Frameworks */, 235 | ); 236 | buildRules = ( 237 | ); 238 | dependencies = ( 239 | 0DC9041820C8F8780072B3D4 /* PBXTargetDependency */, 240 | ); 241 | name = SyncEngineDemo; 242 | productName = SyncEngineDemo; 243 | productReference = 0DC903F720C8F0C60072B3D4 /* SyncEngineDemo.app */; 244 | productType = "com.apple.product-type.application"; 245 | }; 246 | /* End PBXNativeTarget section */ 247 | 248 | /* Begin PBXProject section */ 249 | 0DC903C420C7CA040072B3D4 /* Project object */ = { 250 | isa = PBXProject; 251 | attributes = { 252 | LastSwiftUpdateCheck = 0940; 253 | LastUpgradeCheck = 0940; 254 | ORGANIZATIONNAME = "Purkylin King"; 255 | TargetAttributes = { 256 | 0DC903CC20C7CA040072B3D4 = { 257 | CreatedOnToolsVersion = 9.4; 258 | }; 259 | 0DC903F620C8F0C60072B3D4 = { 260 | CreatedOnToolsVersion = 9.4; 261 | SystemCapabilities = { 262 | com.apple.BackgroundModes = { 263 | enabled = 1; 264 | }; 265 | com.apple.Push = { 266 | enabled = 1; 267 | }; 268 | com.apple.iCloud = { 269 | enabled = 1; 270 | }; 271 | }; 272 | }; 273 | }; 274 | }; 275 | buildConfigurationList = 0DC903C720C7CA040072B3D4 /* Build configuration list for PBXProject "SyncEngine" */; 276 | compatibilityVersion = "Xcode 9.3"; 277 | developmentRegion = en; 278 | hasScannedForEncodings = 0; 279 | knownRegions = ( 280 | en, 281 | Base, 282 | ); 283 | mainGroup = 0DC903C320C7CA040072B3D4; 284 | productRefGroup = 0DC903CE20C7CA040072B3D4 /* Products */; 285 | projectDirPath = ""; 286 | projectRoot = ""; 287 | targets = ( 288 | 0DC903CC20C7CA040072B3D4 /* SyncEngine */, 289 | 0DC903F620C8F0C60072B3D4 /* SyncEngineDemo */, 290 | ); 291 | }; 292 | /* End PBXProject section */ 293 | 294 | /* Begin PBXResourcesBuildPhase section */ 295 | 0DC903CB20C7CA040072B3D4 /* Resources */ = { 296 | isa = PBXResourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | 0DC903F520C8F0C60072B3D4 /* Resources */ = { 303 | isa = PBXResourcesBuildPhase; 304 | buildActionMask = 2147483647; 305 | files = ( 306 | 0DC9040420C8F0C70072B3D4 /* LaunchScreen.storyboard in Resources */, 307 | 0DC9040120C8F0C70072B3D4 /* Assets.xcassets in Resources */, 308 | 0DC903FF20C8F0C60072B3D4 /* Main.storyboard in Resources */, 309 | ); 310 | runOnlyForDeploymentPostprocessing = 0; 311 | }; 312 | /* End PBXResourcesBuildPhase section */ 313 | 314 | /* Begin PBXShellScriptBuildPhase section */ 315 | 0DC903E520C7CBDC0072B3D4 /* ShellScript */ = { 316 | isa = PBXShellScriptBuildPhase; 317 | buildActionMask = 2147483647; 318 | files = ( 319 | ); 320 | inputPaths = ( 321 | "$(SRCROOT)/Carthage/Build/iOS/RealmSwift.framework", 322 | "$(SRCROOT)/Carthage/Build/iOS/Realm.framework", 323 | ); 324 | outputPaths = ( 325 | "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Realm.framework", 326 | "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/RealmSwift.framework", 327 | ); 328 | runOnlyForDeploymentPostprocessing = 0; 329 | shellPath = /bin/sh; 330 | shellScript = "/usr/local/bin/carthage copy-frameworks"; 331 | }; 332 | /* End PBXShellScriptBuildPhase section */ 333 | 334 | /* Begin PBXSourcesBuildPhase section */ 335 | 0DC903C820C7CA040072B3D4 /* Sources */ = { 336 | isa = PBXSourcesBuildPhase; 337 | buildActionMask = 2147483647; 338 | files = ( 339 | 0DC903DE20C7CA780072B3D4 /* HandleCloudKitError.swift in Sources */, 340 | 0DC903E420C7CAA70072B3D4 /* KeyStore.swift in Sources */, 341 | 0DC903DF20C7CA780072B3D4 /* CKDatabase+Subscription.swift in Sources */, 342 | 0DC903EC20C7CCFE0072B3D4 /* SyncBaseModel.swift in Sources */, 343 | 0D24605020DD03D20091F4EC /* CKError+Detail.swift in Sources */, 344 | 0DC903E120C7CA780072B3D4 /* LocalCache.swift in Sources */, 345 | 0DC903EE20C7CD8E0072B3D4 /* DefaultsKey.swift in Sources */, 346 | 0DC903E220C7CA780072B3D4 /* Database.swift in Sources */, 347 | 0DC903E020C7CA780072B3D4 /* SyncEngine.swift in Sources */, 348 | ); 349 | runOnlyForDeploymentPostprocessing = 0; 350 | }; 351 | 0DC903F320C8F0C60072B3D4 /* Sources */ = { 352 | isa = PBXSourcesBuildPhase; 353 | buildActionMask = 2147483647; 354 | files = ( 355 | 0DC903FC20C8F0C60072B3D4 /* ViewController.swift in Sources */, 356 | 0DC903FA20C8F0C60072B3D4 /* AppDelegate.swift in Sources */, 357 | 0DC9041120C8F4B30072B3D4 /* EditViewController.swift in Sources */, 358 | 0DC9041320C8F59D0072B3D4 /* Note.swift in Sources */, 359 | 0DC9042020C9029E0072B3D4 /* InfoViewController.swift in Sources */, 360 | ); 361 | runOnlyForDeploymentPostprocessing = 0; 362 | }; 363 | /* End PBXSourcesBuildPhase section */ 364 | 365 | /* Begin PBXTargetDependency section */ 366 | 0DC9041820C8F8780072B3D4 /* PBXTargetDependency */ = { 367 | isa = PBXTargetDependency; 368 | target = 0DC903CC20C7CA040072B3D4 /* SyncEngine */; 369 | targetProxy = 0DC9041720C8F8780072B3D4 /* PBXContainerItemProxy */; 370 | }; 371 | /* End PBXTargetDependency section */ 372 | 373 | /* Begin PBXVariantGroup section */ 374 | 0DC903FD20C8F0C60072B3D4 /* Main.storyboard */ = { 375 | isa = PBXVariantGroup; 376 | children = ( 377 | 0DC903FE20C8F0C60072B3D4 /* Base */, 378 | ); 379 | name = Main.storyboard; 380 | sourceTree = ""; 381 | }; 382 | 0DC9040220C8F0C70072B3D4 /* LaunchScreen.storyboard */ = { 383 | isa = PBXVariantGroup; 384 | children = ( 385 | 0DC9040320C8F0C70072B3D4 /* Base */, 386 | ); 387 | name = LaunchScreen.storyboard; 388 | sourceTree = ""; 389 | }; 390 | /* End PBXVariantGroup section */ 391 | 392 | /* Begin XCBuildConfiguration section */ 393 | 0DC903D320C7CA040072B3D4 /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ALWAYS_SEARCH_USER_PATHS = NO; 397 | CLANG_ANALYZER_NONNULL = YES; 398 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 399 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 400 | CLANG_CXX_LIBRARY = "libc++"; 401 | CLANG_ENABLE_MODULES = YES; 402 | CLANG_ENABLE_OBJC_ARC = YES; 403 | CLANG_ENABLE_OBJC_WEAK = YES; 404 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 405 | CLANG_WARN_BOOL_CONVERSION = YES; 406 | CLANG_WARN_COMMA = YES; 407 | CLANG_WARN_CONSTANT_CONVERSION = YES; 408 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 409 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 410 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 411 | CLANG_WARN_EMPTY_BODY = YES; 412 | CLANG_WARN_ENUM_CONVERSION = YES; 413 | CLANG_WARN_INFINITE_RECURSION = YES; 414 | CLANG_WARN_INT_CONVERSION = YES; 415 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 416 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 417 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 418 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 419 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 420 | CLANG_WARN_STRICT_PROTOTYPES = YES; 421 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 422 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 423 | CLANG_WARN_UNREACHABLE_CODE = YES; 424 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 425 | CODE_SIGN_IDENTITY = "iPhone Developer"; 426 | COPY_PHASE_STRIP = NO; 427 | CURRENT_PROJECT_VERSION = 1; 428 | DEBUG_INFORMATION_FORMAT = dwarf; 429 | ENABLE_STRICT_OBJC_MSGSEND = YES; 430 | ENABLE_TESTABILITY = YES; 431 | GCC_C_LANGUAGE_STANDARD = gnu11; 432 | GCC_DYNAMIC_NO_PIC = NO; 433 | GCC_NO_COMMON_BLOCKS = YES; 434 | GCC_OPTIMIZATION_LEVEL = 0; 435 | GCC_PREPROCESSOR_DEFINITIONS = ( 436 | "DEBUG=1", 437 | "$(inherited)", 438 | ); 439 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 440 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 441 | GCC_WARN_UNDECLARED_SELECTOR = YES; 442 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 443 | GCC_WARN_UNUSED_FUNCTION = YES; 444 | GCC_WARN_UNUSED_VARIABLE = YES; 445 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 446 | MTL_ENABLE_DEBUG_INFO = YES; 447 | ONLY_ACTIVE_ARCH = YES; 448 | SDKROOT = iphoneos; 449 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 450 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 451 | VERSIONING_SYSTEM = "apple-generic"; 452 | VERSION_INFO_PREFIX = ""; 453 | }; 454 | name = Debug; 455 | }; 456 | 0DC903D420C7CA040072B3D4 /* Release */ = { 457 | isa = XCBuildConfiguration; 458 | buildSettings = { 459 | ALWAYS_SEARCH_USER_PATHS = NO; 460 | CLANG_ANALYZER_NONNULL = YES; 461 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 462 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 463 | CLANG_CXX_LIBRARY = "libc++"; 464 | CLANG_ENABLE_MODULES = YES; 465 | CLANG_ENABLE_OBJC_ARC = YES; 466 | CLANG_ENABLE_OBJC_WEAK = YES; 467 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 468 | CLANG_WARN_BOOL_CONVERSION = YES; 469 | CLANG_WARN_COMMA = YES; 470 | CLANG_WARN_CONSTANT_CONVERSION = YES; 471 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 472 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 473 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 474 | CLANG_WARN_EMPTY_BODY = YES; 475 | CLANG_WARN_ENUM_CONVERSION = YES; 476 | CLANG_WARN_INFINITE_RECURSION = YES; 477 | CLANG_WARN_INT_CONVERSION = YES; 478 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 479 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 480 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 481 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 482 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 483 | CLANG_WARN_STRICT_PROTOTYPES = YES; 484 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 485 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 486 | CLANG_WARN_UNREACHABLE_CODE = YES; 487 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 488 | CODE_SIGN_IDENTITY = "iPhone Developer"; 489 | COPY_PHASE_STRIP = NO; 490 | CURRENT_PROJECT_VERSION = 1; 491 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 492 | ENABLE_NS_ASSERTIONS = NO; 493 | ENABLE_STRICT_OBJC_MSGSEND = YES; 494 | GCC_C_LANGUAGE_STANDARD = gnu11; 495 | GCC_NO_COMMON_BLOCKS = YES; 496 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 497 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 498 | GCC_WARN_UNDECLARED_SELECTOR = YES; 499 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 500 | GCC_WARN_UNUSED_FUNCTION = YES; 501 | GCC_WARN_UNUSED_VARIABLE = YES; 502 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 503 | MTL_ENABLE_DEBUG_INFO = NO; 504 | SDKROOT = iphoneos; 505 | SWIFT_COMPILATION_MODE = wholemodule; 506 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 507 | VALIDATE_PRODUCT = YES; 508 | VERSIONING_SYSTEM = "apple-generic"; 509 | VERSION_INFO_PREFIX = ""; 510 | }; 511 | name = Release; 512 | }; 513 | 0DC903D620C7CA040072B3D4 /* Debug */ = { 514 | isa = XCBuildConfiguration; 515 | buildSettings = { 516 | CODE_SIGN_IDENTITY = ""; 517 | CODE_SIGN_STYLE = Automatic; 518 | DEFINES_MODULE = YES; 519 | DEVELOPMENT_TEAM = QJSM3KYT6N; 520 | DYLIB_COMPATIBILITY_VERSION = 1; 521 | DYLIB_CURRENT_VERSION = 1; 522 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 523 | FRAMEWORK_SEARCH_PATHS = ( 524 | "$(inherited)", 525 | "$(PROJECT_DIR)/SyncEngine/Carthage/Checkouts/Build/iOS", 526 | "$(PROJECT_DIR)/Carthage/Build/iOS", 527 | ); 528 | INFOPLIST_FILE = SyncEngine/Info.plist; 529 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 530 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 531 | LD_RUNPATH_SEARCH_PATHS = ( 532 | "$(inherited)", 533 | "@executable_path/Frameworks", 534 | "@loader_path/Frameworks", 535 | ); 536 | PRODUCT_BUNDLE_IDENTIFIER = com.purkylin.SyncEngine; 537 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 538 | SKIP_INSTALL = YES; 539 | SWIFT_VERSION = 4.0; 540 | TARGETED_DEVICE_FAMILY = "1,2"; 541 | }; 542 | name = Debug; 543 | }; 544 | 0DC903D720C7CA040072B3D4 /* Release */ = { 545 | isa = XCBuildConfiguration; 546 | buildSettings = { 547 | CODE_SIGN_IDENTITY = ""; 548 | CODE_SIGN_STYLE = Automatic; 549 | DEFINES_MODULE = YES; 550 | DEVELOPMENT_TEAM = QJSM3KYT6N; 551 | DYLIB_COMPATIBILITY_VERSION = 1; 552 | DYLIB_CURRENT_VERSION = 1; 553 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 554 | FRAMEWORK_SEARCH_PATHS = ( 555 | "$(inherited)", 556 | "$(PROJECT_DIR)/SyncEngine/Carthage/Checkouts/Build/iOS", 557 | "$(PROJECT_DIR)/Carthage/Build/iOS", 558 | ); 559 | INFOPLIST_FILE = SyncEngine/Info.plist; 560 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 561 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 562 | LD_RUNPATH_SEARCH_PATHS = ( 563 | "$(inherited)", 564 | "@executable_path/Frameworks", 565 | "@loader_path/Frameworks", 566 | ); 567 | PRODUCT_BUNDLE_IDENTIFIER = com.purkylin.SyncEngine; 568 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 569 | SKIP_INSTALL = YES; 570 | SWIFT_VERSION = 4.0; 571 | TARGETED_DEVICE_FAMILY = "1,2"; 572 | }; 573 | name = Release; 574 | }; 575 | 0DC9040720C8F0C70072B3D4 /* Debug */ = { 576 | isa = XCBuildConfiguration; 577 | buildSettings = { 578 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 579 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 580 | CODE_SIGN_ENTITLEMENTS = SyncEngineDemo/SyncEngineDemo.entitlements; 581 | CODE_SIGN_STYLE = Automatic; 582 | DEVELOPMENT_TEAM = QJSM3KYT6N; 583 | FRAMEWORK_SEARCH_PATHS = ( 584 | "$(inherited)", 585 | "$(PROJECT_DIR)/Carthage/Build/iOS", 586 | ); 587 | INFOPLIST_FILE = SyncEngineDemo/Info.plist; 588 | LD_RUNPATH_SEARCH_PATHS = ( 589 | "$(inherited)", 590 | "@executable_path/Frameworks", 591 | ); 592 | PRODUCT_BUNDLE_IDENTIFIER = com.purkylin.SyncEngineDemo; 593 | PRODUCT_NAME = "$(TARGET_NAME)"; 594 | SWIFT_VERSION = 4.0; 595 | TARGETED_DEVICE_FAMILY = "1,2"; 596 | }; 597 | name = Debug; 598 | }; 599 | 0DC9040820C8F0C70072B3D4 /* Release */ = { 600 | isa = XCBuildConfiguration; 601 | buildSettings = { 602 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 603 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 604 | CODE_SIGN_ENTITLEMENTS = SyncEngineDemo/SyncEngineDemo.entitlements; 605 | CODE_SIGN_STYLE = Automatic; 606 | DEVELOPMENT_TEAM = QJSM3KYT6N; 607 | FRAMEWORK_SEARCH_PATHS = ( 608 | "$(inherited)", 609 | "$(PROJECT_DIR)/Carthage/Build/iOS", 610 | ); 611 | INFOPLIST_FILE = SyncEngineDemo/Info.plist; 612 | LD_RUNPATH_SEARCH_PATHS = ( 613 | "$(inherited)", 614 | "@executable_path/Frameworks", 615 | ); 616 | PRODUCT_BUNDLE_IDENTIFIER = com.purkylin.SyncEngineDemo; 617 | PRODUCT_NAME = "$(TARGET_NAME)"; 618 | SWIFT_VERSION = 4.0; 619 | TARGETED_DEVICE_FAMILY = "1,2"; 620 | }; 621 | name = Release; 622 | }; 623 | /* End XCBuildConfiguration section */ 624 | 625 | /* Begin XCConfigurationList section */ 626 | 0DC903C720C7CA040072B3D4 /* Build configuration list for PBXProject "SyncEngine" */ = { 627 | isa = XCConfigurationList; 628 | buildConfigurations = ( 629 | 0DC903D320C7CA040072B3D4 /* Debug */, 630 | 0DC903D420C7CA040072B3D4 /* Release */, 631 | ); 632 | defaultConfigurationIsVisible = 0; 633 | defaultConfigurationName = Release; 634 | }; 635 | 0DC903D520C7CA040072B3D4 /* Build configuration list for PBXNativeTarget "SyncEngine" */ = { 636 | isa = XCConfigurationList; 637 | buildConfigurations = ( 638 | 0DC903D620C7CA040072B3D4 /* Debug */, 639 | 0DC903D720C7CA040072B3D4 /* Release */, 640 | ); 641 | defaultConfigurationIsVisible = 0; 642 | defaultConfigurationName = Release; 643 | }; 644 | 0DC9040620C8F0C70072B3D4 /* Build configuration list for PBXNativeTarget "SyncEngineDemo" */ = { 645 | isa = XCConfigurationList; 646 | buildConfigurations = ( 647 | 0DC9040720C8F0C70072B3D4 /* Debug */, 648 | 0DC9040820C8F0C70072B3D4 /* Release */, 649 | ); 650 | defaultConfigurationIsVisible = 0; 651 | defaultConfigurationName = Release; 652 | }; 653 | /* End XCConfigurationList section */ 654 | }; 655 | rootObject = 0DC903C420C7CA040072B3D4 /* Project object */; 656 | } 657 | -------------------------------------------------------------------------------- /SyncEngine.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SyncEngine.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SyncEngine.xcodeproj/xcshareddata/xcschemes/SyncEngine.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /SyncEngine.xcodeproj/xcuserdata/purkylinking.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SyncEngine.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | SyncEngineDemo.xcscheme 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 0DC903CC20C7CA040072B3D4 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /SyncEngine/Core/CKDatabase+Subscription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKDatabase+Extension.swift 3 | // Engine 4 | // 5 | // Created by Purkylin King on 2018/6/5. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CloudKit 11 | 12 | extension CKDatabase { 13 | 14 | var name: String { // Provide a display name for the database. 15 | switch databaseScope { 16 | case .public: return "Public" 17 | case .private: return "Private" 18 | case .shared: return "Shared" 19 | } 20 | } 21 | 22 | private func add(_ operation: CKDatabaseOperation, to queue: OperationQueue?) { 23 | if let operationQueue = queue { 24 | operation.database = self 25 | operationQueue.addOperation(operation) 26 | } else { 27 | add(operation) 28 | } 29 | } 30 | 31 | func addDatabaseSubscription(subscriptionID: String, operationQueue: OperationQueue?, completionHandler: @escaping ((Error?) -> Void)) { 32 | let subscription = CKDatabaseSubscription(subscriptionID: subscriptionID) 33 | let notificationInfo = CKNotificationInfo() 34 | subscription.notificationInfo = notificationInfo 35 | 36 | let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: nil) 37 | operation.modifySubscriptionsCompletionBlock = { _, _, error in 38 | completionHandler(error) 39 | } 40 | 41 | if let operationQueue = operationQueue { 42 | operation.database = self 43 | operationQueue.addOperation(operation) 44 | } else { 45 | add(operation) 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /SyncEngine/Core/CKError+Detail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKError+Detail.swift 3 | // SyncEngine 4 | // 5 | // Created by Purkylin King on 2018/6/22. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CloudKit 11 | 12 | extension CKError { 13 | public func isSpecificErrorCode(code: CKError.Code) -> Bool { 14 | var match = false 15 | if self.code == code { 16 | match = true 17 | } 18 | else if self.code == .partialFailure { 19 | // This is a multiple-issue error. Check the underlying array 20 | // of errors to see if it contains a match for the error in question. 21 | guard let errors = partialErrorsByItemID else { 22 | return false 23 | } 24 | for (_, error) in errors { 25 | if let cke = error as? CKError { 26 | if cke.code == code { 27 | match = true 28 | break 29 | } 30 | } 31 | } 32 | } 33 | return match 34 | } 35 | 36 | public func isRecordNotFound() -> Bool { 37 | return isSpecificErrorCode(code: .unknownItem) 38 | } 39 | 40 | public func isWriteFailure() -> Bool { 41 | return isSpecificErrorCode(code: .permissionFailure) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SyncEngine/Core/Database.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Database.swift 3 | // Engine 4 | // 5 | // Created by Purkylin King on 2018/6/5. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CloudKit 11 | import RealmSwift 12 | 13 | class Database { // Wrap a CKDatabase, its change token, and its zones. 14 | var serverChangeToken: CKServerChangeToken? 15 | let cloudKitDB: CKDatabase 16 | var zones = [CKRecordZone]() 17 | let notificationObject = ObjectCacheDidChange() 18 | 19 | var isShared: Bool { 20 | return cloudKitDB.databaseScope == .shared 21 | } 22 | 23 | init(cloudKitDB: CKDatabase) { 24 | self.cloudKitDB = cloudKitDB 25 | self.serverChangeToken = readChangeToken() 26 | } 27 | 28 | private lazy var cacheQueue: DispatchQueue = { 29 | return DispatchQueue(label: "LocalCache", attributes: .concurrent) 30 | }() 31 | 32 | func performWriterBlock(_ writerBlock: @escaping () -> Void) { 33 | cacheQueue.async(flags: .barrier) { 34 | writerBlock() 35 | } 36 | } 37 | 38 | func addSubscription(to operationQueue: OperationQueue) { 39 | var key: String 40 | switch self.cloudKitDB.databaseScope { 41 | case .public: 42 | key = DefaultsKey.publicSubscriptionSaveKey 43 | case .private: 44 | key = DefaultsKey.privateSubscriptionSaveKey 45 | case .shared: 46 | key = DefaultsKey.sharedSubscriptionSaveKey 47 | } 48 | 49 | if UserDefaults.standard.bool(forKey: key) { 50 | return 51 | } 52 | 53 | cloudKitDB.addDatabaseSubscription(subscriptionID: cloudKitDB.name, operationQueue: operationQueue) { error in 54 | if error != nil { 55 | kinglog("save subscription failed: \(error!.localizedDescription)") 56 | } 57 | 58 | guard handleCloudKitError(error, operation: .modifySubscriptions) == nil else { return } 59 | UserDefaults.standard.set(true, forKey: key) 60 | } 61 | } 62 | 63 | // MARK: - Token 64 | public func readChangeToken() -> CKServerChangeToken? { 65 | let key = "ServerChangeToken-\(cloudKitDB.name)" 66 | return readToken(of: key) 67 | } 68 | 69 | public func save(changeToken: CKServerChangeToken?) { 70 | serverChangeToken = changeToken 71 | let key = "ServerChangeToken-\(cloudKitDB.name)" 72 | write(token: changeToken, key: key) 73 | } 74 | 75 | private func write(token: CKServerChangeToken?, key: String) { 76 | let defaults = UserDefaults.standard 77 | 78 | if token == nil { 79 | defaults.set(nil, forKey: key) 80 | } else { 81 | let data = NSKeyedArchiver.archivedData(withRootObject: token!) 82 | defaults.setValue(data, forKey: key) 83 | } 84 | } 85 | 86 | private func readToken(of key: String) -> CKServerChangeToken? { 87 | guard let data = UserDefaults.standard.data(forKey: key) else { return nil } 88 | return NSKeyedUnarchiver.unarchiveObject(with: data) as? CKServerChangeToken 89 | } 90 | 91 | func save(zoneName: String, changeToken: CKServerChangeToken?) { 92 | let key = "ServerChangeToken-\(cloudKitDB.name)-zone-\(zoneName)" 93 | write(token: changeToken, key: key) 94 | } 95 | 96 | func readChangeToken(of zoneName: String) -> CKServerChangeToken? { 97 | let key = "ServerChangeToken-\(cloudKitDB.name)-zone-\(zoneName)" 98 | return readToken(of: key) 99 | } 100 | 101 | func generateOptions(zoneIDs: [CKRecordZoneID]) -> [CKRecordZoneID : CKFetchRecordZoneChangesOptions] { 102 | var result = [CKRecordZoneID : CKFetchRecordZoneChangesOptions]() 103 | for zoneID in zoneIDs { 104 | let options = CKFetchRecordZoneChangesOptions() 105 | options.previousServerChangeToken = readChangeToken(of: zoneID.zoneName) 106 | result[zoneID] = options 107 | } 108 | 109 | return result 110 | } 111 | 112 | // MARK: - Fetch 113 | 114 | func fetchZoneChanges(zoneIDs: [CKRecordZoneID]) { 115 | guard zoneIDs.count > 0 else { return } 116 | 117 | let options = generateOptions(zoneIDs: zoneIDs) 118 | let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zoneIDs, optionsByRecordZoneID: options) 119 | operation.fetchAllChanges = true 120 | 121 | var recordsChanged = [CKRecord]() 122 | var recordIDsDeleted = [CKRecordID]() 123 | 124 | operation.fetchRecordZoneChangesCompletionBlock = { error in 125 | if let ckError = handleCloudKitError(error, operation: .fetchChanges, 126 | affectedObjects: nil) { 127 | print("Error in fetchRecordZoneChangesCompletionBlock: \(ckError)") 128 | } 129 | 130 | // The IDs in recordsChanged can be in recordIDsDeleted, meaning a changed record can be deleted, 131 | // So filter out the updated but deleted IDs. 132 | // 133 | recordsChanged = recordsChanged.filter { record in 134 | return !recordIDsDeleted.contains(record.recordID) 135 | } 136 | 137 | // Push recordIDsDeleted and recordsChanged into notification payload. 138 | // 139 | self.notificationObject.payload = ObjectCacheChanges(recordIDsDeleted: recordIDsDeleted, 140 | recordsChanged: recordsChanged) 141 | 142 | self.performWriterBlock { // Do the update. 143 | self.updateWithRecordIDsDeleted(recordIDsDeleted) 144 | self.updateWithRecordsChanged(recordsChanged) 145 | } 146 | print("\(#function):Deleted \(recordIDsDeleted.count); Changed \(recordsChanged.count)") 147 | } 148 | 149 | operation.recordZoneFetchCompletionBlock = { zoneID, serverChangeToken, clientChangeTokenData, moreComing, error in 150 | if let ckError = handleCloudKitError(error, operation: .fetchChanges), 151 | ckError.code == .changeTokenExpired { 152 | self.performWriterBlock { self.save(zoneName: zoneID.zoneName, changeToken: nil) } 153 | self.fetchZoneChanges(zoneIDs: [zoneID]) 154 | return 155 | } 156 | 157 | self.performWriterBlock { self.save(zoneName: zoneID.zoneName, changeToken: nil) } 158 | } 159 | 160 | operation.recordChangedBlock = { record in 161 | recordsChanged.append(record) 162 | } 163 | 164 | operation.recordWithIDWasDeletedBlock = { recordID, _ in 165 | recordIDsDeleted.append(recordID) 166 | } 167 | 168 | operation.recordZoneChangeTokensUpdatedBlock = { zoneID, changeToken, _ in 169 | self.save(zoneName: zoneID.zoneName, changeToken: changeToken) 170 | } 171 | 172 | cloudKitDB.add(operation) 173 | } 174 | 175 | // MARK: - Modify 176 | 177 | func updateWithRecordIDsDeleted(_ recordIDs: [CKRecordID]) { 178 | guard recordIDs.count > 0 else { return } 179 | 180 | for recordID in recordIDs { 181 | deleteRecord(recordID: recordID) 182 | } 183 | } 184 | 185 | private func deleteRecord(recordID: CKRecordID) { 186 | let realm = try! Realm() 187 | var foundObj: SyncBaseModel? 188 | 189 | for model in SyncEngine.default.models { 190 | let obj = realm.objects(model).first { $0.id == recordID.recordName } 191 | if obj != nil { 192 | foundObj = obj 193 | break 194 | } 195 | } 196 | 197 | if let obj = foundObj { 198 | try! realm.write { 199 | realm.delete(obj) 200 | } 201 | 202 | KeyStore.shared[recordID.recordName] = nil 203 | } 204 | } 205 | 206 | func updateWithRecordsChanged(_ records: [CKRecord]) { 207 | guard records.count > 0 else { return } 208 | 209 | if self.cloudKitDB.databaseScope != .private { 210 | // Do something 211 | } 212 | 213 | for record in records { 214 | KeyStore.shared.save(record: record) 215 | 216 | if record.isKind(of: CKShare.self) { 217 | // TODO Readwrite 218 | } else { 219 | KeyStore.shared.save(record: record) 220 | let realm = try! Realm() 221 | // TODO: Compare 222 | let obj = SyncBaseModel.from(record: record) 223 | obj.synced = true 224 | obj.modifiedAt = Date() 225 | obj.ownerName = record.recordID.zoneID.ownerName 226 | kinglog("save record to db success") 227 | 228 | try! realm.write { 229 | realm.add(obj, update: true) 230 | } 231 | } 232 | } 233 | } 234 | 235 | // MARK: - Sync local changes 236 | func syncLocalChanges() { 237 | guard cloudKitDB.databaseScope != .public else { return } 238 | 239 | var toSaveRecords = [CKRecord]() 240 | var toDeleteRecordIDs = [CKRecordID]() 241 | 242 | let realm = try! Realm() 243 | 244 | for model in SyncEngine.default.models { 245 | let toSyncObjects = realm.objects(model).filter { obj in 246 | obj.synced == false && obj.shared == self.isShared 247 | } 248 | 249 | for object in toSyncObjects { 250 | let record = object.syncRecord 251 | if object.deleted { 252 | toDeleteRecordIDs.append(record.recordID) 253 | } else { 254 | toSaveRecords.append(record) 255 | } 256 | } 257 | } 258 | 259 | if toSaveRecords.count > 0 || toDeleteRecordIDs.count > 0 { 260 | kinglog("sync: update(\(toSaveRecords.count)) delete(\(toDeleteRecordIDs.count))") 261 | syncChanges(recordsToSave: toSaveRecords, recordIDsToDelete: toDeleteRecordIDs) 262 | } 263 | } 264 | 265 | private func syncChanges(recordsToSave: [CKRecord]?, recordIDsToDelete: [CKRecordID]?) { 266 | let operation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete) 267 | operation.modifyRecordsCompletionBlock = { (saveRecords, deleteRecordIDs, error) in 268 | if error != nil { 269 | kinglog(error!.localizedDescription) 270 | } 271 | 272 | let failure = self.retryWhenPossible(with: error, block: { 273 | self.syncChanges(recordsToSave: saveRecords, recordIDsToDelete: deleteRecordIDs) 274 | }) 275 | 276 | if failure == nil { 277 | kinglog("sync success update \(saveRecords?.count ?? 0) delete \(deleteRecordIDs?.count ?? 0)") 278 | self.performWriterBlock { 279 | self.updateWithRecordsChanged(saveRecords ?? []) 280 | self.updateWithRecordIDsDeleted(deleteRecordIDs ?? []) 281 | } 282 | } else { 283 | self.handleError(error: error!, completion: { (records, error) in 284 | if error != nil { 285 | print(error!.localizedDescription) 286 | return 287 | } 288 | 289 | if let count = records?.count, count > 0 { 290 | self.syncChanges(recordsToSave: records, recordIDsToDelete: nil) 291 | } 292 | }) 293 | } 294 | } 295 | 296 | cloudKitDB.add(operation) 297 | } 298 | 299 | // MARK: - Resolve conflict 300 | 301 | private func retryWhenPossible(with error: Error?, block: @escaping () -> ()) -> Error? { 302 | guard let effectiveError = error as? CKError else { 303 | // not a CloudKit error or no error present, just return the original error 304 | return error 305 | } 306 | 307 | guard let retryAfter = effectiveError.retryAfterSeconds else { 308 | // CloudKit error, can't be retried, return the error 309 | return effectiveError 310 | } 311 | 312 | // CloudKit operation can be retried, schedule `block` to be executed later 313 | 314 | DispatchQueue.main.asyncAfter(deadline: .now() + retryAfter) { 315 | block() 316 | } 317 | 318 | return nil 319 | } 320 | 321 | private func handleError(error: Error, completion: ([CKRecord]?, Error?) -> Void) { 322 | if let ckerror = error as? CKError { 323 | print(ckerror.localizedDescription) 324 | 325 | if ckerror.isWriteFailure() { 326 | completion(nil, error) 327 | return 328 | } 329 | 330 | switch ckerror.code { 331 | case .partialFailure: 332 | let adjustedRecords = resolveConflict(error: ckerror, resolver: overrideUseClient) 333 | completion(adjustedRecords, nil) 334 | return 335 | default: 336 | break 337 | } 338 | } 339 | 340 | completion(nil, error) 341 | } 342 | 343 | private func resolveConflict(error: CKError, resolver: (_ serverRecord: CKRecord, _ clientRecord: CKRecord, _ ancestorRecord: CKRecord) -> CKRecord) -> [CKRecord]? { 344 | guard let dict = error.partialErrorsByItemID else { return nil } 345 | 346 | var adjustRecords = [CKRecord]() 347 | 348 | for (_, itemError) in dict { 349 | if let ckerror = itemError as? CKError { 350 | switch ckerror.code { 351 | case .serverRecordChanged: 352 | guard let serverRecord = ckerror.serverRecord, 353 | let clientRecord = ckerror.clientRecord, 354 | let ancestorRecord = ckerror.ancestorRecord else { return nil } 355 | 356 | if serverRecord.recordChangeTag != clientRecord.recordChangeTag { 357 | let adjustRecord = resolver(serverRecord, clientRecord, ancestorRecord) 358 | adjustRecords.append(adjustRecord) 359 | } else { 360 | print("conflict: save version") 361 | } 362 | case .batchRequestFailed: 363 | print("batch failed") 364 | // should retry 365 | default: 366 | print("Not deal with other conflict") 367 | print(ckerror.localizedDescription) 368 | break 369 | } 370 | } 371 | } 372 | 373 | return adjustRecords 374 | } 375 | 376 | private func overrideUseClient(serverRecord: CKRecord, clientRecord: CKRecord, ancestorRecord: CKRecord) -> CKRecord { 377 | let adjustedRecord = serverRecord 378 | for key in clientRecord.allKeys() { 379 | adjustedRecord[key] = clientRecord[key] 380 | } 381 | return adjustedRecord 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /SyncEngine/Core/DefaultsKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultsKey.swift 3 | // SyncEngine 4 | // 5 | // Created by Purkylin King on 2018/6/6. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class DefaultsKey { 12 | static let createCustomeZone = "createCustomeZone" 13 | static let subscriptionSaveKey = "subscriptionSaved" 14 | static let publicSubscriptionSaveKey = "publicSubscriptionSaved" 15 | static let privateSubscriptionSaveKey = "privateSubscriptionSaved" 16 | static let sharedSubscriptionSaveKey = "sharedSubscriptionSaved" 17 | 18 | static let privateChangesToken = "privateChangesToken" 19 | static let shareChangesToken = "shareChangesToken" 20 | static let fetchChangesToken = "fetchChangesToken" 21 | } 22 | -------------------------------------------------------------------------------- /SyncEngine/Core/HandleCloudKitError.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2018 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | A function to handle CloudKit errors. 7 | */ 8 | 9 | import UIKit 10 | import CloudKit 11 | 12 | // Operation types that identifying what is doing. 13 | // 14 | enum CloudKitOperationType: String { 15 | case accountStatus = "AccountStatus"// Doing account check with CKContainer.accountStatus. 16 | case fetchRecords = "FetchRecords" // Fetching data from the CloudKit server. 17 | case modifyRecords = "ModifyRecords"// Modifying records (.serverRecordChanged should be handled). 18 | case deleteRecords = "DeleteRecords"// Deleting records. 19 | case modifyZones = "ModifyZones" // Modifying zones (.serverRecordChanged should be handled). 20 | case deleteZones = "DeleteZones" // Deleting zones. 21 | case fetchZones = "FetchZones" // Fetching zones. 22 | case modifySubscriptions = "ModifySubscriptions" // Modifying subscriptions. 23 | case deleteSubscriptions = "DeleteSubscriptions" // Deleting subscriptions. 24 | case fetchChanges = "FetchChanges" // Fetching changes (.changeTokenExpired should be handled). 25 | case acceptShare = "AcceptShare" // Doing CKAcceptSharesOperation. 26 | } 27 | 28 | // Return nil: no error or the error is ignorable. 29 | // Return a CKError: there is an error; calls should determine how to handle it. 30 | // 31 | func handleCloudKitError(_ error: Error?, operation: CloudKitOperationType, 32 | affectedObjects: [Any]? = nil, alert: Bool = false) -> CKError? { 33 | 34 | // nsError == nil: Everything goes well, callers can continue. 35 | // 36 | guard let nsError = error as NSError? else { return nil } 37 | 38 | // Partial errors can happen when fetching or changing the database. 39 | // 40 | // When modifying zones, records, and subscription,.serverRecordChanged may happen if 41 | // the other peer changed the item at the same time. In that case, retrieve the first 42 | // CKError object and return to callers. 43 | // 44 | // In the case of .fetchRecords and fetchChanges, the specified items or zone may just 45 | // be deleted by the other peer and don't exist in the database (.unknownItem or .zoneNotFound). 46 | // 47 | if let partialError = nsError.userInfo[CKPartialErrorsByItemIDKey] as? NSDictionary { 48 | 49 | let errors = affectedObjects?.map({ partialError[$0] }).filter({ $0 != nil }) 50 | 51 | // If the error doesn't affect the affectedObjects, ignore it. 52 | // Only handle the first error. 53 | // 54 | guard let ckError = errors?.first as? CKError else { return nil } 55 | 56 | // Items not found. Sliently ignore for the delete operation. 57 | // 58 | if operation == .deleteZones || operation == .deleteRecords || operation == .deleteSubscriptions { 59 | if ckError.code == .unknownItem { 60 | return nil 61 | } 62 | } 63 | 64 | if ckError.code == .serverRecordChanged { 65 | print("Server record changed. Consider using serverRecord and ignore this error!") 66 | } else if ckError.code == .zoneNotFound { 67 | print("Zone not found. May have been deleted. Probably ignore!") 68 | } else if ckError.code == .unknownItem { 69 | print("Unknow item. May have been deleted. Probably ignore!") 70 | } else if ckError.code == .batchRequestFailed { 71 | print("Atomic failure!") 72 | } else { 73 | if alert { 74 | presentAlert(operation: operation, error: nsError) 75 | } 76 | print("!!!!!\(operation.rawValue) operation error: \(nsError)") 77 | } 78 | return ckError 79 | } 80 | 81 | // In the case of fetching changes: 82 | // .changeTokenExpired: return for callers to refetch with nil server token. 83 | // .zoneNotFound: return for callers to switch zone, as the current zone has been deleted. 84 | // .partialFailure: zoneNotFound will trigger a partial error as well. 85 | // 86 | if operation == .fetchChanges { 87 | if let ckError = error as? CKError { 88 | if ckError.code == .changeTokenExpired || ckError.code == .zoneNotFound { 89 | return ckError 90 | } 91 | } 92 | } 93 | 94 | // If the client wants an alert, alert the error, or append the error message to an existing alert. 95 | // 96 | if alert { 97 | presentAlert(operation: operation, error: nsError) 98 | } 99 | print("!!!!!\(operation.rawValue) operation error: \(nsError)") 100 | return error as? CKError 101 | } 102 | 103 | private func presentAlert(operation: CloudKitOperationType, error: NSError) { 104 | 105 | DispatchQueue.main.async { 106 | guard let window = UIApplication.shared.delegate?.window, 107 | let viewController = window?.rootViewController else { return } 108 | 109 | let message = "\(operation.rawValue) operation hit this error:\n\(error)" 110 | 111 | if let currentAlert = viewController.presentedViewController as? UIAlertController { 112 | currentAlert.message = (currentAlert.message ?? "") + "\n\n\(message)" 113 | return 114 | } 115 | 116 | let alert = UIAlertController(title: "CloudKit Operation Error!", 117 | message: message, preferredStyle: .alert) 118 | alert.addAction(UIAlertAction(title: "OK", style: .default)) 119 | 120 | viewController.present(alert, animated: true) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /SyncEngine/Core/KeyStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyStore.swift 3 | // Engine 4 | // 5 | // Created by Purkylin King on 2018/6/5. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | class KeyStoreModel: Object { 13 | @objc dynamic var key: String = "" 14 | @objc dynamic var value: Data = Data() 15 | 16 | override static func primaryKey() -> String? { 17 | return "key" 18 | } 19 | 20 | override static func indexedProperties() -> [String] { 21 | return ["key"] 22 | } 23 | } 24 | 25 | final public class KeyStore { 26 | public static let shared = KeyStore() 27 | 28 | public subscript(key: String) -> Data? { 29 | get { 30 | let realm = try! Realm() 31 | return realm.object(ofType: KeyStoreModel.self, forPrimaryKey: key)?.value 32 | } 33 | set { 34 | let realm = try! Realm() 35 | if let obj = realm.object(ofType: KeyStoreModel.self, forPrimaryKey: key) { 36 | try! realm.write { 37 | if newValue == nil { 38 | realm.delete(obj) 39 | } else { 40 | obj.value = newValue! 41 | } 42 | } 43 | } else { 44 | if newValue != nil { 45 | let newObj = KeyStoreModel() 46 | newObj.key = key 47 | newObj.value = newValue! 48 | 49 | try! realm.write { 50 | realm.add(newObj) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SyncEngine/Core/LocalCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalCache.swift 3 | // Engine 4 | // 5 | // Created by Purkylin King on 2018/6/6. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CloudKit 11 | 12 | extension Notification.Name { 13 | static let zoneCacheDidChange = Notification.Name("zoneCacheDidChange") 14 | static let topicCacheDidChange = Notification.Name("objectCacheDidChange") 15 | static let zoneDidSwitch = Notification.Name("zoneDidSwtich") 16 | } 17 | 18 | struct ZoneCacheChanges { 19 | private(set) var database: Database 20 | private(set) var zoneIDsDeleted = [CKRecordZoneID]() 21 | private(set) var zoneIDsChanged = [CKRecordZoneID]() 22 | } 23 | 24 | struct ObjectCacheChanges { 25 | private(set) var recordIDsDeleted = [CKRecordID]() 26 | private(set) var recordsChanged = [CKRecord]() 27 | } 28 | 29 | class NotificationObject { 30 | var payload: T? 31 | 32 | init(payload: T? = nil) { 33 | self.payload = payload 34 | } 35 | } 36 | 37 | typealias ZoneCacheDidChange = NotificationObject 38 | typealias ObjectCacheDidChange = NotificationObject 39 | -------------------------------------------------------------------------------- /SyncEngine/Core/SyncBaseModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncBaseModel.swift 3 | // Engine 4 | // 5 | // Created by Purkylin King on 2018/6/3. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import CloudKit 12 | 13 | extension CKRecord { 14 | /// Convert record to data which contain system fields 15 | func systemData() -> Data { 16 | let data = NSMutableData() 17 | let archiver = NSKeyedArchiver(forWritingWith: data) 18 | archiver.requiresSecureCoding = true 19 | self.encodeSystemFields(with: archiver) 20 | archiver.finishEncoding() 21 | return data as Data 22 | } 23 | 24 | /// Get a instance from data 25 | static func recover(from data: Data) -> CKRecord? { 26 | let unarchiver = NSKeyedUnarchiver(forReadingWith: data) 27 | unarchiver.requiresSecureCoding = true 28 | return CKRecord(coder: unarchiver) 29 | } 30 | 31 | /// Get cache shared record 32 | public func sharedRecord() -> CKShare? { 33 | guard let id = share?.recordID.recordName else { return nil } 34 | return KeyStore.shared.record(id: id) as? CKShare 35 | } 36 | 37 | public var isOwner: Bool { 38 | return self.creatorUserRecordID!.recordName == "__defaultOwner__" 39 | } 40 | 41 | public var zoneName: String { 42 | if isOwner { 43 | return recordID.zoneID.zoneName 44 | } else { 45 | return share!.recordID.zoneID.zoneName 46 | } 47 | } 48 | } 49 | 50 | extension KeyStore { 51 | public func record(id: String) -> CKRecord? { 52 | guard let data = self[id], data.count > 0 else { return nil } 53 | return CKRecord.recover(from: data) 54 | } 55 | 56 | func save(record: CKRecord) { 57 | self[record.recordID.recordName] = record.systemData() 58 | } 59 | } 60 | 61 | private let excludeSyncPropertyNames = ["synced", "deleted", "ownerName", "readWrite", "modifiedAt"] 62 | private let defaultOwnerName = "__defaultOwner__" 63 | 64 | open class SyncBaseModel: Object { 65 | @objc public dynamic var id = UUID().uuidString 66 | 67 | // Only used in local, you shouldn't add these properties in dashboard 68 | @objc public dynamic var modifiedAt = Date() 69 | @objc public dynamic var deleted = false 70 | @objc public dynamic var synced = false 71 | @objc public dynamic var readWrite = true 72 | @objc public dynamic var ownerName = defaultOwnerName // used for shared 73 | 74 | static var recordType: String { 75 | return className() 76 | } 77 | 78 | public var shared: Bool { 79 | return ownerName != defaultOwnerName 80 | } 81 | 82 | public var recordID: CKRecordID { 83 | let zoneID = CKRecordZoneID(zoneName: customZoneName, ownerName: CKCurrentUserDefaultName) 84 | return CKRecordID(recordName: id, zoneID: zoneID) 85 | } 86 | 87 | public var syncRecord: CKRecord { 88 | var record: CKRecord 89 | if let r = KeyStore.shared.record(id: id) { 90 | record = r 91 | } else { 92 | let typeName = type(of: self).recordType 93 | assert(!typeName.contains("."), "Invalid class name, Model class should use @objc") 94 | record = CKRecord(recordType: typeName, recordID: recordID) 95 | KeyStore.shared.save(record: record) 96 | } 97 | 98 | for property in self.objectSchema.properties { 99 | if excludeSyncPropertyNames.contains(property.name) { 100 | continue 101 | } 102 | 103 | switch property.type { 104 | case .int, .string, .bool, .date, .float, .double: 105 | record[property.name] = self.value(forKey: property.name) as? CKRecordValue 106 | case .object: 107 | break 108 | default: 109 | print("Error: Unsupport property type") 110 | break 111 | } 112 | } 113 | 114 | return record 115 | } 116 | 117 | public static func from(record: CKRecord) -> SyncBaseModel { 118 | guard let modelClass = NSClassFromString(record.recordType) as? SyncBaseModel.Type else { 119 | fatalError("Invalid class name, Model class should use @objc") 120 | } 121 | 122 | let model = modelClass.init() 123 | for property in model.objectSchema.properties { 124 | let key = property.name 125 | if !excludeSyncPropertyNames.contains(key) { 126 | model.setValue(record[key], forKey: key) 127 | } 128 | } 129 | return model 130 | } 131 | 132 | override open class func primaryKey() -> String? { 133 | return "id" 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /SyncEngine/Core/SyncEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncEngine.swift 3 | // Engine 4 | // 5 | // Created by Purkylin King on 2018/6/5. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CloudKit 11 | import RealmSwift 12 | 13 | internal func kinglog(_ string: String) { 14 | #if syncengine_sync_log 15 | print(string) 16 | #endif 17 | } 18 | 19 | internal let customZoneName = "kingprivatezone" 20 | 21 | public final class SyncEngine { 22 | public static let `default` = SyncEngine() 23 | let container = CKContainer.default() 24 | let databases: [Database] 25 | 26 | var disabled = true 27 | 28 | internal var models = [SyncBaseModel.Type]() 29 | 30 | lazy var operationQueue: OperationQueue = { 31 | return OperationQueue() 32 | }() 33 | 34 | private lazy var cacheQueue: DispatchQueue = { 35 | return DispatchQueue(label: "LocalCache", attributes: .concurrent) 36 | }() 37 | 38 | func performWriterBlock(_ writerBlock: @escaping () -> Void) { 39 | cacheQueue.async(flags: .barrier) { 40 | writerBlock() 41 | } 42 | } 43 | 44 | // Register sync model 45 | public func register(models: [SyncBaseModel.Type]) { 46 | assert(models.count > 0) 47 | self.models = models 48 | } 49 | 50 | public func sync() { 51 | guard disabled == false else { return } 52 | assert(models.count > 0, "Error You havn't register any model") 53 | databases.forEach { $0.syncLocalChanges() } 54 | } 55 | 56 | public func start() { 57 | print("Start sync engine") 58 | disabled = false 59 | 60 | addZone(with: customZoneName, to: databases[0]) 61 | 62 | // Add item CKSharingSupported in your Info.plist if you use share 63 | for database in databases { 64 | database.addSubscription(to: operationQueue) 65 | } 66 | 67 | fetchChanges() 68 | 69 | NotificationCenter.default.addObserver(self, selector: #selector(zoneCacheDidChange(_:)), name: .zoneCacheDidChange, object: nil) 70 | } 71 | 72 | public func stop() { 73 | print("Stop sync engine") 74 | disabled = true 75 | NotificationCenter.default.removeObserver(self) 76 | } 77 | 78 | public func fetchChanges() { 79 | guard disabled == false else { return } 80 | for database in databases { 81 | fetchChanges(from: database) 82 | } 83 | } 84 | 85 | public func checkiCloudAvailable(completion: @escaping (Bool) -> Void) { 86 | CKContainer.default().accountStatus { (status, error) in 87 | completion(status == .available) 88 | } 89 | } 90 | 91 | public func didReceiveRemoteNotification(userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 92 | let appState = UIApplication.shared.applicationState 93 | guard let userInfo = userInfo as? [String: NSObject], 94 | appState != .inactive else { return } 95 | guard disabled == false else { return } 96 | 97 | let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) 98 | guard let subscriptionID = notification.subscriptionID else { return } 99 | if notification.notificationType == .database { 100 | for database in SyncEngine.default.databases { 101 | if database.cloudKitDB.name == subscriptionID { 102 | SyncEngine.default.fetchChanges(from: database) 103 | break 104 | } 105 | 106 | } 107 | } 108 | completionHandler(.noData) 109 | } 110 | 111 | // Post the notification after all the operations are done so that observers can update the UI 112 | // This method can be tr-entried 113 | // 114 | func postWhenOperationQueueClear(name: NSNotification.Name, object: Any? = nil) { 115 | DispatchQueue.global().async { 116 | self.operationQueue.waitUntilAllOperationsAreFinished() 117 | DispatchQueue.main.async { 118 | NotificationCenter.default.post(name: name, object: object) 119 | } 120 | } 121 | } 122 | 123 | init() { 124 | databases = [ 125 | Database(cloudKitDB: container.privateCloudDatabase), 126 | Database(cloudKitDB: container.sharedCloudDatabase) 127 | ] 128 | } 129 | 130 | func fetchChanges(from database: Database) { 131 | var zoneIDsChanged = [CKRecordZoneID](), zoneIDsDeleted = [CKRecordZoneID]() 132 | let changeToken = database.serverChangeToken 133 | 134 | let operation = CKFetchDatabaseChangesOperation(previousServerChangeToken: changeToken) 135 | let notificationObject = ZoneCacheDidChange() 136 | 137 | operation.changeTokenUpdatedBlock = { changeToken in 138 | self.performWriterBlock { 139 | database.save(changeToken: changeToken) 140 | } 141 | } 142 | 143 | operation.recordZoneWithIDWasDeletedBlock = { zoneID in 144 | zoneIDsDeleted.append(zoneID) 145 | } 146 | 147 | operation.recordZoneWithIDChangedBlock = { zoneID in 148 | zoneIDsChanged.append(zoneID) 149 | } 150 | 151 | operation.fetchDatabaseChangesCompletionBlock = { changeToken, more, error in 152 | if let ckError = handleCloudKitError(error, operation: .fetchChanges, alert: true), 153 | ckError.code == .changeTokenExpired { 154 | 155 | self.performWriterBlock { database.save(changeToken: nil) } 156 | self.fetchChanges(from: database) // Fetch changes again with nil token. 157 | return 158 | } 159 | 160 | self.performWriterBlock { database.save(changeToken: changeToken) } 161 | 162 | // filter deletedID 163 | zoneIDsChanged = zoneIDsChanged.filter { zoneID in return !zoneIDsDeleted.contains(zoneID) } 164 | 165 | notificationObject.payload = ZoneCacheChanges( 166 | database: database, zoneIDsDeleted: zoneIDsDeleted, zoneIDsChanged: zoneIDsChanged) 167 | 168 | // fetch zone 169 | } 170 | 171 | operation.database = database.cloudKitDB 172 | operationQueue.addOperation(operation) 173 | postWhenOperationQueueClear(name: .zoneCacheDidChange, object: notificationObject) 174 | } 175 | 176 | // MARK: - Modify zones 177 | 178 | func addZone(with zoneName: String, to database: Database) { 179 | if UserDefaults.standard.bool(forKey: DefaultsKey.createCustomeZone) { 180 | return 181 | } 182 | 183 | let zoneID = CKRecordZoneID(zoneName: zoneName, ownerName: CKCurrentUserDefaultName) 184 | let newZone = CKRecordZone(zoneID: zoneID) 185 | let operation = CKModifyRecordZonesOperation(recordZonesToSave: [newZone], recordZoneIDsToDelete: nil) 186 | operation.modifyRecordZonesCompletionBlock = { zones, zoneIDs, error in 187 | guard handleCloudKitError(error, operation: .modifyZones, alert: true) == nil, 188 | let savedZone = zones?[0] else { return } 189 | self.performWriterBlock { 190 | database.zones.append(savedZone) 191 | UserDefaults.standard.setValue(true, forKey: DefaultsKey.createCustomeZone) 192 | } 193 | } 194 | 195 | operation.database = database.cloudKitDB 196 | operationQueue.addOperation(operation) 197 | } 198 | 199 | func deleteZone(_ zone: CKRecordZone, from database: Database) { 200 | let operation = CKModifyRecordZonesOperation(recordZonesToSave: nil, recordZoneIDsToDelete: [zone.zoneID]) 201 | operation.modifyRecordZonesCompletionBlock = { (_, _, error) in 202 | 203 | guard handleCloudKitError(error, operation: .modifyRecords, alert: true) == nil else { return } 204 | 205 | self.performWriterBlock { 206 | if let index = database.zones.index(of: zone) { 207 | database.zones.remove(at: index) 208 | } 209 | } 210 | } 211 | operation.database = database.cloudKitDB 212 | operationQueue.addOperation(operation) 213 | } 214 | 215 | // MARK: - Notification 216 | 217 | @objc func zoneCacheDidChange(_ notification: Notification) { 218 | guard let zoneChanges = (notification.object as? ZoneCacheDidChange)?.payload else { return } 219 | zoneChanges.database.fetchZoneChanges(zoneIDs: zoneChanges.zoneIDsChanged) 220 | 221 | performWriterBlock { 222 | // Delete zones 223 | let realm = try! Realm() 224 | for zoneID in zoneChanges.zoneIDsDeleted { 225 | let ownerName = zoneID.ownerName 226 | for model in self.models { 227 | let toDeleteObjs = realm.objects(model).filter {$0.ownerName == ownerName } 228 | try! realm.write { 229 | realm.delete(toDeleteObjs) 230 | } 231 | } 232 | } 233 | } 234 | } 235 | 236 | // MARK: - Others 237 | 238 | public func userDidAcceptCloudKitShare(with metadata: CKShareMetadata) { 239 | let acceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [metadata]) 240 | acceptSharesOperation.acceptSharesCompletionBlock = { error in 241 | guard handleCloudKitError(error, operation: .acceptShare, alert: true) == nil else { return } 242 | // TODO: Fetch 243 | // print("access success") 244 | } 245 | container.add(acceptSharesOperation) 246 | } 247 | 248 | public func saveShare(record: CKRecord, completion:@escaping (CKShare?, Error?) -> Void) { 249 | let share = CKShare(rootRecord: record) 250 | let modifyRecordsOperation = CKModifyRecordsOperation( 251 | recordsToSave: [record, share], 252 | recordIDsToDelete: nil) 253 | 254 | modifyRecordsOperation.timeoutIntervalForRequest = 10 255 | modifyRecordsOperation.timeoutIntervalForResource = 10 256 | 257 | modifyRecordsOperation.modifyRecordsCompletionBlock = 258 | { records, recordIDs, error in 259 | completion(share, error) 260 | } 261 | 262 | modifyRecordsOperation.database = container.privateCloudDatabase 263 | operationQueue.addOperation(modifyRecordsOperation) 264 | } 265 | 266 | public func fetchShare(recordID: CKRecordID, isOwner: Bool, completion: @escaping (CKShare?, Error?) -> Void) { 267 | let operation = CKFetchRecordsOperation(recordIDs: [recordID]) 268 | operation.fetchRecordsCompletionBlock = { info, error in 269 | completion(info?[recordID] as? CKShare, error) 270 | } 271 | 272 | operation.database = isOwner ? container.privateCloudDatabase : container.sharedCloudDatabase 273 | operationQueue.addOperation(operation) 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /SyncEngine/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SyncEngine/SyncEngine.h: -------------------------------------------------------------------------------- 1 | // 2 | // SyncEngine.h 3 | // SyncEngine 4 | // 5 | // Created by Purkylin King on 2018/6/6. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SyncEngine. 12 | FOUNDATION_EXPORT double SyncEngineVersionNumber; 13 | 14 | //! Project version string for SyncEngine. 15 | FOUNDATION_EXPORT const unsigned char SyncEngineVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /SyncEngineDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SyncEngineDemo 4 | // 5 | // Created by Purkylin King on 2018/6/7. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SyncEngine 11 | import CloudKit 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | var window: UIWindow? 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 19 | 20 | let engine = SyncEngine.default 21 | engine.register(models: [Note.self]) 22 | engine.start() 23 | 24 | printPath() 25 | return true 26 | } 27 | 28 | func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) { 29 | SyncEngine.default.userDidAcceptCloudKitShare(with: cloudKitShareMetadata) 30 | } 31 | 32 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 33 | SyncEngine.default.didReceiveRemoteNotification(userInfo: userInfo, fetchCompletionHandler: completionHandler) 34 | } 35 | 36 | func printPath() { 37 | let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! 38 | print("document path: \(path)") 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /SyncEngineDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SyncEngineDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SyncEngineDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SyncEngineDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 148 | 154 | 160 | 166 | 172 | 178 | 179 | 180 | 181 | 182 | 183 | 189 | 195 | 201 | 207 | 213 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /SyncEngineDemo/EditViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditViewController.swift 3 | // SyncEngineDemo 4 | // 5 | // Created by Purkylin King on 2018/6/7. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | 12 | class EditViewController: UIViewController { 13 | var note: Note? 14 | 15 | @IBOutlet weak var textField: UITextField! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | textField.text = note?.title ?? "" 21 | } 22 | 23 | @IBAction func btnSaveClicked(_ sender: Any) { 24 | if note == nil { 25 | note = Note() 26 | } 27 | 28 | let realm = try! Realm() 29 | try! realm.write { 30 | note!.title = textField.text ?? "" 31 | note?.synced = false 32 | note?.modifiedAt = Date() 33 | realm.add(note!) 34 | } 35 | 36 | self.navigationController?.popViewController(animated: true) 37 | } 38 | 39 | @IBAction func btnInfoclicked(_ sender: Any) { 40 | 41 | } 42 | 43 | override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { 44 | return note != nil 45 | } 46 | 47 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 48 | let vc = segue.destination as? InfoViewController 49 | vc?.note = note! 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SyncEngineDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | CKSharingSupported 22 | 23 | LSRequiresIPhoneOS 24 | 25 | UIBackgroundModes 26 | 27 | fetch 28 | remote-notification 29 | 30 | UILaunchStoryboardName 31 | LaunchScreen 32 | UIMainStoryboardFile 33 | Main 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /SyncEngineDemo/InfoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfoViewController.swift 3 | // SyncEngineDemo 4 | // 5 | // Created by Purkylin King on 2018/6/7. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SyncEngine 11 | import CloudKit 12 | 13 | struct Platform { 14 | static var isSimulator: Bool { 15 | return TARGET_OS_SIMULATOR != 0 16 | } 17 | } 18 | 19 | class InfoViewController: UIViewController { 20 | var note: Note! 21 | 22 | @IBOutlet weak var createrLabel: UILabel! 23 | @IBOutlet weak var countLabel: UILabel! 24 | @IBOutlet weak var deviceLabel: UILabel! 25 | @IBOutlet weak var modifyDateLabel: UILabel! 26 | @IBOutlet weak var modifierLabel: UILabel! 27 | @IBOutlet weak var readWriteLabel: UILabel! 28 | 29 | let formatter = DateFormatter() 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | let item = UIBarButtonItem(title: "Share", style: .plain, target: self, action: #selector(btnShareClicked)) 35 | self.navigationItem.rightBarButtonItem = item 36 | 37 | formatter.dateFormat = "yyyy-MM-dd hh:mm:ss" 38 | countLabel.text = "\(note.title.count)" 39 | 40 | 41 | let record = note.syncRecord 42 | // var share: CKShare? = nil 43 | deviceLabel.text = record["modifiedByDevice"] as? String 44 | 45 | if note.shared { 46 | if let share = record.sharedRecord() { 47 | readWriteLabel.text = share.publicPermission == .readWrite ? "true" : "false" 48 | } 49 | 50 | 51 | } else { 52 | readWriteLabel.text = "true" 53 | createrLabel.text = "owner" 54 | } 55 | 56 | modifyDateLabel.text = toString(date: record.modificationDate) 57 | } 58 | 59 | func toString(date: Date?) -> String { 60 | guard date != nil else { return "" } 61 | return formatter.string(from: date!) 62 | } 63 | 64 | func showAddShare() { 65 | let vc = UICloudSharingController { (controller, completion) in 66 | SyncEngine.default.saveShare(record: self.note.syncRecord, completion: { (share, error) in 67 | // guard let share = share, error == nil else { return } 68 | completion(share, CKContainer.default(), error) 69 | }) 70 | } 71 | 72 | vc.popoverPresentationController?.sourceView = self.view 73 | vc.delegate = self 74 | self.present(vc, animated: true, completion: nil) 75 | } 76 | 77 | @objc func btnShareClicked() { 78 | if let recordID = note.syncRecord.share?.recordID { 79 | SyncEngine.default.fetchShare(recordID: recordID, isOwner: note.syncRecord.isOwner) { (share, error) in 80 | if let error = error as? CKError { 81 | if error.isRecordNotFound() { 82 | self.showAddShare() 83 | } else { 84 | print(error.localizedDescription) 85 | } 86 | return 87 | } 88 | 89 | guard error == nil else { return } 90 | let vc = UICloudSharingController(share: share!, container: CKContainer.default()) 91 | vc.popoverPresentationController?.sourceView = self.view 92 | self.present(vc, animated: true, completion: nil) 93 | vc.delegate = self 94 | } 95 | } else { 96 | showAddShare() 97 | } 98 | } 99 | } 100 | 101 | extension InfoViewController: UICloudSharingControllerDelegate { 102 | func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) { 103 | print("Error: share") 104 | } 105 | 106 | func itemTitle(for csc: UICloudSharingController) -> String? { 107 | return "Hello Share" 108 | } 109 | 110 | func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) { 111 | if Platform.isSimulator { 112 | SyncEngine.default.fetchChanges() 113 | } 114 | print("share success") 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /SyncEngineDemo/Note.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Note.swift 3 | // SyncEngineDemo 4 | // 5 | // Created by Purkylin King on 2018/6/7. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SyncEngine 11 | 12 | @objc(Note) 13 | class Note: SyncBaseModel { 14 | @objc dynamic var title: String = "" 15 | } 16 | -------------------------------------------------------------------------------- /SyncEngineDemo/SyncEngineDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.icloud-container-identifiers 8 | 9 | iCloud.$(CFBundleIdentifier) 10 | 11 | com.apple.developer.icloud-services 12 | 13 | CloudKit 14 | 15 | com.apple.developer.ubiquity-kvstore-identifier 16 | $(TeamIdentifierPrefix)$(CFBundleIdentifier) 17 | 18 | 19 | -------------------------------------------------------------------------------- /SyncEngineDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SyncEngineDemo 4 | // 5 | // Created by Purkylin King on 2018/6/7. 6 | // Copyright © 2018年 Purkylin King. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | import SyncEngine 12 | 13 | class ViewController: UIViewController { 14 | var notes: Results! 15 | var notificationToken: NotificationToken? = nil 16 | 17 | @IBOutlet weak var tableView: UITableView! 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | setup() 21 | } 22 | 23 | func setup() { 24 | let realm = try! Realm() 25 | notes = realm.objects(Note.self).filter("deleted == false") 26 | 27 | notificationToken = notes.observe({ [weak self] (changs: RealmCollectionChange) in 28 | guard let tableView = self?.tableView else { return } 29 | 30 | switch changs { 31 | case .initial: 32 | tableView.reloadData() 33 | case .update(_, let deletions, let insertions, let modifications): 34 | // select 35 | tableView.beginUpdates() 36 | tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0)}), with: .automatic) 37 | tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), with: .automatic) 38 | tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0)}), with: .automatic) 39 | tableView.endUpdates() 40 | case .error(let error): 41 | fatalError("\(error)") 42 | } 43 | }) 44 | } 45 | 46 | @IBAction func btnSyncClicked(_ sender: Any) { 47 | SyncEngine.default.sync() 48 | SyncEngine.default.fetchChanges() 49 | } 50 | 51 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 52 | if segue.identifier == "detail" { 53 | guard let indexPath = tableView.indexPathForSelectedRow else { return } 54 | let vc = segue.destination as? EditViewController 55 | vc?.note = notes[indexPath.row] 56 | } else { // add 57 | // do nothing 58 | } 59 | } 60 | } 61 | 62 | extension ViewController: UITableViewDataSource, UITableViewDelegate { 63 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 64 | return notes.count 65 | } 66 | 67 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 68 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 69 | cell.textLabel?.text = notes[indexPath.row].title 70 | return cell 71 | } 72 | 73 | func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { 74 | let action = UITableViewRowAction(style: .destructive, title: "Delete") { (rowAction, indexPath) in 75 | let realm = try! Realm() 76 | let obj = self.notes[indexPath.row] 77 | try! realm.write { 78 | obj.deleted = true 79 | obj.synced = false 80 | } 81 | } 82 | return [action] 83 | } 84 | } 85 | 86 | --------------------------------------------------------------------------------