From 219194e2730d4000bdd09fba97a56d41fb2b362e Mon Sep 17 00:00:00 2001 From: Vasily Ulianov Date: Tue, 26 Dec 2017 00:41:35 +0300 Subject: [PATCH] Version 2.0.0 (#13) ## New features * Existing data will be automatically uploaded if CloudKit doesn't exists (fix #9) * Framework now handles next CloudKit errors: * `userDeletedZone`: all local cached Core Data will removed (user asked to purge data) * `zoneNotFound`: existing data will be automatically uploaded to CloudKit * `changeTokenExpired`: tokens will be reset and all data downloaded again * `isMore`: if you fetch a lot of data from Cloud, fetch requests will be divided to several ones * Sync status and errors are reported to `CloudCoreDelegate` instead of notifications, it's more Swift way. ## Improvements * Changed API calls (check changes at CloudCore), that version is not compatible with `1.x` * Numerous bug fixes (really, alpha version wasn't usable) * More clean folder structure * Documented 100% of public methods and variables. * Combined all targets to one multiplatform target. Thanks for [that guide](http://ilya.puchka.me/xcode-cross-platform-frameworks/). ## Removed * Removed Swift Package Manager support, because it doesn't well support manager for iOS & macOS applications. * Notifications removed, use delegate. ## New example application Absolutely new example application with more realistic use cases. All changes can be made by pressing *Edit* button, and it will be saved when you click *Done*. --- .travis.yml | 54 +- CloudCore.podspec | 12 +- CloudCore.xcodeproj/project.pbxproj | 506 ++++-------------- .../xcschemes/CloudCore-Mac.xcscheme | 101 ---- ...udCore-iOS.xcscheme => CloudCore.xcscheme} | 12 +- .../project.pbxproj | 160 +++++- .../contents.xcworkspacedata | 10 + Example/Podfile | 10 + .../AppIcon.appiconset/Contents.json | 5 + .../avatar_1.imageset/Contents.json | 21 + .../avatar_1.imageset/avatar_1.jpg | Bin 0 -> 5488 bytes .../avatar_2.imageset/Contents.json | 21 + .../avatar_2.imageset/avatar_2.jpg | Bin 0 -> 5874 bytes .../avatar_3.imageset/Contents.json | 21 + .../avatar_3.imageset/avatar_3.jpg | Bin 0 -> 5874 bytes .../avatar_4.imageset/Contents.json | 21 + .../avatar_4.imageset/avatar_4.jpg | Bin 0 -> 5728 bytes .../avatar_5.imageset/Contents.json | 21 + .../avatar_5.imageset/avatar_5.jpg | Bin 0 -> 6380 bytes .../avatar_6.imageset/Contents.json | 21 + .../avatar_6.imageset/avatar_6.jpg | Bin 0 -> 4344 bytes .../avatar_7.imageset/Contents.json | 21 + .../avatar_7.imageset/avatar_7.jpg | Bin 0 -> 3218 bytes .../avatar_8.imageset/Contents.json | 21 + .../avatar_8.imageset/avatar_8.jpg | Bin 0 -> 6741 bytes .../avatar_9.imageset/Contents.json | 21 + .../avatar_9.imageset/avatar_9.jpg | Bin 0 -> 3747 bytes Example/Resources/Base.lproj/Main.storyboard | 214 +++++--- .../Model.xcdatamodel/contents | 47 +- Example/Sources/AppDelegate.swift | 53 +- .../Class/FRCTableViewDataSource.swift | 102 ++++ Example/Sources/Class/ModelFactory.swift | 64 +++ .../Sources/Class/NotificationsObserver.swift | 35 ++ Example/Sources/DetailViewController.swift | 45 -- .../Sources/MasterViewController+FRC.swift | 79 --- Example/Sources/MasterViewController.swift | 118 ---- .../DetailViewController.swift | 107 ++++ .../MasterViewController.swift | 106 ++++ .../Sources/View/EmployeeTableViewCell.swift | 18 + Package.swift | 6 - README.md | 83 ++- Resources/Info-iOS.plist | 26 - .../Classes}/AsynchronousOperation.swift | 0 Source/Classes/CloudCore.swift | 209 ++++++++ .../Classes/ErrorBlockProxy.swift | 0 .../Classes/Fetch/FetchAndSaveOperation.swift | 120 +++++ .../FetchPublicSubscriptionsOperation.swift | 0 .../PublicDatabaseSubscriptions.swift | 0 .../DeleteFromCoreDataOperation.swift | 2 - .../FetchRecordZoneChangesOperation.swift | 54 +- .../PurgeLocalDatabaseOperation.swift | 58 ++ .../RecordToCoreDataOperation.swift | 27 +- .../Save}/CloudSaveOperationQueue.swift | 30 +- .../Classes/Save}/CoreDataListener.swift | 61 ++- .../Save}/Model/RecordIDWithDatabase.swift | 0 .../Save}/Model/RecordWithDatabase.swift | 0 .../ObjectToRecord/CoreDataAttribute.swift | 0 .../ObjectToRecord/CoreDataRelationship.swift | 4 + .../ObjectToRecordConverter.swift | 16 +- .../ObjectToRecordOperation.swift | 1 - .../CreateCloudCoreZoneOperation.swift | 34 ++ .../Setup Operation/SetupOperation.swift | 82 +++ .../Setup Operation/SubscribeOperation.swift | 81 +++ .../UploadAllLocalDataOperation.swift | 77 +++ {Sources => Source}/Enum/CloudCoreError.swift | 7 + {Sources => Source}/Enum/FetchResult.swift | 0 Source/Enum/Module.swift | 20 + .../Extensions/CKRecordID.swift | 0 .../Extensions/NSEntityDescription.swift | 0 .../Extensions/NSManagedObject.swift | 0 Source/Extensions/NSManagedObjectModel.swift | 25 + {Sources => Source}/Model/CKRecord.swift | 0 .../Model/CloudCoreConfig.swift | 4 - .../Model/CloudKitAttribute.swift | 10 +- .../Model/ServiceAttributeName.swift | 0 {Sources => Source}/Model/Tokens.swift | 52 +- Source/Protocols/CloudCoreDelegate.swift | 49 ++ .../Resources/Info.plist | 0 Sources/Classes/CloudCore.swift | 157 ------ .../Classes/Fetch/FetchAndSaveOperation.swift | 130 ----- .../FetchDatabaseChangesOperation.swift | 55 -- Sources/Classes/SetupOperation.swift | 153 ------ Sources/Extensions/NotificationName.swift | 34 -- .../Protocols/CloudCoreErrorDelegate.swift | 17 - .../Classes/ErrorBlockProxyTests.swift | 0 .../DeleteFromCoreDataOperationTests.swift | 0 .../RecordToCoreDataOperationTests.swift | 19 +- .../CoreDataAttributeTests.swift | 0 .../CoreDataRelationshipTests.swift | 0 .../ObjectToRecordOperationTests.swift | 0 .../CustomFunctions.swift | 0 .../Extensions/CKRecordIDTests.swift | 0 .../Extensions/NSEntityDescriptionTests.swift | 0 .../Extensions/NSManagedObjectTests.swift | 0 .../Resources => CloudCoreTests}/Info.plist | 0 .../Model/CKRecordTests.swift | 0 .../model.xcdatamodel/contents | 0 .../App/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../App/Base.lproj/LaunchScreen.storyboard | 0 .../App/Base.lproj/Main.storyboard | 0 .../App/Info.plist | 0 .../App/TestableApp.entitlements | 0 .../.xccurrentversion | 0 .../TestableApp.xcdatamodel/contents | 0 .../App/ViewController.swift | 0 .../CloudKitTests.swift | 28 +- .../CorrectObjectExtension.swift | 0 .../Sources => CloudKitTests}/Helpers.swift | 40 +- .../Resources/Info.plist | 0 .../model.xcdatamodel/contents | 0 111 files changed, 2119 insertions(+), 1699 deletions(-) delete mode 100755 CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore-Mac.xcscheme rename CloudCore.xcodeproj/xcshareddata/xcschemes/{CloudCore-iOS.xcscheme => CloudCore.xcscheme} (92%) mode change 100755 => 100644 create mode 100644 Example/CloudCoreExample.xcworkspace/contents.xcworkspacedata create mode 100644 Example/Podfile create mode 100644 Example/Resources/Assets.xcassets/avatar_1.imageset/Contents.json create mode 100644 Example/Resources/Assets.xcassets/avatar_1.imageset/avatar_1.jpg create mode 100644 Example/Resources/Assets.xcassets/avatar_2.imageset/Contents.json create mode 100644 Example/Resources/Assets.xcassets/avatar_2.imageset/avatar_2.jpg create mode 100644 Example/Resources/Assets.xcassets/avatar_3.imageset/Contents.json create mode 100644 Example/Resources/Assets.xcassets/avatar_3.imageset/avatar_3.jpg create mode 100644 Example/Resources/Assets.xcassets/avatar_4.imageset/Contents.json create mode 100644 Example/Resources/Assets.xcassets/avatar_4.imageset/avatar_4.jpg create mode 100644 Example/Resources/Assets.xcassets/avatar_5.imageset/Contents.json create mode 100644 Example/Resources/Assets.xcassets/avatar_5.imageset/avatar_5.jpg create mode 100644 Example/Resources/Assets.xcassets/avatar_6.imageset/Contents.json create mode 100644 Example/Resources/Assets.xcassets/avatar_6.imageset/avatar_6.jpg create mode 100644 Example/Resources/Assets.xcassets/avatar_7.imageset/Contents.json create mode 100644 Example/Resources/Assets.xcassets/avatar_7.imageset/avatar_7.jpg create mode 100644 Example/Resources/Assets.xcassets/avatar_8.imageset/Contents.json create mode 100644 Example/Resources/Assets.xcassets/avatar_8.imageset/avatar_8.jpg create mode 100644 Example/Resources/Assets.xcassets/avatar_9.imageset/Contents.json create mode 100644 Example/Resources/Assets.xcassets/avatar_9.imageset/avatar_9.jpg create mode 100644 Example/Sources/Class/FRCTableViewDataSource.swift create mode 100644 Example/Sources/Class/ModelFactory.swift create mode 100644 Example/Sources/Class/NotificationsObserver.swift delete mode 100644 Example/Sources/DetailViewController.swift delete mode 100644 Example/Sources/MasterViewController+FRC.swift delete mode 100644 Example/Sources/MasterViewController.swift create mode 100644 Example/Sources/View Controller/DetailViewController.swift create mode 100644 Example/Sources/View Controller/MasterViewController.swift create mode 100644 Example/Sources/View/EmployeeTableViewCell.swift delete mode 100644 Package.swift delete mode 100755 Resources/Info-iOS.plist rename {Sources/Classes/Fetch/SubOperations => Source/Classes}/AsynchronousOperation.swift (100%) create mode 100644 Source/Classes/CloudCore.swift rename {Sources => Source}/Classes/ErrorBlockProxy.swift (100%) create mode 100644 Source/Classes/Fetch/FetchAndSaveOperation.swift rename {Sources => Source}/Classes/Fetch/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift (100%) rename {Sources => Source}/Classes/Fetch/PublicSubscriptions/PublicDatabaseSubscriptions.swift (100%) rename {Sources => Source}/Classes/Fetch/SubOperations/DeleteFromCoreDataOperation.swift (98%) rename {Sources => Source}/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift (51%) create mode 100644 Source/Classes/Fetch/SubOperations/PurgeLocalDatabaseOperation.swift rename {Sources => Source}/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift (83%) rename {Sources/Classes/Upload => Source/Classes/Save}/CloudSaveOperationQueue.swift (78%) rename {Sources/Classes/Upload => Source/Classes/Save}/CoreDataListener.swift (54%) rename {Sources/Classes/Upload => Source/Classes/Save}/Model/RecordIDWithDatabase.swift (100%) rename {Sources/Classes/Upload => Source/Classes/Save}/Model/RecordWithDatabase.swift (100%) rename {Sources/Classes/Upload => Source/Classes/Save}/ObjectToRecord/CoreDataAttribute.swift (100%) rename {Sources/Classes/Upload => Source/Classes/Save}/ObjectToRecord/CoreDataRelationship.swift (95%) rename {Sources/Classes/Upload => Source/Classes/Save}/ObjectToRecord/ObjectToRecordConverter.swift (86%) rename {Sources/Classes/Upload => Source/Classes/Save}/ObjectToRecord/ObjectToRecordOperation.swift (98%) create mode 100644 Source/Classes/Setup Operation/CreateCloudCoreZoneOperation.swift create mode 100644 Source/Classes/Setup Operation/SetupOperation.swift create mode 100644 Source/Classes/Setup Operation/SubscribeOperation.swift create mode 100644 Source/Classes/Setup Operation/UploadAllLocalDataOperation.swift rename {Sources => Source}/Enum/CloudCoreError.swift (75%) rename {Sources => Source}/Enum/FetchResult.swift (100%) create mode 100644 Source/Enum/Module.swift rename {Sources => Source}/Extensions/CKRecordID.swift (100%) rename {Sources => Source}/Extensions/NSEntityDescription.swift (100%) rename {Sources => Source}/Extensions/NSManagedObject.swift (100%) create mode 100644 Source/Extensions/NSManagedObjectModel.swift rename {Sources => Source}/Model/CKRecord.swift (100%) rename {Sources => Source}/Model/CloudCoreConfig.swift (87%) rename {Sources => Source}/Model/CloudKitAttribute.swift (88%) rename {Sources => Source}/Model/ServiceAttributeName.swift (100%) rename {Sources => Source}/Model/Tokens.swift (64%) create mode 100644 Source/Protocols/CloudCoreDelegate.swift rename Resources/Info-Mac.plist => Source/Resources/Info.plist (100%) delete mode 100644 Sources/Classes/CloudCore.swift delete mode 100644 Sources/Classes/Fetch/FetchAndSaveOperation.swift delete mode 100644 Sources/Classes/Fetch/SubOperations/FetchDatabaseChangesOperation.swift delete mode 100644 Sources/Classes/SetupOperation.swift delete mode 100644 Sources/Extensions/NotificationName.swift delete mode 100644 Sources/Protocols/CloudCoreErrorDelegate.swift rename Tests/{Unit/Sources => CloudCoreTests}/Classes/ErrorBlockProxyTests.swift (100%) rename Tests/{Unit/Sources => CloudCoreTests}/Classes/Fetch/Operations/DeleteFromCoreDataOperationTests.swift (100%) rename Tests/{Unit/Sources => CloudCoreTests}/Classes/Fetch/Operations/RecordToCoreDataOperationTests.swift (77%) rename Tests/{Unit/Sources => CloudCoreTests}/Classes/Upload/ObjectToRecord/CoreDataAttributeTests.swift (100%) rename Tests/{Unit/Sources => CloudCoreTests}/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift (100%) rename Tests/{Unit/Sources => CloudCoreTests}/Classes/Upload/ObjectToRecord/ObjectToRecordOperationTests.swift (100%) rename Tests/{Unit/Sources => CloudCoreTests}/CustomFunctions.swift (100%) rename Tests/{Unit/Sources => CloudCoreTests}/Extensions/CKRecordIDTests.swift (100%) rename Tests/{Unit/Sources => CloudCoreTests}/Extensions/NSEntityDescriptionTests.swift (100%) rename Tests/{Unit/Sources => CloudCoreTests}/Extensions/NSManagedObjectTests.swift (100%) rename Tests/{Unit/Resources => CloudCoreTests}/Info.plist (100%) rename Tests/{Unit/Sources => CloudCoreTests}/Model/CKRecordTests.swift (100%) rename Tests/{Unit/Resources => CloudCoreTests}/model.xcdatamodeld/model.xcdatamodel/contents (100%) rename Tests/{CloudKit => CloudKitTests}/App/AppDelegate.swift (100%) rename Tests/{CloudKit => CloudKitTests}/App/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Tests/{CloudKit => CloudKitTests}/App/Base.lproj/LaunchScreen.storyboard (100%) rename Tests/{CloudKit => CloudKitTests}/App/Base.lproj/Main.storyboard (100%) rename Tests/{CloudKit => CloudKitTests}/App/Info.plist (100%) rename TestableApp.entitlements => Tests/CloudKitTests/App/TestableApp.entitlements (100%) rename Tests/{CloudKit => CloudKitTests}/App/TestableApp.xcdatamodeld/.xccurrentversion (100%) rename Tests/{CloudKit => CloudKitTests}/App/TestableApp.xcdatamodeld/TestableApp.xcdatamodel/contents (100%) rename Tests/{CloudKit => CloudKitTests}/App/ViewController.swift (100%) rename Tests/{CloudKit/Sources => CloudKitTests}/CloudKitTests.swift (71%) rename Tests/{CloudKit/Sources => CloudKitTests}/CorrectObjectExtension.swift (100%) rename Tests/{CloudKit/Sources => CloudKitTests}/Helpers.swift (72%) rename Tests/{CloudKit => CloudKitTests}/Resources/Info.plist (100%) rename Tests/{CloudKit => CloudKitTests}/Resources/model.xcdatamodeld/model.xcdatamodel/contents (100%) diff --git a/.travis.yml b/.travis.yml index 9f08b9e8..a60a057b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,45 +1,37 @@ -osx_image: xcode9 +osx_image: xcode9.2 language: objective-c +podfile: "Example/Podfile" branches: only: - master env: - global: - - PROJECT="CloudCore.xcodeproj" - - EXAMPLE_PROJECT="Example/CloudCoreExample.xcodeproj" - - - IOS_FRAMEWORK_SCHEME="CloudCore-iOS" - - MACOS_FRAMEWORK_SCHEME="CloudCore-Mac" - - EXAMPLE_SCHEME="CloudCoreExample" - matrix: - - DESTINATION="OS=11.0,name=iPhone 7 Plus" SCHEME="$IOS_FRAMEWORK_SCHEME" BUILD_EXAMPLE="YES" POD_LINT="YES" - - DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" BUILD_EXAMPLE="NO" POD_LINT="NO" + - DESTINATION='platform=OS X' POD_LINT="YES" + - DESTINATION='platform=iOS Simulator,name=iPhone 6S' BUILD_EXAMPLE="YES" + - DESTINATION='platform=watchOS Simulator,name=Apple Watch - 38mm' SKIP_TEST="YES" + - DESTINATION='platform=tvOS Simulator,name=Apple TV 4K' before_install: - - gem install cocoapods --pre --no-rdoc --no-ri --no-document --quiet - gem install xcpretty-travis-formatter script: - - set -o pipefail - - # Build and test in debug - - xcodebuild -project "$PROJECT" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty -f `xcpretty-travis-formatter` - # Build and test in release - - xcodebuild -project "$PROJECT" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty -f `xcpretty-travis-formatter` - - # Build example project - - if [ $BUILD_EXAMPLE == "YES" ]; then - xcodebuild -project "$EXAMPLE_PROJECT" -scheme "$EXAMPLE_SCHEME" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty -f `xcpretty-travis-formatter`; - fi - - # Run on Pull Request - - if [ $POD_LINT == "YES" && $TRAVIS_PULL_REQUEST != "false" ]; then + - set -o pipefail + - xcodebuild -scheme CloudCore -destination "$DESTINATION" build | xcpretty -f `xcpretty-travis-formatter` + - if [ "$SKIP_TEST" != "YES" ]; then + xcodebuild -scheme CloudCore -destination "$DESTINATION" test | xcpretty -f `xcpretty-travis-formatter`; + fi + + # Example + - if [ "$BUILD_EXAMPLE" = "YES" ]; then + xcodebuild -workspace "Example/CloudCoreExample.xcworkspace" -scheme "CloudCoreExample" -destination "$DESTINATION" build | xcpretty -f `xcpretty-travis-formatter`; + fi + + - if [ "$POD_LINT" = "YES" ]; then pod lib lint --allow-warnings; - fi + fi -# Run release to master branch - - if [ $POD_LINT == "YES" && $TRAVIS_BRANCH = "master" ]; then - pod spec lint --allow-warnings; - fi + # Run release to master branch + - if [ "$POD_LINT" = "YES" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then + pod spec lint --allow-warnings; + fi diff --git a/CloudCore.podspec b/CloudCore.podspec index 17a31e50..6f813d13 100755 --- a/CloudCore.podspec +++ b/CloudCore.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "CloudCore" - s.summary = "Framework that enables syncing between iCloud (CloudKit) and Core Data" - s.version = "1.0.1" + s.summary = "Framework that enables synchronization between CloudKit (iCloud) and Core Data. Can be used as CloudKit caching mechanism." + s.version = "2.0.0" s.homepage = "https://github.com/sorix/CloudCore" s.license = 'MIT' s.author = { "Vasily Ulianov" => "vasily@me.com" } @@ -12,14 +12,14 @@ Pod::Spec.new do |s| s.ios.deployment_target = '10.0' s.osx.deployment_target = '10.12' + s.tvos.deployment_target = '10.0' + s.watchos.deployment_target = '3.0' - s.ios.source_files = 'Sources/**/*.swift' - # s.tvos.source_files = 'Sources/**/*.swift' - s.osx.source_files = 'Sources/**/*.swift' + s.source_files = 'Source/**/*.swift' s.ios.frameworks = 'Foundation', 'CloudKit', 'CoreData' s.osx.frameworks = 'Foundation', 'CloudKit', 'CoreData' s.pod_target_xcconfig = { 'SWIFT_VERSION' => '4.0' } - s.documentation_url = 'https://github.com/Sorix/CloudCore/wiki' + s.documentation_url = 'http://cocoadocs.org/docsets/CloudCore/' end diff --git a/CloudCore.xcodeproj/project.pbxproj b/CloudCore.xcodeproj/project.pbxproj index 3215e920..6a51003b 100755 --- a/CloudCore.xcodeproj/project.pbxproj +++ b/CloudCore.xcodeproj/project.pbxproj @@ -7,6 +7,14 @@ objects = { /* Begin PBXBuildFile section */ + D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9089D491FE14E57000FC60C /* SetupOperation.swift */; }; + D97465F81FE319930060EA66 /* CloudCoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F71FE319930060EA66 /* CloudCoreDelegate.swift */; }; + D97465FA1FE31A650060EA66 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97465F91FE31A650060EA66 /* Module.swift */; }; + D985DE9D1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985DE9C1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift */; }; + D985DEA41FE026D400236870 /* CreateCloudCoreZoneOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985DEA31FE026D400236870 /* CreateCloudCoreZoneOperation.swift */; }; + D985DEA81FE0292000236870 /* SubscribeOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985DEA71FE0292000236870 /* SubscribeOperation.swift */; }; + D985DEAB1FE0335800236870 /* UploadAllLocalDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985DEAA1FE0335800236870 /* UploadAllLocalDataOperation.swift */; }; + D985DEAE1FE034A900236870 /* NSManagedObjectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985DEAD1FE034A900236870 /* NSManagedObjectModel.swift */; }; D9B3C6F61FCEF38D00CDB7FF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B3C6F51FCEF38D00CDB7FF /* AppDelegate.swift */; }; D9B3C6F81FCEF38D00CDB7FF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B3C6F71FCEF38D00CDB7FF /* ViewController.swift */; }; D9B3C6FB1FCEF38D00CDB7FF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D9B3C6F91FCEF38D00CDB7FF /* Main.storyboard */; }; @@ -20,10 +28,8 @@ D9B3C7311FCEFC9C00CDB7FF /* CloudCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5B2E89F1C3A780C00C0327D /* CloudCore.framework */; }; D9B3C7341FCEFD9100CDB7FF /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9B3C7331FCEFD9100CDB7FF /* CloudKit.framework */; }; D9B3C7361FCF02F000CDB7FF /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B3C7351FCF02F000CDB7FF /* Helpers.swift */; }; - D9B3C7381FCF0C9E00CDB7FF /* CorrectObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B3C72B1FCEFAB800CDB7FF /* CorrectObject.swift */; }; D9B3C7391FCF0C9E00CDB7FF /* CorrectObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B3C72B1FCEFAB800CDB7FF /* CorrectObject.swift */; }; D9B3C73A1FCF0CA900CDB7FF /* CoreDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B3C7271FCEFA6F00CDB7FF /* CoreDataTestCase.swift */; }; - D9B3C73B1FCF0CA900CDB7FF /* CoreDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B3C7271FCEFA6F00CDB7FF /* CoreDataTestCase.swift */; }; D9B3C73D1FCF0D2700CDB7FF /* CorrectObjectExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B3C73C1FCF0D2700CDB7FF /* CorrectObjectExtension.swift */; }; E200D44D1E48E13200B707D4 /* CloudCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E200D44C1E48E13200B707D4 /* CloudCore.swift */; }; E2075FF91E4BBEAC00E31F1F /* AsynchronousOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2075FF81E4BBEAC00E31F1F /* AsynchronousOperation.swift */; }; @@ -33,12 +39,6 @@ E22A53DA1E4A8743009286C0 /* CloudKitAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22A53D91E4A8743009286C0 /* CloudKitAttribute.swift */; }; E22C40461E42956C009469A1 /* CoreDataListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22C40451E42956C009469A1 /* CoreDataListener.swift */; }; E23C478C1E48A404004310F9 /* CloudSaveOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23C478B1E48A404004310F9 /* CloudSaveOperationQueue.swift */; }; - E23C47901E48A587004310F9 /* CloudCoreErrorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23C478F1E48A587004310F9 /* CloudCoreErrorDelegate.swift */; }; - E23C47921E48B210004310F9 /* NotificationName.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23C47911E48B210004310F9 /* NotificationName.swift */; }; - E23C47941E48CE9C004310F9 /* CloudCoreErrorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23C478F1E48A587004310F9 /* CloudCoreErrorDelegate.swift */; }; - E23C47951E48CE9C004310F9 /* CoreDataRelationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F44A51E4595B900F78819 /* CoreDataRelationship.swift */; }; - E23C47971E48CE9C004310F9 /* NotificationName.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23C47911E48B210004310F9 /* NotificationName.swift */; }; - E23C47981E48CE9C004310F9 /* CloudSaveOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23C478B1E48A404004310F9 /* CloudSaveOperationQueue.swift */; }; E247EF8D1E67775500EBD75E /* ErrorBlockProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */; }; E247EF971E67873E00EBD75E /* DeleteFromCoreDataOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF951E67873900EBD75E /* DeleteFromCoreDataOperationTests.swift */; }; E247EF9A1E678EAC00EBD75E /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF981E678EA200EBD75E /* CustomFunctions.swift */; }; @@ -48,13 +48,6 @@ E28F0B9F1E67245A00BF532A /* CKRecordIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0B9D1E67245600BF532A /* CKRecordIDTests.swift */; }; E28F0BA21E67260900BF532A /* NSEntityDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0BA01E6725E700BF532A /* NSEntityDescriptionTests.swift */; }; E28F0BA31E67280100BF532A /* NSManagedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F44A81E459E3E00F78819 /* NSManagedObjectTests.swift */; }; - E28FBCE01E43DE6F0081FF3B /* CKRecordID.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB21D1E433E050020F5B6 /* CKRecordID.swift */; }; - E28FBCE11E43DE6F0081FF3B /* CKRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB2201E4344E80020F5B6 /* CKRecord.swift */; }; - E28FBCE21E43DE6F0081FF3B /* NSManagedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB2221E4346FF0020F5B6 /* NSManagedObject.swift */; }; - E28FBCE31E43DE6F0081FF3B /* CloudCoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB21B1E43381D0020F5B6 /* CloudCoreError.swift */; }; - E28FBCE41E43DE6F0081FF3B /* CloudCoreConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB2191E4334590020F5B6 /* CloudCoreConfig.swift */; }; - E28FBCEA1E43DE760081FF3B /* CoreDataListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22C40451E42956C009469A1 /* CoreDataListener.swift */; }; - E28FBCEC1E43DE760081FF3B /* CoreDataAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB2361E4377F80020F5B6 /* CoreDataAttribute.swift */; }; E29BB21A1E4334590020F5B6 /* CloudCoreConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB2191E4334590020F5B6 /* CloudCoreConfig.swift */; }; E29BB21C1E43381D0020F5B6 /* CloudCoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB21B1E43381D0020F5B6 /* CloudCoreError.swift */; }; E29BB21E1E433E050020F5B6 /* CKRecordID.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB21D1E433E050020F5B6 /* CKRecordID.swift */; }; @@ -65,50 +58,17 @@ E29BB2371E4377F80020F5B6 /* CoreDataAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29BB2361E4377F80020F5B6 /* CoreDataAttribute.swift */; }; E29D117A1E69813F00E3DCBF /* CoreDataAttributeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D11791E69813F00E3DCBF /* CoreDataAttributeTests.swift */; }; E29D117D1E69A47700E3DCBF /* CoreDataRelationshipTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D117B1E69A44C00E3DCBF /* CoreDataRelationshipTests.swift */; }; - E29D11871E69B30C00E3DCBF /* CloudCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5C629401C3A7FAA007F7B7C /* CloudCore.framework */; }; - E29D118D1E69B31E00E3DCBF /* AsynchronousOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2075FF81E4BBEAC00E31F1F /* AsynchronousOperation.swift */; }; - E29D118E1E69B31E00E3DCBF /* DeleteFromCoreDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C02A181E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift */; }; - E29D118F1E69B31E00E3DCBF /* FetchDatabaseChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C02A0F1E4CBEBB001B2871 /* FetchDatabaseChangesOperation.swift */; }; - E29D11901E69B31E00E3DCBF /* FetchRecordZoneChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C02A131E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift */; }; - E29D11911E69B31E00E3DCBF /* RecordToCoreDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21FA03D1E4A7E7200B1DAA2 /* RecordToCoreDataOperation.swift */; }; - E29D11931E69B32400E3DCBF /* ObjectToRecordConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C02A0D1E4C99AD001B2871 /* ObjectToRecordConverter.swift */; }; - E29D11941E69B32400E3DCBF /* ObjectToRecordOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2075FFE1E4BCD7E00E31F1F /* ObjectToRecordOperation.swift */; }; - E29D11951E69B32900E3DCBF /* CloudCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E200D44C1E48E13200B707D4 /* CloudCore.swift */; }; - E29D11971E69B32900E3DCBF /* ErrorBlockProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2564BFE1E5061BC002E518B /* ErrorBlockProxy.swift */; }; - E29D11981E69B32E00E3DCBF /* FetchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C3A6D01E4A8EAF009151F3 /* FetchResult.swift */; }; - E29D11991E69B33100E3DCBF /* NSEntityDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D390071E4A49350019BBCD /* NSEntityDescription.swift */; }; - E29D119A1E69B33400E3DCBF /* CloudKitAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22A53D91E4A8743009286C0 /* CloudKitAttribute.swift */; }; - E29D119B1E69B33400E3DCBF /* ServiceAttributeName.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EE20061E4E6DCE0060F769 /* ServiceAttributeName.swift */; }; - E29D119C1E69B33400E3DCBF /* Tokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E296C91E49DA0800E7D6ED /* Tokens.swift */; }; - E29D119D1E69B36700E3DCBF /* ErrorBlockProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */; }; - E29D119E1E69B36700E3DCBF /* DeleteFromCoreDataOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF951E67873900EBD75E /* DeleteFromCoreDataOperationTests.swift */; }; - E29D119F1E69B36700E3DCBF /* RecordToCoreDataOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20A73CB1E68608100A6851A /* RecordToCoreDataOperationTests.swift */; }; - E29D11A01E69B36700E3DCBF /* CoreDataAttributeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D11791E69813F00E3DCBF /* CoreDataAttributeTests.swift */; }; - E29D11A11E69B36700E3DCBF /* CoreDataRelationshipTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D117B1E69A44C00E3DCBF /* CoreDataRelationshipTests.swift */; }; - E29D11A21E69B36700E3DCBF /* CKRecordIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0B9D1E67245600BF532A /* CKRecordIDTests.swift */; }; - E29D11A31E69B36700E3DCBF /* NSEntityDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0BA01E6725E700BF532A /* NSEntityDescriptionTests.swift */; }; - E29D11A41E69B36700E3DCBF /* NSManagedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F44A81E459E3E00F78819 /* NSManagedObjectTests.swift */; }; - E29D11A51E69B36700E3DCBF /* CKRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28F0B911E671E6500BF532A /* CKRecordTests.swift */; }; - E29D11A81E69B36700E3DCBF /* CustomFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E247EF981E678EA200EBD75E /* CustomFunctions.swift */; }; - E29D11A91E69B3B200E3DCBF /* model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E29BB2331E436F720020F5B6 /* model.xcdatamodeld */; }; E2A3F9451E69B6EC007A65EB /* ObjectToRecordOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A3F9441E69B6EC007A65EB /* ObjectToRecordOperationTests.swift */; }; - E2A3F9461E69B6EC007A65EB /* ObjectToRecordOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A3F9441E69B6EC007A65EB /* ObjectToRecordOperationTests.swift */; }; - E2BB748E1E7EA8690048C129 /* SetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2BB748D1E7EA8690048C129 /* SetupOperation.swift */; }; - E2BB748F1E7EB2B00048C129 /* SetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2BB748D1E7EA8690048C129 /* SetupOperation.swift */; }; E2C02A0E1E4C99AD001B2871 /* ObjectToRecordConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C02A0D1E4C99AD001B2871 /* ObjectToRecordConverter.swift */; }; - E2C02A101E4CBEBB001B2871 /* FetchDatabaseChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C02A0F1E4CBEBB001B2871 /* FetchDatabaseChangesOperation.swift */; }; E2C02A141E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C02A131E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift */; }; E2C02A191E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C02A181E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift */; }; E2C3A6D11E4A8EAF009151F3 /* FetchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C3A6D01E4A8EAF009151F3 /* FetchResult.swift */; }; E2D390081E4A49350019BBCD /* NSEntityDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D390071E4A49350019BBCD /* NSEntityDescription.swift */; }; E2E296CA1E49DA0800E7D6ED /* Tokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E296C91E49DA0800E7D6ED /* Tokens.swift */; }; - E2E4D8401E76D5A500550CBE /* FetchAndSaveOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E4D83D1E76D4EF00550CBE /* FetchAndSaveOperation.swift */; }; E2E4D8411E76D5A600550CBE /* FetchAndSaveOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E4D83D1E76D4EF00550CBE /* FetchAndSaveOperation.swift */; }; E2EE20071E4E6DCE0060F769 /* ServiceAttributeName.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EE20061E4E6DCE0060F769 /* ServiceAttributeName.swift */; }; E2FA74441E769BF900C3489D /* RecordWithDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FA74431E769BF900C3489D /* RecordWithDatabase.swift */; }; - E2FA74451E769BF900C3489D /* RecordWithDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FA74431E769BF900C3489D /* RecordWithDatabase.swift */; }; E2FA74481E769D9400C3489D /* RecordIDWithDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FA74471E769D9400C3489D /* RecordIDWithDatabase.swift */; }; - E2FA74491E769D9400C3489D /* RecordIDWithDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FA74471E769D9400C3489D /* RecordIDWithDatabase.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -126,20 +86,19 @@ remoteGlobalIDString = D5B2E89E1C3A780C00C0327D; remoteInfo = "CloudCore-iOS"; }; - E29D11881E69B30C00E3DCBF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D5B2E8961C3A780C00C0327D /* Project object */; - proxyType = 1; - remoteGlobalIDString = D5C6293F1C3A7FAA007F7B7C; - remoteInfo = "CloudCore-Mac"; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ D5B2E89F1C3A780C00C0327D /* CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D5C629401C3A7FAA007F7B7C /* CloudCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CloudCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D5C6298B1C3A8BBD007F7B7C /* Info-iOS.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-iOS.plist"; sourceTree = ""; }; - D5C6298C1C3A8BBD007F7B7C /* Info-Mac.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-Mac.plist"; sourceTree = ""; }; + D5C6298B1C3A8BBD007F7B7C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D9089D491FE14E57000FC60C /* SetupOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupOperation.swift; sourceTree = ""; }; + D97465F71FE319930060EA66 /* CloudCoreDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudCoreDelegate.swift; sourceTree = ""; }; + D97465F91FE31A650060EA66 /* Module.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Module.swift; sourceTree = ""; }; + D985DE9C1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurgeLocalDatabaseOperation.swift; sourceTree = ""; }; + D985DEA31FE026D400236870 /* CreateCloudCoreZoneOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateCloudCoreZoneOperation.swift; sourceTree = ""; }; + D985DEA71FE0292000236870 /* SubscribeOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeOperation.swift; sourceTree = ""; }; + D985DEAA1FE0335800236870 /* UploadAllLocalDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadAllLocalDataOperation.swift; sourceTree = ""; }; + D985DEAD1FE034A900236870 /* NSManagedObjectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectModel.swift; sourceTree = ""; }; D9B3C6F31FCEF38D00CDB7FF /* TestableApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestableApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; D9B3C6F51FCEF38D00CDB7FF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D9B3C6F71FCEF38D00CDB7FF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -167,12 +126,9 @@ E22C40441E4291FB009469A1 /* CloudCore.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = CloudCore.podspec; sourceTree = ""; }; E22C40451E42956C009469A1 /* CoreDataListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataListener.swift; sourceTree = ""; }; E23C478B1E48A404004310F9 /* CloudSaveOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudSaveOperationQueue.swift; sourceTree = ""; }; - E23C478F1E48A587004310F9 /* CloudCoreErrorDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudCoreErrorDelegate.swift; sourceTree = ""; }; - E23C47911E48B210004310F9 /* NotificationName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationName.swift; sourceTree = ""; }; E247EF8B1E67773F00EBD75E /* ErrorBlockProxyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorBlockProxyTests.swift; sourceTree = ""; }; E247EF951E67873900EBD75E /* DeleteFromCoreDataOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteFromCoreDataOperationTests.swift; sourceTree = ""; }; E247EF981E678EA200EBD75E /* CustomFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctions.swift; sourceTree = ""; }; - E24BD55D1E788C2200D092E6 /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; E24F44A51E4595B900F78819 /* CoreDataRelationship.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataRelationship.swift; sourceTree = ""; }; E24F44A81E459E3E00F78819 /* NSManagedObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObjectTests.swift; sourceTree = ""; }; E2564BFE1E5061BC002E518B /* ErrorBlockProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorBlockProxy.swift; sourceTree = ""; }; @@ -186,17 +142,14 @@ E29BB21D1E433E050020F5B6 /* CKRecordID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CKRecordID.swift; sourceTree = ""; }; E29BB2201E4344E80020F5B6 /* CKRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CKRecord.swift; sourceTree = ""; }; E29BB2221E4346FF0020F5B6 /* NSManagedObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObject.swift; sourceTree = ""; }; - E29BB2281E436F310020F5B6 /* CloudCoreTests-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CloudCoreTests-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + E29BB2281E436F310020F5B6 /* CloudCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CloudCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; E29BB22C1E436F310020F5B6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E29BB2341E436F720020F5B6 /* model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = model.xcdatamodel; sourceTree = ""; }; E29BB2361E4377F80020F5B6 /* CoreDataAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataAttribute.swift; sourceTree = ""; }; E29D11791E69813F00E3DCBF /* CoreDataAttributeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataAttributeTests.swift; sourceTree = ""; }; E29D117B1E69A44C00E3DCBF /* CoreDataRelationshipTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataRelationshipTests.swift; sourceTree = ""; }; - E29D11821E69B30C00E3DCBF /* CloudCoreTests-macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CloudCoreTests-macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; E2A3F9441E69B6EC007A65EB /* ObjectToRecordOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectToRecordOperationTests.swift; sourceTree = ""; }; - E2BB748D1E7EA8690048C129 /* SetupOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupOperation.swift; sourceTree = ""; }; E2C02A0D1E4C99AD001B2871 /* ObjectToRecordConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectToRecordConverter.swift; sourceTree = ""; }; - E2C02A0F1E4CBEBB001B2871 /* FetchDatabaseChangesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchDatabaseChangesOperation.swift; sourceTree = ""; }; E2C02A131E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRecordZoneChangesOperation.swift; sourceTree = ""; }; E2C02A181E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteFromCoreDataOperation.swift; sourceTree = ""; }; E2C3A6D01E4A8EAF009151F3 /* FetchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchResult.swift; sourceTree = ""; }; @@ -216,13 +169,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D5C6293C1C3A7FAA007F7B7C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; D9B3C6F01FCEF38D00CDB7FF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -247,27 +193,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - E29D117F1E69B30C00E3DCBF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - E29D11871E69B30C00E3DCBF /* CloudCore.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ D5B2E8951C3A780C00C0327D = { isa = PBXGroup; children = ( - D9B3C7321FCEFD8100CDB7FF /* TestableApp.entitlements */, D5B2E8A01C3A780C00C0327D /* Products */, - E2FA744B1E76B7AC00C3489D /* Resources */, - D5C629691C3A809D007F7B7C /* Sources */, + D5C629691C3A809D007F7B7C /* Source */, E29BB2291E436F310020F5B6 /* Tests */, E22C40441E4291FB009469A1 /* CloudCore.podspec */, - E24BD55D1E788C2200D092E6 /* Package.swift */, D9B3C7301FCEFC9C00CDB7FF /* Frameworks */, ); sourceTree = ""; @@ -276,39 +211,54 @@ isa = PBXGroup; children = ( D5B2E89F1C3A780C00C0327D /* CloudCore.framework */, - D5C629401C3A7FAA007F7B7C /* CloudCore.framework */, - E29BB2281E436F310020F5B6 /* CloudCoreTests-iOS.xctest */, - E29D11821E69B30C00E3DCBF /* CloudCoreTests-macOS.xctest */, + E29BB2281E436F310020F5B6 /* CloudCoreTests.xctest */, D9B3C6F31FCEF38D00CDB7FF /* TestableApp.app */, D9B3C71C1FCEF96D00CDB7FF /* CloudKitTests.xctest */, ); name = Products; sourceTree = ""; }; - D5C629691C3A809D007F7B7C /* Sources */ = { + D5C629691C3A809D007F7B7C /* Source */ = { isa = PBXGroup; children = ( + E2FA744B1E76B7AC00C3489D /* Resources */, E2075FF11E4BB6EF00E31F1F /* Classes */, E2D507961E464FEA0038B6F8 /* Enum */, E29BB21F1E433FDA0020F5B6 /* Extensions */, E2E296C81E49DA0100E7D6ED /* Model */, E23C47931E48CE54004310F9 /* Protocols */, ); - path = Sources; + path = Source; + sourceTree = ""; + }; + D90811681FE2BE3A00898F24 /* CloudCoreTests */ = { + isa = PBXGroup; + children = ( + E247EF8A1E67771C00EBD75E /* Classes */, + E28F0B9C1E67244A00BF532A /* Extensions */, + E23C47871E487CEA004310F9 /* Model */, + E247EF981E678EA200EBD75E /* CustomFunctions.swift */, + E29BB22C1E436F310020F5B6 /* Info.plist */, + E29BB2331E436F720020F5B6 /* model.xcdatamodeld */, + ); + path = CloudCoreTests; sourceTree = ""; }; - D9B3C6EE1FCEF30600CDB7FF /* Unit */ = { + D9089D481FE14E4A000FC60C /* Setup Operation */ = { isa = PBXGroup; children = ( - E2FA744A1E76B5A100C3489D /* Resources */, - E28FBCDF1E43D8B40081FF3B /* Sources */, + D9089D491FE14E57000FC60C /* SetupOperation.swift */, + D985DEA31FE026D400236870 /* CreateCloudCoreZoneOperation.swift */, + D985DEAA1FE0335800236870 /* UploadAllLocalDataOperation.swift */, + D985DEA71FE0292000236870 /* SubscribeOperation.swift */, ); - path = Unit; + path = "Setup Operation"; sourceTree = ""; }; D9B3C6F41FCEF38D00CDB7FF /* App */ = { isa = PBXGroup; children = ( + D9B3C7321FCEFD8100CDB7FF /* TestableApp.entitlements */, D9B3C6F51FCEF38D00CDB7FF /* AppDelegate.swift */, D9B3C6F71FCEF38D00CDB7FF /* ViewController.swift */, D9B3C6F91FCEF38D00CDB7FF /* Main.storyboard */, @@ -320,24 +270,16 @@ path = App; sourceTree = ""; }; - D9B3C7161FCEF39300CDB7FF /* CloudKit */ = { - isa = PBXGroup; - children = ( - D9B3C72C1FCEFAD900CDB7FF /* Resources */, - D9B3C71D1FCEF96D00CDB7FF /* Sources */, - D9B3C6F41FCEF38D00CDB7FF /* App */, - ); - path = CloudKit; - sourceTree = ""; - }; - D9B3C71D1FCEF96D00CDB7FF /* Sources */ = { + D9B3C7161FCEF39300CDB7FF /* CloudKitTests */ = { isa = PBXGroup; children = ( D9B3C71E1FCEF96D00CDB7FF /* CloudKitTests.swift */, D9B3C7351FCF02F000CDB7FF /* Helpers.swift */, D9B3C73C1FCF0D2700CDB7FF /* CorrectObjectExtension.swift */, + D9B3C72C1FCEFAD900CDB7FF /* Resources */, + D9B3C6F41FCEF38D00CDB7FF /* App */, ); - path = Sources; + path = CloudKitTests; sourceTree = ""; }; D9B3C72C1FCEFAD900CDB7FF /* Resources */ = { @@ -369,16 +311,17 @@ E2075FF11E4BB6EF00E31F1F /* Classes */ = { isa = PBXGroup; children = ( + D9089D481FE14E4A000FC60C /* Setup Operation */, + E2075FF81E4BBEAC00E31F1F /* AsynchronousOperation.swift */, E2075FF31E4BB70D00E31F1F /* Fetch */, - E2075FF21E4BB6F700E31F1F /* Upload */, + E2075FF21E4BB6F700E31F1F /* Save */, E200D44C1E48E13200B707D4 /* CloudCore.swift */, E2564BFE1E5061BC002E518B /* ErrorBlockProxy.swift */, - E2BB748D1E7EA8690048C129 /* SetupOperation.swift */, ); path = Classes; sourceTree = ""; }; - E2075FF21E4BB6F700E31F1F /* Upload */ = { + E2075FF21E4BB6F700E31F1F /* Save */ = { isa = PBXGroup; children = ( E2FA74461E769D8700C3489D /* Model */, @@ -386,7 +329,7 @@ E23C478B1E48A404004310F9 /* CloudSaveOperationQueue.swift */, E22C40451E42956C009469A1 /* CoreDataListener.swift */, ); - path = Upload; + path = Save; sourceTree = ""; }; E2075FF31E4BB70D00E31F1F /* Fetch */ = { @@ -410,7 +353,7 @@ E23C47931E48CE54004310F9 /* Protocols */ = { isa = PBXGroup; children = ( - E23C478F1E48A587004310F9 /* CloudCoreErrorDelegate.swift */, + D97465F71FE319930060EA66 /* CloudCoreDelegate.swift */, ); path = Protocols; sourceTree = ""; @@ -472,24 +415,13 @@ path = Extensions; sourceTree = ""; }; - E28FBCDF1E43D8B40081FF3B /* Sources */ = { - isa = PBXGroup; - children = ( - E247EF8A1E67771C00EBD75E /* Classes */, - E28F0B9C1E67244A00BF532A /* Extensions */, - E23C47871E487CEA004310F9 /* Model */, - E247EF981E678EA200EBD75E /* CustomFunctions.swift */, - ); - path = Sources; - sourceTree = ""; - }; E29BB21F1E433FDA0020F5B6 /* Extensions */ = { isa = PBXGroup; children = ( E29BB21D1E433E050020F5B6 /* CKRecordID.swift */, + D985DEAD1FE034A900236870 /* NSManagedObjectModel.swift */, E2D390071E4A49350019BBCD /* NSEntityDescription.swift */, E29BB2221E4346FF0020F5B6 /* NSManagedObject.swift */, - E23C47911E48B210004310F9 /* NotificationName.swift */, ); path = Extensions; sourceTree = ""; @@ -497,9 +429,9 @@ E29BB2291E436F310020F5B6 /* Tests */ = { isa = PBXGroup; children = ( + D90811681FE2BE3A00898F24 /* CloudCoreTests */, D9B3C7371FCF0C9200CDB7FF /* Shared */, - D9B3C7161FCEF39300CDB7FF /* CloudKit */, - D9B3C6EE1FCEF30600CDB7FF /* Unit */, + D9B3C7161FCEF39300CDB7FF /* CloudKitTests */, ); path = Tests; sourceTree = ""; @@ -525,11 +457,10 @@ E2C02A171E4CDEDA001B2871 /* SubOperations */ = { isa = PBXGroup; children = ( - E2075FF81E4BBEAC00E31F1F /* AsynchronousOperation.swift */, E2C02A181E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift */, - E2C02A0F1E4CBEBB001B2871 /* FetchDatabaseChangesOperation.swift */, E2C02A131E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift */, E21FA03D1E4A7E7200B1DAA2 /* RecordToCoreDataOperation.swift */, + D985DE9C1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift */, ); path = SubOperations; sourceTree = ""; @@ -539,6 +470,7 @@ children = ( E29BB21B1E43381D0020F5B6 /* CloudCoreError.swift */, E2C3A6D01E4A8EAF009151F3 /* FetchResult.swift */, + D97465F91FE31A650060EA66 /* Module.swift */, ); path = Enum; sourceTree = ""; @@ -564,20 +496,10 @@ path = Model; sourceTree = ""; }; - E2FA744A1E76B5A100C3489D /* Resources */ = { - isa = PBXGroup; - children = ( - E29BB22C1E436F310020F5B6 /* Info.plist */, - E29BB2331E436F720020F5B6 /* model.xcdatamodeld */, - ); - path = Resources; - sourceTree = ""; - }; E2FA744B1E76B7AC00C3489D /* Resources */ = { isa = PBXGroup; children = ( - D5C6298C1C3A8BBD007F7B7C /* Info-Mac.plist */, - D5C6298B1C3A8BBD007F7B7C /* Info-iOS.plist */, + D5C6298B1C3A8BBD007F7B7C /* Info.plist */, ); path = Resources; sourceTree = ""; @@ -592,19 +514,12 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D5C6293D1C3A7FAA007F7B7C /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - D5B2E89E1C3A780C00C0327D /* CloudCore-iOS */ = { + D5B2E89E1C3A780C00C0327D /* CloudCore */ = { isa = PBXNativeTarget; - buildConfigurationList = D5B2E8B31C3A780C00C0327D /* Build configuration list for PBXNativeTarget "CloudCore-iOS" */; + buildConfigurationList = D5B2E8B31C3A780C00C0327D /* Build configuration list for PBXNativeTarget "CloudCore" */; buildPhases = ( D5B2E89A1C3A780C00C0327D /* Sources */, D5B2E89B1C3A780C00C0327D /* Frameworks */, @@ -615,29 +530,11 @@ ); dependencies = ( ); - name = "CloudCore-iOS"; + name = CloudCore; productName = CloudCore; productReference = D5B2E89F1C3A780C00C0327D /* CloudCore.framework */; productType = "com.apple.product-type.framework"; }; - D5C6293F1C3A7FAA007F7B7C /* CloudCore-Mac */ = { - isa = PBXNativeTarget; - buildConfigurationList = D5C629511C3A7FAA007F7B7C /* Build configuration list for PBXNativeTarget "CloudCore-Mac" */; - buildPhases = ( - D5C6293B1C3A7FAA007F7B7C /* Sources */, - D5C6293C1C3A7FAA007F7B7C /* Frameworks */, - D5C6293D1C3A7FAA007F7B7C /* Headers */, - D5C6293E1C3A7FAA007F7B7C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "CloudCore-Mac"; - productName = "CloudCore-Mac"; - productReference = D5C629401C3A7FAA007F7B7C /* CloudCore.framework */; - productType = "com.apple.product-type.framework"; - }; D9B3C6F21FCEF38D00CDB7FF /* TestableApp */ = { isa = PBXNativeTarget; buildConfigurationList = D9B3C7101FCEF38D00CDB7FF /* Build configuration list for PBXNativeTarget "TestableApp" */; @@ -673,9 +570,9 @@ productReference = D9B3C71C1FCEF96D00CDB7FF /* CloudKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - E29BB2271E436F310020F5B6 /* CloudCoreTests-iOS */ = { + E29BB2271E436F310020F5B6 /* CloudCoreTests */ = { isa = PBXNativeTarget; - buildConfigurationList = E29BB2301E436F310020F5B6 /* Build configuration list for PBXNativeTarget "CloudCoreTests-iOS" */; + buildConfigurationList = E29BB2301E436F310020F5B6 /* Build configuration list for PBXNativeTarget "CloudCoreTests" */; buildPhases = ( E29BB2241E436F310020F5B6 /* Sources */, E29BB2251E436F310020F5B6 /* Frameworks */, @@ -686,27 +583,9 @@ dependencies = ( E29BB22F1E436F310020F5B6 /* PBXTargetDependency */, ); - name = "CloudCoreTests-iOS"; + name = CloudCoreTests; productName = "CloudCoreTests-iOS"; - productReference = E29BB2281E436F310020F5B6 /* CloudCoreTests-iOS.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - E29D11811E69B30C00E3DCBF /* CloudCoreTests-macOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = E29D118A1E69B30C00E3DCBF /* Build configuration list for PBXNativeTarget "CloudCoreTests-macOS" */; - buildPhases = ( - E29D117E1E69B30C00E3DCBF /* Sources */, - E29D117F1E69B30C00E3DCBF /* Frameworks */, - E29D11801E69B30C00E3DCBF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - E29D11891E69B30C00E3DCBF /* PBXTargetDependency */, - ); - name = "CloudCoreTests-macOS"; - productName = "CloudCoreTests-macOS"; - productReference = E29D11821E69B30C00E3DCBF /* CloudCoreTests-macOS.xctest */; + productReference = E29BB2281E436F310020F5B6 /* CloudCoreTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ @@ -724,10 +603,6 @@ LastSwiftMigration = 0900; ProvisioningStyle = Automatic; }; - D5C6293F1C3A7FAA007F7B7C = { - CreatedOnToolsVersion = 7.2; - ProvisioningStyle = Automatic; - }; D9B3C6F21FCEF38D00CDB7FF = { CreatedOnToolsVersion = 9.1; DevelopmentTeam = 7X2PJ6H6YM; @@ -752,10 +627,6 @@ LastSwiftMigration = 0900; ProvisioningStyle = Automatic; }; - E29D11811E69B30C00E3DCBF = { - CreatedOnToolsVersion = 8.2.1; - ProvisioningStyle = Automatic; - }; }; }; buildConfigurationList = D5B2E8991C3A780C00C0327D /* Build configuration list for PBXProject "CloudCore" */; @@ -771,10 +642,8 @@ projectDirPath = ""; projectRoot = ""; targets = ( - D5B2E89E1C3A780C00C0327D /* CloudCore-iOS */, - D5C6293F1C3A7FAA007F7B7C /* CloudCore-Mac */, - E29BB2271E436F310020F5B6 /* CloudCoreTests-iOS */, - E29D11811E69B30C00E3DCBF /* CloudCoreTests-macOS */, + D5B2E89E1C3A780C00C0327D /* CloudCore */, + E29BB2271E436F310020F5B6 /* CloudCoreTests */, D9B3C6F21FCEF38D00CDB7FF /* TestableApp */, D9B3C71B1FCEF96D00CDB7FF /* CloudKitTests */, ); @@ -789,13 +658,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D5C6293E1C3A7FAA007F7B7C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; D9B3C6F11FCEF38D00CDB7FF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -820,13 +682,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - E29D11801E69B30C00E3DCBF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -835,73 +690,41 @@ buildActionMask = 2147483647; files = ( E21FA03E1E4A7E7200B1DAA2 /* RecordToCoreDataOperation.swift in Sources */, + D97465F81FE319930060EA66 /* CloudCoreDelegate.swift in Sources */, E2E4D8411E76D5A600550CBE /* FetchAndSaveOperation.swift in Sources */, E2C02A141E4CC2A5001B2871 /* FetchRecordZoneChangesOperation.swift in Sources */, - E2C02A101E4CBEBB001B2871 /* FetchDatabaseChangesOperation.swift in Sources */, E2C02A191E4CDEF1001B2871 /* DeleteFromCoreDataOperation.swift in Sources */, E29BB21A1E4334590020F5B6 /* CloudCoreConfig.swift in Sources */, E2EE20071E4E6DCE0060F769 /* ServiceAttributeName.swift in Sources */, E29BB21E1E433E050020F5B6 /* CKRecordID.swift in Sources */, - E23C47901E48A587004310F9 /* CloudCoreErrorDelegate.swift in Sources */, - E23C47921E48B210004310F9 /* NotificationName.swift in Sources */, + D985DEAE1FE034A900236870 /* NSManagedObjectModel.swift in Sources */, E23C478C1E48A404004310F9 /* CloudSaveOperationQueue.swift in Sources */, E2FA74441E769BF900C3489D /* RecordWithDatabase.swift in Sources */, E2C3A6D11E4A8EAF009151F3 /* FetchResult.swift in Sources */, E22C40461E42956C009469A1 /* CoreDataListener.swift in Sources */, E2075FF91E4BBEAC00E31F1F /* AsynchronousOperation.swift in Sources */, E24F44A61E4595B900F78819 /* CoreDataRelationship.swift in Sources */, - E2BB748E1E7EA8690048C129 /* SetupOperation.swift in Sources */, + D9089D4A1FE14E57000FC60C /* SetupOperation.swift in Sources */, + D97465FA1FE31A650060EA66 /* Module.swift in Sources */, E29BB2371E4377F80020F5B6 /* CoreDataAttribute.swift in Sources */, E2E296CA1E49DA0800E7D6ED /* Tokens.swift in Sources */, E2075FFF1E4BCD7E00E31F1F /* ObjectToRecordOperation.swift in Sources */, E2564BFF1E5061BC002E518B /* ErrorBlockProxy.swift in Sources */, + D985DEA41FE026D400236870 /* CreateCloudCoreZoneOperation.swift in Sources */, + D985DEAB1FE0335800236870 /* UploadAllLocalDataOperation.swift in Sources */, E2C02A0E1E4C99AD001B2871 /* ObjectToRecordConverter.swift in Sources */, + D985DE9D1FDFF9D400236870 /* PurgeLocalDatabaseOperation.swift in Sources */, E2FA74481E769D9400C3489D /* RecordIDWithDatabase.swift in Sources */, E29BB2211E4344E80020F5B6 /* CKRecord.swift in Sources */, E2D390081E4A49350019BBCD /* NSEntityDescription.swift in Sources */, E29BB2231E4346FF0020F5B6 /* NSManagedObject.swift in Sources */, E200D44D1E48E13200B707D4 /* CloudCore.swift in Sources */, + D985DEA81FE0292000236870 /* SubscribeOperation.swift in Sources */, E29BB21C1E43381D0020F5B6 /* CloudCoreError.swift in Sources */, E22A53DA1E4A8743009286C0 /* CloudKitAttribute.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - D5C6293B1C3A7FAA007F7B7C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - E2E4D8401E76D5A500550CBE /* FetchAndSaveOperation.swift in Sources */, - E29D11911E69B31E00E3DCBF /* RecordToCoreDataOperation.swift in Sources */, - E28FBCE41E43DE6F0081FF3B /* CloudCoreConfig.swift in Sources */, - E29D11971E69B32900E3DCBF /* ErrorBlockProxy.swift in Sources */, - E29D118E1E69B31E00E3DCBF /* DeleteFromCoreDataOperation.swift in Sources */, - E23C47981E48CE9C004310F9 /* CloudSaveOperationQueue.swift in Sources */, - E29D11991E69B33100E3DCBF /* NSEntityDescription.swift in Sources */, - E29D11931E69B32400E3DCBF /* ObjectToRecordConverter.swift in Sources */, - E23C47971E48CE9C004310F9 /* NotificationName.swift in Sources */, - E29D11981E69B32E00E3DCBF /* FetchResult.swift in Sources */, - E2FA74451E769BF900C3489D /* RecordWithDatabase.swift in Sources */, - E28FBCEC1E43DE760081FF3B /* CoreDataAttribute.swift in Sources */, - E29D119A1E69B33400E3DCBF /* CloudKitAttribute.swift in Sources */, - E28FBCEA1E43DE760081FF3B /* CoreDataListener.swift in Sources */, - E23C47951E48CE9C004310F9 /* CoreDataRelationship.swift in Sources */, - E2BB748F1E7EB2B00048C129 /* SetupOperation.swift in Sources */, - E29D11951E69B32900E3DCBF /* CloudCore.swift in Sources */, - E28FBCE31E43DE6F0081FF3B /* CloudCoreError.swift in Sources */, - E29D119C1E69B33400E3DCBF /* Tokens.swift in Sources */, - E28FBCE21E43DE6F0081FF3B /* NSManagedObject.swift in Sources */, - E29D11941E69B32400E3DCBF /* ObjectToRecordOperation.swift in Sources */, - E2FA74491E769D9400C3489D /* RecordIDWithDatabase.swift in Sources */, - E23C47941E48CE9C004310F9 /* CloudCoreErrorDelegate.swift in Sources */, - E28FBCE01E43DE6F0081FF3B /* CKRecordID.swift in Sources */, - E29D118F1E69B31E00E3DCBF /* FetchDatabaseChangesOperation.swift in Sources */, - E28FBCE11E43DE6F0081FF3B /* CKRecord.swift in Sources */, - E29D118D1E69B31E00E3DCBF /* AsynchronousOperation.swift in Sources */, - E29D11901E69B31E00E3DCBF /* FetchRecordZoneChangesOperation.swift in Sources */, - E29D119B1E69B33400E3DCBF /* ServiceAttributeName.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; D9B3C6EF1FCEF38D00CDB7FF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -946,27 +769,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - E29D117E1E69B30C00E3DCBF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D9B3C73B1FCF0CA900CDB7FF /* CoreDataTestCase.swift in Sources */, - E29D119F1E69B36700E3DCBF /* RecordToCoreDataOperationTests.swift in Sources */, - E29D11A01E69B36700E3DCBF /* CoreDataAttributeTests.swift in Sources */, - E29D11A51E69B36700E3DCBF /* CKRecordTests.swift in Sources */, - E2A3F9461E69B6EC007A65EB /* ObjectToRecordOperationTests.swift in Sources */, - E29D119E1E69B36700E3DCBF /* DeleteFromCoreDataOperationTests.swift in Sources */, - E29D11A11E69B36700E3DCBF /* CoreDataRelationshipTests.swift in Sources */, - E29D11A81E69B36700E3DCBF /* CustomFunctions.swift in Sources */, - E29D11A21E69B36700E3DCBF /* CKRecordIDTests.swift in Sources */, - E29D11A91E69B3B200E3DCBF /* model.xcdatamodeld in Sources */, - E29D11A31E69B36700E3DCBF /* NSEntityDescriptionTests.swift in Sources */, - D9B3C7381FCF0C9E00CDB7FF /* CorrectObject.swift in Sources */, - E29D119D1E69B36700E3DCBF /* ErrorBlockProxyTests.swift in Sources */, - E29D11A41E69B36700E3DCBF /* NSManagedObjectTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -977,14 +779,9 @@ }; E29BB22F1E436F310020F5B6 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = D5B2E89E1C3A780C00C0327D /* CloudCore-iOS */; + target = D5B2E89E1C3A780C00C0327D /* CloudCore */; targetProxy = E29BB22E1E436F310020F5B6 /* PBXContainerItemProxy */; }; - E29D11891E69B30C00E3DCBF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D5C6293F1C3A7FAA007F7B7C /* CloudCore-Mac */; - targetProxy = E29D11881E69B30C00E3DCBF /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -1056,10 +853,10 @@ MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "macosx watchsimulator watchos appletvsimulator appletvos iphonesimulator iphoneos"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 3.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -1107,10 +904,10 @@ IPHONEOS_DEPLOYMENT_TARGET = 10.0; MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "macosx watchsimulator watchos appletvsimulator appletvos iphonesimulator iphoneos"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 3.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3,4"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -1127,16 +924,18 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "$(SRCROOT)/Resources/Info-iOS.plist"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Resources/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "uvasily.CloudCore-iOS"; + PRODUCT_BUNDLE_IDENTIFIER = uvasily.CloudCore; PRODUCT_NAME = CloudCore; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; + TVOS_DEPLOYMENT_TARGET = 10.0; + WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Debug; }; @@ -1150,64 +949,17 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "$(SRCROOT)/Resources/Info-iOS.plist"; + INFOPLIST_FILE = "$(SRCROOT)/Source/Resources/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "uvasily.CloudCore-iOS"; + PRODUCT_BUNDLE_IDENTIFIER = uvasily.CloudCore; PRODUCT_NAME = CloudCore; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; - }; - name = Release; - }; - D5C629521C3A7FAA007F7B7C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = "$(SRCROOT)/Resources/Info-Mac.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "uvasily.CloudCore-Mac"; - PRODUCT_NAME = CloudCore; - PROVISIONING_PROFILE_SPECIFIER = ""; - SDKROOT = macosx; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - }; - name = Debug; - }; - D5C629531C3A7FAA007F7B7C /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = "$(SRCROOT)/Resources/Info-Mac.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "uvasily.CloudCore-Mac"; - PRODUCT_NAME = CloudCore; - PROVISIONING_PROFILE_SPECIFIER = ""; - SDKROOT = macosx; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; + TVOS_DEPLOYMENT_TARGET = 10.0; + WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Release; }; @@ -1220,13 +972,13 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = TestableApp.entitlements; + CODE_SIGN_ENTITLEMENTS = Tests/CloudKitTests/App/TestableApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7X2PJ6H6YM; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_SYMBOLS_PRIVATE_EXTERN = NO; - INFOPLIST_FILE = Tests/CloudKit/App/Info.plist; + INFOPLIST_FILE = Tests/CloudKitTests/App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = cloudtests.TestableApp; @@ -1246,13 +998,13 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = TestableApp.entitlements; + CODE_SIGN_ENTITLEMENTS = Tests/CloudKitTests/App/TestableApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7X2PJ6H6YM; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_SYMBOLS_PRIVATE_EXTERN = NO; - INFOPLIST_FILE = Tests/CloudKit/App/Info.plist; + INFOPLIST_FILE = Tests/CloudKitTests/App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = cloudtests.TestableApp; @@ -1275,7 +1027,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7X2PJ6H6YM; GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = Tests/CloudKit/Resources/Info.plist; + INFOPLIST_FILE = Tests/CloudKitTests/Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = cloudtests.CloudKitTests; @@ -1300,7 +1052,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7X2PJ6H6YM; GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = Tests/CloudKit/Resources/Info.plist; + INFOPLIST_FILE = Tests/CloudKitTests/Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = cloudtests.CloudKitTests; @@ -1319,10 +1071,11 @@ CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = Tests/Unit/Resources/Info.plist; + INFOPLIST_FILE = Tests/CloudCoreTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "uvasily.CloudCoreTests-iOS"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks @loader_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = uvasily.CloudCoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; @@ -1338,10 +1091,11 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CODE_SIGN_IDENTITY = ""; DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = Tests/Unit/Resources/Info.plist; + INFOPLIST_FILE = Tests/CloudCoreTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "uvasily.CloudCoreTests-iOS"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks @loader_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = uvasily.CloudCoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_SWIFT3_OBJC_INFERENCE = Off; @@ -1349,42 +1103,6 @@ }; name = Release; }; - E29D118B1E69B30C00E3DCBF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = Tests/Unit/Resources/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "uvasily.CloudCoreTests-macOS"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 4.0; - }; - name = Debug; - }; - E29D118C1E69B30C00E3DCBF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CODE_SIGN_IDENTITY = ""; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = Tests/Unit/Resources/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "uvasily.CloudCoreTests-macOS"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = macosx; - SWIFT_VERSION = 4.0; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1397,7 +1115,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - D5B2E8B31C3A780C00C0327D /* Build configuration list for PBXNativeTarget "CloudCore-iOS" */ = { + D5B2E8B31C3A780C00C0327D /* Build configuration list for PBXNativeTarget "CloudCore" */ = { isa = XCConfigurationList; buildConfigurations = ( D5B2E8B41C3A780C00C0327D /* Debug */, @@ -1406,15 +1124,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - D5C629511C3A7FAA007F7B7C /* Build configuration list for PBXNativeTarget "CloudCore-Mac" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D5C629521C3A7FAA007F7B7C /* Debug */, - D5C629531C3A7FAA007F7B7C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; D9B3C7101FCEF38D00CDB7FF /* Build configuration list for PBXNativeTarget "TestableApp" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1433,7 +1142,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - E29BB2301E436F310020F5B6 /* Build configuration list for PBXNativeTarget "CloudCoreTests-iOS" */ = { + E29BB2301E436F310020F5B6 /* Build configuration list for PBXNativeTarget "CloudCoreTests" */ = { isa = XCConfigurationList; buildConfigurations = ( E29BB2311E436F310020F5B6 /* Debug */, @@ -1442,15 +1151,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - E29D118A1E69B30C00E3DCBF /* Build configuration list for PBXNativeTarget "CloudCoreTests-macOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - E29D118B1E69B30C00E3DCBF /* Debug */, - E29D118C1E69B30C00E3DCBF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ diff --git a/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore-Mac.xcscheme b/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore-Mac.xcscheme deleted file mode 100755 index 99eb4994..00000000 --- a/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore-Mac.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore-iOS.xcscheme b/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore.xcscheme old mode 100755 new mode 100644 similarity index 92% rename from CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore-iOS.xcscheme rename to CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore.xcscheme index a36153e0..ac2a4ade --- a/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore-iOS.xcscheme +++ b/CloudCore.xcodeproj/xcshareddata/xcschemes/CloudCore.xcscheme @@ -16,7 +16,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "D5B2E89E1C3A780C00C0327D" BuildableName = "CloudCore.framework" - BlueprintName = "CloudCore-iOS" + BlueprintName = "CloudCore" ReferencedContainer = "container:CloudCore.xcodeproj"> @@ -35,8 +35,8 @@ @@ -46,7 +46,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "D5B2E89E1C3A780C00C0327D" BuildableName = "CloudCore.framework" - BlueprintName = "CloudCore-iOS" + BlueprintName = "CloudCore" ReferencedContainer = "container:CloudCore.xcodeproj"> @@ -69,7 +69,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "D5B2E89E1C3A780C00C0327D" BuildableName = "CloudCore.framework" - BlueprintName = "CloudCore-iOS" + BlueprintName = "CloudCore" ReferencedContainer = "container:CloudCore.xcodeproj"> @@ -87,7 +87,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "D5B2E89E1C3A780C00C0327D" BuildableName = "CloudCore.framework" - BlueprintName = "CloudCore-iOS" + BlueprintName = "CloudCore" ReferencedContainer = "container:CloudCore.xcodeproj"> diff --git a/Example/CloudCoreExample.xcodeproj/project.pbxproj b/Example/CloudCoreExample.xcodeproj/project.pbxproj index a4b6a352..8fc53242 100644 --- a/Example/CloudCoreExample.xcodeproj/project.pbxproj +++ b/Example/CloudCoreExample.xcodeproj/project.pbxproj @@ -7,10 +7,14 @@ objects = { /* Begin PBXBuildFile section */ + B4532A37427BB629A3A47821 /* Pods_CloudCoreExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5710AB9C0BE90A85D15BCD9F /* Pods_CloudCoreExample.framework */; }; + D97438161FE168D800650541 /* FRCTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97438151FE168D800650541 /* FRCTableViewDataSource.swift */; }; + D974381D1FE16E6E00650541 /* ModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D974381C1FE16E6E00650541 /* ModelFactory.swift */; }; + D974381F1FE18ED100650541 /* NotificationsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D974381E1FE18ED100650541 /* NotificationsObserver.swift */; }; + D97438231FE199F500650541 /* EmployeeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97438221FE199F500650541 /* EmployeeTableViewCell.swift */; }; E23BE70C1EA4FD78008F4F23 /* CloudCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E23BE6FB1EA4CC1C008F4F23 /* CloudCore.framework */; }; E23BE70D1EA4FD78008F4F23 /* CloudCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E23BE6FB1EA4CC1C008F4F23 /* CloudCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E261E0581EAFEA8A00F1CA61 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E261E0571EAFEA8A00F1CA61 /* CloudKit.framework */; }; - E26BDD991E75A0AC00994CE7 /* MasterViewController+FRC.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26BDD981E75A0AC00994CE7 /* MasterViewController+FRC.swift */; }; E2C3E3541E53299800A733BF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C3E3531E53299800A733BF /* AppDelegate.swift */; }; E2C3E3571E53299800A733BF /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E2C3E3551E53299800A733BF /* Model.xcdatamodeld */; }; E2C3E3591E53299800A733BF /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C3E3581E53299800A733BF /* MasterViewController.swift */; }; @@ -21,6 +25,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + D9DC6DC01FDFEFF100017652 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = D9B3C6F31FCEF38D00CDB7FF; + remoteInfo = TestableApp; + }; + D9DC6DC21FDFEFF100017652 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = D9B3C71C1FCEF96D00CDB7FF; + remoteInfo = CloudKitTests; + }; E23BE6FA1EA4CC1C008F4F23 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */; @@ -73,9 +91,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2AD0596598E464554C061BBB /* Pods-CloudCoreExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCoreExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample.release.xcconfig"; sourceTree = ""; }; + 5710AB9C0BE90A85D15BCD9F /* Pods_CloudCoreExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CloudCoreExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D97438151FE168D800650541 /* FRCTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRCTableViewDataSource.swift; sourceTree = ""; }; + D974381C1FE16E6E00650541 /* ModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelFactory.swift; sourceTree = ""; }; + D974381E1FE18ED100650541 /* NotificationsObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsObserver.swift; sourceTree = ""; }; + D97438221FE199F500650541 /* EmployeeTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmployeeTableViewCell.swift; sourceTree = ""; }; E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = CloudCore.xcodeproj; path = ../CloudCore.xcodeproj; sourceTree = ""; }; E261E0571EAFEA8A00F1CA61 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; - E26BDD981E75A0AC00994CE7 /* MasterViewController+FRC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MasterViewController+FRC.swift"; sourceTree = ""; }; E2C3E3501E53299800A733BF /* CloudCoreExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CloudCoreExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; E2C3E3531E53299800A733BF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; E2C3E3561E53299800A733BF /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; @@ -86,6 +109,7 @@ E2C3E3621E53299800A733BF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; E2C3E3641E53299800A733BF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E2C3E36A1E532A6E00A733BF /* CloudCoreExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CloudCoreExample.entitlements; sourceTree = ""; }; + E9E8795228293D77FA3CBD0F /* Pods-CloudCoreExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CloudCoreExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -95,12 +119,49 @@ files = ( E23BE70C1EA4FD78008F4F23 /* CloudCore.framework in Frameworks */, E261E0581EAFEA8A00F1CA61 /* CloudKit.framework in Frameworks */, + B4532A37427BB629A3A47821 /* Pods_CloudCoreExample.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + BFBADA7DFB65C4DA1FA75BBE /* Pods */ = { + isa = PBXGroup; + children = ( + E9E8795228293D77FA3CBD0F /* Pods-CloudCoreExample.debug.xcconfig */, + 2AD0596598E464554C061BBB /* Pods-CloudCoreExample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + D97438201FE1919300650541 /* View Controller */ = { + isa = PBXGroup; + children = ( + E2C3E3581E53299800A733BF /* MasterViewController.swift */, + E2C3E35A1E53299800A733BF /* DetailViewController.swift */, + ); + path = "View Controller"; + sourceTree = ""; + }; + D97438211FE191A500650541 /* Class */ = { + isa = PBXGroup; + children = ( + D974381E1FE18ED100650541 /* NotificationsObserver.swift */, + D974381C1FE16E6E00650541 /* ModelFactory.swift */, + D97438151FE168D800650541 /* FRCTableViewDataSource.swift */, + ); + path = Class; + sourceTree = ""; + }; + D97438241FE19A0E00650541 /* View */ = { + isa = PBXGroup; + children = ( + D97438221FE199F500650541 /* EmployeeTableViewCell.swift */, + ); + path = View; + sourceTree = ""; + }; E23BE6F41EA4CC1C008F4F23 /* Products */ = { isa = PBXGroup; children = ( @@ -108,6 +169,8 @@ E23BE6FD1EA4CC1C008F4F23 /* CloudCore.framework */, E23BE6FF1EA4CC1C008F4F23 /* CloudCoreTests-iOS.xctest */, E23BE7011EA4CC1C008F4F23 /* CloudCoreTests-macOS.xctest */, + D9DC6DC11FDFEFF100017652 /* TestableApp.app */, + D9DC6DC31FDFEFF100017652 /* CloudKitTests.xctest */, ); name = Products; sourceTree = ""; @@ -133,6 +196,7 @@ E26BDD971E759D5E00994CE7 /* Resources */, E2C3E3521E53299800A733BF /* Sources */, E23BE6F31EA4CC1C008F4F23 /* CloudCore.xcodeproj */, + BFBADA7DFB65C4DA1FA75BBE /* Pods */, ); sourceTree = ""; }; @@ -148,9 +212,9 @@ isa = PBXGroup; children = ( E2C3E3531E53299800A733BF /* AppDelegate.swift */, - E2C3E35A1E53299800A733BF /* DetailViewController.swift */, - E26BDD981E75A0AC00994CE7 /* MasterViewController+FRC.swift */, - E2C3E3581E53299800A733BF /* MasterViewController.swift */, + D97438201FE1919300650541 /* View Controller */, + D97438211FE191A500650541 /* Class */, + D97438241FE19A0E00650541 /* View */, ); path = Sources; sourceTree = ""; @@ -159,6 +223,7 @@ isa = PBXGroup; children = ( E261E0571EAFEA8A00F1CA61 /* CloudKit.framework */, + 5710AB9C0BE90A85D15BCD9F /* Pods_CloudCoreExample.framework */, ); name = Frameworks; sourceTree = ""; @@ -170,10 +235,13 @@ isa = PBXNativeTarget; buildConfigurationList = E2C3E3671E53299800A733BF /* Build configuration list for PBXNativeTarget "CloudCoreExample" */; buildPhases = ( + 74EEB05A875696C8E3F6398D /* [CP] Check Pods Manifest.lock */, E2C3E34C1E53299800A733BF /* Sources */, E2C3E34D1E53299800A733BF /* Frameworks */, E2C3E34E1E53299800A733BF /* Resources */, E23BE7101EA4FD78008F4F23 /* Embed Frameworks */, + D0F793BBB16236A031D0746F /* [CP] Embed Pods Frameworks */, + 141EBAF67A939CF3C08FF52E /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -197,6 +265,7 @@ TargetAttributes = { E2C3E34F1E53299800A733BF = { CreatedOnToolsVersion = 8.2.1; + DevelopmentTeam = 7X2PJ6H6YM; LastSwiftMigration = 0900; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -238,6 +307,20 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ + D9DC6DC11FDFEFF100017652 /* TestableApp.app */ = { + isa = PBXReferenceProxy; + fileType = wrapper.application; + path = TestableApp.app; + remoteRef = D9DC6DC01FDFEFF100017652 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + D9DC6DC31FDFEFF100017652 /* CloudKitTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = CloudKitTests.xctest; + remoteRef = D9DC6DC21FDFEFF100017652 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; E23BE6FB1EA4CC1C008F4F23 /* CloudCore.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; @@ -281,14 +364,71 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 141EBAF67A939CF3C08FF52E /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 74EEB05A875696C8E3F6398D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-CloudCoreExample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D0F793BBB16236A031D0746F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Fakery/Fakery.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Fakery.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CloudCoreExample/Pods-CloudCoreExample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ E2C3E34C1E53299800A733BF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D974381F1FE18ED100650541 /* NotificationsObserver.swift in Sources */, + D97438231FE199F500650541 /* EmployeeTableViewCell.swift in Sources */, E2C3E3571E53299800A733BF /* Model.xcdatamodeld in Sources */, E2C3E3541E53299800A733BF /* AppDelegate.swift in Sources */, - E26BDD991E75A0AC00994CE7 /* MasterViewController+FRC.swift in Sources */, + D974381D1FE16E6E00650541 /* ModelFactory.swift in Sources */, + D97438161FE168D800650541 /* FRCTableViewDataSource.swift in Sources */, E2C3E3591E53299800A733BF /* MasterViewController.swift in Sources */, E2C3E35B1E53299800A733BF /* DetailViewController.swift in Sources */, ); @@ -431,14 +571,16 @@ }; E2C3E3681E53299800A733BF /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = E9E8795228293D77FA3CBD0F /* Pods-CloudCoreExample.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Resources/CloudCoreExample.entitlements; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 7X2PJ6H6YM; INFOPLIST_FILE = Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = changeMe.change2; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; @@ -447,14 +589,16 @@ }; E2C3E3691E53299800A733BF /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 2AD0596598E464554C061BBB /* Pods-CloudCoreExample.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Resources/CloudCoreExample.entitlements; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 7X2PJ6H6YM; INFOPLIST_FILE = Resources/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = changeMe.change2; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; diff --git a/Example/CloudCoreExample.xcworkspace/contents.xcworkspacedata b/Example/CloudCoreExample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..8893373a --- /dev/null +++ b/Example/CloudCoreExample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Example/Podfile b/Example/Podfile new file mode 100644 index 00000000..f614da88 --- /dev/null +++ b/Example/Podfile @@ -0,0 +1,10 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '10.0' + +target 'CloudCoreExample' do + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + + # Pods for CloudCoreExample + pod 'Fakery', '~> 3.3.0' +end diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index b8236c65..19882d56 100644 --- a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -39,6 +39,11 @@ "idiom" : "iphone", "size" : "60x60", "scale" : "3x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" } ], "info" : { diff --git a/Example/Resources/Assets.xcassets/avatar_1.imageset/Contents.json b/Example/Resources/Assets.xcassets/avatar_1.imageset/Contents.json new file mode 100644 index 00000000..5b12237b --- /dev/null +++ b/Example/Resources/Assets.xcassets/avatar_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "avatar_1.jpg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/avatar_1.imageset/avatar_1.jpg b/Example/Resources/Assets.xcassets/avatar_1.imageset/avatar_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..43ece19071e88b9ac992ea034fc87af1f5d1d479 GIT binary patch literal 5488 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 Y*^#VU*L^0Jma=AZ8u0OpMC`Z!0zH7!Jpcdz literal 0 HcmV?d00001 diff --git a/Example/Resources/Assets.xcassets/avatar_5.imageset/Contents.json b/Example/Resources/Assets.xcassets/avatar_5.imageset/Contents.json new file mode 100644 index 00000000..c9eaffed --- /dev/null +++ b/Example/Resources/Assets.xcassets/avatar_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "avatar_5.jpg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/avatar_5.imageset/avatar_5.jpg b/Example/Resources/Assets.xcassets/avatar_5.imageset/avatar_5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fcd6723d4dbf01389d7bb2a340d9e07b17bdf1de GIT binary patch literal 6380 zcmb7o_ct6~^zDpZqW2o26TOQrgwaQ@i5kM_M9B!EccYi+jNS(`2|~1tAX@Y?N<@j? zNj#tT{k}io?Yq`JXWg^UKKsYB?)}{T8h{+6p``%;0)YU{hXc4@1gHV9vHrtF904rSi_>grT{1bcIg-!fG2}CYx2>e7z4!hBWRCu zBKZdk+#qHsk9!c6!w7@z*T2GvPnVtbM;MLZ?BU4O5ms1*G)t!ZZe3(D$2+4<)5SyL z6KXJlP^#V+VABnB4A@vh{e8+Ou{`YiUZJ*gvxR-(*T&&@98p|DH1CIt4yT9)p(dVE zzfAXh=YqToU;1l?39moCjeK`~>)$IZZl<&@_ep||x@l_jTL$I(EjUcSBJ5`43`u<> zkUU$H%tcAP&dsf4Su2`GI-PGZkWcg4N|!Rk&V{ROCZ�(GvPP71e~vB|=ab4)HM%L&p~Q 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 KU~=Ss{{H|gNflNA literal 0 HcmV?d00001 diff --git a/Example/Resources/Assets.xcassets/avatar_6.imageset/Contents.json b/Example/Resources/Assets.xcassets/avatar_6.imageset/Contents.json new file mode 100644 index 00000000..1b365680 --- /dev/null +++ b/Example/Resources/Assets.xcassets/avatar_6.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "avatar_6.jpg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/avatar_6.imageset/avatar_6.jpg b/Example/Resources/Assets.xcassets/avatar_6.imageset/avatar_6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1de43e9256f271d1f72293a4448f26935a260c57 GIT binary patch literal 4344 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 ZG1L^Bt#Spj6lpW#wX#22F4%xJ^*=-f|E2%{ literal 0 HcmV?d00001 diff --git a/Example/Resources/Assets.xcassets/avatar_7.imageset/Contents.json b/Example/Resources/Assets.xcassets/avatar_7.imageset/Contents.json new file mode 100644 index 00000000..2a103867 --- /dev/null +++ b/Example/Resources/Assets.xcassets/avatar_7.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "avatar_7.jpg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/avatar_7.imageset/avatar_7.jpg b/Example/Resources/Assets.xcassets/avatar_7.imageset/avatar_7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eb41bf6e4b2ed2b373f19522228359a353b336d2 GIT binary patch literal 3218 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 literal 0 HcmV?d00001 diff --git a/Example/Resources/Assets.xcassets/avatar_8.imageset/Contents.json b/Example/Resources/Assets.xcassets/avatar_8.imageset/Contents.json new file mode 100644 index 00000000..92393841 --- /dev/null +++ b/Example/Resources/Assets.xcassets/avatar_8.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "avatar_8.jpg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/avatar_8.imageset/avatar_8.jpg b/Example/Resources/Assets.xcassets/avatar_8.imageset/avatar_8.jpg new file mode 100644 index 0000000000000000000000000000000000000000..606b1a6f7930f9a4c5490641545c75705592126e GIT binary patch literal 6741 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 Bg24a) literal 0 HcmV?d00001 diff --git a/Example/Resources/Assets.xcassets/avatar_9.imageset/Contents.json b/Example/Resources/Assets.xcassets/avatar_9.imageset/Contents.json new file mode 100644 index 00000000..dc067da6 --- /dev/null +++ b/Example/Resources/Assets.xcassets/avatar_9.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "avatar_9.jpg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Resources/Assets.xcassets/avatar_9.imageset/avatar_9.jpg b/Example/Resources/Assets.xcassets/avatar_9.imageset/avatar_9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..84791099891eca1ac3243e43dcfe8a4e6a1e41e9 GIT binary patch literal 3747 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 B2?_uJ literal 0 HcmV?d00001 diff --git a/Example/Resources/Base.lproj/Main.storyboard b/Example/Resources/Base.lproj/Main.storyboard index aae6b60f..eb869eb0 100644 --- a/Example/Resources/Base.lproj/Main.storyboard +++ b/Example/Resources/Base.lproj/Main.storyboard @@ -1,109 +1,137 @@ - + - + + + - - - - - - - - - - - - - - - - - - + + - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - - + + + + @@ -113,36 +141,54 @@ - - + + + - + + + + + + + + + + + + + + + + - + - + - - - + + + + + - + - + - + - + diff --git a/Example/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents b/Example/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents index 348c9d19..4bec3c35 100644 --- a/Example/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Example/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,38 +1,39 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - + + + + - + - - + + + + + - - + + \ No newline at end of file diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index 662199fd..7251cea8 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -13,28 +13,17 @@ import CloudCore let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate, CloudCoreErrorDelegate { - - // MARK: - CloudCore - - func cloudCore(saveToCloudDidFailed error: Error) { - print("SaveToCloudDidFailed: \(error)") - } +class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate { + let delegateHandler = CloudCoreDelegateHandler() + func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { // Register for push notifications about changes - UIApplication.shared.registerForRemoteNotifications() + application.registerForRemoteNotifications() // Enable uploading changed local data to CoreData - CloudCore.observeCoreDataChanges(persistentContainer: self.persistentContainer, errorDelegate: self) - - // Sync on startup if push notifications is missed, disabled etc - // Also it acts as initial sync if no sync was done before - CloudCore.fetchAndSave(container: persistentContainer, error: { (error) in - print("\(error)") - }) { - NSLog("On-startup sync completed") - } + CloudCore.delegate = delegateHandler + CloudCore.enable(persistentContainer: persistentContainer) return true } @@ -44,7 +33,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele // Check if it CloudKit's and CloudCore notification if CloudCore.isCloudCoreNotification(withUserInfo: userInfo) { // Fetch changed data from iCloud - CloudCore.fetchAndSave(using: userInfo, container: self.persistentContainer, error: nil, completion: { (fetchResult) in + CloudCore.fetchAndSave(using: userInfo, to: persistentContainer, error: { + print("fetchAndSave from didReceiveRemoteNotification error: \($0)") + }, completion: { (fetchResult) in completionHandler(fetchResult.uiBackgroundFetchResult) }) } @@ -60,33 +51,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - let splitViewController = self.window!.rootViewController as! UISplitViewController - let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController - navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem - splitViewController.delegate = self - - self.persistentContainer.viewContext.automaticallyMergesChangesFromParent = true - - let masterNavigationController = splitViewController.viewControllers[0] as! UINavigationController - let controller = masterNavigationController.topViewController as! MasterViewController - controller.managedObjectContext = self.persistentContainer.viewContext - return true } - - - // MARK: Split view - - func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool { - guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false } - guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false } - if topAsDetailController.detailItem == nil { - // Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded. - return true - } - return false - } // MARK: Core Data stack @@ -114,6 +80,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele fatalError("Unresolved error \(error), \(error.userInfo)") } }) + container.viewContext.automaticallyMergesChangesFromParent = true return container }() diff --git a/Example/Sources/Class/FRCTableViewDataSource.swift b/Example/Sources/Class/FRCTableViewDataSource.swift new file mode 100644 index 00000000..3abfcd47 --- /dev/null +++ b/Example/Sources/Class/FRCTableViewDataSource.swift @@ -0,0 +1,102 @@ +// FRCTableViewDataSource.swift +// Gist from: https://gist.github.com/Sorix/987af88f82c95ff8c30b51b6a5620657 + +import UIKit +import CoreData + +protocol FRCTableViewDelegate: class { + func frcTableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell +} + +class FRCTableViewDataSource: NSObject, UITableViewDataSource, NSFetchedResultsControllerDelegate { + + let frc: NSFetchedResultsController + weak var tableView: UITableView? + weak var delegate: FRCTableViewDelegate? + + init(fetchRequest: NSFetchRequest, context: NSManagedObjectContext, sectionNameKeyPath: String?) { + frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: sectionNameKeyPath, cacheName: nil) + + super.init() + + frc.delegate = self + } + + convenience init(fetchRequest: NSFetchRequest, context: NSManagedObjectContext, sectionNameKeyPath: String?, delegate: FRCTableViewDelegate, tableView: UITableView) { + self.init(fetchRequest: fetchRequest, context: context, sectionNameKeyPath: sectionNameKeyPath) + + self.delegate = delegate + self.tableView = tableView + } + + func performFetch() throws { + try frc.performFetch() + } + + func object(at indexPath: IndexPath) -> FetchRequestResult { + return frc.object(at: indexPath) + } + + // MARK: - UITableViewDataSource + + func numberOfSections(in tableView: UITableView) -> Int { + return frc.sections?.count ?? 0 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard let sections = frc.sections else { return 0 } + + return sections[section].numberOfObjects + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if let delegate = delegate { + return delegate.frcTableView(tableView, cellForRowAt: indexPath) + } else { + return UITableViewCell() + } + } + + // MARK: - NSFetchedResultsControllerDelegate + + func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + tableView?.beginUpdates() + } + + func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { + let sectionIndexSet = IndexSet(integer: sectionIndex) + + switch type { + case .insert: tableView?.insertSections(sectionIndexSet, with: .automatic) + case .delete: tableView?.deleteSections(sectionIndexSet, with: .automatic) + case .update: tableView?.reloadSections(sectionIndexSet, with: .automatic) + case .move: break + } + } + + func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + switch type { + case .insert: + guard let newIndexPath = newIndexPath else { break } + tableView?.insertRows(at: [newIndexPath], with: .automatic) + case .delete: + guard let indexPath = indexPath else { break } + tableView?.deleteRows(at: [indexPath], with: .automatic) + case .update: + guard let indexPath = indexPath else { break } + tableView?.reloadRows(at: [indexPath], with: .automatic) + case .move: + guard let indexPath = indexPath, let newIndexPath = newIndexPath else { return } + tableView?.moveRow(at: indexPath, to: newIndexPath) + } + } + + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + tableView?.endUpdates() + } + + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {} + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return nil } + +} + diff --git a/Example/Sources/Class/ModelFactory.swift b/Example/Sources/Class/ModelFactory.swift new file mode 100644 index 00000000..66a29dfd --- /dev/null +++ b/Example/Sources/Class/ModelFactory.swift @@ -0,0 +1,64 @@ +// +// ModelFactory.swift +// CloudCoreExample +// +// Created by Vasily Ulianov on 13/12/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import Foundation +import Fakery +import CoreData + +class ModelFactory { + + private static let faker: Faker = { + let locale = Locale.preferredLanguages.first ?? "en" + return Faker(locale: locale) + }() + + @discardableResult + static func insertOrganizationWithEmployees(context: NSManagedObjectContext) -> Organization { + let org = self.insertOrganization(context: context) + org.sort = Int32(faker.number.randomInt(min: 1, max: 1000)) + + for _ in 0...faker.number.randomInt(min: 0, max: 3) { + let user = self.insertEmployee(context: context) + user.organization = org + } + + return org + } + + // MARK: - Private methods + + private static func insertOrganization(context: NSManagedObjectContext) -> Organization { + let org = Organization(context: context) + org.name = faker.company.name() + org.bs = faker.company.bs() + org.founded = Date(timeIntervalSince1970: faker.number.randomDouble(min: 1292250324, max: 1513175137)) + + return org + } + + static func insertEmployee(context: NSManagedObjectContext) -> Employee { + let user = Employee(context: context) + user.department = faker.commerce.department() + user.name = faker.name.name() + user.workingSince = Date(timeIntervalSince1970: faker.number.randomDouble(min: 661109847, max: 1513186653)) + user.photoData = randomAvatar() + + return user + } + + private static func randomAvatar() -> Data? { + let randomNumber = String(faker.number.randomInt(min: 1, max: 9)) + let image = UIImage(named: "avatar_" + randomNumber)! + return UIImagePNGRepresentation(image) + } + + static func newCompanyName() -> String { + return faker.company.name() + } + +} diff --git a/Example/Sources/Class/NotificationsObserver.swift b/Example/Sources/Class/NotificationsObserver.swift new file mode 100644 index 00000000..0cc34643 --- /dev/null +++ b/Example/Sources/Class/NotificationsObserver.swift @@ -0,0 +1,35 @@ +// +// NotificationsObserver.swift +// CloudCoreExample +// +// Created by Vasily Ulianov on 13/12/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import Foundation +import CloudCore +import os.log + +class CloudCoreDelegateHandler: CloudCoreDelegate { + + func willSyncFromCloud() { + os_log("🔁 Started fetching from iCloud", log: OSLog.default, type: .debug) + } + + func didSyncFromCloud() { + os_log("✅ Finishing fetching from iCloud", log: OSLog.default, type: .debug) + } + + func willSyncToCloud() { + os_log("💾 Started saving to iCloud", log: OSLog.default, type: .debug) + } + + func didSyncToCloud() { + os_log("✅ Finished saving to iCloud", log: OSLog.default, type: .debug) + } + + func error(error: Error, module: Module?) { + print("⚠️ CloudCore error detected in module \(String(describing: module)): \(error)") + } + +} diff --git a/Example/Sources/DetailViewController.swift b/Example/Sources/DetailViewController.swift deleted file mode 100644 index 03d106d2..00000000 --- a/Example/Sources/DetailViewController.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// DetailViewController.swift -// CloudTest2 -// -// Created by Vasily Ulianov on 14.02.17. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import UIKit -import CloudCore - -class DetailViewController: UIViewController { - - @IBOutlet weak var detailDescriptionLabel: UILabel! - - func configureView() { - // Update the user interface for the detail item. - if let detail = self.detailItem { - if let label = self.detailDescriptionLabel { - label.text = detail.timestamp!.description - } - } - } - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. - self.configureView() - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - var detailItem: Event? { - didSet { - // Update the view. - self.configureView() - } - } - - -} - diff --git a/Example/Sources/MasterViewController+FRC.swift b/Example/Sources/MasterViewController+FRC.swift deleted file mode 100644 index 8165321a..00000000 --- a/Example/Sources/MasterViewController+FRC.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// MasterViewController+FRC.swift -// CloudCoreExample -// -// Created by Vasily Ulianov on 12/03/2017. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import UIKit -import CoreData - -extension MasterViewController: NSFetchedResultsControllerDelegate { - - var fetchedResultsController: NSFetchedResultsController { - if _fetchedResultsController != nil { - return _fetchedResultsController! - } - - let fetchRequest: NSFetchRequest = Event.fetchRequest() - - // Set the batch size to a suitable number. - fetchRequest.fetchBatchSize = 20 - - // Edit the sort key as appropriate. - let sortDescriptor = NSSortDescriptor(key: "timestamp", ascending: false) - - fetchRequest.sortDescriptors = [sortDescriptor] - - // Edit the section name key path and cache name if appropriate. - // nil for section name key path means "no sections". - let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil) - aFetchedResultsController.delegate = self - _fetchedResultsController = aFetchedResultsController - - do { - try _fetchedResultsController!.performFetch() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nserror = error as NSError - fatalError("Unresolved error \(nserror), \(nserror.userInfo)") - } - - return _fetchedResultsController! - } - - func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - self.tableView.beginUpdates() - } - - func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { - switch type { - case .insert: - self.tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade) - case .delete: - self.tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade) - default: - return - } - } - - func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { - switch type { - case .insert: - tableView.insertRows(at: [newIndexPath!], with: .fade) - case .delete: - tableView.deleteRows(at: [indexPath!], with: .fade) - case .update: - self.configureCell(tableView.cellForRow(at: indexPath!)!, withEvent: anObject as! Event) - case .move: - tableView.moveRow(at: indexPath!, to: newIndexPath!) - } - } - - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - self.tableView.endUpdates() - } - -} diff --git a/Example/Sources/MasterViewController.swift b/Example/Sources/MasterViewController.swift deleted file mode 100644 index 6360ff69..00000000 --- a/Example/Sources/MasterViewController.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// MasterViewController.swift -// CloudTest2 -// -// Created by Vasily Ulianov on 14.02.17. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import UIKit -import CoreData -import CloudCore - -class MasterViewController: UITableViewController { - - var detailViewController: DetailViewController? = nil - var managedObjectContext: NSManagedObjectContext? = nil - - var _fetchedResultsController: NSFetchedResultsController? = nil - - // MARK: - CloudCore - - // Force refresh data from iCloud - @IBAction func refreshButton(_ sender: UIBarButtonItem) { - NSLog(">> (CloudCore.fetchAndSave) Started updating from iCloud") - CloudCore.fetchAndSave(container: persistentContainer, error: { (error) in - print(error) - }) { - NSLog("<< (CloudCore.fetchAndSave) Fetch from iCloud completed") - } - } - - @objc func insertNewObject(_ sender: Any) { - let context = self.fetchedResultsController.managedObjectContext - let newEvent = Event(context: context) - - // If appropriate, configure the new managed object. - newEvent.timestamp = Date() - newEvent.asset = UIImageJPEGRepresentation(#imageLiteral(resourceName: "TestImage"), 0.8) as Data? - - let subevent = Subevent(context: context) - subevent.timestamp = Date() - subevent.event = newEvent - - try! context.save() - } - - // MARK: - UIViewController methods - - override func viewDidLoad() { - super.viewDidLoad() - - let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:))) - self.navigationItem.rightBarButtonItem = addButton - if let split = self.splitViewController { - let controllers = split.viewControllers - self.detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController - } - } - - override func viewWillAppear(_ animated: Bool) { - self.clearsSelectionOnViewWillAppear = self.splitViewController!.isCollapsed - super.viewWillAppear(animated) - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - // MARK: - Segues - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if segue.identifier == "showDetail" { - if let indexPath = self.tableView.indexPathForSelectedRow { - let object = self.fetchedResultsController.object(at: indexPath) - let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController - controller.detailItem = object - controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem - controller.navigationItem.leftItemsSupplementBackButton = true - } - } - } - - // MARK: - Table View - - override func numberOfSections(in tableView: UITableView) -> Int { - return self.fetchedResultsController.sections?.count ?? 0 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let sectionInfo = self.fetchedResultsController.sections![section] - return sectionInfo.numberOfObjects - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) - let event = self.fetchedResultsController.object(at: indexPath) - self.configureCell(cell, withEvent: event) - return cell - } - - override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - return true - } - - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { - if editingStyle == .delete { - let context = self.fetchedResultsController.managedObjectContext - context.delete(self.fetchedResultsController.object(at: indexPath)) - try! context.save() - } - } - - func configureCell(_ cell: UITableViewCell, withEvent event: Event) { - cell.textLabel!.text = event.timestamp!.description - } -} - diff --git a/Example/Sources/View Controller/DetailViewController.swift b/Example/Sources/View Controller/DetailViewController.swift new file mode 100644 index 00000000..d9614003 --- /dev/null +++ b/Example/Sources/View Controller/DetailViewController.swift @@ -0,0 +1,107 @@ +// +// DetailViewController.swift +// CloudTest2 +// +// Created by Vasily Ulianov on 14.02.17. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import UIKit +import CoreData + +class DetailViewController: UITableViewController { + + var organizationID: NSManagedObjectID! + let context = persistentContainer.viewContext + + private var tableDataSource: DetailTableDataSource! + + override func viewDidLoad() { + super.viewDidLoad() + + let fetchRequest: NSFetchRequest = Employee.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "organization == %@", organizationID) + + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)] + + tableDataSource = DetailTableDataSource(fetchRequest: fetchRequest, context: context, sectionNameKeyPath: nil, delegate: self, tableView: tableView) + tableView.dataSource = tableDataSource + try! tableDataSource.performFetch() + + navigationItem.rightBarButtonItem = editButtonItem + } + + override func setEditing(_ editing: Bool, animated: Bool) { + super.setEditing(editing, animated: animated) + + if editing { + let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(navAddButtonDidTap(_:))) + navigationItem.setLeftBarButton(addButton, animated: animated) + + let renameButton = UIBarButtonItem(title: "Rename", style: .plain, target: self, action: #selector(navRenameButtonDidTap(_:))) + navigationItem.setRightBarButtonItems([editButtonItem, renameButton], animated: animated) + } else { + navigationItem.setLeftBarButton(nil, animated: animated) + navigationItem.setRightBarButtonItems([editButtonItem], animated: animated) + try! context.save() + } + } + + @objc private func navAddButtonDidTap(_ sender: UIBarButtonItem) { + let employee = ModelFactory.insertEmployee(context: context) + let organization = context.object(with: organizationID) as! Organization + employee.organization = organization + } + + @objc private func navRenameButtonDidTap(_ sender: UIBarButtonItem) { + let organization = context.object(with: organizationID) as! Organization + organization.name = ModelFactory.newCompanyName() + self.title = organization.name + } + +} + +extension DetailViewController: FRCTableViewDelegate { + + func frcTableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Employee", for: indexPath) as! EmployeeTableViewCell + let employee = tableDataSource.object(at: indexPath) + + cell.nameLabel.text = employee.name + + if let imageData = employee.photoData, let image = UIImage(data: imageData) { + cell.photoImageView.image = image + } else { + cell.photoImageView.image = nil + } + + var departmentText = employee.department ?? "No" + departmentText += " department" + cell.departmentLabel.text = departmentText + + var miniText = "Since " + if let workingSince = employee.workingSince { + miniText += DateFormatter.localizedString(from: workingSince, dateStyle: .medium, timeStyle: .none) + } else { + miniText += "unknown date" + } + + cell.sinceLabel.text = miniText + + return cell + } + +} + +fileprivate class DetailTableDataSource: FRCTableViewDataSource { + + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { + let context = frc.managedObjectContext + + switch editingStyle { + case .delete: context.delete(object(at: indexPath)) + default: return + } + } + +} diff --git a/Example/Sources/View Controller/MasterViewController.swift b/Example/Sources/View Controller/MasterViewController.swift new file mode 100644 index 00000000..e41fecb8 --- /dev/null +++ b/Example/Sources/View Controller/MasterViewController.swift @@ -0,0 +1,106 @@ +// +// MasterViewController.swift +// CloudTest2 +// +// Created by Vasily Ulianov on 14.02.17. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import UIKit +import CoreData +import CloudCore + +class MasterViewController: UITableViewController { + + private var tableDataSource: MasterTableViewDataSource! + + var mockAddNewOrganization: Organization? + let context = persistentContainer.viewContext + + // MARK: - UIViewController methods + + override func viewDidLoad() { + super.viewDidLoad() + + let fetchRequest: NSFetchRequest = Organization.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "sort", ascending: true)] + tableDataSource = MasterTableViewDataSource(fetchRequest: fetchRequest, context: context, sectionNameKeyPath: nil, delegate: self, tableView: tableView) + tableView.dataSource = tableDataSource + try! tableDataSource.performFetch() + + self.clearsSelectionOnViewWillAppear = true + + self.navigationItem.rightBarButtonItem = editButtonItem + } + + override func setEditing(_ editing: Bool, animated: Bool) { + super.setEditing(editing, animated: animated) + + // Save on editing end + if !editing { + try! context.save() + } + } + + @IBAction func addButtonClicked(_ sender: UIBarButtonItem) { + ModelFactory.insertOrganizationWithEmployees(context: context) + try! context.save() + } + + @IBAction func refreshValueChanged(_ sender: UIRefreshControl) { + CloudCore.fetchAndSave(to: persistentContainer, error: { (error) in + print("⚠️ FetchAndSave error: \(error)") + DispatchQueue.main.async { + sender.endRefreshing() + } + }) { + DispatchQueue.main.async { + sender.endRefreshing() + } + } + } + + // MARK: - Segues + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if let cell = sender as? UITableViewCell, let indexPath = tableView.indexPath(for: cell), let detailVC = segue.destination as? DetailViewController { + let organization = tableDataSource.object(at: indexPath) + detailVC.organizationID = organization.objectID + detailVC.title = organization.name + } + } + + override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle { + return .delete + } + +} + +extension MasterViewController: FRCTableViewDelegate { + + func frcTableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "RightDetail", for: indexPath) + let organization = tableDataSource.object(at: indexPath) + + cell.textLabel?.text = organization.name + + let employeesCount = organization.employees?.count ?? 0 + cell.detailTextLabel?.text = String(employeesCount) + " employees" + + return cell + } + +} + +fileprivate class MasterTableViewDataSource: FRCTableViewDataSource { + + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { + let context = frc.managedObjectContext + + switch editingStyle { + case .delete: context.delete(object(at: indexPath)) + default: return + } + } + +} diff --git a/Example/Sources/View/EmployeeTableViewCell.swift b/Example/Sources/View/EmployeeTableViewCell.swift new file mode 100644 index 00000000..71021935 --- /dev/null +++ b/Example/Sources/View/EmployeeTableViewCell.swift @@ -0,0 +1,18 @@ +// +// EmployeeTableViewCell.swift +// CloudCoreExample +// +// Created by Vasily Ulianov on 13/12/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import UIKit + +class EmployeeTableViewCell: UITableViewCell { + + @IBOutlet weak var photoImageView: UIImageView! + @IBOutlet weak var nameLabel: UILabel! + @IBOutlet weak var departmentLabel: UILabel! + @IBOutlet weak var sinceLabel: UILabel! + +} diff --git a/Package.swift b/Package.swift deleted file mode 100644 index b948d97f..00000000 --- a/Package.swift +++ /dev/null @@ -1,6 +0,0 @@ -import PackageDescription - -let package = Package( - name: "CloudCore", - exclude: ["Tests"] -) diff --git a/README.md b/README.md index 4ce5328e..25a94618 100755 --- a/README.md +++ b/README.md @@ -1,20 +1,29 @@ # CloudCore -[![Build Status](https://travis-ci.org/Sorix/CloudCore.svg?branch=master)](https://travis-ci.org/Sorix/CloudCore) [![Documentation](https://img.shields.io/cocoapods/metrics/doc-percent/CloudCore.svg)](http://cocoadocs.org/docsets/CloudCore/) [![Version](https://img.shields.io/cocoapods/v/CloudCore.svg?style=flat)](https://cocoapods.org/pods/CloudCore) ![Platform](https://img.shields.io/cocoapods/p/CloudCore.svg?style=flat) -![Status](https://img.shields.io/badge/status-alpha-red.svg) +![Status](https://img.shields.io/badge/status-beta-orange.svg) ![Swift](https://img.shields.io/badge/swift-4-orange.svg) -**CloudCore** is a framework that manages syncing between iCloud (CloudKit) and Core Data written on native Swift. +**CloudCore** is a framework that manages syncing between iCloud (CloudKit) and Core Data written on native Swift. It maybe used are CloudKit caching. #### Features -* Differential sync, only changed values in objects are uploaded and downloaded -* Support of all relationship types -* Respect of Core Data options (cascade deletions, external storage options) -* Unit and performance tests -* Private and shared CloudKit databases (to be tested) are supported +* Sync manually or on **push notifications**. +* **Differential sync**, only changed object and values are uploaded and downloaded. CloudCore even differs changed and not changed values inside objects. +* Respects of Core Data options (cascade deletions, external storage). +* Knows and manages with CloudKit errors like `userDeletedZone`, `zoneNotFound`, `changeTokenExpired`, `isMore`. +* Covered with Unit and CloudKit online **tests**. +* All public methods are **[100% documented](http://cocoadocs.org/docsets/CloudCore/)**. +* Currently only **private database** is supported. + +## How it works? +CloudCore is built using "black box" architecture, so it works invisibly for your application, you just need to add several lines to `AppDelegate` to enable it. Synchronization and error resolving is managed automatically. + +1. CloudCore stores *change tokens* from CloudKit, so only changed data is downloaded. +2. When CloudCore is enabled (`CloudCore.enable`) it fetches changed data from CloudKit and subscribes to CloudKit push notifications about new changes. +3. When `CloudCore.fetchAndSave` is called manually or by push notification, CloudCore fetches and saves changed data to Core Data. +4. When data is written to persistent container (parent context is saved) CloudCore founds locally changed data and uploads it to CloudKit. ## Installation @@ -23,25 +32,15 @@ it, simply add the following line to your Podfile: ```ruby -pod 'CloudCore', '~> 1.0' -``` - -### Swift Package Manager -The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the swift compiler. You can read more about package manager in [An Introduction to the Swift Package Manager](https://www.raywenderlich.com/148832/introduction-swift-package-manager) article. - -Once you have set up Swift package for your application, just add CloudCore as dependency at your `Package.swift`: - -```swift -dependencies: [ - .Package(url: "https://github.com/Sorix/CloudCore", majorVersion: 1) -] +pod 'CloudCore', '~> 2.0' ``` ## How to help? Current version of framework hasn't been deeply tested and may contain errors. If you can test framework, I will be very glad. If you found an error, please post [an issue](https://github.com/Sorix/CloudCore/issues). ## Documentation -Detailed documentation is [available at CocoaDocs](http://cocoadocs.org/docsets/CloudCore/). +All public methods are documented using [XCode Markup](https://developer.apple.com/library/content/documentation/Xcode/Reference/xcode_markup_formatting_ref/) and available inside XCode. +HTML-generated version of that documentation is [available here at CocoaDocs](http://cocoadocs.org/docsets/CloudCore/). ## Quick start 1. Enable CloudKit capability for you application: @@ -55,30 +54,27 @@ Detailed documentation is [available at CocoaDocs](http://cocoadocs.org/docsets/ ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { - // Register for push notifications about changes - UIApplication.shared.registerForRemoteNotifications() - - // Enable uploading changed local data to CoreData - CloudCore.observeCoreDataChanges(persistentContainer: persistentContainer, errorDelegate: nil) + // Register for push notifications about changes + application.registerForRemoteNotifications() - // Sync on startup if push notifications is missed, disabled etc - // Also it acts as initial sync if no sync was done before - CloudCore.fetchAndSave(container: persistentContainer, error: nil, completion: nil) + // Enable CloudCore syncing + CloudCore.enable(persistentContainer: persistentContainer, errorDelegate: self) - return true + return true } +// Notification from CloudKit about changes in remote database func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - // Check if it CloudKit's and CloudCore notification - if CloudCore.isCloudCoreNotification(withUserInfo: userInfo) { - // Fetch changed data from iCloud - CloudCore.fetchAndSave(using: userInfo, container: self.persistentContainer, error: nil, completion: { (fetchResult) in - completionHandler(fetchResult.uiBackgroundFetchResult) - }) - } + // Check if it CloudKit's and CloudCore notification + if CloudCore.isCloudCoreNotification(withUserInfo: userInfo) { + // Fetch changed data from iCloud + CloudCore.fetchAndSave(using: userInfo, to: persistentContainer, error: nil, completion: { (fetchResult) in + completionHandler(fetchResult.uiBackgroundFetchResult) + }) + } } -func applicationDidEnterBackground(_ application: UIApplication) { +func applicationWillTerminate(_ application: UIApplication) { // Save tokens on exit used to differential sync CloudCore.tokens.saveToUserDefaults() } @@ -111,7 +107,6 @@ The most simple way is to name attributes with default names because you don't n * *Record Data* attribute is used to store archived version of `CKRecord` with system fields only (like timestamps, tokens), so don't worry about size, no real data will be stored here. ## Example application - You can find example application at [Example](/Example/) directory. **How to run it:** @@ -128,7 +123,7 @@ You can find example application at [Example](/Example/) directory. CloudKit objects can't be mocked up, that's why I create 2 different types of tests: * `Tests/Unit` here I placed tests that can be performed without CloudKit connection. That tests are executed when you submit a Pull Request. -* `Tests/CloudKit` here are located "manually" tests, they are most important tests that can be run only in configured environment because they work with CloudKit and your Apple ID. +* `Tests/CloudKit` here located "manual" tests, they are most important tests that can be run only in configured environment because they work with CloudKit and your Apple ID. Nothing will be wrong with your account, tests use only private `CKDatabase` for application. @@ -139,12 +134,14 @@ CloudKit objects can't be mocked up, that's why I create 2 different types of te 3. Configure iCloud on that device: Settings.app → iCloud → Login. 4. Run `CloudKitTests`, they are attached to `TestableApp`, so CloudKit connection will work. - ## Roadmap -- [ ] Add more tests, it's crucial for such type of project -- [ ] Add support of migration of existing databases +- [x] Move from alpha to beta status. +- [ ] Add `CloudCore.disable` method +- [ ] Add methods to clear local cache and remote database +- [ ] Add error resolving for `limitExceeded` error (split saves by relationships). ## Author +Open for hire / relocation. Vasily Ulianov, [va...@me.com](http://www.google.com/recaptcha/mailhide/d?k=01eFEpy-HM-qd0Vf6QGABTjw==&c=JrKKY2bjm0Bp58w7zTvPiQ==) diff --git a/Resources/Info-iOS.plist b/Resources/Info-iOS.plist deleted file mode 100755 index 60b9c008..00000000 --- a/Resources/Info-iOS.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0.0 - CFBundleSignature - ???? - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/Sources/Classes/Fetch/SubOperations/AsynchronousOperation.swift b/Source/Classes/AsynchronousOperation.swift similarity index 100% rename from Sources/Classes/Fetch/SubOperations/AsynchronousOperation.swift rename to Source/Classes/AsynchronousOperation.swift diff --git a/Source/Classes/CloudCore.swift b/Source/Classes/CloudCore.swift new file mode 100644 index 00000000..9d551859 --- /dev/null +++ b/Source/Classes/CloudCore.swift @@ -0,0 +1,209 @@ +// +// CloudCore.swift +// CloudCore +// +// Created by Vasily Ulianov on 06.02.17. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import CoreData +import CloudKit + +/** + Main framework class, in most cases you will use only methods from this class, all methods and properties are `static`. + + ## Save to cloud + On application inialization call `CloudCore.enable(persistentContainer:)` method, so framework will automatically monitor changes at Core Data and upload it to iCloud. + + ### Example + ```swift + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Register for push notifications about changes + application.registerForRemoteNotifications() + + // Enable CloudCore syncing + CloudCore.delegate = someDelegate // it is recommended to set delegate to track errors + CloudCore.enable(persistentContainer: persistentContainer) + + return true + } + ``` + + ## Fetch from cloud + When CloudKit data is changed **push notification** is posted to an application. You need to handle it and fetch changed data from CloudKit with `CloudCore.fetchAndSave(using:to:error:completion:)` method. + + ### Example + ```swift + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + // Check if it CloudKit's and CloudCore notification + if CloudCore.isCloudCoreNotification(withUserInfo: userInfo) { + // Fetch changed data from iCloud + CloudCore.fetchAndSave(using: userInfo, to: persistentContainer, error: nil, completion: { (fetchResult) in + completionHandler(fetchResult.uiBackgroundFetchResult) + }) + } + } + ``` + + You can also check for updated data at CloudKit **manually** (e.g. push notifications are not working). Use for that `CloudCore.fetchAndSave(to:error:completion:)` +*/ +open class CloudCore { + + // MARK: - Properties + + private(set) static var coreDataListener: CoreDataListener? + + /// CloudCore configuration, it's recommended to set up before calling any of CloudCore methods. You can read more at `CloudCoreConfig` struct description + public static var config = CloudCoreConfig() + + /// `Tokens` object, read more at class description. By default variable is loaded from User Defaults. + public static var tokens = Tokens.loadFromUserDefaults() + + /// Error and sync actions are reported to that delegate + public static weak var delegate: CloudCoreDelegate? { + didSet { + coreDataListener?.delegate = delegate + } + } + + public typealias NotificationUserInfo = [AnyHashable : Any] + + static private let queue = OperationQueue() + + // MARK: - Methods + + /// Enable CloudKit and Core Data synchronization + /// + /// - Parameters: + /// - container: `NSPersistentContainer` that will be used to save data + public static func enable(persistentContainer container: NSPersistentContainer) { + // Listen for local changes + let listener = CoreDataListener(container: container) + listener.delegate = self.delegate + listener.observe() + self.coreDataListener = listener + + // Subscribe (subscription may be outdated/removed) + #if !os(watchOS) + let subscribeOperation = SubscribeOperation() + subscribeOperation.errorBlock = { handle(subscriptionError: $0, container: container) } + queue.addOperation(subscribeOperation) + #endif + + // Fetch updated data (e.g. push notifications weren't received) + let updateFromCloudOperation = FetchAndSaveOperation(persistentContainer: container) + updateFromCloudOperation.errorBlock = { + self.delegate?.error(error: $0, module: .some(.fetchFromCloud)) + } + + #if !os(watchOS) + updateFromCloudOperation.addDependency(subscribeOperation) + #endif + + queue.addOperation(updateFromCloudOperation) + } + + /// Disables synchronization (push notifications won't be sent also) + public static func disable() { + queue.cancelAllOperations() + + coreDataListener?.stopObserving() + coreDataListener = nil + + // FIXME: unsubscribe + } + + // MARK: Fetchers + + /** Fetch changes from one CloudKit database and save it to CoreData from `didReceiveRemoteNotification` method. + + Don't forget to check notification's UserInfo by calling `isCloudCoreNotification(withUserInfo:)`. If incorrect user info is provided `FetchResult.noData` will be returned at completion block. + + - Parameters: + - userInfo: notification's user info, CloudKit database will be extraced from that notification + - container: `NSPersistentContainer` that will be used to save fetched data + - error: block will be called every time when error occurs during process + - completion: `FetchResult` enumeration with results of operation + */ + public static func fetchAndSave(using userInfo: NotificationUserInfo, to container: NSPersistentContainer, error: ErrorBlock?, completion: @escaping (_ fetchResult: FetchResult) -> Void) { + guard let cloudDatabase = self.database(for: userInfo) else { + completion(.noData) + return + } + + DispatchQueue.global(qos: .utility).async { + let errorProxy = ErrorBlockProxy(destination: error) + let operation = FetchAndSaveOperation(from: [cloudDatabase], persistentContainer: container) + operation.errorBlock = { errorProxy.send(error: $0) } + operation.start() + + if errorProxy.wasError { + completion(FetchResult.failed) + } else { + completion(FetchResult.newData) + } + } + } + + /** Fetch changes from all CloudKit databases and save it to Core Data + + - Parameters: + - container: `NSPersistentContainer` that will be used to save fetched data + - error: block will be called every time when error occurs during process + - completion: `FetchResult` enumeration with results of operation + */ + public static func fetchAndSave(to container: NSPersistentContainer, error: ErrorBlock?, completion: (() -> Void)?) { + let operation = FetchAndSaveOperation(persistentContainer: container) + operation.errorBlock = error + operation.completionBlock = completion + + queue.addOperation(operation) + } + + /** Check if notification is CloudKit notification containing CloudCore data + + - Parameter userInfo: userInfo of notification + - Returns: `true` if notification contains CloudCore data + */ + public static func isCloudCoreNotification(withUserInfo userInfo: NotificationUserInfo) -> Bool { + return (database(for: userInfo) != nil) + } + + static func database(for notificationUserInfo: NotificationUserInfo) -> CKDatabase? { + guard let notificationDictionary = notificationUserInfo as? [String: NSObject] else { return nil } + let notification = CKNotification(fromRemoteNotificationDictionary: notificationDictionary) + + guard let id = notification.subscriptionID else { return nil } + + switch id { + case config.subscriptionIDForPrivateDB: return CKContainer.default().privateCloudDatabase + case config.subscriptionIDForSharedDB: return CKContainer.default().sharedCloudDatabase + case _ where id.hasPrefix(config.publicSubscriptionIDPrefix): return CKContainer.default().publicCloudDatabase + default: return nil + } + } + + static private func handle(subscriptionError: Error, container: NSPersistentContainer) { + guard let cloudError = subscriptionError as? CKError, let partialErrorValues = cloudError.partialErrorsByItemID?.values else { + delegate?.error(error: subscriptionError, module: nil) + return + } + + // Try to find "Zone Not Found" in partial errors + for subError in partialErrorValues { + guard let subError = subError as? CKError else { continue } + + if case .zoneNotFound = subError.code { + // Zone wasn't found, we need to create it + self.queue.cancelAllOperations() + let setupOperation = SetupOperation(container: container, parentContext: nil) + self.queue.addOperation(setupOperation) + + return + } + } + + delegate?.error(error: subscriptionError, module: nil) + } + +} diff --git a/Sources/Classes/ErrorBlockProxy.swift b/Source/Classes/ErrorBlockProxy.swift similarity index 100% rename from Sources/Classes/ErrorBlockProxy.swift rename to Source/Classes/ErrorBlockProxy.swift diff --git a/Source/Classes/Fetch/FetchAndSaveOperation.swift b/Source/Classes/Fetch/FetchAndSaveOperation.swift new file mode 100644 index 00000000..6891f688 --- /dev/null +++ b/Source/Classes/Fetch/FetchAndSaveOperation.swift @@ -0,0 +1,120 @@ +// +// FetchAndSaveOperation.swift +// CloudCore +// +// Created by Vasily Ulianov on 13/03/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import CloudKit +import CoreData + +/// An operation that fetches data from CloudKit and saves it to Core Data, you can use it without calling `CloudCore.fetchAndSave` methods if you application relies on `Operation` +public class FetchAndSaveOperation: Operation { + + /// Private cloud database + public static let allDatabases = [ + // CKContainer.default().publicCloudDatabase, + CKContainer.default().privateCloudDatabase, +// CKContainer.default().sharedCloudDatabase + ] + + public typealias NotificationUserInfo = [AnyHashable : Any] + + private let tokens: Tokens + private let databases: [CKDatabase] + private let persistentContainer: NSPersistentContainer + + /// Called every time if error occurs + public var errorBlock: ErrorBlock? + + private let queue = OperationQueue() + + /// Initialize operation, it's recommended to set `errorBlock` + /// + /// - Parameters: + /// - databases: list of databases to fetch data from (only private is supported now) + /// - persistentContainer: `NSPersistentContainer` that will be used to save data + /// - tokens: previously saved `Tokens`, you can generate new ones if you want to fetch all data + public init(from databases: [CKDatabase] = FetchAndSaveOperation.allDatabases, persistentContainer: NSPersistentContainer, tokens: Tokens = CloudCore.tokens) { + self.tokens = tokens + self.databases = databases + self.persistentContainer = persistentContainer + + queue.name = "FetchAndSaveQueue" + } + + /// Performs the receiver’s non-concurrent task. + override public func main() { + if self.isCancelled { return } + + CloudCore.delegate?.willSyncFromCloud() + + let backgroundContext = persistentContainer.newBackgroundContext() + backgroundContext.name = CloudCore.config.contextName + + for database in self.databases { + self.addRecordZoneChangesOperation(recordZoneIDs: [CloudCore.config.zoneID], database: database, context: backgroundContext) + } + + self.queue.waitUntilAllOperationsAreFinished() + + do { + try backgroundContext.save() + } catch { + errorBlock?(error) + } + + CloudCore.delegate?.didSyncFromCloud() + } + + private func addRecordZoneChangesOperation(recordZoneIDs: [CKRecordZoneID], database: CKDatabase, context: NSManagedObjectContext) { + if recordZoneIDs.isEmpty { return } + + let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, recordZoneIDs: recordZoneIDs, tokens: tokens) + + recordZoneChangesOperation.recordChangedBlock = { + // Convert and write CKRecord To NSManagedObject Operation + let convertOperation = RecordToCoreDataOperation(parentContext: context, record: $0) + convertOperation.errorBlock = { self.errorBlock?($0) } + self.queue.addOperation(convertOperation) + } + + recordZoneChangesOperation.recordWithIDWasDeletedBlock = { + // Delete NSManagedObject with specified recordID Operation + let deleteOperation = DeleteFromCoreDataOperation(parentContext: context, recordID: $0) + deleteOperation.errorBlock = { self.errorBlock?($0) } + self.queue.addOperation(deleteOperation) + } + + recordZoneChangesOperation.errorBlock = { zoneID, error in + self.handle(recordZoneChangesError: error, in: zoneID, database: database, context: context) + } + + queue.addOperation(recordZoneChangesOperation) + } + + private func handle(recordZoneChangesError: Error, in zoneId: CKRecordZoneID, database: CKDatabase, context: NSManagedObjectContext) { + guard let cloudError = recordZoneChangesError as? CKError else { + errorBlock?(recordZoneChangesError) + return + } + + switch cloudError.code { + // User purged cloud database, we need to delete local cache (according Apple Guidelines) + case .userDeletedZone: + queue.cancelAllOperations() + + let purgeOperation = PurgeLocalDatabaseOperation(parentContext: context, managedObjectModel: persistentContainer.managedObjectModel) + purgeOperation.errorBlock = errorBlock + queue.addOperation(purgeOperation) + + // Our token is expired, we need to refetch everything again + case .changeTokenExpired: + tokens.tokensByRecordZoneID[zoneId] = nil + self.addRecordZoneChangesOperation(recordZoneIDs: [CloudCore.config.zoneID], database: database, context: context) + default: errorBlock?(cloudError) + } + } + +} diff --git a/Sources/Classes/Fetch/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift b/Source/Classes/Fetch/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift similarity index 100% rename from Sources/Classes/Fetch/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift rename to Source/Classes/Fetch/PublicSubscriptions/FetchPublicSubscriptionsOperation.swift diff --git a/Sources/Classes/Fetch/PublicSubscriptions/PublicDatabaseSubscriptions.swift b/Source/Classes/Fetch/PublicSubscriptions/PublicDatabaseSubscriptions.swift similarity index 100% rename from Sources/Classes/Fetch/PublicSubscriptions/PublicDatabaseSubscriptions.swift rename to Source/Classes/Fetch/PublicSubscriptions/PublicDatabaseSubscriptions.swift diff --git a/Sources/Classes/Fetch/SubOperations/DeleteFromCoreDataOperation.swift b/Source/Classes/Fetch/SubOperations/DeleteFromCoreDataOperation.swift similarity index 98% rename from Sources/Classes/Fetch/SubOperations/DeleteFromCoreDataOperation.swift rename to Source/Classes/Fetch/SubOperations/DeleteFromCoreDataOperation.swift index ab33cd15..9e2155ad 100644 --- a/Sources/Classes/Fetch/SubOperations/DeleteFromCoreDataOperation.swift +++ b/Source/Classes/Fetch/SubOperations/DeleteFromCoreDataOperation.swift @@ -32,8 +32,6 @@ class DeleteFromCoreDataOperation: Operation { // Iterate through each entity to fetch and delete object with our recordData guard let entities = childContext.persistentStoreCoordinator?.managedObjectModel.entities else { return } for entity in entities { - if self.isCancelled { return } - guard let serviceAttributeNames = entity.serviceAttributeNames else { continue } do { diff --git a/Sources/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift b/Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift similarity index 51% rename from Sources/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift rename to Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift index b52858c7..67f0a407 100644 --- a/Sources/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift +++ b/Source/Classes/Fetch/SubOperations/FetchRecordZoneChangesOperation.swift @@ -8,22 +8,33 @@ import CloudKit -class FetchRecordZoneChangesOperation: AsynchronousOperation { +class FetchRecordZoneChangesOperation: Operation { // Set on init let tokens: Tokens let recordZoneIDs: [CKRecordZoneID] let database: CKDatabase // - var errorBlock: ErrorBlock? + var errorBlock: ((CKRecordZoneID, Error) -> Void)? var recordChangedBlock: ((CKRecord) -> Void)? var recordWithIDWasDeletedBlock: ((CKRecordID) -> Void)? + private let optionsByRecordZoneID: [CKRecordZoneID: CKFetchRecordZoneChangesOptions] + private let fetchQueue = OperationQueue() + init(from database: CKDatabase, recordZoneIDs: [CKRecordZoneID], tokens: Tokens) { self.tokens = tokens self.database = database self.recordZoneIDs = recordZoneIDs + var optionsByRecordZoneID = [CKRecordZoneID: CKFetchRecordZoneChangesOptions]() + for zoneID in recordZoneIDs { + let options = CKFetchRecordZoneChangesOptions() + options.previousServerChangeToken = self.tokens.tokensByRecordZoneID[zoneID] + optionsByRecordZoneID[zoneID] = options + } + self.optionsByRecordZoneID = optionsByRecordZoneID + super.init() self.name = "FetchRecordZoneChangesOperation" @@ -32,35 +43,38 @@ class FetchRecordZoneChangesOperation: AsynchronousOperation { override func main() { super.main() - // Set tokens for zones - var optionsByRecordZoneID = [CKRecordZoneID: CKFetchRecordZoneChangesOptions]() - for zoneID in recordZoneIDs { - let options = CKFetchRecordZoneChangesOptions() - options.previousServerChangeToken = self.tokens.tokensByRecordZoneID[zoneID] - optionsByRecordZoneID[zoneID] = options - } + let fetchOperation = self.makeFetchOperation(optionsByRecordZoneID: optionsByRecordZoneID) + self.fetchQueue.addOperation(fetchOperation) + fetchQueue.waitUntilAllOperationsAreFinished() + } + + private func makeFetchOperation(optionsByRecordZoneID: [CKRecordZoneID: CKFetchRecordZoneChangesOptions]) -> CKFetchRecordZoneChangesOperation { // Init Fetch Operation let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: recordZoneIDs, optionsByRecordZoneID: optionsByRecordZoneID) - fetchOperation.recordChangedBlock = { self.recordChangedBlock?($0) } - fetchOperation.recordWithIDWasDeletedBlock = { recordID, _ in - self.recordWithIDWasDeletedBlock?(recordID) - } - fetchOperation.recordZoneChangeTokensUpdatedBlock = { recordZoneID, serverChangeToken, _ in - self.tokens.tokensByRecordZoneID[recordZoneID] = serverChangeToken + fetchOperation.recordChangedBlock = { + self.recordChangedBlock?($0) } - - fetchOperation.fetchRecordZoneChangesCompletionBlock = { error in + fetchOperation.recordWithIDWasDeletedBlock = { recordID, _ in + self.recordWithIDWasDeletedBlock?(recordID) + } + fetchOperation.recordZoneFetchCompletionBlock = { zoneId, serverChangeToken, clientChangeTokenData, isMore, error in + self.tokens.tokensByRecordZoneID[zoneId] = serverChangeToken + if let error = error { - self.errorBlock?(error) + self.errorBlock?(zoneId, error) } - self.state = .finished + if isMore { + let moreOperation = self.makeFetchOperation(optionsByRecordZoneID: optionsByRecordZoneID) + self.fetchQueue.addOperation(moreOperation) + } } fetchOperation.qualityOfService = self.qualityOfService fetchOperation.database = self.database - fetchOperation.start() + + return fetchOperation } } diff --git a/Source/Classes/Fetch/SubOperations/PurgeLocalDatabaseOperation.swift b/Source/Classes/Fetch/SubOperations/PurgeLocalDatabaseOperation.swift new file mode 100644 index 00000000..8c8e4fc3 --- /dev/null +++ b/Source/Classes/Fetch/SubOperations/PurgeLocalDatabaseOperation.swift @@ -0,0 +1,58 @@ +// +// PurgeLocalDatabaseOperation.swift +// CloudCore +// +// Created by Vasily Ulianov on 12/12/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import CoreData + +class PurgeLocalDatabaseOperation: Operation { + + let parentContext: NSManagedObjectContext + let managedObjectModel: NSManagedObjectModel + var errorBlock: ErrorBlock? + + init(parentContext: NSManagedObjectContext, managedObjectModel: NSManagedObjectModel) { + self.parentContext = parentContext + self.managedObjectModel = managedObjectModel + + super.init() + } + + override func main() { + super.main() + + let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + childContext.parent = parentContext + + for entity in managedObjectModel.cloudCoreEnabledEntities { + guard let entityName = entity.name else { continue } + + let fetchRequest = NSFetchRequest(entityName: entityName) + fetchRequest.includesPropertyValues = false + fetchRequest.includesSubentities = false + + do { + // I don't user `NSBatchDeleteRequest` because we can't notify viewContextes about changes + guard let objects = try childContext.fetch(fetchRequest) as? [NSManagedObject] else { continue } + + for object in objects { + childContext.delete(object) + } + } catch { + errorBlock?(error) + } + } + + do { + try childContext.save() + } catch { + errorBlock?(error) + } + } + + + +} diff --git a/Sources/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift b/Source/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift similarity index 83% rename from Sources/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift rename to Source/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift index aa073847..bbc99627 100644 --- a/Sources/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift +++ b/Source/Classes/Fetch/SubOperations/RecordToCoreDataOperation.swift @@ -10,11 +10,14 @@ import CoreData import CloudKit /// Convert CKRecord to NSManagedObject and save it to parent context, thread-safe -class RecordToCoreDataOperation: Operation { +class RecordToCoreDataOperation: AsynchronousOperation { let parentContext: NSManagedObjectContext let record: CKRecord var errorBlock: ErrorBlock? + /// - Parameters: + /// - parentContext: operation will be safely performed in that context, **operation doesn't save that context** you need to do it manually + /// - record: record that will be converted to `NSManagedObject` init(parentContext: NSManagedObjectContext, record: CKRecord) { self.parentContext = parentContext self.record = record @@ -26,16 +29,16 @@ class RecordToCoreDataOperation: Operation { override func main() { if self.isCancelled { return } - - let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - childContext.parent = parentContext - - do { - try self.setManagedObject(in: childContext) - try childContext.save() - } catch { - self.errorBlock?(error) - } + + parentContext.perform { + do { + try self.setManagedObject(in: self.parentContext) + } catch { + self.errorBlock?(error) + } + + self.state = .finished + } } /// Create or update existing NSManagedObject from CKRecord @@ -71,8 +74,6 @@ class RecordToCoreDataOperation: Operation { /// - recordDataAttributeName: attribute name containing recordData private func fill(object: NSManagedObject, entityName: String, serviceAttributeNames: ServiceAttributeNames, context: NSManagedObjectContext) throws { for key in record.allKeys() { - if self.isCancelled { return } - let recordValue = record.value(forKey: key) let attribute = CloudKitAttribute(value: recordValue, fieldName: key, entityName: entityName, serviceAttributes: serviceAttributeNames, context: context) diff --git a/Sources/Classes/Upload/CloudSaveOperationQueue.swift b/Source/Classes/Save/CloudSaveOperationQueue.swift similarity index 78% rename from Sources/Classes/Upload/CloudSaveOperationQueue.swift rename to Source/Classes/Save/CloudSaveOperationQueue.swift index b9c99f24..583cde4d 100644 --- a/Sources/Classes/Upload/CloudSaveOperationQueue.swift +++ b/Source/Classes/Save/CloudSaveOperationQueue.swift @@ -7,6 +7,7 @@ // import CloudKit +import CoreData class CloudSaveOperationQueue: OperationQueue { var errorBlock: ErrorBlock? @@ -37,26 +38,16 @@ class CloudSaveOperationQueue: OperationQueue { } } - let initialSetupOperation = makeSetupOperationIfNeeded() - // Perform for databaseModifier in datasource { - addOperation(recordsToSave: databaseModifier.save, recordIDsToDelete: databaseModifier.delete, database: databaseModifier.database, dependency: initialSetupOperation) + addOperation(recordsToSave: databaseModifier.save, recordIDsToDelete: databaseModifier.delete, database: databaseModifier.database) } } - /// - Returns: `SetupOperation` if setup wasn't performed before otherwise `nil` will be returned - func makeSetupOperationIfNeeded() -> SetupOperation? { - if SetupOperation.isFinishedBefore { return nil } - - let setupOperation = SetupOperation() - setupOperation.errorBlock = errorBlock - return setupOperation - } - - private func addOperation(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecordID], database: CKDatabase, dependency: Operation?) { + private func addOperation(recordsToSave: [CKRecord], recordIDsToDelete: [CKRecordID], database: CKDatabase) { // Modify CKRecord Operation let modifyOperation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete) + modifyOperation.savePolicy = .changedKeys modifyOperation.perRecordCompletionBlock = { record, error in if let error = error { @@ -66,18 +57,14 @@ class CloudSaveOperationQueue: OperationQueue { } } - modifyOperation.modifyRecordsCompletionBlock = { [weak self] savedRecords, _, error in + modifyOperation.modifyRecordsCompletionBlock = { _, _, error in if let error = error { - self?.errorBlock?(error) + self.errorBlock?(error) } } modifyOperation.database = database - - if let dependency = dependency { - modifyOperation.addDependency(dependency) - } - + self.addOperation(modifyOperation) } @@ -88,9 +75,10 @@ class CloudSaveOperationQueue: OperationQueue { try? FileManager.default.removeItem(at: asset.fileURL) } } + } -private class DatabaseModifyDataSource { +fileprivate class DatabaseModifyDataSource { let database: CKDatabase var save = [CKRecord]() var delete = [CKRecordID]() diff --git a/Sources/Classes/Upload/CoreDataListener.swift b/Source/Classes/Save/CoreDataListener.swift similarity index 54% rename from Sources/Classes/Upload/CoreDataListener.swift rename to Source/Classes/Save/CoreDataListener.swift index e4f24a35..7410706f 100644 --- a/Sources/Classes/Upload/CoreDataListener.swift +++ b/Source/Classes/Save/CoreDataListener.swift @@ -8,6 +8,7 @@ import Foundation import CoreData +import CloudKit /// Class responsible for taking action on Core Data save notifications class CoreDataListener { @@ -18,10 +19,14 @@ class CoreDataListener { let cloudContextName = "CloudCoreSync" - public init(container: NSPersistentContainer, errorBlock: ErrorBlock?) { + // Used for errors delegation + weak var delegate: CloudCoreDelegate? + + public init(container: NSPersistentContainer) { self.container = container - converter.errorBlock = errorBlock - cloudSaveOperationQueue.errorBlock = errorBlock + converter.errorBlock = { [weak self] in + self?.delegate?.error(error: $0, module: .some(.saveToCloud)) + } } /// Observe Core Data willSave and didSave notifications @@ -62,16 +67,62 @@ class CoreDataListener { DispatchQueue.global(qos: .utility).async { [weak self] in guard let listener = self else { return } - NotificationCenter.default.post(name: .CloudCoreWillSyncToCloud, object: nil) + CloudCore.delegate?.willSyncToCloud() let backgroundContext = listener.container.newBackgroundContext() backgroundContext.name = listener.cloudContextName let records = listener.converter.confirmConvertOperationsAndWait(in: backgroundContext) + listener.cloudSaveOperationQueue.errorBlock = { listener.handle(error: $0, parentContext: backgroundContext) } listener.cloudSaveOperationQueue.addOperations(recordsToSave: records.recordsToSave, recordIDsToDelete: records.recordIDsToDelete) listener.cloudSaveOperationQueue.waitUntilAllOperationsAreFinished() - NotificationCenter.default.post(name: .CloudCoreDidSyncToCloud, object: nil) + do { + if backgroundContext.hasChanges { + try backgroundContext.save() + } + } catch { + listener.delegate?.error(error: error, module: .some(.saveToCloud)) + } + + CloudCore.delegate?.didSyncToCloud() } } + + private func handle(error: Error, parentContext: NSManagedObjectContext) { + guard let cloudError = error as? CKError else { + delegate?.error(error: error, module: .some(.saveToCloud)) + return + } + + switch cloudError.code { + // Zone was accidentally deleted (NOT PURGED), we need to reupload all data accroding Apple Guidelines + case .zoneNotFound: + cloudSaveOperationQueue.cancelAllOperations() + + // Create CloudCore Zone + let createZoneOperation = CreateCloudCoreZoneOperation() + createZoneOperation.errorBlock = { + self.delegate?.error(error: $0, module: .some(.saveToCloud)) + self.cloudSaveOperationQueue.cancelAllOperations() + } + + // Subscribe operation + #if !os(watchOS) + let subscribeOperation = SubscribeOperation() + subscribeOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.saveToCloud)) } + subscribeOperation.addDependency(createZoneOperation) + cloudSaveOperationQueue.addOperation(subscribeOperation) + #endif + + // Upload all local data + let uploadOperation = UploadAllLocalDataOperation(parentContext: parentContext, managedObjectModel: container.managedObjectModel) + uploadOperation.errorBlock = { self.delegate?.error(error: $0, module: .some(.saveToCloud)) } + + cloudSaveOperationQueue.addOperations([createZoneOperation, uploadOperation], waitUntilFinished: true) + case .operationCancelled: return + default: delegate?.error(error: cloudError, module: .some(.saveToCloud)) + } + } + } diff --git a/Sources/Classes/Upload/Model/RecordIDWithDatabase.swift b/Source/Classes/Save/Model/RecordIDWithDatabase.swift similarity index 100% rename from Sources/Classes/Upload/Model/RecordIDWithDatabase.swift rename to Source/Classes/Save/Model/RecordIDWithDatabase.swift diff --git a/Sources/Classes/Upload/Model/RecordWithDatabase.swift b/Source/Classes/Save/Model/RecordWithDatabase.swift similarity index 100% rename from Sources/Classes/Upload/Model/RecordWithDatabase.swift rename to Source/Classes/Save/Model/RecordWithDatabase.swift diff --git a/Sources/Classes/Upload/ObjectToRecord/CoreDataAttribute.swift b/Source/Classes/Save/ObjectToRecord/CoreDataAttribute.swift similarity index 100% rename from Sources/Classes/Upload/ObjectToRecord/CoreDataAttribute.swift rename to Source/Classes/Save/ObjectToRecord/CoreDataAttribute.swift diff --git a/Sources/Classes/Upload/ObjectToRecord/CoreDataRelationship.swift b/Source/Classes/Save/ObjectToRecord/CoreDataRelationship.swift similarity index 95% rename from Sources/Classes/Upload/ObjectToRecord/CoreDataRelationship.swift rename to Source/Classes/Save/ObjectToRecord/CoreDataRelationship.swift index 20ddecc1..d2087cc4 100644 --- a/Sources/Classes/Upload/ObjectToRecord/CoreDataRelationship.swift +++ b/Source/Classes/Save/ObjectToRecord/CoreDataRelationship.swift @@ -40,6 +40,10 @@ class CoreDataRelationship { /// - Returns: `CKReference` or `[CKReference]` func makeRecordValue() throws -> Any? { if self.description.isToMany { + if value is NSOrderedSet { + throw CloudCoreError.orderedSetRelationshipIsNotSupported(description) + } + guard let objectsSet = value as? NSSet else { return nil } var referenceList = [CKReference]() diff --git a/Sources/Classes/Upload/ObjectToRecord/ObjectToRecordConverter.swift b/Source/Classes/Save/ObjectToRecord/ObjectToRecordConverter.swift similarity index 86% rename from Sources/Classes/Upload/ObjectToRecord/ObjectToRecordConverter.swift rename to Source/Classes/Save/ObjectToRecord/ObjectToRecordConverter.swift index a22546aa..bfac7e78 100644 --- a/Sources/Classes/Upload/ObjectToRecord/ObjectToRecordConverter.swift +++ b/Source/Classes/Save/ObjectToRecord/ObjectToRecordConverter.swift @@ -37,7 +37,21 @@ class ObjectToRecordConverter { guard let serviceAttributeNames = object.entity.serviceAttributeNames else { continue } do { - let recordWithSystemFields = try object.setRecordInformation() + let recordWithSystemFields: CKRecord + + if let restoredRecord = try object.restoreRecordWithSystemFields() { + switch changeType { + case .inserted: + // Create record with same ID but wihout token data (that record was accidently deleted from CloudKit perhaps, recordID exists in CoreData, but record doesn't exist in CloudKit + let recordID = restoredRecord.recordID + recordWithSystemFields = CKRecord(recordType: restoredRecord.recordType, recordID: recordID) + case .updated: + recordWithSystemFields = restoredRecord + } + } else { + recordWithSystemFields = try object.setRecordInformation() + } + var changedAttributes: [String]? // Save changes keys only for updated object, for inserted objects full sync will be used diff --git a/Sources/Classes/Upload/ObjectToRecord/ObjectToRecordOperation.swift b/Source/Classes/Save/ObjectToRecord/ObjectToRecordOperation.swift similarity index 98% rename from Sources/Classes/Upload/ObjectToRecord/ObjectToRecordOperation.swift rename to Source/Classes/Save/ObjectToRecord/ObjectToRecordOperation.swift index 8c177b4d..917200bd 100644 --- a/Sources/Classes/Upload/ObjectToRecord/ObjectToRecordOperation.swift +++ b/Source/Classes/Save/ObjectToRecord/ObjectToRecordOperation.swift @@ -59,7 +59,6 @@ class ObjectToRecordOperation: Operation { let changedValues = managedObject.committedValues(forKeys: changedAttributes) for (attributeName, value) in changedValues { - if self.isCancelled { return } if attributeName == serviceAttributeNames.recordData || attributeName == serviceAttributeNames.recordID { continue } if let attribute = CoreDataAttribute(value: value, attributeName: attributeName, entity: managedObject.entity) { diff --git a/Source/Classes/Setup Operation/CreateCloudCoreZoneOperation.swift b/Source/Classes/Setup Operation/CreateCloudCoreZoneOperation.swift new file mode 100644 index 00000000..4ba5d3ab --- /dev/null +++ b/Source/Classes/Setup Operation/CreateCloudCoreZoneOperation.swift @@ -0,0 +1,34 @@ +// +// CreateCloudCoreZoneOperation.swift +// CloudCore +// +// Created by Vasily Ulianov on 12/12/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import Foundation +import CloudKit + +class CreateCloudCoreZoneOperation: AsynchronousOperation { + + var errorBlock: ErrorBlock? + private var createZoneOperation: CKModifyRecordZonesOperation? + + override func main() { + super.main() + + let cloudCoreZone = CKRecordZone(zoneName: CloudCore.config.zoneID.zoneName) + let recordZoneOperation = CKModifyRecordZonesOperation(recordZonesToSave: [cloudCoreZone], recordZoneIDsToDelete: nil) + recordZoneOperation.modifyRecordZonesCompletionBlock = { + if let error = $2 { + self.errorBlock?(error) + } + + self.state = .finished + } + + CKContainer.default().privateCloudDatabase.add(recordZoneOperation) + self.createZoneOperation = recordZoneOperation + } + +} diff --git a/Source/Classes/Setup Operation/SetupOperation.swift b/Source/Classes/Setup Operation/SetupOperation.swift new file mode 100644 index 00000000..ceaddc8b --- /dev/null +++ b/Source/Classes/Setup Operation/SetupOperation.swift @@ -0,0 +1,82 @@ +// +// SetupOperation.swift +// CloudCore-iOS +// +// Created by Vasily Ulianov on 13/12/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import Foundation +import CoreData + +/** + Performs several setup operations: + + 1. Create CloudCore Zone. + 2. Subscribe to that zone. + 3. Upload all local data to cloud. +*/ +class SetupOperation: Operation { + + var errorBlock: ErrorBlock? + let container: NSPersistentContainer + let parentContext: NSManagedObjectContext? + + /// - Parameters: + /// - container: persistent container to get managedObject model from + /// - parentContext: context where changed data will be save (recordID's). If it is `nil`, new context will be created from `container` and saved + init(container: NSPersistentContainer, parentContext: NSManagedObjectContext?) { + self.container = container + self.parentContext = parentContext + } + + private let queue = OperationQueue() + + override func main() { + super.main() + + let childContext: NSManagedObjectContext + + if let parentContext = self.parentContext { + childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + childContext.parent = parentContext + } else { + childContext = container.newBackgroundContext() + } + + // Create CloudCore Zone + let createZoneOperation = CreateCloudCoreZoneOperation() + createZoneOperation.errorBlock = { + self.errorBlock?($0) + self.queue.cancelAllOperations() + } + + // Subscribe operation + #if !os(watchOS) + let subscribeOperation = SubscribeOperation() + subscribeOperation.errorBlock = errorBlock + subscribeOperation.addDependency(createZoneOperation) + queue.addOperation(subscribeOperation) + #endif + + // Upload all local data + let uploadOperation = UploadAllLocalDataOperation(parentContext: childContext, managedObjectModel: container.managedObjectModel) + uploadOperation.errorBlock = errorBlock + + #if !os(watchOS) + uploadOperation.addDependency(subscribeOperation) + #endif + + queue.addOperations([createZoneOperation, uploadOperation], waitUntilFinished: true) + + if self.parentContext == nil { + do { + // It's safe to save because we instatinated that context in current thread + try childContext.save() + } catch { + errorBlock?(error) + } + } + } + +} diff --git a/Source/Classes/Setup Operation/SubscribeOperation.swift b/Source/Classes/Setup Operation/SubscribeOperation.swift new file mode 100644 index 00000000..c4c39452 --- /dev/null +++ b/Source/Classes/Setup Operation/SubscribeOperation.swift @@ -0,0 +1,81 @@ +// +// SubscribeOperation.swift +// CloudCore +// +// Created by Vasily Ulianov on 12/12/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import Foundation +import CloudKit + +#if !os(watchOS) +@available(watchOS, unavailable) +class SubscribeOperation: AsynchronousOperation { + + var errorBlock: ErrorBlock? + + private let queue = OperationQueue() + + override func main() { + super.main() + + let container = CKContainer.default() + + // Subscribe operation + let subcribeToPrivate = self.makeRecordZoneSubscriptionOperation(for: container.privateCloudDatabase, id: CloudCore.config.subscriptionIDForPrivateDB) + + // Fetch subscriptions and cancel subscription operation if subscription is already exists + let fetchPrivateSubscriptions = makeFetchSubscriptionOperation(for: container.privateCloudDatabase, + searchForSubscriptionID: CloudCore.config.subscriptionIDForPrivateDB, + operationToCancelIfSubcriptionExists: subcribeToPrivate) + + subcribeToPrivate.addDependency(fetchPrivateSubscriptions) + + // Finish operation + let finishOperation = BlockOperation { + self.state = .finished + } + finishOperation.addDependency(subcribeToPrivate) + finishOperation.addDependency(fetchPrivateSubscriptions) + + queue.addOperations([subcribeToPrivate, fetchPrivateSubscriptions, finishOperation], waitUntilFinished: false) + } + + private func makeRecordZoneSubscriptionOperation(for database: CKDatabase, id: String) -> CKModifySubscriptionsOperation { + let notificationInfo = CKNotificationInfo() + notificationInfo.shouldSendContentAvailable = true + + let subscription = CKRecordZoneSubscription(zoneID: CloudCore.config.zoneID, subscriptionID: id) + subscription.notificationInfo = notificationInfo + + let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) + operation.modifySubscriptionsCompletionBlock = { + if let error = $2 { + // Cancellation is not an error + if case CKError.operationCancelled = error { return } + + self.errorBlock?(error) + } + } + + operation.database = database + + return operation + } + + private func makeFetchSubscriptionOperation(for database: CKDatabase, searchForSubscriptionID subscriptionID: String, operationToCancelIfSubcriptionExists operationToCancel: CKModifySubscriptionsOperation) -> CKFetchSubscriptionsOperation { + let fetchSubscriptions = CKFetchSubscriptionsOperation(subscriptionIDs: [subscriptionID]) + fetchSubscriptions.database = database + fetchSubscriptions.fetchSubscriptionCompletionBlock = { subscriptions, error in + // If no errors = subscription is found and we don't need to subscribe again + if error == nil { + operationToCancel.cancel() + } + } + + return fetchSubscriptions + } + +} +#endif diff --git a/Source/Classes/Setup Operation/UploadAllLocalDataOperation.swift b/Source/Classes/Setup Operation/UploadAllLocalDataOperation.swift new file mode 100644 index 00000000..fe45d28b --- /dev/null +++ b/Source/Classes/Setup Operation/UploadAllLocalDataOperation.swift @@ -0,0 +1,77 @@ +// +// UploadAllLocalDataOperation.swift +// CloudCore +// +// Created by Vasily Ulianov on 12/12/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import Foundation +import CoreData + +class UploadAllLocalDataOperation: Operation { + + let managedObjectModel: NSManagedObjectModel + let parentContext: NSManagedObjectContext + + var errorBlock: ErrorBlock? { + didSet { + converter.errorBlock = errorBlock + cloudSaveOperationQueue.errorBlock = errorBlock + } + } + + private let converter = ObjectToRecordConverter() + private let cloudSaveOperationQueue = CloudSaveOperationQueue() + + init(parentContext: NSManagedObjectContext, managedObjectModel: NSManagedObjectModel) { + self.parentContext = parentContext + self.managedObjectModel = managedObjectModel + } + + override func main() { + super.main() + + CloudCore.delegate?.willSyncToCloud() + defer { + CloudCore.delegate?.didSyncToCloud() + } + + let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + childContext.parent = parentContext + + var allManagedObjects = Set() + for entity in managedObjectModel.cloudCoreEnabledEntities { + guard let entityName = entity.name else { continue } + let fetchRequest = NSFetchRequest(entityName: entityName) + + do { + guard let fetchedObjects = try childContext.fetch(fetchRequest) as? [NSManagedObject] else { + continue + } + + allManagedObjects.formUnion(fetchedObjects) + } catch { + errorBlock?(error) + } + } + + converter.setUnconfirmedOperations(inserted: allManagedObjects, updated: Set(), deleted: Set()) + let recordsToSave = converter.confirmConvertOperationsAndWait(in: childContext).recordsToSave + cloudSaveOperationQueue.addOperations(recordsToSave: recordsToSave, recordIDsToDelete: [RecordIDWithDatabase]()) + cloudSaveOperationQueue.waitUntilAllOperationsAreFinished() + + do { + try childContext.save() + } catch { + errorBlock?(error) + } + } + + override func cancel() { + cloudSaveOperationQueue.cancelAllOperations() + + super.cancel() + } + +} diff --git a/Sources/Enum/CloudCoreError.swift b/Source/Enum/CloudCoreError.swift similarity index 75% rename from Sources/Enum/CloudCoreError.swift rename to Source/Enum/CloudCoreError.swift index 38f95703..70c4e98b 100644 --- a/Sources/Enum/CloudCoreError.swift +++ b/Source/Enum/CloudCoreError.swift @@ -19,7 +19,13 @@ public enum CloudCoreError: Error, CustomStringConvertible { /// Some CoreData error case coreData(String) + + /// Custom error, description is placed inside associated value case custom(String) + + + /// CloudCore doesn't support relationships with `NSOrderedSet` type + case orderedSetRelationshipIsNotSupported(NSRelationshipDescription) /// A textual representation of error public var localizedDescription: String { @@ -30,6 +36,7 @@ public enum CloudCoreError: Error, CustomStringConvertible { case .cloudKit(let text): return "iCloud error: \(text)" case .coreData(let text): return "Core Data error: \(text)" case .custom(let error): return error + case .orderedSetRelationshipIsNotSupported(let relationship): return "Relationships with NSOrderedSet type are not supported. Error occured in: \(relationship)" } } diff --git a/Sources/Enum/FetchResult.swift b/Source/Enum/FetchResult.swift similarity index 100% rename from Sources/Enum/FetchResult.swift rename to Source/Enum/FetchResult.swift diff --git a/Source/Enum/Module.swift b/Source/Enum/Module.swift new file mode 100644 index 00000000..2ff7525b --- /dev/null +++ b/Source/Enum/Module.swift @@ -0,0 +1,20 @@ +// +// Module.swift +// CloudCore +// +// Created by Vasily Ulianov on 14/12/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import Foundation + +/// Enumeration with module name that issued an error in `CloudCoreErrorDelegate` +public enum Module { + + /// Save to CloudKit module + case saveToCloud + + /// Fetch from CloudKit module + case fetchFromCloud + +} diff --git a/Sources/Extensions/CKRecordID.swift b/Source/Extensions/CKRecordID.swift similarity index 100% rename from Sources/Extensions/CKRecordID.swift rename to Source/Extensions/CKRecordID.swift diff --git a/Sources/Extensions/NSEntityDescription.swift b/Source/Extensions/NSEntityDescription.swift similarity index 100% rename from Sources/Extensions/NSEntityDescription.swift rename to Source/Extensions/NSEntityDescription.swift diff --git a/Sources/Extensions/NSManagedObject.swift b/Source/Extensions/NSManagedObject.swift similarity index 100% rename from Sources/Extensions/NSManagedObject.swift rename to Source/Extensions/NSManagedObject.swift diff --git a/Source/Extensions/NSManagedObjectModel.swift b/Source/Extensions/NSManagedObjectModel.swift new file mode 100644 index 00000000..49bbbdbc --- /dev/null +++ b/Source/Extensions/NSManagedObjectModel.swift @@ -0,0 +1,25 @@ +// +// NSManagedObjectModel.swift +// CloudCore +// +// Created by Vasily Ulianov on 12/12/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import CoreData + +extension NSManagedObjectModel { + + var cloudCoreEnabledEntities: [NSEntityDescription] { + var cloudCoreEntities = [NSEntityDescription]() + + for entity in self.entities { + if entity.serviceAttributeNames != nil { + cloudCoreEntities.append(entity) + } + } + + return cloudCoreEntities + } + +} diff --git a/Sources/Model/CKRecord.swift b/Source/Model/CKRecord.swift similarity index 100% rename from Sources/Model/CKRecord.swift rename to Source/Model/CKRecord.swift diff --git a/Sources/Model/CloudCoreConfig.swift b/Source/Model/CloudCoreConfig.swift similarity index 87% rename from Sources/Model/CloudCoreConfig.swift rename to Source/Model/CloudCoreConfig.swift index 08292a36..983841a0 100644 --- a/Sources/Model/CloudCoreConfig.swift +++ b/Source/Model/CloudCoreConfig.swift @@ -57,8 +57,4 @@ public struct CloudCoreConfig { /// Default value is `CloudCoreTokens` public var userDefaultsKeyTokens = "CloudCoreTokens" - /// UserDefault's key to store boolean value of CloudCore performed one-time initialization per application installation - /// - /// Default value is `CloudCoreIsSetuped` - public var userDefaultsKeyIsSetuped = "CloudCoreIsSetuped" } diff --git a/Sources/Model/CloudKitAttribute.swift b/Source/Model/CloudKitAttribute.swift similarity index 88% rename from Sources/Model/CloudKitAttribute.swift rename to Source/Model/CloudKitAttribute.swift index f4d02a7f..8440723d 100644 --- a/Sources/Model/CloudKitAttribute.swift +++ b/Source/Model/CloudKitAttribute.swift @@ -49,10 +49,16 @@ class CloudKitAttribute { let targetEntityName = try findTargetEntityName() let fetchRequest = NSFetchRequest(entityName: targetEntityName) + // FIXME: user serviceAttributes.recordID from target entity (not from me) + fetchRequest.predicate = NSPredicate(format: serviceAttributes.recordID + " == %@" , recordID.encodedString) fetchRequest.fetchLimit = 1 - - return try context.fetch(fetchRequest).first as? NSManagedObject + fetchRequest.includesPropertyValues = false + fetchRequest.includesSubentities = false + + let foundObject = try context.fetch(fetchRequest).first as? NSManagedObject + + return foundObject } private var myRelationship: NSRelationshipDescription? { diff --git a/Sources/Model/ServiceAttributeName.swift b/Source/Model/ServiceAttributeName.swift similarity index 100% rename from Sources/Model/ServiceAttributeName.swift rename to Source/Model/ServiceAttributeName.swift diff --git a/Sources/Model/Tokens.swift b/Source/Model/Tokens.swift similarity index 64% rename from Sources/Model/Tokens.swift rename to Source/Model/Tokens.swift index d351f4ae..a91ab8a8 100644 --- a/Sources/Model/Tokens.swift +++ b/Source/Model/Tokens.swift @@ -28,53 +28,53 @@ import CloudKit ``` */ open class Tokens: NSObject, NSCoding { - var serverChangeToken: CKServerChangeToken? + var tokensByRecordZoneID = [CKRecordZoneID: CKServerChangeToken]() private struct ArchiverKey { - static let serverToken = "serverChangeToken" static let tokensByRecordZoneID = "tokensByRecordZoneID" } + /// Create fresh object without any Tokens inside. Can be used to fetch full data. public override init() { super.init() } - // MARK: NSCoding - - /// Returns an object initialized from data in a given unarchiver. - public required init?(coder aDecoder: NSCoder) { - self.serverChangeToken = aDecoder.decodeObject(forKey: ArchiverKey.serverToken) as? CKServerChangeToken - self.tokensByRecordZoneID = aDecoder.decodeObject(forKey: ArchiverKey.tokensByRecordZoneID) as? [CKRecordZoneID: CKServerChangeToken] ?? [CKRecordZoneID: CKServerChangeToken]() - } - - /// Encodes the receiver using a given archiver. - open func encode(with aCoder: NSCoder) { - aCoder.encode(serverChangeToken, forKey: ArchiverKey.serverToken) - aCoder.encode(tokensByRecordZoneID, forKey: ArchiverKey.tokensByRecordZoneID) - } - // MARK: User Defaults - /// Load saved Tokens from UserDefaults + /// Load saved Tokens from UserDefaults. Key is used from `CloudCoreConfig.userDefaultsKeyTokens` /// - /// - Parameter fromKey: UserDefaults key, default is `CloudCore.config.userDefaultsKeyTokens` /// - Returns: previously saved `Token` object, if tokens weren't saved before newly initialized `Tokens` object will be returned - open static func loadFromUserDefaults(fromKey: String = CloudCore.config.userDefaultsKeyTokens) -> Tokens { - guard let tokensData = UserDefaults.standard.data(forKey: fromKey), + open static func loadFromUserDefaults() -> Tokens { + guard let tokensData = UserDefaults.standard.data(forKey: CloudCore.config.userDefaultsKeyTokens), let tokens = NSKeyedUnarchiver.unarchiveObject(with: tokensData) as? Tokens else { return Tokens() } - + return tokens } - /// Save tokens to UserDefaults and synchronize - /// - /// - Parameter forKey: UserDefaults key, default is `CloudCore.config.userDefaultsKeyTokens` - open func saveToUserDefaults(forKey: String = CloudCore.config.userDefaultsKeyTokens) { + /// Save tokens to UserDefaults and synchronize. Key is used from `CloudCoreConfig.userDefaultsKeyTokens` + open func saveToUserDefaults() { let tokensData = NSKeyedArchiver.archivedData(withRootObject: self) - UserDefaults.standard.set(tokensData, forKey: forKey) + UserDefaults.standard.set(tokensData, forKey: CloudCore.config.userDefaultsKeyTokens) UserDefaults.standard.synchronize() } + + // MARK: NSCoding + + /// Returns an object initialized from data in a given unarchiver. + public required init?(coder aDecoder: NSCoder) { + if let decodedTokens = aDecoder.decodeObject(forKey: ArchiverKey.tokensByRecordZoneID) as? [CKRecordZoneID: CKServerChangeToken] { + self.tokensByRecordZoneID = decodedTokens + } else { + return nil + } + } + + /// Encodes the receiver using a given archiver. + open func encode(with aCoder: NSCoder) { + aCoder.encode(tokensByRecordZoneID, forKey: ArchiverKey.tokensByRecordZoneID) + } + } diff --git a/Source/Protocols/CloudCoreDelegate.swift b/Source/Protocols/CloudCoreDelegate.swift new file mode 100644 index 00000000..d0f28cf0 --- /dev/null +++ b/Source/Protocols/CloudCoreDelegate.swift @@ -0,0 +1,49 @@ +// +// CloudCoreDelegate.swift +// CloudCore +// +// Created by Vasily Ulianov on 14/12/2017. +// Copyright © 2017 Vasily Ulianov. All rights reserved. +// + +import Foundation + +/// Delegate for framework that can be used for proccesses tracking and error handling. +/// Maybe usefull to activate `UIApplication.networkActivityIndicatorVisible`. +/// All methods are optional. +public protocol CloudCoreDelegate: class { + + // MARK: Notifications + + /// Tells the delegate that fetching data from CloudKit is about to begin + func willSyncFromCloud() + + /// Tells the delegate that data fetching from CloudKit and updating local objects processes are now completed + func didSyncFromCloud() + + /// Tells the delegate that conversion operations (NSManagedObject to CKRecord) and data uploading to CloudKit is about to begin + func willSyncToCloud() + + /// Tells the delegate that data has been uploaded to CloudKit + func didSyncToCloud() + + // MARK: Error + + /// Tells the delegate that error has been occured, maybe called multiple times + /// + /// - Parameters: + /// - error: in most cases contains `CloudCoreError` or `CKError` + /// - module: framework's module that throwed an error + func error(error: Error, module: Module?) + +} + +public extension CloudCoreDelegate { + + func willSyncFromCloud() { } + func didSyncFromCloud() { } + func willSyncToCloud() { } + func didSyncToCloud() { } + func error(error: Error, module: Module?) { } + +} diff --git a/Resources/Info-Mac.plist b/Source/Resources/Info.plist similarity index 100% rename from Resources/Info-Mac.plist rename to Source/Resources/Info.plist diff --git a/Sources/Classes/CloudCore.swift b/Sources/Classes/CloudCore.swift deleted file mode 100644 index 8db6c82c..00000000 --- a/Sources/Classes/CloudCore.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// CloudCore.swift -// CloudCore -// -// Created by Vasily Ulianov on 06.02.17. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import CoreData -import CloudKit - -/** - Main framework class, in most cases you will use only methods from that class, all methods/properties are static. - - ## Save to cloud - On application inialization call `observeCoreDataChanges` method, so framework will automatically monitor changes at Core Data and upload it to iCloud. - - ### Example - - ```swift - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { - // Register for push notifications about changes - UIApplication.shared.registerForRemoteNotifications() - // Enable uploading changed local data to CoreData - CloudCore.observeCoreDataChanges(persistentContainer: self.persistentContainer, errorDelegate: nil) - return true - } - ``` - - ## Fetch from cloud - Updated objects from Core Data can be fetched with `CloudCore.fetchAndSave` methods. If you have called any of CloudCore methods before, CloudCore has automatically subscribed to hidden push notifications about data changes in CloudKit, so after you receive remoteNotifications about that changes, please call appropriate method to redirect that notification to CloudCore and framework will sync data for you. - - If you want you can sync use force sync method. - - Please use method with notification user info parameter if you're calling it from `didReceiveRemoteNotification`, because CloudCore extracts CloudKit database from notification to make less network requests on fetching. - - ### Example - - ```swift - func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - if CloudCore.isCloudCoreNotification(withUserInfo: userInfo) { - CloudCore.fetchAndSave(using: userInfo, container: self.persistentContainer, error: { error in - NSLog("CloudKit fetch error: %@", error.localizedDescription) - }, completion: { (fetchResult) in - completionHandler(fetchResult.uiBackgroundFetchResult) - }) - } - } - ``` -*/ -open class CloudCore { - - // MARK: Properties - - private(set) static var coreDataListener: CoreDataListener? - - /// CloudCore configuration, it's recommended to set up before calling any of CloudCore methods. You can read more at `CloudCoreConfig` struct description - public static var config = CloudCoreConfig() - - /// `Tokens` object, read more at class description. By default variable is loaded from User Defaults. - public static var tokens = Tokens.loadFromUserDefaults() - - public typealias NotificationUserInfo = [AnyHashable : Any] - - // MARK: Save to cloud - - /** Enable observing of changes at local database and saving them to iCloud - - - Parameters: - - persistentContainer: contextes without parents will be observed in that container, because saving of that context results writing information to disk or memory - - errorDelegate: all errors that were occurred during upload processes will be reported to `errorDelegat`e and will contain `Error` or `CloudCoreError` objects. - */ - public static func observeCoreDataChanges(persistentContainer: NSPersistentContainer, errorDelegate: CloudCoreErrorDelegate?) { - let errorBlock: ErrorBlock = { errorDelegate?.cloudCore(saveToCloudDidFailed: $0) } - - let listener = CoreDataListener(container: persistentContainer, errorBlock: errorBlock) - listener.observe() - self.coreDataListener = listener - } - - /// Remove oberserver that was created by `observeCoreDataChanges` method - public static func removeCoreDataObserver() { - coreDataListener?.stopObserving() - coreDataListener = nil - } - - // MARK: Fetch from cloud - - /** Fetch changes from one CloudKit database and save it to CoreData - - Don't forget to check notification's userinfo by calling `isCloudCoreNotification(withUserInfo:)` before calling that method. If incorrect user info is provided `FetchResult.noData` will be returned at completion block. - - - Parameters: - - userInfo: notification's user info, CloudKit database will be extraced from that notification - - container: `NSPersistentContainer` that will be used to save fetched data - - error: block will be called every time when error occurs during process - - completion: `FetchResult` enumeration with results of operation - */ - public static func fetchAndSave(using userInfo: NotificationUserInfo, container: NSPersistentContainer, error: ErrorBlock?, completion: @escaping (_ fetchResult: FetchResult) -> Void) { - guard let cloudDatabase = self.database(for: userInfo) else { - completion(.noData) - return - } - - DispatchQueue.global(qos: .utility).async { - let errorProxy = ErrorBlockProxy(destination: error) - let operation = FetchAndSaveOperation(from: [cloudDatabase], persistentContainer: container) - operation.errorBlock = { errorProxy.send(error: $0) } - operation.start() - - if errorProxy.wasError { - completion(FetchResult.failed) - } else { - completion(FetchResult.newData) - } - } - } - - /** Fetch changes from all CloudKit databases and save it to Core Data - - - Parameters: - - container: `NSPersistentContainer` that will be used to save fetched data - - error: block will be called every time when error occurs during process - - completion: `FetchResult` enumeration with results of operation - */ - public static func fetchAndSave(container: NSPersistentContainer, error: ErrorBlock?, completion: (() -> Void)?) { - DispatchQueue.global(qos: .utility).async { - let operation = FetchAndSaveOperation(persistentContainer: container) - operation.errorBlock = error - operation.completionBlock = completion - operation.start() - } - } - - /** Check if notification is CloudKit notification containing CloudCore data - - - Parameter userInfo: userInfo of notification - - Returns: `true` if notification contains CloudCore data - */ - public static func isCloudCoreNotification(withUserInfo userInfo: NotificationUserInfo) -> Bool { - return (database(for: userInfo) != nil) - } - - static func database(for notificationUserInfo: NotificationUserInfo) -> CKDatabase? { - guard let notificationDictionary = notificationUserInfo as? [String: NSObject] else { return nil } - let notification = CKNotification(fromRemoteNotificationDictionary: notificationDictionary) - - guard let id = notification.subscriptionID else { return nil } - - switch id { - case config.subscriptionIDForPrivateDB: return CKContainer.default().privateCloudDatabase - case config.subscriptionIDForSharedDB: return CKContainer.default().sharedCloudDatabase - case _ where id.hasPrefix(config.publicSubscriptionIDPrefix): return CKContainer.default().publicCloudDatabase - default: return nil - } - } -} diff --git a/Sources/Classes/Fetch/FetchAndSaveOperation.swift b/Sources/Classes/Fetch/FetchAndSaveOperation.swift deleted file mode 100644 index 2850678f..00000000 --- a/Sources/Classes/Fetch/FetchAndSaveOperation.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// FetchAndSaveOperation.swift -// CloudCore -// -// Created by Vasily Ulianov on 13/03/2017. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import CloudKit -import CoreData - -/// An operation that fetches data from CloudKit and saves it to Core Data, you can use it without calling `CloudCore.fetchAndSave` methods if you application relies on `Operation` -public class FetchAndSaveOperation: Operation { - - /// Private and Shared cloud databases - public static let allDatabases = [ -// CKContainer.default().publicCloudDatabase, - CKContainer.default().privateCloudDatabase, - CKContainer.default().sharedCloudDatabase - ] - - public typealias NotificationUserInfo = [AnyHashable : Any] - - private let tokens: Tokens - private let databases: [CKDatabase] - private let persistentContainer: NSPersistentContainer - - /// Called every time if error occurs - public var errorBlock: ErrorBlock? - - private let fetchOperationQueue = OperationQueue() - private let coreDataOperationQueue = OperationQueue() - - /// Initialize operation, it's recommended to set `errorBlock` - /// - /// - Parameters: - /// - databases: list of databases to fetch data from (now supported: private and shared) - /// - persistentContainer: `NSPersistentContainer` that will be used to save data - /// - tokens: previously saved `Tokens`, you can generate new ones if you want to fetch all data - public init(from databases: [CKDatabase] = FetchAndSaveOperation.allDatabases, persistentContainer: NSPersistentContainer, tokens: Tokens = CloudCore.tokens) { - self.tokens = tokens - self.databases = databases - self.persistentContainer = persistentContainer - - fetchOperationQueue.name = "CloudCoreFetchFromCloud" - coreDataOperationQueue.name = "CloudCoreFetchFromCloud CoreData" - coreDataOperationQueue.maxConcurrentOperationCount = 1 - } - - /// Performs the receiver’s non-concurrent task. - override public func main() { - if self.isCancelled { return } - - // Check if CloudCore is initially setupped - if !SetupOperation.isFinishedBefore { - let setupOperation = SetupOperation() - setupOperation.errorBlock = errorBlock - setupOperation.start() - } - - NotificationCenter.default.post(name: .CloudCoreWillSyncFromCloud, object: nil) - - let backgroundContext = persistentContainer.newBackgroundContext() - backgroundContext.name = CloudCore.config.contextName - - for database in self.databases { - // It will subadd fetch and save operations to queue - self.addFetchDatabaseChangesOperation(from: database, using: backgroundContext) - } - - self.fetchOperationQueue.waitUntilAllOperationsAreFinished() - self.coreDataOperationQueue.waitUntilAllOperationsAreFinished() - - do { - if self.isCancelled { return } - try backgroundContext.save() - } catch { - errorBlock?(error) - } - - NotificationCenter.default.post(name: .CloudCoreDidSyncFromCloud, object: nil) - } - - /// Advises the operation object that it should stop executing its task. - public override func cancel() { - self.fetchOperationQueue.cancelAllOperations() - self.coreDataOperationQueue.cancelAllOperations() - - super.cancel() - } - - private func addFetchDatabaseChangesOperation(from database: CKDatabase, using context: NSManagedObjectContext) { - let databaseChangesOperation = FetchDatabaseChangesOperation(from: database, zoneName: CloudCore.config.zoneID.zoneName, tokens: tokens) - - databaseChangesOperation.fetchDatabaseChangesCompletionBlock = { recordZoneIDs, error in - if let error = error { - self.errorBlock?(error) - } else { - self.addRecordZoneChangesOperation(recordZoneIDs: recordZoneIDs, database: database, parentContext: context) - } - } - - fetchOperationQueue.addOperation(databaseChangesOperation) - } - - private func addRecordZoneChangesOperation(recordZoneIDs: [CKRecordZoneID], database: CKDatabase, parentContext: NSManagedObjectContext) { - if recordZoneIDs.isEmpty { return } - - let recordZoneChangesOperation = FetchRecordZoneChangesOperation(from: database, recordZoneIDs: recordZoneIDs, tokens: tokens) - - recordZoneChangesOperation.recordChangedBlock = { - // Convert and write CKRecord To NSManagedObject Operation - let convertOperation = RecordToCoreDataOperation(parentContext: parentContext, record: $0) - convertOperation.errorBlock = { self.errorBlock?($0) } - self.coreDataOperationQueue.addOperation(convertOperation) - } - - recordZoneChangesOperation.recordWithIDWasDeletedBlock = { - // Delete NSManagedObject with specified recordID Operation - let deleteOperation = DeleteFromCoreDataOperation(parentContext: parentContext, recordID: $0) - deleteOperation.errorBlock = { self.errorBlock?($0) } - self.coreDataOperationQueue.addOperation(deleteOperation) - } - - recordZoneChangesOperation.errorBlock = { self.errorBlock?($0) } - - fetchOperationQueue.addOperation(recordZoneChangesOperation) - } - -} diff --git a/Sources/Classes/Fetch/SubOperations/FetchDatabaseChangesOperation.swift b/Sources/Classes/Fetch/SubOperations/FetchDatabaseChangesOperation.swift deleted file mode 100644 index 2375fdd7..00000000 --- a/Sources/Classes/Fetch/SubOperations/FetchDatabaseChangesOperation.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// FetchChangesOperation.swift -// CloudCore -// -// Created by Vasily Ulianov on 09.02.17. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import CloudKit - -class FetchDatabaseChangesOperation: AsynchronousOperation { - // Set on init - let tokens: Tokens - let zoneName: String - let database: CKDatabase - // - - var fetchDatabaseChangesCompletionBlock: (([CKRecordZoneID], Error?) -> Void)? - - var changed = [CKRecordZoneID]() - - init(from database: CKDatabase, zoneName: String, tokens: Tokens) { - self.tokens = tokens - self.database = database - self.zoneName = zoneName - - super.init() - - self.name = "FetchDatabaseChangesOperation" - } - - override func main() { - super.main() - - let changesOperation = CKFetchDatabaseChangesOperation(previousServerChangeToken: tokens.serverChangeToken) - changesOperation.fetchAllChanges = true - changesOperation.recordZoneWithIDChangedBlock = { - if $0.zoneName != self.zoneName { return } - self.changed.append($0) - } - changesOperation.changeTokenUpdatedBlock = { self.tokens.serverChangeToken = $0 } - - // Fetch completed - changesOperation.fetchDatabaseChangesCompletionBlock = { - (newToken: CKServerChangeToken?, _, error: Error?) -> Void in - self.tokens.serverChangeToken = newToken - self.fetchDatabaseChangesCompletionBlock?(self.changed, error) - - self.state = .finished - } - - changesOperation.qualityOfService = self.qualityOfService - self.database.add(changesOperation) - } -} diff --git a/Sources/Classes/SetupOperation.swift b/Sources/Classes/SetupOperation.swift deleted file mode 100644 index 050bd037..00000000 --- a/Sources/Classes/SetupOperation.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// SetupOperation.swift -// CloudCore -// -// Created by Vasily Ulianov on 19/03/2017. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import Foundation -import CloudKit - -/** - An operation that performs initial setup task, have to be run one-time per application installation. - That operation is automaticly run by every method that is communicating with CloudKit, if `isFinishedBefore` is `false`. - - You can use that method directly (e.g. in "Enable iCloud" view) or use it to debug if that operation doesn't complete - successfully. -*/ -public class SetupOperation: Operation { - // MARK: Variables - - /// Variable indicating if that operation was performed before. - public static var isFinishedBefore: Bool { - get { - if let cachedValue = _isFinishedBefore { return cachedValue } - - let defaultsValue = UserDefaults.standard.bool(forKey: CloudCore.config.userDefaultsKeyIsSetuped) - _isFinishedBefore = defaultsValue - return defaultsValue - } - set { - _isFinishedBefore = newValue - UserDefaults.standard.set(newValue, forKey: CloudCore.config.userDefaultsKeyIsSetuped) - UserDefaults.standard.synchronize() - } - } - static private var _isFinishedBefore: Bool? - - /// The default service level to apply to operations executed using the queue. - override public var qualityOfService: QualityOfService { - didSet { - queue.qualityOfService = qualityOfService - } - } - - private let queue = OperationQueue() - - /// All errors will reported to this block, operation will continue execution even errors were found. - public var errorBlock: ErrorBlock? - private let errorProxy = ErrorBlockProxy(destination: nil) - - // MARK: Operation methods - - /// Performs the receiver’s non-concurrent task. - override public func main() { - if self.isCancelled { return } - - errorProxy.destination = errorBlock - - // Create zone - let createZone = self.createZonesOperation(withNames: [CloudCore.config.zoneID.zoneName]) - - // Subscriptions - let container = CKContainer.default() - - // Subscribe operations - let subcribeToPrivate = self.databaseSubscriptionOperation(database: container.privateCloudDatabase, id: CloudCore.config.subscriptionIDForPrivateDB) - let subcribeToShared = self.databaseSubscriptionOperation(database: container.sharedCloudDatabase, id: CloudCore.config.subscriptionIDForSharedDB) - - // Fetch existing subscriptions - let fetchPrivateSubscriptions = fetchSubscriptionOperation(in: container.privateCloudDatabase, searchForID: CloudCore.config.subscriptionIDForPrivateDB, cancelOperationIfIDFound: subcribeToPrivate) - let fetchSharedSubcriptions = fetchSubscriptionOperation(in: container.sharedCloudDatabase, searchForID: CloudCore.config.subscriptionIDForSharedDB, cancelOperationIfIDFound: subcribeToShared) - -// let fetchPublicSubscriptions = FetchPublicSubscriptionsOperation() -// fetchPublicSubscriptions.errorBlock = errorBlock -// fetchPublicSubscriptions.fetchCompletionBlock = { PublicDatabaseSubscriptions.setCache(from: $0) } - - queue.addOperations([createZone, fetchPrivateSubscriptions, fetchSharedSubcriptions, subcribeToPrivate, subcribeToShared], waitUntilFinished: true) - - if !errorProxy.wasError { - SetupOperation.isFinishedBefore = true - } - } - - - /** Fetch subscriptions and cancel subscribe operation if we're already subscribe - - Postcondition: Fetch operation dependecy is added to `subscribeOperation` - - Parameters: - - database: in what database we search for subscriptiong - - subscriptionID: check if we're already subscribe for that id - - subscribeOperation: cancel that operation if subscription with `subscriptionID` if found - */ - private func fetchSubscriptionOperation(in database: CKDatabase, searchForID subscriptionID: String, cancelOperationIfIDFound subscribeOperation: CKModifySubscriptionsOperation) -> CKFetchSubscriptionsOperation { - let fetchSubscriptions = CKFetchSubscriptionsOperation(subscriptionIDs: [subscriptionID]) - fetchSubscriptions.database = database - fetchSubscriptions.fetchSubscriptionCompletionBlock = { subscriptions, error in - // If no errors = subscription is found and we don't need to subscribe again - if error == nil { - subscribeOperation.cancel() - } - } - - // Subscribe operation has no to be performed before fetch operation - subscribeOperation.addDependency(fetchSubscriptions) - - return fetchSubscriptions - } - - /// Create new record zones with specified names - private func createZonesOperation(withNames names: [String]) -> CKModifyRecordZonesOperation { - assert(!names.isEmpty, "List of zones is empty") - - var newZones = [CKRecordZone]() - for name in names { - newZones += [CKRecordZone(zoneName: name)] - } - - let recordZoneOperation = CKModifyRecordZonesOperation(recordZonesToSave: newZones, recordZoneIDsToDelete: nil) - recordZoneOperation.modifyRecordZonesCompletionBlock = { self.errorProxy.send(error: $2) } - - recordZoneOperation.timeoutIntervalForResource = 20 - recordZoneOperation.database = CKContainer.default().privateCloudDatabase - - return recordZoneOperation - } - - /// Subscribe to all changes at CloudKit Private & Shared databases - private func databaseSubscriptionOperation(database: CKDatabase, id: String) -> CKModifySubscriptionsOperation { - let notificationInfo = CKNotificationInfo() - notificationInfo.shouldSendContentAvailable = true - - let subscription = CKDatabaseSubscription(subscriptionID: id) - subscription.notificationInfo = notificationInfo - - let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) - operation.modifySubscriptionsCompletionBlock = { - // Cancellation is not an error - if case .some(CKError.operationCancelled) = $2 { return } - self.errorProxy.send(error: $2) - } - - operation.timeoutIntervalForResource = 20 - operation.database = database - - return operation - } - - /// Advises the operation object that it should stop executing its task. - override public func cancel() { - self.queue.cancelAllOperations() - super.cancel() - } -} diff --git a/Sources/Extensions/NotificationName.swift b/Sources/Extensions/NotificationName.swift deleted file mode 100644 index 45e74dec..00000000 --- a/Sources/Extensions/NotificationName.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// ActivityIndicatable.swift -// CloudCore -// -// Created by Vasily Ulianov on 06.02.17. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import Foundation -import CoreData - -/// Framework custom notifications that posted during execution -public extension Notification.Name { - - /// Posted when CloudCore begins fetching data from iCloud - public static var CloudCoreWillSyncFromCloud: Notification.Name { - return Notification.Name(rawValue: "\(#function)") - } - - /// Posted when CloudCore finished fetching data from iCloud and updated local objects - public static var CloudCoreDidSyncFromCloud: Notification.Name { - return Notification.Name(rawValue: "\(#function)") - } - - /// Posted when CloudCore begins conversion operations (NSManagedObject to CKRecord) and starts uploading to iCloud - public static var CloudCoreWillSyncToCloud: Notification.Name { - return Notification.Name(rawValue: "\(#function)") - } - - /// Posted when CloudCore completed uploading data to iCloud - public static var CloudCoreDidSyncToCloud: Notification.Name { - return Notification.Name(rawValue: "\(#function)") - } -} diff --git a/Sources/Protocols/CloudCoreErrorDelegate.swift b/Sources/Protocols/CloudCoreErrorDelegate.swift deleted file mode 100644 index 7b4d0184..00000000 --- a/Sources/Protocols/CloudCoreErrorDelegate.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ErrorReporter.swift -// CloudCore -// -// Created by Vasily Ulianov on 06.02.17. -// Copyright © 2017 Vasily Ulianov. All rights reserved. -// - -import Foundation - -public protocol CloudCoreErrorDelegate { - - /// Save to cloud operation throwed an error - /// - /// - Parameter error: `Error` or `CloudCoreError` object - func cloudCore(saveToCloudDidFailed error: Error) -} diff --git a/Tests/Unit/Sources/Classes/ErrorBlockProxyTests.swift b/Tests/CloudCoreTests/Classes/ErrorBlockProxyTests.swift similarity index 100% rename from Tests/Unit/Sources/Classes/ErrorBlockProxyTests.swift rename to Tests/CloudCoreTests/Classes/ErrorBlockProxyTests.swift diff --git a/Tests/Unit/Sources/Classes/Fetch/Operations/DeleteFromCoreDataOperationTests.swift b/Tests/CloudCoreTests/Classes/Fetch/Operations/DeleteFromCoreDataOperationTests.swift similarity index 100% rename from Tests/Unit/Sources/Classes/Fetch/Operations/DeleteFromCoreDataOperationTests.swift rename to Tests/CloudCoreTests/Classes/Fetch/Operations/DeleteFromCoreDataOperationTests.swift diff --git a/Tests/Unit/Sources/Classes/Fetch/Operations/RecordToCoreDataOperationTests.swift b/Tests/CloudCoreTests/Classes/Fetch/Operations/RecordToCoreDataOperationTests.swift similarity index 77% rename from Tests/Unit/Sources/Classes/Fetch/Operations/RecordToCoreDataOperationTests.swift rename to Tests/CloudCoreTests/Classes/Fetch/Operations/RecordToCoreDataOperationTests.swift index 7f0619a1..38b4633a 100644 --- a/Tests/Unit/Sources/Classes/Fetch/Operations/RecordToCoreDataOperationTests.swift +++ b/Tests/CloudCoreTests/Classes/Fetch/Operations/RecordToCoreDataOperationTests.swift @@ -17,9 +17,20 @@ class RecordToCoreDataOperationTests: CoreDataTestCase { // - MARK: Tests func testOperation() { - let (operation, record) = makeConvertOperation(in: self.context) - operation.start() - fetchAndCheck(record: record, in: self.context) + let finishExpectation = expectation(description: "conversionFinished") + let queue = OperationQueue() + let (convertOperation, record) = makeConvertOperation(in: self.context) + + let checkOperation = BlockOperation { + finishExpectation.fulfill() + } + checkOperation.addDependency(convertOperation) + + queue.addOperations([convertOperation, checkOperation], waitUntilFinished: false) + + wait(for: [finishExpectation], timeout: 2) + + self.fetchAndCheck(record: record, in: self.context) } func testOperationsPerformance() { @@ -56,7 +67,7 @@ class RecordToCoreDataOperationTests: CoreDataTestCase { fetchRequest.predicate = NSPredicate(format: "recordID = %@", record.recordID.encodedString) do { guard let managedObject = try context.fetch(fetchRequest).first else { - XCTFail() + XCTFail("Couldn't find converted object") return } diff --git a/Tests/Unit/Sources/Classes/Upload/ObjectToRecord/CoreDataAttributeTests.swift b/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataAttributeTests.swift similarity index 100% rename from Tests/Unit/Sources/Classes/Upload/ObjectToRecord/CoreDataAttributeTests.swift rename to Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataAttributeTests.swift diff --git a/Tests/Unit/Sources/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift b/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift similarity index 100% rename from Tests/Unit/Sources/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift rename to Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/CoreDataRelationshipTests.swift diff --git a/Tests/Unit/Sources/Classes/Upload/ObjectToRecord/ObjectToRecordOperationTests.swift b/Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/ObjectToRecordOperationTests.swift similarity index 100% rename from Tests/Unit/Sources/Classes/Upload/ObjectToRecord/ObjectToRecordOperationTests.swift rename to Tests/CloudCoreTests/Classes/Upload/ObjectToRecord/ObjectToRecordOperationTests.swift diff --git a/Tests/Unit/Sources/CustomFunctions.swift b/Tests/CloudCoreTests/CustomFunctions.swift similarity index 100% rename from Tests/Unit/Sources/CustomFunctions.swift rename to Tests/CloudCoreTests/CustomFunctions.swift diff --git a/Tests/Unit/Sources/Extensions/CKRecordIDTests.swift b/Tests/CloudCoreTests/Extensions/CKRecordIDTests.swift similarity index 100% rename from Tests/Unit/Sources/Extensions/CKRecordIDTests.swift rename to Tests/CloudCoreTests/Extensions/CKRecordIDTests.swift diff --git a/Tests/Unit/Sources/Extensions/NSEntityDescriptionTests.swift b/Tests/CloudCoreTests/Extensions/NSEntityDescriptionTests.swift similarity index 100% rename from Tests/Unit/Sources/Extensions/NSEntityDescriptionTests.swift rename to Tests/CloudCoreTests/Extensions/NSEntityDescriptionTests.swift diff --git a/Tests/Unit/Sources/Extensions/NSManagedObjectTests.swift b/Tests/CloudCoreTests/Extensions/NSManagedObjectTests.swift similarity index 100% rename from Tests/Unit/Sources/Extensions/NSManagedObjectTests.swift rename to Tests/CloudCoreTests/Extensions/NSManagedObjectTests.swift diff --git a/Tests/Unit/Resources/Info.plist b/Tests/CloudCoreTests/Info.plist similarity index 100% rename from Tests/Unit/Resources/Info.plist rename to Tests/CloudCoreTests/Info.plist diff --git a/Tests/Unit/Sources/Model/CKRecordTests.swift b/Tests/CloudCoreTests/Model/CKRecordTests.swift similarity index 100% rename from Tests/Unit/Sources/Model/CKRecordTests.swift rename to Tests/CloudCoreTests/Model/CKRecordTests.swift diff --git a/Tests/Unit/Resources/model.xcdatamodeld/model.xcdatamodel/contents b/Tests/CloudCoreTests/model.xcdatamodeld/model.xcdatamodel/contents similarity index 100% rename from Tests/Unit/Resources/model.xcdatamodeld/model.xcdatamodel/contents rename to Tests/CloudCoreTests/model.xcdatamodeld/model.xcdatamodel/contents diff --git a/Tests/CloudKit/App/AppDelegate.swift b/Tests/CloudKitTests/App/AppDelegate.swift similarity index 100% rename from Tests/CloudKit/App/AppDelegate.swift rename to Tests/CloudKitTests/App/AppDelegate.swift diff --git a/Tests/CloudKit/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tests/CloudKitTests/App/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Tests/CloudKit/App/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Tests/CloudKitTests/App/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Tests/CloudKit/App/Base.lproj/LaunchScreen.storyboard b/Tests/CloudKitTests/App/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Tests/CloudKit/App/Base.lproj/LaunchScreen.storyboard rename to Tests/CloudKitTests/App/Base.lproj/LaunchScreen.storyboard diff --git a/Tests/CloudKit/App/Base.lproj/Main.storyboard b/Tests/CloudKitTests/App/Base.lproj/Main.storyboard similarity index 100% rename from Tests/CloudKit/App/Base.lproj/Main.storyboard rename to Tests/CloudKitTests/App/Base.lproj/Main.storyboard diff --git a/Tests/CloudKit/App/Info.plist b/Tests/CloudKitTests/App/Info.plist similarity index 100% rename from Tests/CloudKit/App/Info.plist rename to Tests/CloudKitTests/App/Info.plist diff --git a/TestableApp.entitlements b/Tests/CloudKitTests/App/TestableApp.entitlements similarity index 100% rename from TestableApp.entitlements rename to Tests/CloudKitTests/App/TestableApp.entitlements diff --git a/Tests/CloudKit/App/TestableApp.xcdatamodeld/.xccurrentversion b/Tests/CloudKitTests/App/TestableApp.xcdatamodeld/.xccurrentversion similarity index 100% rename from Tests/CloudKit/App/TestableApp.xcdatamodeld/.xccurrentversion rename to Tests/CloudKitTests/App/TestableApp.xcdatamodeld/.xccurrentversion diff --git a/Tests/CloudKit/App/TestableApp.xcdatamodeld/TestableApp.xcdatamodel/contents b/Tests/CloudKitTests/App/TestableApp.xcdatamodeld/TestableApp.xcdatamodel/contents similarity index 100% rename from Tests/CloudKit/App/TestableApp.xcdatamodeld/TestableApp.xcdatamodel/contents rename to Tests/CloudKitTests/App/TestableApp.xcdatamodeld/TestableApp.xcdatamodel/contents diff --git a/Tests/CloudKit/App/ViewController.swift b/Tests/CloudKitTests/App/ViewController.swift similarity index 100% rename from Tests/CloudKit/App/ViewController.swift rename to Tests/CloudKitTests/App/ViewController.swift diff --git a/Tests/CloudKit/Sources/CloudKitTests.swift b/Tests/CloudKitTests/CloudKitTests.swift similarity index 71% rename from Tests/CloudKit/Sources/CloudKitTests.swift rename to Tests/CloudKitTests/CloudKitTests.swift index 2b4c9737..17421bae 100644 --- a/Tests/CloudKit/Sources/CloudKitTests.swift +++ b/Tests/CloudKitTests/CloudKitTests.swift @@ -27,12 +27,12 @@ class CloudKitTests: CoreDataTestCase { } func testLocalToRemote() { - CloudCore.observeCoreDataChanges(persistentContainer: self.persistentContainer, errorDelegate: self) - defer { - CloudCore.removeCoreDataObserver() - } - - let didSyncExpectation = expectation(forNotification: .CloudCoreDidSyncToCloud, object: nil, handler: nil) + CloudCore.enable(persistentContainer: persistentContainer) + + let didSyncExpectation = expectation(description: "didSyncToCloudBlock") + let delegateListener = CloudCoreDelegateToBlock() + delegateListener.didSyncToCloudBlock = { didSyncExpectation.fulfill() } + CloudCore.delegate = delegateListener // Insert and save managed object let object = CorrectObject() @@ -43,19 +43,19 @@ class CloudKitTests: CoreDataTestCase { wait(for: [didSyncExpectation], timeout: 10) // Prepare fresh DB and nullify CloudCore to fetch uploaded data - CloudCore.removeCoreDataObserver() + CloudCore.disable() CloudCore.tokens = Tokens() let freshPersistentContainer = loadPersistenContainer() - context.automaticallyMergesChangesFromParent = true + freshPersistentContainer.viewContext.automaticallyMergesChangesFromParent = true // Fetch data from CloudKit let fetchExpectation = expectation(description: "fetchExpectation") - CloudCore.fetchAndSave(container: freshPersistentContainer, error: { (error) in + CloudCore.fetchAndSave(to: freshPersistentContainer, error: { (error) in XCTFail("Error while trying to fetch from CloudKit: \(error)") }) { fetchExpectation.fulfill() } - + wait(for: [fetchExpectation], timeout: 10) // Fetch data from CoreData @@ -66,11 +66,3 @@ class CloudKitTests: CoreDataTestCase { } } - -extension CloudKitTests: CloudCoreErrorDelegate { - - func cloudCore(saveToCloudDidFailed error: Error) { - XCTFail("saveToCloudDidFailed: \(error)") - } - -} diff --git a/Tests/CloudKit/Sources/CorrectObjectExtension.swift b/Tests/CloudKitTests/CorrectObjectExtension.swift similarity index 100% rename from Tests/CloudKit/Sources/CorrectObjectExtension.swift rename to Tests/CloudKitTests/CorrectObjectExtension.swift diff --git a/Tests/CloudKit/Sources/Helpers.swift b/Tests/CloudKitTests/Helpers.swift similarity index 72% rename from Tests/CloudKit/Sources/Helpers.swift rename to Tests/CloudKitTests/Helpers.swift index f47b4ab3..dc3d941d 100644 --- a/Tests/CloudKit/Sources/Helpers.swift +++ b/Tests/CloudKitTests/Helpers.swift @@ -15,14 +15,15 @@ extension CoreDataTestCase { func configureCloudKitIfNeeded() { if UserDefaults.standard.bool(forKey: "isCloudKitConfigured") { return } - - CloudCore.observeCoreDataChanges(persistentContainer: persistentContainer, errorDelegate: nil) - defer { - CloudCore.removeCoreDataObserver() - } - - let didSyncExpectation = expectation(forNotification: .CloudCoreDidSyncToCloud, object: nil, handler: nil) - + + // Setup delegate and expectation + let didSyncExpectation = expectation(description: "didSyncToCloudBlock") + let delegateListener = CloudCoreDelegateToBlock() + delegateListener.didSyncToCloudBlock = { didSyncExpectation.fulfill() } + CloudCore.delegate = delegateListener + + CloudCore.enable(persistentContainer: persistentContainer) + let object = CorrectObject() let objectMO = object.insert(in: persistentContainer.viewContext) @@ -34,16 +35,17 @@ extension CoreDataTestCase { wait(for: [didSyncExpectation], timeout: 10) - let exp = expectation(description: "fetchAndSave") - - CloudCore.fetchAndSave(container: persistentContainer, error: { (error) in - XCTFail("saveToCloudDidFailed: \(error)") + let fetchAndSaveExpectation = expectation(description: "fetchAndSave") + CloudCore.fetchAndSave(to: persistentContainer, error: { (error) in + XCTFail("fetchAndSave error: \(error)") }) { - exp.fulfill() + fetchAndSaveExpectation.fulfill() } - wait(for: [exp], timeout: 20) + wait(for: [fetchAndSaveExpectation], timeout: 10) UserDefaults.standard.set(true, forKey: "isCloudKitConfigured") + + delegateListener.didSyncToCloudBlock = nil } static func deleteAllRecordsFromCloudKit() { @@ -84,3 +86,13 @@ extension CoreDataTestCase { } } + +class CloudCoreDelegateToBlock: CloudCoreDelegate { + + var didSyncToCloudBlock: (() -> Void)? + + func didSyncToCloud() { + didSyncToCloudBlock?() + } + +} diff --git a/Tests/CloudKit/Resources/Info.plist b/Tests/CloudKitTests/Resources/Info.plist similarity index 100% rename from Tests/CloudKit/Resources/Info.plist rename to Tests/CloudKitTests/Resources/Info.plist diff --git a/Tests/CloudKit/Resources/model.xcdatamodeld/model.xcdatamodel/contents b/Tests/CloudKitTests/Resources/model.xcdatamodeld/model.xcdatamodel/contents similarity index 100% rename from Tests/CloudKit/Resources/model.xcdatamodeld/model.xcdatamodel/contents rename to Tests/CloudKitTests/Resources/model.xcdatamodeld/model.xcdatamodel/contents