├── IceCream ├── Assets │ └── .gitkeep ├── Classes │ ├── .gitkeep │ ├── Notification+Name.swift │ ├── CreamLocation.swift │ ├── Syncable.swift │ ├── Manifest.swift │ ├── BackgroundWorker.swift │ ├── PendingRelationshipsWorker.swift │ ├── PublicDatabaseManager.swift │ ├── SyncEngine.swift │ ├── DatabaseManagerProtocol.swift │ ├── SyncObject.swift │ ├── CKRecordRecoverable.swift │ ├── CKRecordConvertible.swift │ ├── CreamAsset.swift │ ├── ErrorHandler.swift │ └── PrivateDatabaseManager.swift ├── IceCream.h └── Info.plist ├── Cartfile ├── Cartfile.resolved ├── scripts └── push.sh ├── Example ├── IceCream_Example │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── smile_dog.imageset │ │ │ ├── dog1.png │ │ │ └── Contents.json │ │ ├── tongue_dog.imageset │ │ │ ├── dog2.png │ │ │ └── Contents.json │ │ ├── dull_cat.imageset │ │ │ ├── dull_cat.png │ │ │ └── Contents.json │ │ ├── heart_cat.imageset │ │ │ ├── heart_cat.jpg │ │ │ └── Contents.json │ │ ├── cat_placeholder.imageset │ │ │ ├── cat-placeholder.png │ │ │ └── Contents.json │ │ ├── dog_placeholder.imageset │ │ │ ├── dog_placeholder.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── IceCream_Example.entitlements │ ├── Person.swift │ ├── Cat.swift │ ├── Dog.swift │ ├── TabBarViewController.swift │ ├── Info.plist │ ├── OwnerDetailViewController.swift │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── OwnersViewController.swift │ ├── CatsViewController.swift │ └── DogsViewController.swift └── IceCream_Example.xcodeproj │ ├── project.xcworkspace │ └── contents.xcworkspacedata │ └── project.pbxproj ├── .travis.yml ├── IceCream.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── IceCream.xcscheme │ ├── IceCream-tvOS.xcscheme │ ├── IceCream-watchOS.xcscheme │ └── IceCream-macOS.xcscheme ├── .github ├── workflows │ └── deploy_to_cocoapods.yml └── FUNDING.yml ├── Package.resolved ├── docs ├── issue_template.md ├── CONTRIBUTING.md └── CHANGELOG.md ├── Package.swift ├── .gitignore ├── LICENSE ├── IceCream.podspec └── README.md /IceCream/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /IceCream/Classes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "realm/realm-cocoa" 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "realm/realm-cocoa" "v3.16.1" 2 | -------------------------------------------------------------------------------- /scripts/push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ~/.rvm/scripts/rvm 4 | rvm use default 5 | pod trunk push --allow-warnings 6 | -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/smile_dog.imageset/dog1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiyue1993/IceCream/HEAD/Example/IceCream_Example/Images.xcassets/smile_dog.imageset/dog1.png -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/tongue_dog.imageset/dog2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiyue1993/IceCream/HEAD/Example/IceCream_Example/Images.xcassets/tongue_dog.imageset/dog2.png -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/dull_cat.imageset/dull_cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiyue1993/IceCream/HEAD/Example/IceCream_Example/Images.xcassets/dull_cat.imageset/dull_cat.png -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/heart_cat.imageset/heart_cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiyue1993/IceCream/HEAD/Example/IceCream_Example/Images.xcassets/heart_cat.imageset/heart_cat.jpg -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/cat_placeholder.imageset/cat-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiyue1993/IceCream/HEAD/Example/IceCream_Example/Images.xcassets/cat_placeholder.imageset/cat-placeholder.png -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/dog_placeholder.imageset/dog_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiyue1993/IceCream/HEAD/Example/IceCream_Example/Images.xcassets/dog_placeholder.imageset/dog_placeholder.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | language: swift 6 | osx_image: xcode11.3 7 | script: 8 | - swift build 9 | 10 | -------------------------------------------------------------------------------- /Example/IceCream_Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /IceCream.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/dull_cat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dull_cat.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/smile_dog.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/tongue_dog.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/heart_cat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "heart_cat.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/cat_placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat-placeholder.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.xcassets/dog_placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_placeholder.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /IceCream/IceCream.h: -------------------------------------------------------------------------------- 1 | // 2 | // IceCream.h 3 | // IceCream 4 | // 5 | // Created by 蔡越 on 07/02/2018. 6 | // Copyright © 2018 蔡越. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | //! Project version number for IceCream. 12 | FOUNDATION_EXPORT double IceCreamVersionNumber; 13 | 14 | //! Project version string for IceCream. 15 | FOUNDATION_EXPORT const unsigned char IceCreamVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/deploy_to_cocoapods.yml: -------------------------------------------------------------------------------- 1 | name: deploy_to_cocoapods 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: macOS-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v1 13 | 14 | - name: Install Cocoapods 15 | run: gem install cocoapods 16 | 17 | - name: Deploy to Cocoapods 18 | run: | 19 | set -eo pipefail 20 | pod lib lint --allow-warnings --verbose 21 | pod trunk push --allow-warnings --verbose 22 | env: 23 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: caiyue1993 4 | patreon: # Replace with a single Patreon username 5 | open_collective: icecream 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://www.paypal.me/yuecai 13 | -------------------------------------------------------------------------------- /IceCream/Classes/Notification+Name.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification+Name.swift 3 | // IceCream 4 | // 5 | // Created by 蔡越 on 09/12/2017. 6 | // 7 | 8 | import Foundation 9 | 10 | /// I believe this should be the best practice for creating custom notifications. 11 | /// https://stackoverflow.com/questions/37899778/how-do-you-create-custom-notifications-in-swift-3 12 | 13 | public protocol NotificationName { 14 | var name: Notification.Name { get } 15 | } 16 | 17 | extension RawRepresentable where RawValue == String, Self: NotificationName { 18 | public var name: Notification.Name { 19 | get { 20 | return Notification.Name(self.rawValue) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Realm", 6 | "repositoryURL": "https://github.com/realm/realm-cocoa", 7 | "state": { 8 | "branch": null, 9 | "revision": "11d7853b750f367525b174331e72f5431e15dfcc", 10 | "version": "4.1.1" 11 | } 12 | }, 13 | { 14 | "package": "RealmCore", 15 | "repositoryURL": "https://github.com/realm/realm-core", 16 | "state": { 17 | "branch": null, 18 | "revision": "d2f6573960c84ebea3e0236047b3d976f95a5d7a", 19 | "version": "5.23.6" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Example/IceCream_Example/IceCream_Example.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 | -------------------------------------------------------------------------------- /docs/issue_template.md: -------------------------------------------------------------------------------- 1 | > Below is the issue template. You can fill each part then submit your issue. 2 | > Or you can just delete all of these and describe your questions in you-like style. 3 | > But please remember: the more detailed info you offered, the greater possibility your problem will be solved. 😜 4 | 5 | ## Expected behavior 6 | 7 | // Please replace this line with your expected behavior. 8 | 9 | ## Actual behavior(optional) 10 | 11 | // Please replace this line with the actual behavior. 12 | 13 | ## Steps to reproduce the problem(optional) 14 | 15 | // Please provided to steps to reproduce the problem 16 | 17 | ## Reference links(optional) 18 | 19 | // Please offer the reference links to us. We appreciate it. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "IceCream", 6 | platforms: [ 7 | .macOS(.v10_12), .iOS(.v10), .tvOS(.v10), .watchOS(.v3) 8 | ], 9 | products: [ 10 | .library( 11 | name: "IceCream", 12 | targets: ["IceCream"]), 13 | ], 14 | dependencies: [ 15 | .package( 16 | url: "https://github.com/realm/realm-cocoa", 17 | from: "4.1.1" 18 | ) 19 | ], 20 | targets: [ 21 | .target( 22 | name: "IceCream", 23 | dependencies: ["RealmSwift", "Realm"], 24 | path: "IceCream", 25 | sources: ["Classes"]) 26 | ], 27 | swiftLanguageVersions: [.v5] 28 | ) 29 | -------------------------------------------------------------------------------- /Example/IceCream_Example/Person.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Person.swift 3 | // IceCream_Example 4 | // 5 | // Created by 蔡越 on 2018/7/15. 6 | // Copyright © 2018 蔡越. All rights reserved. 7 | // 8 | 9 | import RealmSwift 10 | import CloudKit 11 | import IceCream 12 | 13 | class Person: Object { 14 | @objc dynamic var id = NSUUID().uuidString 15 | @objc dynamic var name = "Jim" 16 | @objc dynamic var isDeleted = false 17 | 18 | let cats = List() 19 | 20 | override class func primaryKey() -> String? { 21 | return "id" 22 | } 23 | } 24 | 25 | extension Person: CKRecordConvertible { 26 | // static var databaseScope: CKDatabase.Scope { 27 | // return .public 28 | // } 29 | } 30 | 31 | extension Person: CKRecordRecoverable { 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Example/IceCream_Example/Cat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cat.swift 3 | // IceCream_Example 4 | // 5 | // Created by 蔡越 on 22/05/2018. 6 | // Copyright © 2018 蔡越. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import IceCream 12 | import CloudKit 13 | 14 | class Cat: Object { 15 | @objc dynamic var id = NSUUID().uuidString 16 | @objc dynamic var name = "" 17 | @objc dynamic var age = 0 18 | @objc dynamic var isDeleted = false 19 | 20 | static let AVATAR_KEY = "avatar" 21 | @objc dynamic var avatar: CreamAsset? 22 | 23 | override class func primaryKey() -> String? { 24 | return "id" 25 | } 26 | } 27 | 28 | extension Cat: CKRecordRecoverable { 29 | 30 | } 31 | 32 | extension Cat: CKRecordConvertible { 33 | 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | Pods/ 6 | _Pods.xcodeproj 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata/ 17 | *.xccheckout 18 | profile 19 | *.moved-aside 20 | DerivedData 21 | *.hmap 22 | *.ipa 23 | 24 | # Bundler 25 | .bundle 26 | 27 | Carthage 28 | # We recommend against adding the Pods directory to your .gitignore. However 29 | # you should judge for yourself, the pros and cons are mentioned at: 30 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 31 | # 32 | # Note: if you ignore the Pods directory, make sure to uncomment 33 | # `pod install` in .travis.yml 34 | # 35 | # Pods/ 36 | *.xcworkspace 37 | -------------------------------------------------------------------------------- /IceCream/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 | -------------------------------------------------------------------------------- /Example/IceCream_Example/Dog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dog.swift 3 | // IceCream_Example 4 | // 5 | // Created by 蔡越 on 23/10/2017. 6 | // Copyright © 2017 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import IceCream 12 | import CloudKit 13 | 14 | class Dog: Object { 15 | @objc dynamic var id = NSUUID().uuidString 16 | @objc dynamic var name = "" 17 | @objc dynamic var age = 0 18 | @objc dynamic var isDeleted = false 19 | 20 | static let AVATAR_KEY = "avatar" 21 | @objc dynamic var avatar: CreamAsset? 22 | 23 | // Relationships usage in Realm: https://realm.io/docs/swift/latest/#relationships 24 | @objc dynamic var owner: Person? // to-one relationships must be optional 25 | 26 | override class func primaryKey() -> String? { 27 | return "id" 28 | } 29 | } 30 | 31 | extension Dog: CKRecordConvertible { 32 | 33 | } 34 | 35 | extension Dog: CKRecordRecoverable { 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Example/IceCream_Example/TabBarViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarViewController.swift 3 | // IceCream_Example 4 | // 5 | // Created by 蔡越 on 22/05/2018. 6 | // Copyright © 2018 蔡越. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class TabBarViewController: UITabBarController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | let dogsViewController = DogsViewController() 17 | dogsViewController.title = "Dogs" 18 | let catsViewController = CatsViewController() 19 | catsViewController.title = "Cats" 20 | let ownersViewController = OwnersViewController() 21 | ownersViewController.title = "Owners" 22 | 23 | viewControllers = [ 24 | UINavigationController(rootViewController: dogsViewController), 25 | UINavigationController(rootViewController: catsViewController), 26 | UINavigationController(rootViewController: ownersViewController) 27 | ] 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /IceCream/Classes/CreamLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Yue Cai on 2022/1/6. 6 | // 7 | 8 | import Foundation 9 | import CloudKit 10 | import RealmSwift 11 | import CoreLocation 12 | 13 | public class CreamLocation: Object { 14 | @objc dynamic public var latitude: CLLocationDegrees = 0 15 | @objc dynamic public var longitude: CLLocationDegrees = 0 16 | 17 | convenience public init(latitude: CLLocationDegrees, longitude: CLLocationDegrees) { 18 | self.init() 19 | self.latitude = latitude 20 | self.longitude = longitude 21 | } 22 | 23 | // MARK: - Used in CKRecordConvertible 24 | 25 | var location: CLLocation { 26 | get { 27 | return CLLocation(latitude: latitude, longitude: longitude) 28 | } 29 | } 30 | 31 | // MARK: - Used in CKRecordRecoverable 32 | 33 | static func make(location: CLLocation) -> CreamLocation { 34 | return CreamLocation(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /IceCream/Classes/Syncable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Syncable.swift 3 | // IceCream 4 | // 5 | // Created by 蔡越 on 24/05/2018. 6 | // 7 | 8 | import Foundation 9 | import CloudKit 10 | import RealmSwift 11 | 12 | /// Since `sync` is an informal version of `synchronize`, so we choose the `syncable` word for 13 | /// the ability of synchronization. 14 | public protocol Syncable: class { 15 | 16 | /// CKRecordZone related 17 | var recordType: String { get } 18 | var zoneID: CKRecordZone.ID { get } 19 | 20 | /// Local storage 21 | var zoneChangesToken: CKServerChangeToken? { get set } 22 | var isCustomZoneCreated: Bool { get set } 23 | 24 | /// Realm Database related 25 | func registerLocalDatabase() 26 | func cleanUp() 27 | func add(record: CKRecord) 28 | func delete(recordID: CKRecord.ID) 29 | 30 | func resolvePendingRelationships() 31 | 32 | /// CloudKit related 33 | func pushLocalObjectsToCloudKit() 34 | 35 | /// Callback 36 | var pipeToEngine: ((_ recordsToStore: [CKRecord], _ recordIDsToDelete: [CKRecord.ID]) -> ())? { get set } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Example/IceCream_Example/Images.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" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 caiyue1993 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /IceCream/Classes/Manifest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogConfig.swift 3 | // IceCream 4 | // 5 | // Created by 蔡越 on 30/01/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// This file is for setting some develop configs for IceCream framework. 11 | 12 | public class IceCream { 13 | 14 | public static let shared = IceCream() 15 | 16 | /// There are quite a lot `print`s in the IceCream source files. 17 | /// If you don't want to see them in your console, just set `enableLogging` property to false. 18 | /// The default value is true. 19 | public var enableLogging: Bool = true 20 | 21 | } 22 | 23 | /// If you want to know more, 24 | /// this post would help: https://medium.com/@maxcampolo/swift-conditional-logging-compiler-flags-54692dc86c5f 25 | internal func print(_ items: Any..., separator: String = " ", terminator: String = "\n") { 26 | if (IceCream.shared.enableLogging) { 27 | #if DEBUG 28 | var i = items.startIndex 29 | repeat { 30 | Swift.print(items[i], separator: separator, terminator: i == (items.endIndex - 1) ? terminator : separator) 31 | i += 1 32 | } while i < items.endIndex 33 | #endif 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## Introduction 4 | Firstly, thank you for considering contributing to IceCream! It's people like you that make the open source community great! 😊 5 | 6 | We welcome any types of contribution, not only code. You can help by 7 | 8 | - **Q&A**: file issues that you're facing. The more detailed, the better. Offering the steps that can reproduce the issue is the best. 9 | - **Marketing**: writing blogs or posts, howto's tutorial and so on 10 | - **Code**: firstly take a look at the [open issues](https://github.com/caiyue1993/IceCream/issues) or [to-do list](https://github.com/caiyue1993/IceCream#make-it-better). You can change the code and raise a PR to us. Even if you couldn't write code, commenting on them, showing that you care about a given issue matters. It really helps. 11 | - **Money**: maintaining a active project takes time and much effort. We welcome [financial support](https://github.com/caiyue1993/IceCream#donation). 12 | 13 | ## How to Pull Request? 14 | 15 | Working on your first Pull Request? You can learn how from this official tutorial: [Creating a pull request from a fork](https://help.github.com/articles/creating-a-pull-request-from-a-fork/). 16 | 17 | ## Questions 18 | 19 | If you have any questions, you can file an issue. Or you can reach us at yuecai.nju@gmail.com 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /IceCream.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint IceCream.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'IceCream' 11 | s.version = '2.1.0' 12 | s.summary = 'Sync Realm with CloudKit' 13 | s.description = <<-DESC 14 | Sync Realm Database with CloudKit, written in Swift. It works just like magic. 15 | DESC 16 | s.homepage = 'https://github.com/caiyue1993/IceCream' 17 | s.license = { :type => 'MIT', :file => 'LICENSE' } 18 | s.author = { 'caiyue1993' => 'yuecai.nju@gmail.com' } 19 | s.source = { :git => 'https://github.com/caiyue1993/IceCream.git', :tag => s.version.to_s } 20 | 21 | s.social_media_url = 'https://twitter.com/caiyue5' 22 | 23 | s.ios.deployment_target = '10.0' 24 | s.osx.deployment_target = '10.12' 25 | s.tvos.deployment_target = '10.0' 26 | s.watchos.deployment_target = '3.0' 27 | s.source_files = ["IceCream/Classes/**/*","IceCream/IceCream.h"] 28 | s.public_header_files = ["IceCream/IceCream.h"] 29 | s.static_framework = true 30 | s.swift_version = '5.0' 31 | 32 | s.dependency 'RealmSwift', '< 10.0.0' 33 | end 34 | -------------------------------------------------------------------------------- /Example/IceCream_Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIBackgroundModes 26 | 27 | fetch 28 | remote-notification 29 | 30 | UILaunchStoryboardName 31 | LaunchScreen 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UIRequiresFullScreen 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /IceCream/Classes/BackgroundWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundWorker.swift 3 | // IceCream 4 | // 5 | // Created by Kit Forge on 5/9/19. 6 | // 7 | 8 | import Foundation 9 | import RealmSwift 10 | 11 | // Based on https://academy.realm.io/posts/realm-notifications-on-background-threads-with-swift/ 12 | // Tweaked a little by Yue Cai 13 | 14 | class BackgroundWorker: NSObject { 15 | 16 | static let shared = BackgroundWorker() 17 | 18 | private var thread: Thread? 19 | private var block: (() -> Void)? 20 | 21 | func start(_ block: @escaping () -> Void) { 22 | self.block = block 23 | 24 | if thread == nil { 25 | thread = Thread { [weak self] in 26 | guard let self = self, let th = self.thread else { 27 | Thread.exit() 28 | return 29 | } 30 | while (!th.isCancelled) { 31 | RunLoop.current.run( 32 | mode: .default, 33 | before: Date.distantFuture) 34 | } 35 | Thread.exit() 36 | } 37 | thread?.name = "\(String(describing: self))-\(UUID().uuidString)" 38 | thread?.start() 39 | } 40 | 41 | if let thread = thread { 42 | perform(#selector(runBlock), 43 | on: thread, 44 | with: nil, 45 | waitUntilDone: true, 46 | modes: [RunLoop.Mode.default.rawValue]) 47 | } 48 | } 49 | 50 | func stop() { 51 | thread?.cancel() 52 | } 53 | 54 | @objc private func runBlock() { 55 | block?() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /IceCream/Classes/PendingRelationshipsWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Soledad on 2021/2/7. 6 | // 7 | 8 | import Foundation 9 | import RealmSwift 10 | 11 | /// PendingRelationshipsWorker is responsible for temporarily storing relationships when objects recovering from CKRecord 12 | final class PendingRelationshipsWorker { 13 | 14 | var realm: Realm? 15 | 16 | var pendingListElementPrimaryKeyValue: [AnyHashable: (String, Object)] = [:] 17 | 18 | func addToPendingList(elementPrimaryKeyValue: AnyHashable, propertyName: String, owner: Object) { 19 | pendingListElementPrimaryKeyValue[elementPrimaryKeyValue] = (propertyName, owner) 20 | } 21 | 22 | func resolvePendingListElements() { 23 | guard let realm = realm, pendingListElementPrimaryKeyValue.count > 0 else { 24 | // Maybe we could add one log here 25 | return 26 | } 27 | BackgroundWorker.shared.start { 28 | for (primaryKeyValue, (propName, owner)) in self.pendingListElementPrimaryKeyValue { 29 | guard let list = owner.value(forKey: propName) as? List else { return } 30 | if let existListElementObject = realm.object(ofType: Element.self, forPrimaryKey: primaryKeyValue) { 31 | try! realm.write { 32 | list.append(existListElementObject) 33 | } 34 | self.pendingListElementPrimaryKeyValue[primaryKeyValue] = nil 35 | } else { 36 | print("Cannot find existing resolving record in Realm") 37 | } 38 | } 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Example/IceCream_Example/OwnerDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OwnerDetailViewController.swift 3 | // IceCream_Example 4 | // 5 | // Created by Soledad on 2020/9/9. 6 | // Copyright © 2020 蔡越. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class OwnerDetailViewController: UIViewController { 12 | 13 | private let cats: [Cat] 14 | 15 | private lazy var tableView: UITableView = { 16 | let tv = UITableView() 17 | tv.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 18 | tv.dataSource = self 19 | return tv 20 | }() 21 | 22 | init(cats: [Cat]) { 23 | self.cats = cats 24 | super.init(nibName: nil, bundle: nil) 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | view.addSubview(tableView) 35 | title = "OwnerDetailViewController" 36 | } 37 | 38 | override func viewWillLayoutSubviews() { 39 | super.viewWillLayoutSubviews() 40 | tableView.frame = view.bounds 41 | } 42 | 43 | } 44 | 45 | extension OwnerDetailViewController: UITableViewDataSource { 46 | 47 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 48 | return cats.count 49 | } 50 | 51 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 52 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 53 | cell.textLabel?.text = cats[indexPath.row].name 54 | if let data = cats[indexPath.row].avatar?.storedData() { 55 | cell.imageView?.image = UIImage(data: data) 56 | } else { 57 | cell.imageView?.image = UIImage(named: "cat_placeholder") 58 | } 59 | return cell 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Example/IceCream_Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // IceCream 4 | // 5 | // Created by 蔡越 on 10/17/2017. 6 | // Copyright (c) 2017 Nanjing University. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import IceCream 11 | import CloudKit 12 | import RealmSwift 13 | 14 | @UIApplicationMain 15 | class AppDelegate: UIResponder, UIApplicationDelegate { 16 | 17 | var window: UIWindow? 18 | var syncEngine: SyncEngine? 19 | 20 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 21 | 22 | syncEngine = SyncEngine(objects: [ 23 | SyncObject(type: Dog.self), 24 | SyncObject(type: Cat.self), 25 | SyncObject(type: Person.self, uListElementType: Cat.self) 26 | ]) 27 | 28 | /// If you wanna test public Database, comment the above syncEngine code and uncomment the following one 29 | /// Besides, uncomment Line 26 to 28 in Person.swift file 30 | // syncEngine = SyncEngine(objects: [SyncObject()], databaseScope: .public) 31 | 32 | application.registerForRemoteNotifications() 33 | 34 | window = UIWindow(frame: UIScreen.main.bounds) 35 | window?.rootViewController = TabBarViewController() 36 | window?.makeKeyAndVisible() 37 | return true 38 | } 39 | 40 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 41 | 42 | if let dict = userInfo as? [String: NSObject], let notification = CKNotification(fromRemoteNotificationDictionary: dict), let subscriptionID = notification.subscriptionID, IceCreamSubscription.allIDs.contains(subscriptionID) { 43 | NotificationCenter.default.post(name: Notifications.cloudKitDataDidChangeRemotely.name, object: nil, userInfo: userInfo) 44 | completionHandler(.newData) 45 | } 46 | 47 | } 48 | 49 | func applicationWillEnterForeground(_ application: UIApplication) { 50 | 51 | // How about fetching changes here? 52 | 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /IceCream.xcodeproj/xcshareddata/xcschemes/IceCream.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 | -------------------------------------------------------------------------------- /IceCream.xcodeproj/xcshareddata/xcschemes/IceCream-tvOS.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 | -------------------------------------------------------------------------------- /IceCream.xcodeproj/xcshareddata/xcschemes/IceCream-watchOS.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 | -------------------------------------------------------------------------------- /Example/IceCream_Example/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /IceCream.xcodeproj/xcshareddata/xcschemes/IceCream-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /IceCream/Classes/PublicDatabaseManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PublicDatabaseManager.swift 3 | // IceCream 4 | // 5 | // Created by caiyue on 2019/4/22. 6 | // 7 | 8 | #if os(macOS) 9 | import Cocoa 10 | #else 11 | import UIKit 12 | #endif 13 | 14 | import CloudKit 15 | 16 | final class PublicDatabaseManager: DatabaseManager { 17 | 18 | let container: CKContainer 19 | let database: CKDatabase 20 | 21 | let syncObjects: [Syncable] 22 | 23 | init(objects: [Syncable], container: CKContainer) { 24 | self.syncObjects = objects 25 | self.container = container 26 | self.database = container.publicCloudDatabase 27 | } 28 | 29 | func fetchChangesInDatabase(_ callback: ((Error?) -> Void)?) { 30 | syncObjects.forEach { [weak self] syncObject in 31 | let predicate = NSPredicate(value: true) 32 | let query = CKQuery(recordType: syncObject.recordType, predicate: predicate) 33 | let queryOperation = CKQueryOperation(query: query) 34 | self?.excuteQueryOperation(queryOperation: queryOperation, on: syncObject, callback: callback) 35 | } 36 | } 37 | 38 | func createCustomZonesIfAllowed() { 39 | 40 | } 41 | 42 | func createDatabaseSubscriptionIfHaveNot() { 43 | syncObjects.forEach { createSubscriptionInPublicDatabase(on: $0) } 44 | } 45 | 46 | func startObservingTermination() { 47 | #if os(iOS) || os(tvOS) 48 | 49 | NotificationCenter.default.addObserver(self, selector: #selector(self.cleanUp), name: UIApplication.willTerminateNotification, object: nil) 50 | 51 | #elseif os(macOS) 52 | 53 | NotificationCenter.default.addObserver(self, selector: #selector(self.cleanUp), name: NSApplication.willTerminateNotification, object: nil) 54 | 55 | #endif 56 | } 57 | 58 | func registerLocalDatabase() { 59 | syncObjects.forEach { object in 60 | DispatchQueue.main.async { 61 | object.registerLocalDatabase() 62 | } 63 | } 64 | } 65 | 66 | // MARK: - Private Methods 67 | private func excuteQueryOperation(queryOperation: CKQueryOperation,on syncObject: Syncable, callback: ((Error?) -> Void)? = nil) { 68 | queryOperation.recordFetchedBlock = { record in 69 | syncObject.add(record: record) 70 | } 71 | 72 | queryOperation.queryCompletionBlock = { [weak self] cursor, error in 73 | guard let self = self else { return } 74 | if let cursor = cursor { 75 | let subsequentQueryOperation = CKQueryOperation(cursor: cursor) 76 | self.excuteQueryOperation(queryOperation: subsequentQueryOperation, on: syncObject, callback: callback) 77 | return 78 | } 79 | switch ErrorHandler.shared.resultType(with: error) { 80 | case .success: 81 | DispatchQueue.main.async { 82 | callback?(nil) 83 | } 84 | case .retry(let timeToWait, _): 85 | ErrorHandler.shared.retryOperationIfPossible(retryAfter: timeToWait, block: { 86 | self.excuteQueryOperation(queryOperation: queryOperation, on: syncObject, callback: callback) 87 | }) 88 | default: 89 | break 90 | } 91 | } 92 | 93 | database.add(queryOperation) 94 | } 95 | 96 | private func createSubscriptionInPublicDatabase(on syncObject: Syncable) { 97 | #if os(iOS) || os(tvOS) || os(macOS) 98 | let predict = NSPredicate(value: true) 99 | let subscription = CKQuerySubscription(recordType: syncObject.recordType, predicate: predict, subscriptionID: IceCreamSubscription.cloudKitPublicDatabaseSubscriptionID.id, options: [CKQuerySubscription.Options.firesOnRecordCreation, CKQuerySubscription.Options.firesOnRecordUpdate, CKQuerySubscription.Options.firesOnRecordDeletion]) 100 | 101 | let notificationInfo = CKSubscription.NotificationInfo() 102 | notificationInfo.shouldSendContentAvailable = true // Silent Push 103 | 104 | subscription.notificationInfo = notificationInfo 105 | 106 | let createOp = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) 107 | createOp.modifySubscriptionsCompletionBlock = { _, _, _ in 108 | 109 | } 110 | createOp.qualityOfService = .utility 111 | database.add(createOp) 112 | #endif 113 | } 114 | 115 | @objc func cleanUp() { 116 | for syncObject in syncObjects { 117 | syncObject.cleanUp() 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /IceCream/Classes/SyncEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncEngine.swift 3 | // IceCream 4 | // 5 | // Created by 蔡越 on 08/11/2017. 6 | // 7 | 8 | import CloudKit 9 | 10 | /// SyncEngine talks to CloudKit directly. 11 | /// Logically, 12 | /// 1. it takes care of the operations of **CKDatabase** 13 | /// 2. it handles all of the CloudKit config stuffs, such as subscriptions 14 | /// 3. it hands over CKRecordZone stuffs to SyncObject so that it can have an effect on local Realm Database 15 | 16 | public final class SyncEngine { 17 | 18 | private let databaseManager: DatabaseManager 19 | 20 | public convenience init(objects: [Syncable], databaseScope: CKDatabase.Scope = .private, container: CKContainer = .default()) { 21 | switch databaseScope { 22 | case .private: 23 | let privateDatabaseManager = PrivateDatabaseManager(objects: objects, container: container) 24 | self.init(databaseManager: privateDatabaseManager) 25 | case .public: 26 | let publicDatabaseManager = PublicDatabaseManager(objects: objects, container: container) 27 | self.init(databaseManager: publicDatabaseManager) 28 | default: 29 | fatalError("Shared database scope is not supported yet") 30 | } 31 | } 32 | 33 | private init(databaseManager: DatabaseManager) { 34 | self.databaseManager = databaseManager 35 | setup() 36 | } 37 | 38 | private func setup() { 39 | databaseManager.prepare() 40 | databaseManager.container.accountStatus { [weak self] (status, error) in 41 | guard let self = self else { return } 42 | switch status { 43 | case .available: 44 | self.databaseManager.registerLocalDatabase() 45 | self.databaseManager.createCustomZonesIfAllowed() 46 | self.databaseManager.fetchChangesInDatabase(nil) 47 | self.databaseManager.resumeLongLivedOperationIfPossible() 48 | self.databaseManager.startObservingRemoteChanges() 49 | self.databaseManager.startObservingTermination() 50 | self.databaseManager.createDatabaseSubscriptionIfHaveNot() 51 | case .noAccount, .restricted: 52 | guard self.databaseManager is PublicDatabaseManager else { break } 53 | self.databaseManager.fetchChangesInDatabase(nil) 54 | self.databaseManager.resumeLongLivedOperationIfPossible() 55 | self.databaseManager.startObservingRemoteChanges() 56 | self.databaseManager.startObservingTermination() 57 | self.databaseManager.createDatabaseSubscriptionIfHaveNot() 58 | case .couldNotDetermine: 59 | break 60 | @unknown default: 61 | break 62 | } 63 | } 64 | } 65 | 66 | } 67 | 68 | // MARK: Public Method 69 | extension SyncEngine { 70 | 71 | /// Fetch data on the CloudKit and merge with local 72 | /// 73 | /// - Parameter completionHandler: Supported in the `privateCloudDatabase` when the fetch data process completes, completionHandler will be called. The error will be returned when anything wrong happens. Otherwise the error will be `nil`. 74 | public func pull(completionHandler: ((Error?) -> Void)? = nil) { 75 | databaseManager.fetchChangesInDatabase(completionHandler) 76 | } 77 | 78 | /// Push all existing local data to CloudKit 79 | /// You should NOT to call this method too frequently 80 | public func pushAll() { 81 | databaseManager.syncObjects.forEach { $0.pushLocalObjectsToCloudKit() } 82 | } 83 | 84 | } 85 | 86 | public enum Notifications: String, NotificationName { 87 | case cloudKitDataDidChangeRemotely 88 | } 89 | 90 | public enum IceCreamKey: String { 91 | /// Tokens 92 | case databaseChangesTokenKey 93 | case zoneChangesTokenKey 94 | 95 | /// Flags 96 | case subscriptionIsLocallyCachedKey 97 | case hasCustomZoneCreatedKey 98 | 99 | var value: String { 100 | return "icecream.keys." + rawValue 101 | } 102 | } 103 | 104 | /// Dangerous part: 105 | /// In most cases, you should not change the string value cause it is related to user settings. 106 | /// e.g.: the cloudKitSubscriptionID, if you don't want to use "private_changes" and use another string. You should remove the old subsription first. 107 | /// Or your user will not save the same subscription again. So you got trouble. 108 | /// The right way is remove old subscription first and then save new subscription. 109 | public enum IceCreamSubscription: String, CaseIterable { 110 | case cloudKitPrivateDatabaseSubscriptionID = "private_changes" 111 | case cloudKitPublicDatabaseSubscriptionID = "cloudKitPublicDatabaseSubcriptionID" 112 | 113 | var id: String { 114 | return rawValue 115 | } 116 | 117 | public static var allIDs: [String] { 118 | return IceCreamSubscription.allCases.map { $0.rawValue } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Example/IceCream_Example/OwnersViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DevelopersViewController.swift 3 | // IceCream_Example 4 | // 5 | // Created by Soledad on 2019/4/13. 6 | // Copyright © 2019 蔡越. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | import RxRealm 12 | import RxSwift 13 | 14 | final class OwnersViewController: UIViewController { 15 | 16 | var owners: [Person] = [] 17 | let bag = DisposeBag() 18 | 19 | let realm = try! Realm() 20 | 21 | private lazy var addBarItem: UIBarButtonItem = { 22 | let b = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add, target: self, action: #selector(add)) 23 | return b 24 | }() 25 | 26 | private lazy var tableView: UITableView = { 27 | let tv = UITableView() 28 | tv.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 29 | tv.delegate = self 30 | tv.dataSource = self 31 | return tv 32 | }() 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | view.addSubview(tableView) 38 | navigationItem.rightBarButtonItem = addBarItem 39 | 40 | bind() 41 | } 42 | 43 | override func viewWillLayoutSubviews() { 44 | super.viewWillLayoutSubviews() 45 | tableView.frame = view.frame 46 | } 47 | 48 | private func bind() { 49 | let realm = try! Realm() 50 | 51 | /// Results instances are live, auto-updating views into the underlying data, which means results never have to be re-fetched. 52 | /// https://realm.io/docs/swift/latest/#objects-with-primary-keys 53 | let owners = realm.objects(Person.self) 54 | 55 | Observable.array(from: owners).subscribe(onNext: { (owners) in 56 | /// When developers data changes in Realm, the following code will be executed 57 | /// It works like magic. 58 | self.owners = owners.filter { !$0.isDeleted } 59 | self.tableView.reloadData() 60 | }).disposed(by: bag) 61 | } 62 | 63 | @objc private func add() { 64 | let user = Person() 65 | user.name = "Yue Cai" 66 | 67 | try! realm.write { 68 | realm.add(user) 69 | } 70 | } 71 | } 72 | 73 | extension OwnersViewController: UITableViewDelegate { 74 | 75 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 76 | guard indexPath.row < owners.count else { return } 77 | let owner = owners[indexPath.row] 78 | let viewController = OwnerDetailViewController(cats: Array(owner.cats).filter { !$0.isDeleted }) 79 | navigationController?.pushViewController(viewController, animated: true) 80 | } 81 | 82 | func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { 83 | let addCatAction = UITableViewRowAction(style: .default, title: "Add cat") { (_, ip) in 84 | guard ip.row < self.owners.count else { return } 85 | let owner = self.owners[ip.row] 86 | let newCat = Cat() 87 | newCat.name = "\(owner.name)'s No.\(owner.cats.count + 1) cat" 88 | newCat.age = ip.row 89 | try! self.realm.write { 90 | owner.cats.append(newCat) 91 | } 92 | } 93 | let deleteCatAction = UITableViewRowAction(style: .default, title: "Delete cat") { (_, ip) in 94 | guard ip.row < self.owners.count else { return } 95 | let owner = self.owners[ip.row] 96 | try! self.realm.write { 97 | owner.cats.last?.isDeleted = true 98 | } 99 | } 100 | let deleteAction = UITableViewRowAction(style: .destructive, title: "Delete") { (_, ip) in 101 | let alert = UIAlertController(title: NSLocalizedString("caution", comment: "caution"), message: NSLocalizedString("sure_to_delete", comment: "sure_to_delete"), preferredStyle: .alert) 102 | let deleteAction = UIAlertAction(title: NSLocalizedString("delete", comment: "delete"), style: .destructive, handler: { (action) in 103 | guard ip.row < self.owners.count else { return } 104 | let owner = self.owners[ip.row] 105 | try! self.realm.write { 106 | owner.isDeleted = true 107 | } 108 | }) 109 | let defaultAction = UIAlertAction(title: NSLocalizedString("cancel", comment: "cancel"), style: .default, handler: nil) 110 | alert.addAction(defaultAction) 111 | alert.addAction(deleteAction) 112 | self.present(alert, animated: true, completion: nil) 113 | } 114 | return [addCatAction, deleteCatAction, deleteAction] 115 | } 116 | } 117 | 118 | extension OwnersViewController: UITableViewDataSource { 119 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 120 | return owners.count 121 | } 122 | 123 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 124 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell") 125 | cell?.textLabel?.text = owners[indexPath.row].name 126 | return cell ?? UITableViewCell() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Example/IceCream_Example/CatsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CatsViewController.swift 3 | // IceCream_Example 4 | // 5 | // Created by 蔡越 on 22/05/2018. 6 | // Copyright © 2018 蔡越. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | import IceCream 12 | import RxRealm 13 | import RxSwift 14 | 15 | final class CatsViewController: UIViewController { 16 | 17 | private var cats: [Cat] = [] 18 | private let bag = DisposeBag() 19 | 20 | private let realm = try! Realm() 21 | 22 | private lazy var addBarItem: UIBarButtonItem = { 23 | let b = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add, target: self, action: #selector(add)) 24 | return b 25 | }() 26 | 27 | private lazy var tableView: UITableView = { 28 | let tv = UITableView() 29 | tv.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 30 | tv.delegate = self 31 | tv.dataSource = self 32 | return tv 33 | }() 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | 38 | view.addSubview(tableView) 39 | navigationItem.rightBarButtonItem = addBarItem 40 | 41 | bind() 42 | } 43 | 44 | override func viewWillLayoutSubviews() { 45 | super.viewWillLayoutSubviews() 46 | tableView.frame = view.frame 47 | } 48 | 49 | private func bind() { 50 | let realm = try! Realm() 51 | 52 | /// Results instances are live, auto-updating views into the underlying data, which means results never have to be re-fetched. 53 | /// https://realm.io/docs/swift/latest/#objects-with-primary-keys 54 | let cats = realm.objects(Cat.self) 55 | 56 | Observable.array(from: cats).subscribe(onNext: { (cats) in 57 | /// When cats data changes in Realm, the following code will be executed 58 | /// It works like magic. 59 | self.cats = cats.filter{ !$0.isDeleted } 60 | self.tableView.reloadData() 61 | }).disposed(by: bag) 62 | } 63 | 64 | @objc private func add() { 65 | let cat = Cat() 66 | cat.name = "Cat Number " + "\(cats.count)" 67 | cat.age = cats.count + 1 68 | 69 | let data = UIImage(named: cat.age % 2 == 1 ? "heart_cat" : "dull_cat")!.jpegData(compressionQuality: 1.0) 70 | cat.avatar = CreamAsset.create(object: cat, propName: Cat.AVATAR_KEY, data: data!) 71 | 72 | try! realm.write { 73 | realm.add(cat) 74 | } 75 | } 76 | 77 | } 78 | 79 | extension CatsViewController: UITableViewDelegate { 80 | func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { 81 | let deleteAction = UITableViewRowAction(style: .destructive, title: "Delete") { (_, ip) in 82 | let alert = UIAlertController(title: NSLocalizedString("caution", comment: "caution"), message: NSLocalizedString("sure_to_delete", comment: "sure_to_delete"), preferredStyle: .alert) 83 | let deleteAction = UIAlertAction(title: NSLocalizedString("delete", comment: "delete"), style: .destructive, handler: { (action) in 84 | guard ip.row < self.cats.count else { return } 85 | let cat = self.cats[ip.row] 86 | try! self.realm.write { 87 | cat.isDeleted = true 88 | } 89 | }) 90 | let defaultAction = UIAlertAction(title: NSLocalizedString("cancel", comment: "cancel"), style: .default, handler: nil) 91 | alert.addAction(defaultAction) 92 | alert.addAction(deleteAction) 93 | self.present(alert, animated: true, completion: nil) 94 | } 95 | 96 | let archiveAction = UITableViewRowAction(style: .normal, title: "Plus") { [weak self](_, ip) in 97 | guard let `self` = self else { return } 98 | guard ip.row < `self`.cats.count else { return } 99 | let cat = `self`.cats[ip.row] 100 | try! `self`.realm.write { 101 | cat.age += 1 102 | } 103 | } 104 | let changeImageAction = UITableViewRowAction(style: .normal, title: "Change Img") { [weak self](_, ip) in 105 | guard let `self` = self else { return } 106 | guard ip.row < `self`.cats.count else { return } 107 | let cat = `self`.cats[ip.row] 108 | try! `self`.realm.write { 109 | if let imageData = UIImage(named: cat.age % 2 == 0 ? "heart_cat" : "dull_cat")!.jpegData(compressionQuality: 1.0) { 110 | cat.avatar = CreamAsset.create(object: cat, propName: Cat.AVATAR_KEY, data: imageData) 111 | } 112 | } 113 | } 114 | changeImageAction.backgroundColor = .blue 115 | let emptyImageAction = UITableViewRowAction(style: .normal, title: "Nil Img") { [weak self](_, ip) in 116 | guard let `self` = self else { return } 117 | guard ip.row < `self`.cats.count else { return } 118 | let cat = `self`.cats[ip.row] 119 | try! `self`.realm.write { 120 | cat.avatar = nil 121 | } 122 | } 123 | emptyImageAction.backgroundColor = .purple 124 | return [deleteAction, archiveAction, changeImageAction, emptyImageAction] 125 | } 126 | } 127 | 128 | extension CatsViewController: UITableViewDataSource { 129 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 130 | return cats.count 131 | } 132 | 133 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 134 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell") 135 | cell?.textLabel?.text = cats[indexPath.row].name + " Age: \(cats[indexPath.row].age)" 136 | if let data = cats[indexPath.row].avatar?.storedData() { 137 | cell?.imageView?.image = UIImage(data: data) 138 | } else { 139 | cell?.imageView?.image = UIImage(named: "cat_placeholder") 140 | } 141 | return cell ?? UITableViewCell() 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Example/IceCream_Example/DogsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // IceCream 4 | // 5 | // Created by 蔡越 on 10/17/2017. 6 | // Copyright (c) 2017 Nanjing University. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | import IceCream 12 | import RxRealm 13 | import RxSwift 14 | 15 | final class DogsViewController: UIViewController { 16 | 17 | private let jim = Person() 18 | private var dogs: [Dog] = [] 19 | private let bag = DisposeBag() 20 | 21 | private let realm = try! Realm() 22 | 23 | private lazy var addBarItem: UIBarButtonItem = { 24 | let b = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add, target: self, action: #selector(add)) 25 | return b 26 | }() 27 | 28 | private lazy var tableView: UITableView = { 29 | let tv = UITableView() 30 | tv.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 31 | tv.delegate = self 32 | tv.dataSource = self 33 | return tv 34 | }() 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | view.addSubview(tableView) 40 | navigationItem.rightBarButtonItem = addBarItem 41 | 42 | bind() 43 | } 44 | 45 | override func viewWillLayoutSubviews() { 46 | super.viewWillLayoutSubviews() 47 | tableView.frame = view.frame 48 | } 49 | 50 | private func bind() { 51 | let realm = try! Realm() 52 | 53 | /// Results instances are live, auto-updating views into the underlying data, which means results never have to be re-fetched. 54 | /// https://realm.io/docs/swift/latest/#objects-with-primary-keys 55 | let dogs = realm.objects(Dog.self) 56 | 57 | Observable.array(from: dogs).subscribe(onNext: { (dogs) in 58 | /// When dogs data changes in Realm, the following code will be executed 59 | /// It works like magic. 60 | self.dogs = dogs.filter{ !$0.isDeleted } 61 | self.tableView.reloadData() 62 | }).disposed(by: bag) 63 | } 64 | 65 | @objc private func add() { 66 | let dog = Dog() 67 | dog.name = "Dog Number " + "\(dogs.count)" 68 | dog.age = dogs.count + 1 69 | dog.owner = jim 70 | 71 | let data = UIImage(named: dog.age % 2 == 1 ? "smile_dog" : "tongue_dog")!.jpegData(compressionQuality: 1.0) 72 | dog.avatar = CreamAsset.create(object: dog, propName: Dog.AVATAR_KEY, data: data!) 73 | try! realm.write { 74 | realm.add(dog) 75 | } 76 | } 77 | } 78 | 79 | extension DogsViewController: UITableViewDelegate { 80 | func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { 81 | let deleteAction = UITableViewRowAction(style: .destructive, title: "Delete") { (_, ip) in 82 | let alert = UIAlertController(title: NSLocalizedString("caution", comment: "caution"), message: NSLocalizedString("sure_to_delete", comment: "sure_to_delete"), preferredStyle: .alert) 83 | let deleteAction = UIAlertAction(title: NSLocalizedString("delete", comment: "delete"), style: .destructive, handler: { (action) in 84 | guard ip.row < self.dogs.count else { return } 85 | let dog = self.dogs[ip.row] 86 | try! self.realm.write { 87 | dog.isDeleted = true 88 | } 89 | }) 90 | let defaultAction = UIAlertAction(title: NSLocalizedString("cancel", comment: "cancel"), style: .default, handler: nil) 91 | alert.addAction(defaultAction) 92 | alert.addAction(deleteAction) 93 | self.present(alert, animated: true, completion: nil) 94 | } 95 | 96 | let archiveAction = UITableViewRowAction(style: .normal, title: "Plus") { [weak self](_, ip) in 97 | guard let `self` = self else { return } 98 | guard ip.row < `self`.dogs.count else { return } 99 | let dog = `self`.dogs[ip.row] 100 | try! `self`.realm.write { 101 | dog.age += 1 102 | } 103 | } 104 | let changeImageAction = UITableViewRowAction(style: .normal, title: "Change Img") { [weak self](_, ip) in 105 | guard let `self` = self else { return } 106 | guard ip.row < `self`.dogs.count else { return } 107 | let dog = `self`.dogs[ip.row] 108 | try! `self`.realm.write { 109 | if let imageData = UIImage(named: dog.age % 2 == 0 ? "smile_dog" : "tongue_dog")!.jpegData(compressionQuality: 1.0) { 110 | dog.avatar = CreamAsset.create(object: dog, propName: Dog.AVATAR_KEY, data: imageData) 111 | } 112 | } 113 | } 114 | changeImageAction.backgroundColor = .blue 115 | let emptyImageAction = UITableViewRowAction(style: .normal, title: "Nil Img") { [weak self](_, ip) in 116 | guard let `self` = self else { return } 117 | guard ip.row < `self`.dogs.count else { return } 118 | let dog = `self`.dogs[ip.row] 119 | try! `self`.realm.write { 120 | dog.avatar = nil 121 | } 122 | } 123 | emptyImageAction.backgroundColor = .purple 124 | return [deleteAction, archiveAction, changeImageAction, emptyImageAction] 125 | } 126 | } 127 | 128 | extension DogsViewController: UITableViewDataSource { 129 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 130 | return dogs.count 131 | } 132 | 133 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 134 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell") 135 | cell?.textLabel?.text = dogs[indexPath.row].name + "\(dogs[indexPath.row].age)" + "owner: \(dogs[indexPath.row].owner?.name)" 136 | if let data = dogs[indexPath.row].avatar?.storedData() { 137 | cell?.imageView?.image = UIImage(data: data) 138 | } else { 139 | cell?.imageView?.image = UIImage(named: "dog_placeholder") 140 | } 141 | return cell ?? UITableViewCell() 142 | } 143 | 144 | } 145 | 146 | -------------------------------------------------------------------------------- /IceCream/Classes/DatabaseManagerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseManager.swift 3 | // IceCream 4 | // 5 | // Created by caiyue on 2019/4/22. 6 | // 7 | 8 | import CloudKit 9 | 10 | protocol DatabaseManager: class { 11 | 12 | /// A conduit for accessing and performing operations on the data of an app container. 13 | var database: CKDatabase { get } 14 | 15 | /// An encapsulation of content associated with an app. 16 | var container: CKContainer { get } 17 | 18 | var syncObjects: [Syncable] { get } 19 | 20 | init(objects: [Syncable], container: CKContainer) 21 | 22 | func prepare() 23 | 24 | func fetchChangesInDatabase(_ callback: ((Error?) -> Void)?) 25 | 26 | /// The CloudKit Best Practice is out of date, now use this: 27 | /// https://developer.apple.com/documentation/cloudkit/ckoperation 28 | /// Which problem does this func solve? E.g.: 29 | /// 1.(Offline) You make a local change, involve a operation 30 | /// 2. App exits or ejected by user 31 | /// 3. Back to app again 32 | /// The operation resumes! All works like a magic! 33 | func resumeLongLivedOperationIfPossible() 34 | 35 | func createCustomZonesIfAllowed() 36 | func startObservingRemoteChanges() 37 | func startObservingTermination() 38 | func createDatabaseSubscriptionIfHaveNot() 39 | func registerLocalDatabase() 40 | 41 | func cleanUp() 42 | } 43 | 44 | extension DatabaseManager { 45 | 46 | func prepare() { 47 | syncObjects.forEach { 48 | $0.pipeToEngine = { [weak self] recordsToStore, recordIDsToDelete in 49 | guard let self = self else { return } 50 | self.syncRecordsToCloudKit(recordsToStore: recordsToStore, recordIDsToDelete: recordIDsToDelete) 51 | } 52 | } 53 | } 54 | 55 | func resumeLongLivedOperationIfPossible() { 56 | container.fetchAllLongLivedOperationIDs { [weak self]( opeIDs, error) in 57 | guard let self = self, error == nil, let ids = opeIDs else { return } 58 | for id in ids { 59 | self.container.fetchLongLivedOperation(withID: id, completionHandler: { [weak self](ope, error) in 60 | guard let self = self, error == nil else { return } 61 | if let modifyOp = ope as? CKModifyRecordsOperation { 62 | modifyOp.modifyRecordsCompletionBlock = { (_,_,_) in 63 | print("Resume modify records success!") 64 | } 65 | // The Apple's example code in doc(https://developer.apple.com/documentation/cloudkit/ckoperation/#1666033) 66 | // tells we add operation in container. But however it crashes on iOS 15 beta versions. 67 | // And the crash log tells us to "CKDatabaseOperations must be submitted to a CKDatabase". 68 | // So I guess there must be something changed in the daemon. We temperorily add this availabilty check. 69 | if #available(iOS 15, *) { 70 | self.database.add(modifyOp) 71 | } else { 72 | self.container.add(modifyOp) 73 | } 74 | } 75 | }) 76 | } 77 | } 78 | } 79 | 80 | func startObservingRemoteChanges() { 81 | NotificationCenter.default.addObserver(forName: Notifications.cloudKitDataDidChangeRemotely.name, object: nil, queue: nil, using: { [weak self](_) in 82 | guard let self = self else { return } 83 | DispatchQueue.global(qos: .utility).async { 84 | self.fetchChangesInDatabase(nil) 85 | } 86 | }) 87 | } 88 | 89 | /// Sync local data to CloudKit 90 | /// For more about the savePolicy: https://developer.apple.com/documentation/cloudkit/ckrecordsavepolicy 91 | public func syncRecordsToCloudKit(recordsToStore: [CKRecord], recordIDsToDelete: [CKRecord.ID], completion: ((Error?) -> ())? = nil) { 92 | let modifyOpe = CKModifyRecordsOperation(recordsToSave: recordsToStore, recordIDsToDelete: recordIDsToDelete) 93 | 94 | if #available(iOS 11.0, OSX 10.13, tvOS 11.0, watchOS 4.0, *) { 95 | let config = CKOperation.Configuration() 96 | config.isLongLived = true 97 | modifyOpe.configuration = config 98 | } else { 99 | // Fallback on earlier versions 100 | modifyOpe.isLongLived = true 101 | } 102 | 103 | // We use .changedKeys savePolicy to do unlocked changes here cause my app is contentious and off-line first 104 | // Apple suggests using .ifServerRecordUnchanged save policy 105 | // For more, see Advanced CloudKit(https://developer.apple.com/videos/play/wwdc2014/231/) 106 | modifyOpe.savePolicy = .changedKeys 107 | 108 | // To avoid CKError.partialFailure, make the operation atomic (if one record fails to get modified, they all fail) 109 | // If you want to handle partial failures, set .isAtomic to false and implement CKOperationResultType .fail(reason: .partialFailure) where appropriate 110 | modifyOpe.isAtomic = true 111 | 112 | modifyOpe.modifyRecordsCompletionBlock = { 113 | [weak self] 114 | (_, _, error) in 115 | 116 | guard let self = self else { return } 117 | 118 | switch ErrorHandler.shared.resultType(with: error) { 119 | case .success: 120 | DispatchQueue.main.async { 121 | completion?(nil) 122 | } 123 | case .retry(let timeToWait, _): 124 | ErrorHandler.shared.retryOperationIfPossible(retryAfter: timeToWait) { 125 | self.syncRecordsToCloudKit(recordsToStore: recordsToStore, recordIDsToDelete: recordIDsToDelete, completion: completion) 126 | } 127 | case .chunk: 128 | /// CloudKit says maximum number of items in a single request is 400. 129 | /// So I think 300 should be fine by them. 130 | let chunkedRecords = recordsToStore.chunkItUp(by: 300) 131 | for chunk in chunkedRecords { 132 | self.syncRecordsToCloudKit(recordsToStore: chunk, recordIDsToDelete: recordIDsToDelete, completion: completion) 133 | } 134 | default: 135 | return 136 | } 137 | } 138 | 139 | database.add(modifyOpe) 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.12.0 - Background Worker](https://github.com/caiyue1993/IceCream/releases/tag/1.12.0) 4 | 5 | #### Add 6 | 7 | * Implement background synchronization [#155](https://github.com/caiyue1993/IceCream/pull/155) 8 | 9 | #### Fix 10 | 11 | * Fix primaryKey wrongly convertion issue [#165](https://github.com/caiyue1993/IceCream/pull/165) 12 | 13 | ## [1.11.0 - You Want This Completion Handler](https://github.com/caiyue1993/IceCream/releases/tag/1.11.0) 14 | 15 | #### Add 16 | 17 | * Add a completionHandler in the pull method [#141](https://github.com/caiyue1993/IceCream/pull/141) 18 | 19 | ## [1.10.1](https://github.com/caiyue1993/IceCream/releases/tag/1.10.1) 20 | 21 | #### Fix 22 | 23 | * Fix Carthage build failing issue on macOS, watchOS and tvOS 24 | 25 | ## [1.10.0 - Swift 5](https://github.com/caiyue1993/IceCream/releases/tag/1.10.0) 26 | 27 | #### Add 28 | 29 | * Now IceCream builds against Swift 5.0 and Xcode 10.2. 30 | 31 | ## [1.9.0 - Make It Public](https://github.com/caiyue1993/IceCream/releases/tag/1.9.0) 32 | 33 | #### Add 34 | 35 | * Add support for public database [#124](https://github.com/caiyue1993/IceCream/pull/124) 36 | 37 | ## [1.8.0 - Customization](https://github.com/caiyue1993/IceCream/releases/tag/1.8.0) 38 | 39 | #### Add 40 | 41 | * Add a option to let developer choose whether to overwrite existing CreamAsset file(the default choice is `true`) [#103](https://github.com/caiyue1993/IceCream/pull/103) 42 | * Add support for custom CKContainers [#104](https://github.com/caiyue1993/IceCream/pull/104) 43 | * Add support for custom Realm [#108](https://github.com/caiyue1993/IceCream/pull/108) 44 | 45 | #### Fix 46 | 47 | * Fix the conversion issue of recordID to Int type primaryKey [#111](https://github.com/caiyue1993/IceCream/pull/111) 48 | 49 | ## [1.7.2 - Realm List of Basic Types](https://github.com/caiyue1993/IceCream/releases/tag/1.7.2) 50 | 51 | #### Add 52 | 53 | * Add support for Lists of basic types [#98](https://github.com/caiyue1993/IceCream/pull/98) 54 | 55 | #### Fix 56 | 57 | * Fix a crash when new no-optional property added [#92](https://github.com/caiyue1993/IceCream/pull/92) 58 | * Avoid force unwrapping `storedData` [#101](https://github.com/caiyue1993/IceCream/pull/101) 59 | 60 | ## [1.7.1 - Optimizations](https://github.com/caiyue1993/IceCream/releases/tag/1.7.1) 61 | 62 | #### Add 63 | 64 | * Add `pushAll` method. 65 | * change method name `sync` to `pull`. 66 | 67 | #### Fix 68 | 69 | * Fetch changes on the non-main thread. 70 | * Move registerLocalDatabase to completion block of createCustomZones. 71 | * Fix `isCustomZoneCreated` setter logic. 72 | * More Swift 4.2 and optimize code style. 73 | 74 | ----- 75 | ## 1.7.0 - Swift 4.2 76 | 77 | #### Add 78 | 79 | * Xcode 10 / Swift 4.2 support 80 | 81 | ----- 82 | ## [1.6.0 - Get me on every Apple platform](https://github.com/caiyue1993/IceCream/releases/tag/1.6.0) 83 | 84 | #### Add 85 | 86 | * Adding support for macOS, tvOS and watchOS. [#79](https://github.com/caiyue1993/IceCream/pull/79),[#85](https://github.com/caiyue1993/IceCream/pull/85) 87 | 88 | ----- 89 | ## [1.5.0 - Dog has an Owner](https://github.com/caiyue1993/IceCream/releases/tag/1.5.0) 90 | 91 | #### Add 92 | 93 | * Many-to-one relationship support. [#74](https://github.com/caiyue1993/IceCream/pull/74) 94 | 95 | #### Fix 96 | 97 | * Carthage nested dependency issue. [#71](https://github.com/caiyue1993/IceCream/pull/71) 98 | 99 | ----- 100 | ## [1.4.2](https://github.com/caiyue1993/IceCream/releases/tag/1.4.2) 101 | 102 | #### Add 103 | 104 | * Expose file path in CreamAsset. [#66](https://github.com/caiyue1993/IceCream/pull/66) 105 | 106 | ----- 107 | ## [1.4.1](https://github.com/caiyue1993/IceCream/releases/tag/1.4.1) 108 | 109 | #### Fix 110 | 111 | * Fix the folder file issue. [#60](https://github.com/caiyue1993/IceCream/pull/60) 112 | 113 | ----- 114 | ## [1.4.0 - Dogs and Cats](https://github.com/caiyue1993/IceCream/releases/tag/1.4.0) 115 | 116 | #### Add 117 | 118 | * Multiple object models support. [#55](https://github.com/caiyue1993/IceCream/pull/55) 119 | 120 | ----- 121 | ## [1.3.3](https://github.com/caiyue1993/IceCream/releases/tag/1.3.3) 122 | 123 | #### Fix 124 | 125 | * Change the deployment target via Carthage. [#50](https://github.com/caiyue1993/IceCream/pull/50) 126 | 127 | ----- 128 | ## [1.3.2 - Faster](https://github.com/caiyue1993/IceCream/releases/tag/1.3.2) 129 | 130 | #### Fix 131 | 132 | * Static Framework Support. [#47](https://github.com/caiyue1993/IceCream/pull/47) 133 | 134 | ----- 135 | ## [1.3.1 - Lean Code](https://github.com/caiyue1993/IceCream/releases/tag/1.3.1) 136 | 137 | #### Fix 138 | 139 | * Use where clause to refactor code. [#46](https://github.com/caiyue1993/IceCream/pull/46) 140 | 141 | ----- 142 | ## [1.3.0 - Decentralized is the Future](https://github.com/caiyue1993/IceCream/releases/tag/1.3.0) 143 | 144 | #### Add 145 | 146 | * Support Carthage. [#34](https://github.com/caiyue1993/IceCream/pull/34) 147 | 148 | #### Fix 149 | 150 | * CreamAsset behaves better. [#32](https://github.com/caiyue1993/IceCream/pull/32) 151 | 152 | ----- 153 | ## [1.2.1 - Make yourself at home](https://github.com/caiyue1993/IceCream/releases/tag/1.2.1) 154 | 155 | #### Add 156 | 157 | * Log or not log. It's up to you. [#23](https://github.com/caiyue1993/IceCream/issues/23) 158 | 159 | ----- 160 | ## [1.2.0 - Colorful World](https://github.com/caiyue1993/IceCream/releases/tag/1.2.0) 161 | 162 | #### Add 163 | 164 | * CKAsset Support. [#24](https://github.com/caiyue1993/IceCream/pull/24) 165 | 166 | #### Fix 167 | 168 | * Make Error Handler perfect. [26](https://github.com/caiyue1993/IceCream/pull/26) 169 | 170 | ----- 171 | 172 | ## [1.1.0 - Error Handler Matters](https://github.com/caiyue1993/IceCream/releases/tag/1.1.0) 173 | 174 | #### Add 175 | 176 | * A powerful Error Handler. [#15](https://github.com/caiyue1993/IceCream/pull/15). 177 | 178 | ----- 179 | 180 | ## [1.0.0 - Dressed Up!](https://github.com/caiyue1993/IceCream/releases/tag/1.0.0) 181 | 182 | #### Fix 183 | 184 | * Subscription bug. [#12](https://github.com/caiyue1993/IceCream/pull/12). 185 | 186 | * Bye bye, magic strings. [#11](https://github.com/caiyue1993/IceCream/pull/11) 187 | 188 | --- 189 | 190 | ## [0.2.0 - One line of code, all settled](https://github.com/caiyue1993/IceCream/releases/tag/0.2.0) 191 | 192 | #### Fix 193 | 194 | * Using protocol extensions to refactor code. Now users just need to add one line of code to use IceCream. [#2](https://github.com/caiyue1993/IceCream/issues/2) 195 | 196 | --- 197 | 198 | ## [0.1.1](https://github.com/caiyue1993/IceCream/releases/tag/0.1.1) 199 | 200 | #### Add 201 | 202 | * Swift version assigned. 203 | 204 | --- 205 | 206 | ## [0.1.0 - The world gonna be mine!](https://github.com/caiyue1993/IceCream/releases/tag/0.1.0) 207 | 208 | IceCream was born! -------------------------------------------------------------------------------- /IceCream/Classes/SyncObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncSource.swift 3 | // IceCream 4 | // 5 | // Created by David Collado on 1/5/18. 6 | // 7 | 8 | import Foundation 9 | import RealmSwift 10 | import CloudKit 11 | 12 | /// SyncObject is for each model you want to sync. 13 | /// Logically, 14 | /// 1. it takes care of the operations of CKRecordZone. 15 | /// 2. it detects the changeSets of Realm Database and directly talks to it. 16 | /// 3. it hands over to SyncEngine so that it can talk to CloudKit. 17 | 18 | public final class SyncObject where T: Object & CKRecordConvertible & CKRecordRecoverable, U: Object, V: Object, W: Object { 19 | 20 | /// Notifications are delivered as long as a reference is held to the returned notification token. We should keep a strong reference to this token on the class registering for updates, as notifications are automatically unregistered when the notification token is deallocated. 21 | /// For more, reference is here: https://realm.io/docs/swift/latest/#notifications 22 | private var notificationToken: NotificationToken? 23 | 24 | public var pipeToEngine: ((_ recordsToStore: [CKRecord], _ recordIDsToDelete: [CKRecord.ID]) -> ())? 25 | 26 | public let realmConfiguration: Realm.Configuration 27 | 28 | private let pendingUTypeRelationshipsWorker = PendingRelationshipsWorker() 29 | private let pendingVTypeRelationshipsWorker = PendingRelationshipsWorker() 30 | private let pendingWTypeRelationshipsWorker = PendingRelationshipsWorker() 31 | 32 | public init( 33 | realmConfiguration: Realm.Configuration = .defaultConfiguration, 34 | type: T.Type, 35 | uListElementType: U.Type? = nil, 36 | vListElementType: V.Type? = nil, 37 | wListElementType: W.Type? = nil 38 | ) { 39 | self.realmConfiguration = realmConfiguration 40 | } 41 | 42 | } 43 | 44 | // MARK: - Zone information 45 | 46 | extension SyncObject: Syncable { 47 | 48 | public var recordType: String { 49 | return T.recordType 50 | } 51 | 52 | public var zoneID: CKRecordZone.ID { 53 | return T.zoneID 54 | } 55 | 56 | public var zoneChangesToken: CKServerChangeToken? { 57 | get { 58 | /// For the very first time when launching, the token will be nil and the server will be giving everything on the Cloud to client 59 | /// In other situation just get the unarchive the data object 60 | guard let tokenData = UserDefaults.standard.object(forKey: T.className() + IceCreamKey.zoneChangesTokenKey.value) as? Data else { return nil } 61 | return NSKeyedUnarchiver.unarchiveObject(with: tokenData) as? CKServerChangeToken 62 | } 63 | set { 64 | guard let n = newValue else { 65 | UserDefaults.standard.removeObject(forKey: T.className() + IceCreamKey.zoneChangesTokenKey.value) 66 | return 67 | } 68 | let data = NSKeyedArchiver.archivedData(withRootObject: n) 69 | UserDefaults.standard.set(data, forKey: T.className() + IceCreamKey.zoneChangesTokenKey.value) 70 | } 71 | } 72 | 73 | public var isCustomZoneCreated: Bool { 74 | get { 75 | guard let flag = UserDefaults.standard.object(forKey: T.className() + IceCreamKey.hasCustomZoneCreatedKey.value) as? Bool else { return false } 76 | return flag 77 | } 78 | set { 79 | UserDefaults.standard.set(newValue, forKey: T.className() + IceCreamKey.hasCustomZoneCreatedKey.value) 80 | } 81 | } 82 | 83 | public func add(record: CKRecord) { 84 | BackgroundWorker.shared.start { 85 | let realm = try! Realm(configuration: self.realmConfiguration) 86 | guard let object = T.parseFromRecord( 87 | record: record, 88 | realm: realm, 89 | notificationToken: self.notificationToken, 90 | pendingUTypeRelationshipsWorker: self.pendingUTypeRelationshipsWorker, 91 | pendingVTypeRelationshipsWorker: self.pendingVTypeRelationshipsWorker, 92 | pendingWTypeRelationshipsWorker: self.pendingWTypeRelationshipsWorker 93 | ) else { 94 | print("There is something wrong with the converson from cloud record to local object") 95 | return 96 | } 97 | self.pendingUTypeRelationshipsWorker.realm = realm 98 | self.pendingVTypeRelationshipsWorker.realm = realm 99 | self.pendingWTypeRelationshipsWorker.realm = realm 100 | 101 | /// If your model class includes a primary key, you can have Realm intelligently update or add objects based off of their primary key values using Realm().add(_:update:). 102 | /// https://realm.io/docs/swift/latest/#objects-with-primary-keys 103 | realm.beginWrite() 104 | realm.add(object, update: .modified) 105 | if let token = self.notificationToken { 106 | try! realm.commitWrite(withoutNotifying: [token]) 107 | } else { 108 | try! realm.commitWrite() 109 | } 110 | } 111 | } 112 | 113 | public func delete(recordID: CKRecord.ID) { 114 | BackgroundWorker.shared.start { 115 | let realm = try! Realm(configuration: self.realmConfiguration) 116 | guard let object = realm.object(ofType: T.self, forPrimaryKey: T.primaryKeyForRecordID(recordID: recordID)) else { 117 | // Not found in local realm database 118 | return 119 | } 120 | CreamAsset.deleteCreamAssetFile(with: recordID.recordName) 121 | realm.beginWrite() 122 | realm.delete(object) 123 | if let token = self.notificationToken { 124 | try! realm.commitWrite(withoutNotifying: [token]) 125 | } else { 126 | try! realm.commitWrite() 127 | } 128 | } 129 | } 130 | 131 | /// When you commit a write transaction to a Realm, all other instances of that Realm will be notified, and be updated automatically. 132 | /// For more: https://realm.io/docs/swift/latest/#writes 133 | public func registerLocalDatabase() { 134 | BackgroundWorker.shared.start { 135 | let realm = try! Realm(configuration: self.realmConfiguration) 136 | self.notificationToken = realm.objects(T.self).observe({ [weak self](changes) in 137 | guard let self = self else { return } 138 | switch changes { 139 | case .initial(_): 140 | break 141 | case .update(let collection, _, let insertions, let modifications): 142 | let recordsToStore = (insertions + modifications).filter { $0 < collection.count }.map { collection[$0] }.filter{ !$0.isDeleted }.map { $0.record } 143 | let recordIDsToDelete = modifications.filter { $0 < collection.count }.map { collection[$0] }.filter { $0.isDeleted }.map { $0.recordID } 144 | 145 | guard recordsToStore.count > 0 || recordIDsToDelete.count > 0 else { return } 146 | self.pipeToEngine?(recordsToStore, recordIDsToDelete) 147 | case .error(_): 148 | break 149 | } 150 | }) 151 | } 152 | } 153 | 154 | public func resolvePendingRelationships() { 155 | pendingUTypeRelationshipsWorker.resolvePendingListElements() 156 | pendingVTypeRelationshipsWorker.resolvePendingListElements() 157 | pendingWTypeRelationshipsWorker.resolvePendingListElements() 158 | } 159 | 160 | public func cleanUp() { 161 | BackgroundWorker.shared.start { 162 | let realm = try! Realm(configuration: self.realmConfiguration) 163 | let objects = realm.objects(T.self).filter { $0.isDeleted } 164 | 165 | var tokens: [NotificationToken] = [] 166 | self.notificationToken.flatMap { tokens = [$0] } 167 | 168 | realm.beginWrite() 169 | objects.forEach({ realm.delete($0) }) 170 | do { 171 | try realm.commitWrite(withoutNotifying: tokens) 172 | } catch { 173 | 174 | } 175 | } 176 | } 177 | 178 | public func pushLocalObjectsToCloudKit() { 179 | let realm = try! Realm(configuration: self.realmConfiguration) 180 | let recordsToStore: [CKRecord] = realm.objects(T.self).filter { !$0.isDeleted }.map { $0.record } 181 | pipeToEngine?(recordsToStore, []) 182 | } 183 | 184 | } 185 | 186 | -------------------------------------------------------------------------------- /IceCream/Classes/CKRecordRecoverable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CKRecordRecoverable.swift 3 | // IceCream 4 | // 5 | // Created by 蔡越 on 26/05/2018. 6 | // 7 | 8 | import CloudKit 9 | import RealmSwift 10 | 11 | public protocol CKRecordRecoverable { 12 | 13 | } 14 | 15 | extension CKRecordRecoverable where Self: Object { 16 | static func parseFromRecord( 17 | record: CKRecord, 18 | realm: Realm, 19 | notificationToken: NotificationToken?, 20 | pendingUTypeRelationshipsWorker: PendingRelationshipsWorker, 21 | pendingVTypeRelationshipsWorker: PendingRelationshipsWorker, 22 | pendingWTypeRelationshipsWorker: PendingRelationshipsWorker 23 | ) -> Self? { 24 | let o = Self() 25 | for prop in o.objectSchema.properties { 26 | var recordValue: Any? 27 | 28 | if prop.isArray { 29 | switch prop.type { 30 | case .int: 31 | guard let value = record.value(forKey: prop.name) as? [Int] else { break } 32 | let list = List() 33 | list.append(objectsIn: value) 34 | recordValue = list 35 | case .string: 36 | guard let value = record.value(forKey: prop.name) as? [String] else { break } 37 | let list = List() 38 | list.append(objectsIn: value) 39 | recordValue = list 40 | case .bool: 41 | guard let value = record.value(forKey: prop.name) as? [Bool] else { break } 42 | let list = List() 43 | list.append(objectsIn: value) 44 | recordValue = list 45 | case .float: 46 | guard let value = record.value(forKey: prop.name) as? [Float] else { break } 47 | let list = List() 48 | list.append(objectsIn: value) 49 | recordValue = list 50 | case .double: 51 | guard let value = record.value(forKey: prop.name) as? [Double] else { break } 52 | let list = List() 53 | list.append(objectsIn: value) 54 | recordValue = list 55 | case .data: 56 | guard let value = record.value(forKey: prop.name) as? [Data] else { break } 57 | let list = List() 58 | list.append(objectsIn: value) 59 | recordValue = list 60 | case .date: 61 | guard let value = record.value(forKey: prop.name) as? [Date] else { break } 62 | let list = List() 63 | list.append(objectsIn: value) 64 | recordValue = list 65 | case .object: 66 | guard let value = record.value(forKey: prop.name) as? [CKRecord.Reference] else { break } 67 | 68 | let uList = List() 69 | let vList = List() 70 | let wList = List() 71 | 72 | for reference in value { 73 | if let objectClassName = prop.objectClassName, 74 | let schema = realm.schema.objectSchema.first(where: { $0.className == objectClassName }), 75 | let primaryKeyValue = primaryKeyForRecordID(recordID: reference.recordID, schema: schema) as? AnyHashable { 76 | if schema.className == U.className() { 77 | if let existObject = realm.object(ofType: U.self, forPrimaryKey: primaryKeyValue) { 78 | uList.append(existObject) 79 | } else { 80 | pendingUTypeRelationshipsWorker.addToPendingList(elementPrimaryKeyValue: primaryKeyValue, propertyName: prop.name, owner: o) 81 | } 82 | } 83 | 84 | if schema.className == V.className() { 85 | if let existObject = realm.object(ofType: V.self, forPrimaryKey: primaryKeyValue) { 86 | vList.append(existObject) 87 | } else { 88 | pendingVTypeRelationshipsWorker.addToPendingList(elementPrimaryKeyValue: primaryKeyValue, propertyName: prop.name, owner: o) 89 | } 90 | } 91 | 92 | if schema.className == W.className() { 93 | if let existObject = realm.object(ofType: W.self, forPrimaryKey: primaryKeyValue) { 94 | wList.append(existObject) 95 | } else { 96 | pendingWTypeRelationshipsWorker.addToPendingList(elementPrimaryKeyValue: primaryKeyValue, propertyName: prop.name, owner: o) 97 | } 98 | } 99 | 100 | } 101 | } 102 | 103 | if prop.objectClassName == U.className() { 104 | recordValue = uList 105 | } 106 | 107 | if prop.objectClassName == V.className() { 108 | recordValue = vList 109 | } 110 | 111 | if prop.objectClassName == W.className() { 112 | recordValue = wList 113 | } 114 | 115 | default: 116 | break 117 | } 118 | o.setValue(recordValue, forKey: prop.name) 119 | continue 120 | } 121 | 122 | switch prop.type { 123 | case .int: 124 | recordValue = record.value(forKey: prop.name) as? Int 125 | case .string: 126 | recordValue = record.value(forKey: prop.name) as? String 127 | case .bool: 128 | recordValue = record.value(forKey: prop.name) as? Bool 129 | case .date: 130 | recordValue = record.value(forKey: prop.name) as? Date 131 | case .float: 132 | recordValue = record.value(forKey: prop.name) as? Float 133 | case .double: 134 | recordValue = record.value(forKey: prop.name) as? Double 135 | case .data: 136 | recordValue = record.value(forKey: prop.name) as? Data 137 | case .object: 138 | if let location = record.value(forKey: prop.name) as? CLLocation { 139 | recordValue = CreamLocation.make(location: location) 140 | } else if let asset = record.value(forKey: prop.name) as? CKAsset { 141 | recordValue = CreamAsset.parse(from: prop.name, record: record, asset: asset) 142 | } else if let owner = record.value(forKey: prop.name) as? CKRecord.Reference, 143 | let ownerType = prop.objectClassName, 144 | let schema = realm.schema.objectSchema.first(where: { $0.className == ownerType }) 145 | { 146 | primaryKeyForRecordID(recordID: owner.recordID, schema: schema).flatMap { 147 | recordValue = realm.dynamicObject(ofType: ownerType, forPrimaryKey: $0) 148 | } 149 | // Because we use the primaryKey as recordName when object converting to CKRecord 150 | } 151 | default: 152 | print("Other types will be supported in the future.") 153 | } 154 | if recordValue != nil || (recordValue == nil && prop.isOptional) { 155 | o.setValue(recordValue, forKey: prop.name) 156 | } 157 | } 158 | return o 159 | } 160 | 161 | /// The primaryKey in Realm could be type of Int or String. However the `recordName` is a String type, we need to make a check. 162 | /// The reversed process happens in `recordID` property in `CKRecordConvertible` protocol. 163 | /// 164 | /// - Parameter recordID: the recordID that CloudKit sent to us 165 | /// - Returns: the specific value of primaryKey in Realm 166 | static func primaryKeyForRecordID(recordID: CKRecord.ID, schema: ObjectSchema? = nil) -> Any? { 167 | let schema = schema ?? Self().objectSchema 168 | guard let objectPrimaryKeyType = schema.primaryKeyProperty?.type else { return nil } 169 | switch objectPrimaryKeyType { 170 | case .string: 171 | return recordID.recordName 172 | case .int: 173 | return Int(recordID.recordName) 174 | default: 175 | fatalError("The type of object primaryKey should be String or Int") 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /IceCream/Classes/CKRecordConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Object+CKRecord.swift 3 | // IceCream 4 | // 5 | // Created by 蔡越 on 11/11/2017. 6 | // 7 | 8 | import Foundation 9 | import CloudKit 10 | import RealmSwift 11 | 12 | public protocol CKRecordConvertible { 13 | static var recordType: String { get } 14 | static var zoneID: CKRecordZone.ID { get } 15 | static var databaseScope: CKDatabase.Scope { get } 16 | 17 | var recordID: CKRecord.ID { get } 18 | var record: CKRecord { get } 19 | 20 | var isDeleted: Bool { get } 21 | } 22 | 23 | extension CKRecordConvertible where Self: Object { 24 | 25 | public static var databaseScope: CKDatabase.Scope { 26 | return .private 27 | } 28 | 29 | public static var recordType: String { 30 | return className() 31 | } 32 | 33 | public static var zoneID: CKRecordZone.ID { 34 | switch Self.databaseScope { 35 | case .private: 36 | return CKRecordZone.ID(zoneName: "\(recordType)sZone", ownerName: CKCurrentUserDefaultName) 37 | case .public: 38 | return CKRecordZone.default().zoneID 39 | default: 40 | fatalError("Shared Database is not supported now") 41 | } 42 | } 43 | 44 | /// recordName : this is the unique identifier for the record, used to locate records on the database. We can create our own ID or leave it to CloudKit to generate a random UUID. 45 | /// For more: https://medium.com/@guilhermerambo/synchronizing-data-with-cloudkit-94c6246a3fda 46 | public var recordID: CKRecord.ID { 47 | guard let sharedSchema = Self.sharedSchema() else { 48 | fatalError("No schema settled. Go to Realm Community to seek more help.") 49 | } 50 | 51 | guard let primaryKeyProperty = sharedSchema.primaryKeyProperty else { 52 | fatalError("You should set a primary key on your Realm object") 53 | } 54 | 55 | switch primaryKeyProperty.type { 56 | case .string: 57 | if let primaryValueString = self[primaryKeyProperty.name] as? String { 58 | // For more: https://developer.apple.com/documentation/cloudkit/ckrecord/id/1500975-init 59 | assert(primaryValueString.allSatisfy({ $0.isASCII }), "Primary value for CKRecord name must contain only ASCII characters") 60 | assert(primaryValueString.count <= 255, "Primary value for CKRecord name must not exceed 255 characters") 61 | assert(!primaryValueString.starts(with: "_"), "Primary value for CKRecord name must not start with an underscore") 62 | return CKRecord.ID(recordName: primaryValueString, zoneID: Self.zoneID) 63 | } else { 64 | assertionFailure("\(primaryKeyProperty.name)'s value should be String type") 65 | } 66 | case .int: 67 | if let primaryValueInt = self[primaryKeyProperty.name] as? Int { 68 | return CKRecord.ID(recordName: "\(primaryValueInt)", zoneID: Self.zoneID) 69 | } else { 70 | assertionFailure("\(primaryKeyProperty.name)'s value should be Int type") 71 | } 72 | default: 73 | assertionFailure("Primary key should be String or Int") 74 | } 75 | fatalError("Should have a reasonable recordID") 76 | } 77 | 78 | // Simultaneously init CKRecord with zoneID and recordID, thanks to this guy: https://stackoverflow.com/questions/45429133/how-to-initialize-ckrecord-with-both-zoneid-and-recordid 79 | public var record: CKRecord { 80 | let r = CKRecord(recordType: Self.recordType, recordID: recordID) 81 | let properties = objectSchema.properties 82 | for prop in properties { 83 | 84 | let item = self[prop.name] 85 | 86 | if prop.isArray { 87 | switch prop.type { 88 | case .int: 89 | guard let list = item as? List, !list.isEmpty else { break } 90 | let array = Array(list) 91 | r[prop.name] = array as CKRecordValue 92 | case .string: 93 | guard let list = item as? List, !list.isEmpty else { break } 94 | let array = Array(list) 95 | r[prop.name] = array as CKRecordValue 96 | case .bool: 97 | guard let list = item as? List, !list.isEmpty else { break } 98 | let array = Array(list) 99 | r[prop.name] = array as CKRecordValue 100 | case .float: 101 | guard let list = item as? List, !list.isEmpty else { break } 102 | let array = Array(list) 103 | r[prop.name] = array as CKRecordValue 104 | case .double: 105 | guard let list = item as? List, !list.isEmpty else { break } 106 | let array = Array(list) 107 | r[prop.name] = array as CKRecordValue 108 | case .data: 109 | guard let list = item as? List, !list.isEmpty else { break } 110 | let array = Array(list) 111 | r[prop.name] = array as CKRecordValue 112 | case .date: 113 | guard let list = item as? List, !list.isEmpty else { break } 114 | let array = Array(list) 115 | r[prop.name] = array as CKRecordValue 116 | case .object: 117 | /// We may get List here 118 | /// The item cannot be casted as List 119 | /// It can be casted at a low-level type `ListBase` 120 | guard let list = item as? ListBase, list.count > 0 else { break } 121 | var referenceArray = [CKRecord.Reference]() 122 | let wrappedArray = list._rlmArray 123 | for index in 0..