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