├── .gitignore ├── Demo_macOS ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Demo_macOS.entitlements ├── Info.plist ├── ViewController.h ├── ViewController.m └── main.m ├── LICENSE ├── PodspecTest ├── Podfile ├── Podfile.lock ├── PodspecTest.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── PodspecTest.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── PodspecTest │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── Main.storyboard │ ├── Info.plist │ ├── PodspecTest.entitlements │ ├── ViewController.h │ ├── ViewController.m │ └── main.m ├── README.md ├── UnitTests ├── Models │ ├── ComplexRecord.h │ ├── ComplexRecord.m │ ├── SimpleRecord.h │ └── SimpleRecord.m ├── test_ZDCArray.m ├── test_ZDCDictionary.m ├── test_ZDCOrder.m ├── test_ZDCOrderedDictionary.m ├── test_ZDCOrderedSet.m ├── test_ZDCRecord.m ├── test_ZDCSet.m └── test_layered.m ├── UnitTests_iOS └── Info.plist ├── UnitTests_macOS └── Info.plist ├── UnitTests_tvOS └── Info.plist ├── ZDCSyncable ├── Internal │ ├── ZDCNull.h │ ├── ZDCNull.m │ ├── ZDCRef.h │ └── ZDCRef.m ├── Utilities │ ├── ZDCObjectSubclass.h │ ├── ZDCOrder.h │ └── ZDCOrder.m ├── ZDCArray.h ├── ZDCArray.m ├── ZDCDictionary.h ├── ZDCDictionary.m ├── ZDCObject.h ├── ZDCObject.m ├── ZDCOrderedDictionary.h ├── ZDCOrderedDictionary.m ├── ZDCOrderedSet.h ├── ZDCOrderedSet.m ├── ZDCRecord.h ├── ZDCRecord.m ├── ZDCSet.h ├── ZDCSet.m ├── ZDCSyncable.h └── ZDCSyncableObjC.h ├── ZDCSyncableObjC.podspec ├── ZDCSyncableObjC.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── ZDCSyncable_iOS └── Info.plist ├── ZDCSyncable_macOS └── Info.plist └── ZDCSyncable_tvOS └── Info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | ## Mac OS X 2 | .DS_Store 3 | _Archived Items 4 | Icon? 5 | 6 | ## Xcode 7 | 8 | # Build generated 9 | DerivedData 10 | 11 | # Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | # Other 23 | *.xccheckout 24 | *.moved-aside 25 | *.xcuserstate 26 | *.xcscmblueprint 27 | 28 | ## CocoaPodsTest 29 | 30 | PodspecTest/Pods/**/* 31 | 32 | ## Jazzy 33 | 34 | Jazzy 35 | -------------------------------------------------------------------------------- /Demo_macOS/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import 7 | 8 | @interface AppDelegate : NSObject 9 | 10 | 11 | @end 12 | 13 | -------------------------------------------------------------------------------- /Demo_macOS/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "AppDelegate.h" 7 | 8 | // You can use a module-style import: 9 | @import ZDCSyncableObjC; 10 | 11 | // Or you can use a classic-style import: 12 | //#import 13 | 14 | /** 15 | * How to use ZDCSyncable project in your app: 16 | */ 17 | @interface FooBar: ZDCRecord // < Just extend ZDCRecord 18 | 19 | @property (nonatomic, copy, readwrite) NSString *someString; // add your properties as usual 20 | @property (nonatomic, readwrite) NSUInteger someInt; // and that's it ! 21 | 22 | @end 23 | 24 | @implementation FooBar 25 | @end 26 | 27 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 28 | #pragma mark - 29 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 30 | 31 | /** 32 | * How to use ZDCSyncable project in your app: 33 | */ 34 | @interface FooBuzz: ZDCRecord // < Just extend ZDCRecord 35 | 36 | @property (nonatomic, readwrite) NSUInteger someInt; // add your properties as usual 37 | @property (nonatomic, readonly) ZDCDictionary *dict; // or use smart containers 38 | 39 | @end 40 | 41 | @implementation FooBuzz 42 | 43 | @synthesize someInt = someInt; 44 | @synthesize dict = dict; 45 | - (instancetype)init 46 | { 47 | if ((self = [super init])) { 48 | dict = [[ZDCDictionary alloc] init]; 49 | } 50 | return self; 51 | } 52 | 53 | - (id)copyWithZone:(NSZone *)zone 54 | { 55 | FooBuzz *copy = [[FooBuzz alloc] init]; 56 | copy->someInt = someInt; 57 | copy->dict = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 58 | 59 | return copy; 60 | } 61 | 62 | @end 63 | 64 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 65 | #pragma mark - 66 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 67 | 68 | @implementation AppDelegate 69 | 70 | - (void)applicationDidFinishLaunching:(NSNotification *)notification 71 | { 72 | [self demo1]; 73 | NSLog(@"-----------------------------------"); 74 | [self demo2]; 75 | NSLog(@"-----------------------------------"); 76 | [self demo3]; 77 | } 78 | 79 | - (void)demo1 80 | { 81 | FooBar *foobar = [[FooBar alloc] init]; 82 | foobar.someString = @"init"; 83 | foobar.someInt = 1; 84 | [foobar clearChangeTracking]; // starting point 85 | 86 | foobar.someString = @"modified"; 87 | foobar.someInt = 2; 88 | 89 | NSDictionary *undo = [foobar changeset]; // changes since starting point 90 | 91 | NSDictionary *redo = [foobar undo:undo error:nil]; // revert to starting point 92 | 93 | // Current state: 94 | // foobar.someString == "init" 95 | // foobar.someInt == 1 96 | 97 | NSLog(@"Post undo:"); 98 | NSLog(@"foobar.someString: %@", foobar.someString); 99 | NSLog(@"foobar.someInt: %d", (int)foobar.someInt); 100 | 101 | [foobar undo:redo error:nil]; // redo == (undo an undo) 102 | 103 | // Current state: 104 | // foobar.someString == "modified" 105 | // foobar.someInt == 2 106 | 107 | NSLog(@"Post redo:"); 108 | NSLog(@"foobar.someString: %@", foobar.someString); 109 | NSLog(@"foobar.someInt: %d", (int)foobar.someInt); 110 | } 111 | 112 | - (void)demo2 113 | { 114 | FooBuzz *foobuzz = [[FooBuzz alloc] init]; 115 | foobuzz.someInt = 1; 116 | foobuzz.dict[@"foo"] = @"buzz"; 117 | [foobuzz clearChangeTracking]; // starting point 118 | 119 | foobuzz.someInt = 2; 120 | foobuzz.dict[@"foo"] = @"modified"; 121 | 122 | NSDictionary *undo = [foobuzz changeset]; // changes since starting point 123 | 124 | NSDictionary *redo = [foobuzz undo:undo error:nil]; // revert to starting point 125 | 126 | // Current state: 127 | // foobuzz.someInt == 1 128 | // foobuzz.dict["foo"] == "buzz" 129 | 130 | NSLog(@"Post undo:"); 131 | NSLog(@"foobuzz.someInt: %d", (int)foobuzz.someInt); 132 | NSLog(@"foobuzz.dict['foo']: %@", foobuzz.dict[@"foo"]); 133 | 134 | [foobuzz undo:redo error:nil]; // redo == (undo an undo) 135 | 136 | // Current state: 137 | // foobuzz.someInt == 2 138 | // foobuzz.dict["foo"] == "modified" 139 | 140 | NSLog(@"Post redo:"); 141 | NSLog(@"foobuzz.someInt: %d", (int)foobuzz.someInt); 142 | NSLog(@"foobuzz.dict['foo']: %@", foobuzz.dict[@"foo"]); 143 | } 144 | 145 | - (void)demo3 146 | { 147 | FooBuzz *local = [[FooBuzz alloc] init]; 148 | local.someInt = 1; 149 | local.dict[@"foo"] = @"buzz"; 150 | [local clearChangeTracking]; // starting point 151 | 152 | FooBuzz *cloud = [local copy]; 153 | NSMutableArray *changesets = [NSMutableArray array]; 154 | 155 | // local modifications 156 | 157 | local.someInt = 2; 158 | local.dict[@"foo"] = @"modified"; 159 | 160 | [changesets addObject:[local changeset]]; // pending local changes 161 | 162 | // cloud modifications 163 | 164 | cloud.dict[@"duck"] = @"quack"; 165 | 166 | // Now merge cloud version into local. 167 | // Automatically take into account our pending local changes. 168 | 169 | [local mergeCloudVersion:cloud withPendingChangesets:changesets error:nil]; 170 | 171 | // Merged state: 172 | // local.someInt == 2 173 | // local.dict["foo"] == "modifed" 174 | // local.dict["duck"] == "quack" 175 | 176 | NSLog(@"Post redo:"); 177 | NSLog(@"local.someInt: %d", (int)local.someInt); 178 | NSLog(@"local.dict['foo']: %@", local.dict[@"foo"]); 179 | NSLog(@"local.dict['duck']: %@", local.dict[@"duck"]); 180 | } 181 | 182 | @end 183 | -------------------------------------------------------------------------------- /Demo_macOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Demo_macOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo_macOS/Demo_macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Demo_macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2019 4th-A Technologies. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /Demo_macOS/ViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface ViewController : NSViewController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Demo_macOS/ViewController.m: -------------------------------------------------------------------------------- 1 | #import "ViewController.h" 2 | 3 | @implementation ViewController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Demo_macOS/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Demo_macOS 4 | // 5 | // Created by Robbie Hanson on 5/29/19. 6 | // Copyright © 2019 4th-A Technologies. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) { 12 | return NSApplicationMain(argc, argv); 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 4th-ATechnologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PodspecTest/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | workspace 'PodspecTest' 4 | project 'PodspecTest.xcodeproj' 5 | 6 | use_frameworks! 7 | 8 | target :'PodspecTest' do 9 | pod 'ZDCSyncableObjC', path: '../', :inhibit_warnings => false 10 | end 11 | 12 | -------------------------------------------------------------------------------- /PodspecTest/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - ZDCSyncableObjC (1.0) 3 | 4 | DEPENDENCIES: 5 | - ZDCSyncableObjC (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | ZDCSyncableObjC: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | ZDCSyncableObjC: 722eb596e2820180b5b678ccfe19937af230ccda 13 | 14 | PODFILE CHECKSUM: c189b1b7c5b5defec19d936b3138999f3ed34c7a 15 | 16 | COCOAPODS: 1.6.1 17 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DCFE4E36229EF16C005C60A1 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFE4E35229EF16C005C60A1 /* AppDelegate.m */; }; 11 | DCFE4E39229EF16C005C60A1 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFE4E38229EF16C005C60A1 /* ViewController.m */; }; 12 | DCFE4E3B229EF16F005C60A1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCFE4E3A229EF16F005C60A1 /* Assets.xcassets */; }; 13 | DCFE4E3E229EF16F005C60A1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCFE4E3C229EF16F005C60A1 /* Main.storyboard */; }; 14 | DCFE4E41229EF16F005C60A1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFE4E40229EF16F005C60A1 /* main.m */; }; 15 | E59F01DF38E17ABBD73D3466 /* Pods_PodspecTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AE55BB164495745B3743AF9 /* Pods_PodspecTest.framework */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 5AE55BB164495745B3743AF9 /* Pods_PodspecTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PodspecTest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 78DE9181A8D4E2F43F0EAEDB /* Pods-PodspecTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PodspecTest.debug.xcconfig"; path = "Target Support Files/Pods-PodspecTest/Pods-PodspecTest.debug.xcconfig"; sourceTree = ""; }; 21 | BD1B0222EA712E3109A14905 /* Pods-PodspecTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PodspecTest.release.xcconfig"; path = "Target Support Files/Pods-PodspecTest/Pods-PodspecTest.release.xcconfig"; sourceTree = ""; }; 22 | DCFE4E31229EF16C005C60A1 /* PodspecTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PodspecTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | DCFE4E34229EF16C005C60A1 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 24 | DCFE4E35229EF16C005C60A1 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 25 | DCFE4E37229EF16C005C60A1 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 26 | DCFE4E38229EF16C005C60A1 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 27 | DCFE4E3A229EF16F005C60A1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | DCFE4E3D229EF16F005C60A1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | DCFE4E3F229EF16F005C60A1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 30 | DCFE4E40229EF16F005C60A1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 31 | DCFE4E42229EF16F005C60A1 /* PodspecTest.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PodspecTest.entitlements; sourceTree = ""; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | DCFE4E2E229EF16C005C60A1 /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | E59F01DF38E17ABBD73D3466 /* Pods_PodspecTest.framework in Frameworks */, 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXFrameworksBuildPhase section */ 44 | 45 | /* Begin PBXGroup section */ 46 | 7F3AF1DB254EAB45AB832B02 /* Frameworks */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | 5AE55BB164495745B3743AF9 /* Pods_PodspecTest.framework */, 50 | ); 51 | name = Frameworks; 52 | sourceTree = ""; 53 | }; 54 | 9399D19CE1002E226C760B3B /* Pods */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 78DE9181A8D4E2F43F0EAEDB /* Pods-PodspecTest.debug.xcconfig */, 58 | BD1B0222EA712E3109A14905 /* Pods-PodspecTest.release.xcconfig */, 59 | ); 60 | name = Pods; 61 | path = Pods; 62 | sourceTree = ""; 63 | }; 64 | DCFE4E28229EF16C005C60A1 = { 65 | isa = PBXGroup; 66 | children = ( 67 | DCFE4E33229EF16C005C60A1 /* PodspecTest */, 68 | DCFE4E32229EF16C005C60A1 /* Products */, 69 | 9399D19CE1002E226C760B3B /* Pods */, 70 | 7F3AF1DB254EAB45AB832B02 /* Frameworks */, 71 | ); 72 | sourceTree = ""; 73 | }; 74 | DCFE4E32229EF16C005C60A1 /* Products */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | DCFE4E31229EF16C005C60A1 /* PodspecTest.app */, 78 | ); 79 | name = Products; 80 | sourceTree = ""; 81 | }; 82 | DCFE4E33229EF16C005C60A1 /* PodspecTest */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | DCFE4E34229EF16C005C60A1 /* AppDelegate.h */, 86 | DCFE4E35229EF16C005C60A1 /* AppDelegate.m */, 87 | DCFE4E37229EF16C005C60A1 /* ViewController.h */, 88 | DCFE4E38229EF16C005C60A1 /* ViewController.m */, 89 | DCFE4E3A229EF16F005C60A1 /* Assets.xcassets */, 90 | DCFE4E3C229EF16F005C60A1 /* Main.storyboard */, 91 | DCFE4E3F229EF16F005C60A1 /* Info.plist */, 92 | DCFE4E40229EF16F005C60A1 /* main.m */, 93 | DCFE4E42229EF16F005C60A1 /* PodspecTest.entitlements */, 94 | ); 95 | path = PodspecTest; 96 | sourceTree = ""; 97 | }; 98 | /* End PBXGroup section */ 99 | 100 | /* Begin PBXNativeTarget section */ 101 | DCFE4E30229EF16C005C60A1 /* PodspecTest */ = { 102 | isa = PBXNativeTarget; 103 | buildConfigurationList = DCFE4E45229EF16F005C60A1 /* Build configuration list for PBXNativeTarget "PodspecTest" */; 104 | buildPhases = ( 105 | C437D64FF4C721C9AC441CD1 /* [CP] Check Pods Manifest.lock */, 106 | DCFE4E2D229EF16C005C60A1 /* Sources */, 107 | DCFE4E2E229EF16C005C60A1 /* Frameworks */, 108 | DCFE4E2F229EF16C005C60A1 /* Resources */, 109 | 1C8EEB4DE5B4152BE512F417 /* [CP] Embed Pods Frameworks */, 110 | ); 111 | buildRules = ( 112 | ); 113 | dependencies = ( 114 | ); 115 | name = PodspecTest; 116 | productName = PodspecTest; 117 | productReference = DCFE4E31229EF16C005C60A1 /* PodspecTest.app */; 118 | productType = "com.apple.product-type.application"; 119 | }; 120 | /* End PBXNativeTarget section */ 121 | 122 | /* Begin PBXProject section */ 123 | DCFE4E29229EF16C005C60A1 /* Project object */ = { 124 | isa = PBXProject; 125 | attributes = { 126 | LastUpgradeCheck = 1020; 127 | ORGANIZATIONNAME = "4th-A Technologies"; 128 | TargetAttributes = { 129 | DCFE4E30229EF16C005C60A1 = { 130 | CreatedOnToolsVersion = 10.2.1; 131 | }; 132 | }; 133 | }; 134 | buildConfigurationList = DCFE4E2C229EF16C005C60A1 /* Build configuration list for PBXProject "PodspecTest" */; 135 | compatibilityVersion = "Xcode 9.3"; 136 | developmentRegion = en; 137 | hasScannedForEncodings = 0; 138 | knownRegions = ( 139 | en, 140 | Base, 141 | ); 142 | mainGroup = DCFE4E28229EF16C005C60A1; 143 | productRefGroup = DCFE4E32229EF16C005C60A1 /* Products */; 144 | projectDirPath = ""; 145 | projectRoot = ""; 146 | targets = ( 147 | DCFE4E30229EF16C005C60A1 /* PodspecTest */, 148 | ); 149 | }; 150 | /* End PBXProject section */ 151 | 152 | /* Begin PBXResourcesBuildPhase section */ 153 | DCFE4E2F229EF16C005C60A1 /* Resources */ = { 154 | isa = PBXResourcesBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | DCFE4E3B229EF16F005C60A1 /* Assets.xcassets in Resources */, 158 | DCFE4E3E229EF16F005C60A1 /* Main.storyboard in Resources */, 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | }; 162 | /* End PBXResourcesBuildPhase section */ 163 | 164 | /* Begin PBXShellScriptBuildPhase section */ 165 | 1C8EEB4DE5B4152BE512F417 /* [CP] Embed Pods Frameworks */ = { 166 | isa = PBXShellScriptBuildPhase; 167 | buildActionMask = 2147483647; 168 | files = ( 169 | ); 170 | inputFileListPaths = ( 171 | ); 172 | inputPaths = ( 173 | "${PODS_ROOT}/Target Support Files/Pods-PodspecTest/Pods-PodspecTest-frameworks.sh", 174 | "${BUILT_PRODUCTS_DIR}/ZDCSyncableObjC/ZDCSyncableObjC.framework", 175 | ); 176 | name = "[CP] Embed Pods Frameworks"; 177 | outputFileListPaths = ( 178 | ); 179 | outputPaths = ( 180 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZDCSyncableObjC.framework", 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | shellPath = /bin/sh; 184 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PodspecTest/Pods-PodspecTest-frameworks.sh\"\n"; 185 | showEnvVarsInLog = 0; 186 | }; 187 | C437D64FF4C721C9AC441CD1 /* [CP] Check Pods Manifest.lock */ = { 188 | isa = PBXShellScriptBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | ); 192 | inputFileListPaths = ( 193 | ); 194 | inputPaths = ( 195 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 196 | "${PODS_ROOT}/Manifest.lock", 197 | ); 198 | name = "[CP] Check Pods Manifest.lock"; 199 | outputFileListPaths = ( 200 | ); 201 | outputPaths = ( 202 | "$(DERIVED_FILE_DIR)/Pods-PodspecTest-checkManifestLockResult.txt", 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | shellPath = /bin/sh; 206 | 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"; 207 | showEnvVarsInLog = 0; 208 | }; 209 | /* End PBXShellScriptBuildPhase section */ 210 | 211 | /* Begin PBXSourcesBuildPhase section */ 212 | DCFE4E2D229EF16C005C60A1 /* Sources */ = { 213 | isa = PBXSourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | DCFE4E39229EF16C005C60A1 /* ViewController.m in Sources */, 217 | DCFE4E41229EF16F005C60A1 /* main.m in Sources */, 218 | DCFE4E36229EF16C005C60A1 /* AppDelegate.m in Sources */, 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | }; 222 | /* End PBXSourcesBuildPhase section */ 223 | 224 | /* Begin PBXVariantGroup section */ 225 | DCFE4E3C229EF16F005C60A1 /* Main.storyboard */ = { 226 | isa = PBXVariantGroup; 227 | children = ( 228 | DCFE4E3D229EF16F005C60A1 /* Base */, 229 | ); 230 | name = Main.storyboard; 231 | sourceTree = ""; 232 | }; 233 | /* End PBXVariantGroup section */ 234 | 235 | /* Begin XCBuildConfiguration section */ 236 | DCFE4E43229EF16F005C60A1 /* Debug */ = { 237 | isa = XCBuildConfiguration; 238 | buildSettings = { 239 | ALWAYS_SEARCH_USER_PATHS = NO; 240 | CLANG_ANALYZER_NONNULL = YES; 241 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 242 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 243 | CLANG_CXX_LIBRARY = "libc++"; 244 | CLANG_ENABLE_MODULES = YES; 245 | CLANG_ENABLE_OBJC_ARC = YES; 246 | CLANG_ENABLE_OBJC_WEAK = YES; 247 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 248 | CLANG_WARN_BOOL_CONVERSION = YES; 249 | CLANG_WARN_COMMA = YES; 250 | CLANG_WARN_CONSTANT_CONVERSION = YES; 251 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 252 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 253 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 254 | CLANG_WARN_EMPTY_BODY = YES; 255 | CLANG_WARN_ENUM_CONVERSION = YES; 256 | CLANG_WARN_INFINITE_RECURSION = YES; 257 | CLANG_WARN_INT_CONVERSION = YES; 258 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 259 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 260 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 261 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 262 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 263 | CLANG_WARN_STRICT_PROTOTYPES = YES; 264 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 265 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 266 | CLANG_WARN_UNREACHABLE_CODE = YES; 267 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 268 | CODE_SIGN_IDENTITY = "Mac Developer"; 269 | COPY_PHASE_STRIP = NO; 270 | DEBUG_INFORMATION_FORMAT = dwarf; 271 | ENABLE_STRICT_OBJC_MSGSEND = YES; 272 | ENABLE_TESTABILITY = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu11; 274 | GCC_DYNAMIC_NO_PIC = NO; 275 | GCC_NO_COMMON_BLOCKS = YES; 276 | GCC_OPTIMIZATION_LEVEL = 0; 277 | GCC_PREPROCESSOR_DEFINITIONS = ( 278 | "DEBUG=1", 279 | "$(inherited)", 280 | ); 281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 283 | GCC_WARN_UNDECLARED_SELECTOR = YES; 284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 285 | GCC_WARN_UNUSED_FUNCTION = YES; 286 | GCC_WARN_UNUSED_VARIABLE = YES; 287 | MACOSX_DEPLOYMENT_TARGET = 10.14; 288 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 289 | MTL_FAST_MATH = YES; 290 | ONLY_ACTIVE_ARCH = YES; 291 | SDKROOT = macosx; 292 | }; 293 | name = Debug; 294 | }; 295 | DCFE4E44229EF16F005C60A1 /* Release */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ALWAYS_SEARCH_USER_PATHS = NO; 299 | CLANG_ANALYZER_NONNULL = YES; 300 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 301 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 302 | CLANG_CXX_LIBRARY = "libc++"; 303 | CLANG_ENABLE_MODULES = YES; 304 | CLANG_ENABLE_OBJC_ARC = YES; 305 | CLANG_ENABLE_OBJC_WEAK = YES; 306 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 307 | CLANG_WARN_BOOL_CONVERSION = YES; 308 | CLANG_WARN_COMMA = YES; 309 | CLANG_WARN_CONSTANT_CONVERSION = YES; 310 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 311 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 312 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 313 | CLANG_WARN_EMPTY_BODY = YES; 314 | CLANG_WARN_ENUM_CONVERSION = YES; 315 | CLANG_WARN_INFINITE_RECURSION = YES; 316 | CLANG_WARN_INT_CONVERSION = YES; 317 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 318 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 319 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 320 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 321 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 322 | CLANG_WARN_STRICT_PROTOTYPES = YES; 323 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 324 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 325 | CLANG_WARN_UNREACHABLE_CODE = YES; 326 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 327 | CODE_SIGN_IDENTITY = "Mac Developer"; 328 | COPY_PHASE_STRIP = NO; 329 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 330 | ENABLE_NS_ASSERTIONS = NO; 331 | ENABLE_STRICT_OBJC_MSGSEND = YES; 332 | GCC_C_LANGUAGE_STANDARD = gnu11; 333 | GCC_NO_COMMON_BLOCKS = YES; 334 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 335 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 336 | GCC_WARN_UNDECLARED_SELECTOR = YES; 337 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 338 | GCC_WARN_UNUSED_FUNCTION = YES; 339 | GCC_WARN_UNUSED_VARIABLE = YES; 340 | MACOSX_DEPLOYMENT_TARGET = 10.14; 341 | MTL_ENABLE_DEBUG_INFO = NO; 342 | MTL_FAST_MATH = YES; 343 | SDKROOT = macosx; 344 | }; 345 | name = Release; 346 | }; 347 | DCFE4E46229EF16F005C60A1 /* Debug */ = { 348 | isa = XCBuildConfiguration; 349 | baseConfigurationReference = 78DE9181A8D4E2F43F0EAEDB /* Pods-PodspecTest.debug.xcconfig */; 350 | buildSettings = { 351 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 352 | CODE_SIGN_ENTITLEMENTS = PodspecTest/PodspecTest.entitlements; 353 | CODE_SIGN_STYLE = Automatic; 354 | COMBINE_HIDPI_IMAGES = YES; 355 | DEVELOPMENT_TEAM = VT5GYGYX83; 356 | INFOPLIST_FILE = PodspecTest/Info.plist; 357 | LD_RUNPATH_SEARCH_PATHS = ( 358 | "$(inherited)", 359 | "@executable_path/../Frameworks", 360 | ); 361 | PRODUCT_BUNDLE_IDENTIFIER = "com.4th-a.PodspecTest"; 362 | PRODUCT_NAME = "$(TARGET_NAME)"; 363 | }; 364 | name = Debug; 365 | }; 366 | DCFE4E47229EF16F005C60A1 /* Release */ = { 367 | isa = XCBuildConfiguration; 368 | baseConfigurationReference = BD1B0222EA712E3109A14905 /* Pods-PodspecTest.release.xcconfig */; 369 | buildSettings = { 370 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 371 | CODE_SIGN_ENTITLEMENTS = PodspecTest/PodspecTest.entitlements; 372 | CODE_SIGN_STYLE = Automatic; 373 | COMBINE_HIDPI_IMAGES = YES; 374 | DEVELOPMENT_TEAM = VT5GYGYX83; 375 | INFOPLIST_FILE = PodspecTest/Info.plist; 376 | LD_RUNPATH_SEARCH_PATHS = ( 377 | "$(inherited)", 378 | "@executable_path/../Frameworks", 379 | ); 380 | PRODUCT_BUNDLE_IDENTIFIER = "com.4th-a.PodspecTest"; 381 | PRODUCT_NAME = "$(TARGET_NAME)"; 382 | }; 383 | name = Release; 384 | }; 385 | /* End XCBuildConfiguration section */ 386 | 387 | /* Begin XCConfigurationList section */ 388 | DCFE4E2C229EF16C005C60A1 /* Build configuration list for PBXProject "PodspecTest" */ = { 389 | isa = XCConfigurationList; 390 | buildConfigurations = ( 391 | DCFE4E43229EF16F005C60A1 /* Debug */, 392 | DCFE4E44229EF16F005C60A1 /* Release */, 393 | ); 394 | defaultConfigurationIsVisible = 0; 395 | defaultConfigurationName = Release; 396 | }; 397 | DCFE4E45229EF16F005C60A1 /* Build configuration list for PBXNativeTarget "PodspecTest" */ = { 398 | isa = XCConfigurationList; 399 | buildConfigurations = ( 400 | DCFE4E46229EF16F005C60A1 /* Debug */, 401 | DCFE4E47229EF16F005C60A1 /* Release */, 402 | ); 403 | defaultConfigurationIsVisible = 0; 404 | defaultConfigurationName = Release; 405 | }; 406 | /* End XCConfigurationList section */ 407 | }; 408 | rootObject = DCFE4E29229EF16C005C60A1 /* Project object */; 409 | } 410 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AppDelegate : NSObject 4 | 5 | @end 6 | 7 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | @import ZDCSyncableObjC; 4 | 5 | @implementation AppDelegate 6 | 7 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 8 | { 9 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 10 | dict[@"foo"] = @"bar"; 11 | 12 | NSLog(@"dict: %@", dict.rawDictionary); 13 | } 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /PodspecTest/PodspecTest/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /PodspecTest/PodspecTest/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2019 4th-A Technologies. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest/PodspecTest.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest/ViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface ViewController : NSViewController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest/ViewController.m: -------------------------------------------------------------------------------- 1 | #import "ViewController.h" 2 | 3 | @implementation ViewController 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /PodspecTest/PodspecTest/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // PodspecTest 4 | // 5 | // Created by Robbie Hanson on 5/29/19. 6 | // Copyright © 2019 4th-A Technologies. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) { 12 | return NSApplicationMain(argc, argv); 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZDCSyncable (objective-c version) 2 | 3 | Undo, redo & merge capabilities for plain objects in Objective-C. 4 | 5 | (There's a Swift version available [here](https://github.com/4th-ATechnologies/ZDCSyncable).) 6 | 7 | By: [ZeroDark.cloud](https://www.zerodark.cloud): A secure sync & messaging framework for your app, built on blockchain & AWS. 8 | 9 |   10 | 11 | ## Undo & Redo 12 | 13 | Example #1 14 | 15 | ```objective-c 16 | @interface FooBar: ZDCRecord // < Just extend ZDCRecord 17 | 18 | // add your properties as usual 19 | @property (nonatomic, copy, readwrite) NSString *someString; 20 | @property (nonatomic, readwrite) NSUInteger someInt; 21 | 22 | @end // That's it ! 23 | 24 | // And now you get undo & redo support (for free!) 25 | 26 | FooBar *foobar = [[FooBar alloc] init]; 27 | foobar.someString = @"init"; 28 | foobar.someInt = 1; 29 | [foobar clearChangeTracking]; // starting point 30 | 31 | foobar.someString = @"modified"; 32 | foobar.someInt = 2; 33 | 34 | NSDictionary *changeset = [foobar changeset]; // changes since starting point 35 | 36 | NSDictionary *redo = [foobar undo:changeset error:nil]; // revert to starting point 37 | 38 | // Current state: 39 | // foobar.someString == "init" 40 | // foobar.someInt == 1 41 | 42 | [foobar undo:redo error:nil]; // redo == (undo an undo) 43 | 44 | // Current state: 45 | // foobar.someString == "modified" 46 | // foobar.someInt == 2 47 | ``` 48 | 49 | Complex objects are supported via container classes: 50 | 51 | - ZDCDictionary 52 | - ZDCOrderedDictionary 53 | - ZDCSet 54 | - ZDCOrderedSet 55 | - ZDCArray 56 | 57 | Example #2 58 | 59 | ```objective-c 60 | @interface FooBuzz: ZDCRecord // < Just extend ZDCRecord 61 | 62 | // add your properties as usual 63 | @property (nonatomic, readwrite) NSUInteger someInt; 64 | 65 | // or use smart containers! 66 | @property (nonatomic, readonly) ZDCDictionary *dict; 67 | 68 | @end 69 | 70 | FooBuzz *foobuzz = [[FooBuzz alloc] init]; 71 | foobuzz.someInt = 1; 72 | foobuzz.dict[@"foo"] = @"buzz"; 73 | [foobuzz clearChangeTracking]; // starting point 74 | 75 | foobuzz.someInt = 2; 76 | foobuzz.dict[@"foo"] = @"modified"; 77 | 78 | NSDictionary *undo = [foobuzz changeset]; // changes since starting point 79 | 80 | NSDictionary *redo = [foobuzz undo:undo error:nil]; // revert to starting point 81 | 82 | // Current state: 83 | // foobuzz.someInt == 1 84 | // foobuzz.dict["foo"] == "buzz" 85 | 86 | [foobuzz undo:redo error:nil]; // redo == (undo an undo) 87 | 88 | // Current state: 89 | // foobuzz.someInt == 2 90 | // foobuzz.dict["foo"] == "modified" 91 | ``` 92 | 93 |   94 | 95 | ## Merge 96 | 97 | You can also merge changes ! (i.e. from the cloud) 98 | 99 | ```objective-c 100 | FooBuzz *local = [[FooBuzz alloc] init]; 101 | local.someInt = 1; 102 | local.dict[@"foo"] = @"buzz"; 103 | [local clearChangeTracking]; // starting point 104 | 105 | FooBuzz *cloud = [local copy]; 106 | NSMutableArray *changesets = [NSMutableArray array]; 107 | 108 | // local modifications 109 | 110 | local.someInt = 2; 111 | local.dict[@"foo"] = @"modified"; 112 | 113 | [changesets addObject:[local changeset]]; 114 | // ^ pending local changes (not yet pushed to cloud) 115 | 116 | // cloud modifications 117 | 118 | cloud.dict[@"duck"] = @"quack"; 119 | 120 | // Now merge cloud version into local. 121 | // Automatically take into account our pending local changes. 122 | 123 | [local mergeCloudVersion:cloud withPendingChangesets:changesets error:nil]; 124 | 125 | // Merged state: 126 | // local.someInt == 2 127 | // local.dict["foo"] == "modified" 128 | // local.dict["duck"] == "quack" 129 | ``` 130 | 131 |   132 | 133 | ## Motivation 134 | 135 | **Syncing data with the cloud requires the ability to properly merge changes. And properly merging changes requires knowing what's been changed.** 136 | 137 | It's a topic that's often glossed over in tutorials, and so people tend to forget about it... until it's actually time to code. And then all hell breaks loose! 138 | 139 | Syncing objects with the cloud means knowing how to merge changes from multiple devices. And this is harder than expected, because by default, this is the only information you have to perform the merge: 140 | 141 | 1. the current version of the object, as it appears in the cloud 142 | 2. the current version of the object, as it sits in your database 143 | 144 | But something is missing. If property `someInt` is different between the two versions, that could mean: 145 | 146 | - it was changed only by a remote device 147 | - it was changed only by the local device 148 | - it was changed by both devices 149 | 150 | In order to properly perform the merge, you need to know the answer to this question. 151 | 152 | What's missing is a list of changes that have been made to the LOCAL object. That is, changes that have been made, but haven't yet been pushed to the cloud. With that information, we can perform a proper merge. Because now we know: 153 | 154 | 1. the current version of the object, as it appears in the cloud 155 | 2. the current version of the object, as it sits in your database 156 | 3. a list of changes that have been made to the local object, including changed keys, and their original values 157 | 158 | So if you want to merge changes properly, you're going to need to track this information. You can do it the hard way (manually), or the easy way (using some base class that provides the tracking for you automatically). Either way, you're not out of the woods yet! 159 | 160 | It's somewhat trivial to track the changes to a simple record. That is, an object with just a few key/value pairs. And where all the values are primitive (numbers, booleans, strings). But what about when your app gets more advanced, and you need more complex objects? 161 | 162 | What if one of your properties is an array? Or a dictionary? Or a set? 163 | 164 | Truth be told, it's not THAT hard to code this stuff. It's not rocket science. But it does require **a TON of unit testing** to get all the little edge-cases correct. Which means you could spend all that time writing those unit tests yourself, or you could use an open-source version that's already been battle-tested by the community. (And then spend your extra time making your app awesome.) 165 | 166 |   167 | 168 | ## Getting Started 169 | 170 | ZDCSyncableObjcC is available via CocoaPods. 171 | 172 | #### CocoaPods 173 | 174 | Add the following to your Podfile: 175 | 176 | ``` 177 | pod 'ZDCSyncableObjC' 178 | ``` 179 | 180 | Then just run `pod install` as usual. And then you can import it via: 181 | 182 | ```objective-c 183 | // using module-style imports: 184 | @import ZDCSyncableObjC; 185 | 186 | // or you can use classic-style imports: 187 | #import 188 | ``` 189 | 190 |   191 | 192 | ## Bonus Feature: Immutability 193 | 194 | All ZDCObject subclasses can be made immutable. This includes all the container classes (such as ZDCDictionary), as well as any custom subclasses of ZDCRecord that you make. 195 | 196 | ``` 197 | myCustomSwiftObject.makeImmutable() // Boom! Cannot be modified now! 198 | ``` 199 | 200 | This is similar in concept to having separate NSDictionary/NSMutableDictionary classes. But this technique is even easier to use. 201 | 202 | (If you're wondering how this works: The change tracking already monitors the objects for changes. So once you mark an object as immutable, attempts to modify the object will throw an exception. If you want to make changes, you just copy the object, and then modify the copy.) 203 | 204 | -------------------------------------------------------------------------------- /UnitTests/Models/ComplexRecord.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCRecord.h" 7 | #import "ZDCDictionary.h" 8 | #import "ZDCSet.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | /** 13 | * Sample class - used for unit testing. 14 | * 15 | * Goal: Test a subclass of ZDCRecord with a bit more complexity. 16 | * Here we have a record that also contains an ZDCDictionary & ZDCSet. 17 | * So changes to these objects should also be included in `changeset`, etc. 18 | */ 19 | @interface ComplexRecord : ZDCRecord 20 | 21 | @property (nonatomic, copy, readwrite) NSString *someString; 22 | @property (nonatomic, assign, readwrite) NSInteger someInteger; 23 | 24 | @property (nonatomic, readonly) ZDCDictionary *dict; 25 | @property (nonatomic, readonly) ZDCSet *set; 26 | 27 | - (BOOL)isEqualToComplexRecord:(ComplexRecord *)another; 28 | 29 | @end 30 | 31 | NS_ASSUME_NONNULL_END 32 | -------------------------------------------------------------------------------- /UnitTests/Models/ComplexRecord.m: -------------------------------------------------------------------------------- 1 | #import "ComplexRecord.h" 2 | 3 | @implementation ComplexRecord 4 | 5 | @synthesize someString = someString; 6 | @synthesize someInteger = someInteger; 7 | 8 | @synthesize dict = dict; 9 | @synthesize set = set; 10 | 11 | - (instancetype)init 12 | { 13 | if ((self = [super init])) 14 | { 15 | dict = [[ZDCDictionary alloc] init]; 16 | set = [[ZDCSet alloc] init]; 17 | } 18 | return self; 19 | } 20 | 21 | - (id)copyWithZone:(NSZone *)zone 22 | { 23 | ComplexRecord *copy = [super copyWithZone:zone]; // [ZDCRecord copyWithZone:] 24 | 25 | copy->someString = self->someString; 26 | copy->someInteger = self->someInteger; 27 | 28 | copy->dict = [self->dict copy]; 29 | copy->set = [self->set copy]; 30 | 31 | return copy; 32 | } 33 | 34 | - (BOOL)isEqualToComplexRecord:(ComplexRecord *)another 35 | { 36 | if (![self->someString isEqualToString:another->someString]) return NO; 37 | if (self->someInteger != another->someInteger) return NO; 38 | 39 | if (![self->dict isEqualToDictionary:another->dict]) return NO; 40 | if (![self->set isEqualToSet:another->set]) return NO; 41 | 42 | return YES; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /UnitTests/Models/SimpleRecord.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCRecord.h" 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | /** 11 | * Sample class - used for unit testing. 12 | * 13 | * Goal: test a simple subclass of ZDCRecord. 14 | */ 15 | @interface SimpleRecord : ZDCRecord 16 | 17 | @property (nonatomic, copy, readwrite, nullable) NSString *someString; 18 | @property (nonatomic, assign, readwrite) NSInteger someInteger; 19 | 20 | - (BOOL)isEqualToSimpleRecord:(SimpleRecord *)another; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /UnitTests/Models/SimpleRecord.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "SimpleRecord.h" 7 | 8 | @implementation SimpleRecord 9 | 10 | @synthesize someString = someString; 11 | @synthesize someInteger = someInteger; 12 | 13 | - (id)copyWithZone:(NSZone *)zone 14 | { 15 | SimpleRecord *copy = [super copyWithZone:zone]; // [ZDCRecord copyWithZone:] 16 | 17 | copy->someString = self->someString; 18 | copy->someInteger = self->someInteger; 19 | 20 | return copy; 21 | } 22 | 23 | - (BOOL)isEqualToSimpleRecord:(SimpleRecord *)another 24 | { 25 | if (![self->someString isEqualToString:another->someString]) return NO; 26 | if (self->someInteger != another->someInteger) return NO; 27 | 28 | return YES; 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /UnitTests/test_ZDCDictionary.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import 7 | #import "ZDCDictionary.h" 8 | 9 | @interface test_ZDCDictionary : XCTestCase 10 | @end 11 | 12 | @implementation test_ZDCDictionary 13 | 14 | - (NSString *)randomLetters:(NSUInteger)length 15 | { 16 | NSString *alphabet = @"abcdefghijklmnopqrstuvwxyz"; 17 | NSUInteger alphabetLength = [alphabet length]; 18 | 19 | NSMutableString *result = [NSMutableString stringWithCapacity:length]; 20 | 21 | NSUInteger i; 22 | for (i = 0; i < length; i++) 23 | { 24 | unichar c = [alphabet characterAtIndex:(NSUInteger)arc4random_uniform((uint32_t)alphabetLength)]; 25 | 26 | [result appendFormat:@"%C", c]; 27 | } 28 | 29 | return result; 30 | } 31 | 32 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 33 | #pragma mark Undo - Basic 34 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 35 | 36 | - (void)test_undo_basic_1 37 | { 38 | ZDCDictionary *dict_a = nil; 39 | ZDCDictionary *dict_b = nil; 40 | 41 | // Basic undo/redo functionality. 42 | // 43 | // - add 44 | 45 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 46 | 47 | // Empty dictionary will be starting state 48 | // 49 | dict_a = [dict immutableCopy]; 50 | 51 | dict[@"cow"] = @"moo"; 52 | dict[@"duck"] = @"quack"; 53 | 54 | XCTAssert(dict.count == 2); 55 | 56 | NSDictionary *changeset_undo = [dict changeset]; 57 | dict_b = [dict immutableCopy]; 58 | 59 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:nil]; 60 | XCTAssert([dict isEqualToDictionary:dict_a]); 61 | 62 | [dict undo:changeset_redo error:nil]; 63 | XCTAssert([dict isEqualToDictionary:dict_b]); 64 | } 65 | 66 | - (void)test_undo_basic_2 67 | { 68 | ZDCDictionary *dict_a = nil; 69 | ZDCDictionary *dict_b = nil; 70 | 71 | // Basic undo/redo functionality. 72 | // 73 | // - remove 74 | 75 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 76 | 77 | dict[@"cow"] = @"moo"; 78 | dict[@"duck"] = @"quack"; 79 | 80 | [dict clearChangeTracking]; 81 | dict_a = [dict immutableCopy]; 82 | 83 | dict[@"cow"] = nil; 84 | 85 | NSDictionary *changeset_undo = [dict changeset]; 86 | dict_b = [dict immutableCopy]; 87 | 88 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:nil]; 89 | XCTAssert([dict isEqualToDictionary:dict_a]); 90 | 91 | [dict undo:changeset_redo error:nil]; 92 | XCTAssert([dict isEqualToDictionary:dict_b]); 93 | } 94 | 95 | - (void)test_undo_basic_3 96 | { 97 | ZDCDictionary *dict_a = nil; 98 | ZDCDictionary *dict_b = nil; 99 | 100 | // Basic undo/redo functionality. 101 | // 102 | // - modify 103 | 104 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 105 | 106 | dict[@"cow"] = @"moo"; 107 | dict[@"duck"] = @"quack"; 108 | 109 | [dict clearChangeTracking]; 110 | dict_a = [dict immutableCopy]; 111 | 112 | dict[@"cow"] = @"mooo"; 113 | 114 | NSDictionary *changeset_undo = [dict changeset]; 115 | dict_b = [dict immutableCopy]; 116 | 117 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:nil]; 118 | XCTAssert([dict isEqualToDictionary:dict_a]); 119 | 120 | [dict undo:changeset_redo error:nil]; 121 | XCTAssert([dict isEqualToDictionary:dict_b]); 122 | } 123 | 124 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 125 | #pragma mark Undo - Fuzz 126 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 127 | 128 | - (void)test_undo_fuzz_everything 129 | { 130 | BOOL const DEBUG_THIS_METHOD = NO; 131 | 132 | for (NSUInteger round = 0; round < 1000; round++) { @autoreleasepool 133 | { 134 | ZDCDictionary *dict_a = nil; 135 | ZDCDictionary *dict_b = nil; 136 | 137 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 138 | 139 | // Start with an object that has a random number of objects [20 - 30) 140 | { 141 | NSUInteger startCount; 142 | if (DEBUG_THIS_METHOD) 143 | startCount = 5; 144 | else 145 | startCount = 20 + (NSUInteger)arc4random_uniform((uint32_t)10); 146 | 147 | for (NSUInteger i = 0; i < startCount; i++) 148 | { 149 | NSString *key = [self randomLetters:8]; 150 | 151 | dict[key] = [self randomLetters:4]; 152 | } 153 | } 154 | 155 | [dict clearChangeTracking]; 156 | dict_a = [dict immutableCopy]; 157 | 158 | // Now make a random number of changes: [1 - 30) 159 | 160 | NSUInteger changeCount; 161 | if (DEBUG_THIS_METHOD) 162 | changeCount = 4; 163 | else 164 | changeCount = 1 + (NSUInteger)arc4random_uniform((uint32_t)29); 165 | 166 | for (NSUInteger i = 0; i < changeCount; i++) 167 | { 168 | uint32_t random = arc4random_uniform((uint32_t)3); 169 | 170 | if (random == 0) 171 | { 172 | // Add an item 173 | 174 | NSString *key = [self randomLetters:8]; 175 | NSString *value = [self randomLetters:4]; 176 | 177 | if (DEBUG_THIS_METHOD) { 178 | NSLog(@"add: key(%@) = %@", key, value); 179 | } 180 | dict[key] = value; 181 | } 182 | else if (random == 1) 183 | { 184 | // Remove an item 185 | 186 | NSUInteger idx = (NSUInteger)arc4random_uniform((uint32_t)dict.count); 187 | 188 | NSString *key = nil; 189 | NSUInteger i = 0; 190 | for (id _key in dict) 191 | { 192 | if (i == idx) { 193 | key = _key; 194 | break; 195 | } 196 | i++; 197 | } 198 | 199 | if (DEBUG_THIS_METHOD) { 200 | NSLog(@"remove: key(%@)", key); 201 | } 202 | [dict removeObjectForKey:key]; 203 | } 204 | else 205 | { 206 | // Modify an item 207 | 208 | NSUInteger idx = (NSUInteger)arc4random_uniform((uint32_t)dict.count); 209 | 210 | NSString *key = nil; 211 | NSUInteger i = 0; 212 | for (id _key in dict) 213 | { 214 | if (i == idx) { 215 | key = _key; 216 | break; 217 | } 218 | i++; 219 | } 220 | 221 | NSString *value = [self randomLetters:4]; 222 | 223 | if (DEBUG_THIS_METHOD) { 224 | NSLog(@"modify: key(%@) = %@", key, value); 225 | } 226 | dict[key] = value; 227 | } 228 | } 229 | 230 | NSDictionary *changeset_undo = [dict changeset]; 231 | dict_b = [dict immutableCopy]; 232 | 233 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:nil]; // a <- b 234 | if (DEBUG_THIS_METHOD && ![dict isEqualToDictionary:dict_a]) { 235 | NSLog(@"It's going to FAIL"); 236 | } 237 | XCTAssert([dict isEqualToDictionary:dict_a]); 238 | 239 | [dict undo:changeset_redo error:nil]; // a -> b 240 | if (DEBUG_THIS_METHOD && ![dict isEqualToDictionary:dict_b]) { 241 | NSLog(@"It's going to FAIL"); 242 | } 243 | XCTAssert([dict isEqualToDictionary:dict_b]); 244 | 245 | if (DEBUG_THIS_METHOD) { 246 | NSLog(@"-------------------------------------------------"); 247 | } 248 | }} 249 | } 250 | 251 | - (void)test_import_fuzz_everything 252 | { 253 | BOOL const DEBUG_THIS_METHOD = NO; 254 | 255 | for (NSUInteger round = 0; round < 1000; round++) { @autoreleasepool 256 | { 257 | ZDCDictionary *dict_a = nil; 258 | ZDCDictionary *dict_b = nil; 259 | NSMutableArray *changesets = [NSMutableArray array]; 260 | NSError *error = nil; 261 | 262 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 263 | 264 | // Start with an object that has a random number of objects [20 - 30) 265 | { 266 | NSUInteger startCount; 267 | if (DEBUG_THIS_METHOD) 268 | startCount = 5; 269 | else 270 | startCount = 20 + (NSUInteger)arc4random_uniform((uint32_t)10); 271 | 272 | for (NSUInteger i = 0; i < startCount; i++) 273 | { 274 | NSString *key = [self randomLetters:8]; 275 | 276 | dict[key] = @""; 277 | } 278 | } 279 | 280 | [dict clearChangeTracking]; 281 | dict_a = [dict immutableCopy]; 282 | 283 | // Make a random number of changesets: [1 - 10) 284 | 285 | NSUInteger changesetCount; 286 | if (DEBUG_THIS_METHOD) 287 | changesetCount = 2; 288 | else 289 | changesetCount = 1 +(NSUInteger)arc4random_uniform((uint32_t)9); 290 | 291 | for (NSUInteger changesetIdx = 0; changesetIdx < changesetCount; changesetIdx++) 292 | { 293 | // Make a random number of changes: [1 - 30) 294 | 295 | NSUInteger changeCount; 296 | if (DEBUG_THIS_METHOD) 297 | changeCount = 2; 298 | else 299 | changeCount = 1 + (NSUInteger)arc4random_uniform((uint32_t)29); 300 | 301 | for (NSUInteger i = 0; i < changeCount; i++) 302 | { 303 | uint32_t random = arc4random_uniform((uint32_t)3); 304 | 305 | if (random == 0) 306 | { 307 | // Add an item 308 | 309 | NSString *key = [self randomLetters:8]; 310 | NSString *value = [self randomLetters:4]; 311 | 312 | if (DEBUG_THIS_METHOD) { 313 | NSLog(@"add: key(%@) = %@", key, value); 314 | } 315 | dict[key] = value; 316 | } 317 | else if (random == 1) 318 | { 319 | // Remove an item 320 | 321 | NSUInteger idx = (NSUInteger)arc4random_uniform((uint32_t)dict.count); 322 | 323 | NSString *key = nil; 324 | NSUInteger i = 0; 325 | for (id _key in dict) 326 | { 327 | if (i == idx) { 328 | key = _key; 329 | break; 330 | } 331 | i++; 332 | } 333 | 334 | if (DEBUG_THIS_METHOD) { 335 | NSLog(@"remove: key(%@)", key); 336 | } 337 | [dict removeObjectForKey:key]; 338 | } 339 | else 340 | { 341 | // Modify an item 342 | 343 | NSUInteger idx = (NSUInteger)arc4random_uniform((uint32_t)dict.count); 344 | 345 | NSString *key = nil; 346 | NSUInteger i = 0; 347 | for (id _key in dict) 348 | { 349 | if (i == idx) { 350 | key = _key; 351 | break; 352 | } 353 | i++; 354 | } 355 | 356 | NSString *value = [self randomLetters:4]; 357 | 358 | if (DEBUG_THIS_METHOD) { 359 | NSLog(@"modify: key(%@) = %@", key, value); 360 | } 361 | dict[key] = value; 362 | } 363 | } 364 | 365 | [changesets addObject:([dict changeset] ?: @{})]; 366 | 367 | if (DEBUG_THIS_METHOD) { 368 | NSLog(@"********************"); 369 | } 370 | } 371 | 372 | dict_b = [dict immutableCopy]; 373 | 374 | error = [dict importChangesets:changesets]; 375 | XCTAssert(error == nil); 376 | 377 | XCTAssert([dict isEqual:dict_b]); 378 | 379 | NSDictionary *changeset_merged = [dict changeset]; 380 | 381 | NSDictionary *changeset_redo = [dict undo:changeset_merged error:&error]; 382 | XCTAssert(error == nil); 383 | if (DEBUG_THIS_METHOD && ![dict isEqual:dict_a]) { 384 | NSLog(@"It's going to FAIL"); 385 | } 386 | XCTAssert([dict isEqual:dict_a]); 387 | 388 | [dict undo:changeset_redo error:&error]; 389 | XCTAssert(error == nil); 390 | if (DEBUG_THIS_METHOD && ![dict isEqual:dict_b]) { 391 | NSLog(@"It's going to FAIL"); 392 | } 393 | XCTAssert([dict isEqual:dict_b]); 394 | 395 | if (DEBUG_THIS_METHOD) { 396 | NSLog(@"-------------------------------------------------"); 397 | } 398 | }} 399 | } 400 | 401 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 402 | #pragma mark Merge - Simple 403 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 404 | 405 | - (void)test_simpleMerge_1 406 | { 407 | NSError *error = nil; 408 | NSMutableArray *changesets = [NSMutableArray array]; 409 | 410 | ZDCDictionary *localDict = [[ZDCDictionary alloc] init]; 411 | localDict[@"string"] = @"abc123"; 412 | localDict[@"integer"] = @(42); 413 | 414 | [localDict clearChangeTracking]; 415 | ZDCDictionary *cloudDict = [localDict copy]; 416 | 417 | { // local changes 418 | 419 | localDict[@"string"] = @"def456"; 420 | [changesets addObject:[localDict changeset]]; 421 | } 422 | { // cloud changes 423 | 424 | cloudDict[@"integer"] = @(43); 425 | [cloudDict makeImmutable]; 426 | } 427 | 428 | [localDict mergeCloudVersion: cloudDict 429 | withPendingChangesets: changesets 430 | error: &error]; 431 | 432 | XCTAssert([localDict[@"string"] isEqualToString:@"def456"]); 433 | XCTAssert([localDict[@"integer"] isEqual:@(43)]); 434 | } 435 | 436 | - (void)test_simpleMerge_2 437 | { 438 | NSError *error = nil; 439 | NSMutableArray *changesets = [NSMutableArray array]; 440 | 441 | ZDCDictionary *localDict = [[ZDCDictionary alloc] init]; 442 | localDict[@"string"] = @"abc123"; 443 | localDict[@"integer"] = @(42); 444 | 445 | [localDict clearChangeTracking]; 446 | ZDCDictionary *cloudDict = [localDict copy]; 447 | 448 | { // local changes 449 | 450 | localDict[@"string"] = @"def456"; 451 | [changesets addObject:[localDict changeset]]; 452 | } 453 | { // cloud changes 454 | 455 | cloudDict[@"string"] = @"xyz789"; 456 | cloudDict[@"integer"] = @(43); 457 | [cloudDict makeImmutable]; 458 | } 459 | 460 | [localDict mergeCloudVersion: cloudDict 461 | withPendingChangesets: changesets 462 | error: &error]; 463 | 464 | XCTAssert([localDict[@"string"] isEqualToString:@"xyz789"]); 465 | XCTAssert([localDict[@"integer"] isEqual:@(43)]); 466 | } 467 | 468 | - (void)test_simpleMerge_3 469 | { 470 | NSError *error = nil; 471 | NSMutableArray *changesets = [NSMutableArray array]; 472 | 473 | ZDCDictionary *localDict = [[ZDCDictionary alloc] init]; 474 | localDict[@"string"] = nil; 475 | localDict[@"integer"] = @(42); 476 | 477 | [localDict clearChangeTracking]; 478 | ZDCDictionary *cloudDict = [localDict copy]; 479 | 480 | { // local changes 481 | 482 | localDict[@"string"] = @"def456"; 483 | [changesets addObject:[localDict changeset]]; 484 | } 485 | { // cloud changes 486 | 487 | cloudDict[@"string"] = @"xyz789"; 488 | cloudDict[@"integer"] = @(43); 489 | [cloudDict makeImmutable]; 490 | } 491 | 492 | [localDict mergeCloudVersion: cloudDict 493 | withPendingChangesets: changesets 494 | error: &error]; 495 | 496 | XCTAssert([localDict[@"string"] isEqualToString:@"xyz789"]); 497 | XCTAssert([localDict[@"integer"] isEqual:@(43)]); 498 | } 499 | 500 | - (void)test_simpleMerge_4 501 | { 502 | NSError *error = nil; 503 | NSMutableArray *changesets = [NSMutableArray array]; 504 | 505 | ZDCDictionary *localDict = [[ZDCDictionary alloc] init]; 506 | localDict[@"string"] = nil; 507 | localDict[@"integer"] = @(42); 508 | 509 | [localDict clearChangeTracking]; 510 | ZDCDictionary *cloudDict = [localDict copy]; 511 | 512 | { // local changes 513 | 514 | localDict[@"string"] = @"def456"; 515 | [changesets addObject:[localDict changeset]]; 516 | } 517 | { // cloud changes 518 | 519 | cloudDict[@"integer"] = @(43); 520 | [cloudDict makeImmutable]; 521 | } 522 | 523 | [localDict mergeCloudVersion: cloudDict 524 | withPendingChangesets: changesets 525 | error: &error]; 526 | 527 | XCTAssert([localDict[@"string"] isEqualToString:@"def456"]); 528 | XCTAssert([localDict[@"integer"] isEqual:@(43)]); 529 | } 530 | 531 | - (void)test_simpleMerge_5 532 | { 533 | NSError *error = nil; 534 | NSMutableArray *changesets = [NSMutableArray array]; 535 | 536 | ZDCDictionary *localDict = [[ZDCDictionary alloc] init]; 537 | localDict[@"string"] = @"abc123"; 538 | localDict[@"integer"] = @(42); 539 | 540 | [localDict clearChangeTracking]; 541 | ZDCDictionary *cloudDict = [localDict copy]; 542 | 543 | { // local changes 544 | 545 | localDict[@"integer"] = @(43); 546 | [changesets addObject:[localDict changeset]]; 547 | } 548 | { // cloud changes 549 | 550 | cloudDict[@"string"] = nil; 551 | [cloudDict makeImmutable]; 552 | } 553 | 554 | [localDict mergeCloudVersion: cloudDict 555 | withPendingChangesets: changesets 556 | error: &error]; 557 | 558 | XCTAssert(localDict[@"string"] == nil); 559 | XCTAssert([localDict[@"integer"] isEqual:@(43)]); 560 | } 561 | 562 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 563 | #pragma mark Merge - Complex 564 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 565 | 566 | - (void)test_complexMerge_1 567 | { 568 | NSError *error = nil; 569 | NSMutableArray *changesets = [NSMutableArray array]; 570 | 571 | ZDCDictionary *localDict = [[ZDCDictionary alloc] init]; 572 | localDict[@"dict"] = [[ZDCDictionary alloc] init]; 573 | localDict[@"dict"][@"dog"] = @"bark"; 574 | 575 | [localDict clearChangeTracking]; 576 | ZDCDictionary *cloudDict = [[ZDCDictionary alloc] initWithDictionary:localDict.rawDictionary copyItems:YES]; 577 | 578 | { // local changes 579 | 580 | localDict[@"string"] = @"abc123"; 581 | localDict[@"dict"][@"cat"] = @"meow"; 582 | [changesets addObject:[localDict changeset]]; 583 | } 584 | { // cloud changes 585 | 586 | cloudDict[@"integer"] = @(43); 587 | cloudDict[@"dict"][@"duck"] = @"quack"; 588 | [cloudDict makeImmutable]; 589 | } 590 | 591 | XCTAssert(localDict[@"dict"][@"duck"] == nil); 592 | 593 | [localDict mergeCloudVersion: cloudDict 594 | withPendingChangesets: changesets 595 | error: &error]; 596 | 597 | XCTAssert([localDict[@"string"] isEqualToString:@"abc123"]); 598 | XCTAssert([localDict[@"integer"] isEqual:@(43)]); 599 | 600 | XCTAssert([localDict[@"dict"][@"dog"] isEqualToString:@"bark"]); 601 | XCTAssert([localDict[@"dict"][@"cat"] isEqualToString:@"meow"]); 602 | XCTAssert([localDict[@"dict"][@"duck"] isEqualToString:@"quack"]); 603 | } 604 | 605 | @end 606 | -------------------------------------------------------------------------------- /UnitTests/test_ZDCOrder.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import 7 | #import "ZDCOrder.h" 8 | 9 | @interface test_ZDCOrder : XCTestCase 10 | @end 11 | 12 | @implementation test_ZDCOrder 13 | 14 | - (NSString *)randomLetters:(NSUInteger)length 15 | { 16 | NSString *alphabet = @"abcdefghijklmnopqrstuvwxyz"; 17 | NSUInteger alphabetLength = [alphabet length]; 18 | 19 | NSMutableString *result = [NSMutableString stringWithCapacity:length]; 20 | 21 | NSUInteger i; 22 | for (i = 0; i < length; i++) 23 | { 24 | unichar c = [alphabet characterAtIndex:(NSUInteger)arc4random_uniform((uint32_t)alphabetLength)]; 25 | 26 | [result appendFormat:@"%C", c]; 27 | } 28 | 29 | return result; 30 | } 31 | 32 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 33 | #pragma mark Fuzz 34 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 35 | 36 | - (void)test_fuzz 37 | { 38 | BOOL const DEBUG_THIS_METHOD = NO; 39 | 40 | for (NSUInteger round = 0; round < 5000; round++) { @autoreleasepool 41 | { 42 | NSArray *src = nil; 43 | 44 | NSUInteger arrayCount; 45 | if (DEBUG_THIS_METHOD) 46 | arrayCount = 10; 47 | else 48 | arrayCount = 20 + (NSUInteger)arc4random_uniform((uint32_t)10); 49 | 50 | // Start with an object that has a random number of objects [20 - 30) 51 | { 52 | NSMutableArray *_src = [NSMutableArray arrayWithCapacity:arrayCount]; 53 | 54 | for (NSUInteger i = 0; i < arrayCount; i++) 55 | { 56 | NSString *key = [self randomLetters:8]; 57 | 58 | [_src addObject:key]; 59 | } 60 | 61 | src = [_src copy]; 62 | } 63 | 64 | NSMutableArray *dst = [src mutableCopy]; 65 | 66 | // Now make a random number of changes: [1 - 20) 67 | 68 | NSUInteger changeCount; 69 | if (DEBUG_THIS_METHOD) 70 | changeCount = 2; 71 | else 72 | changeCount = 1 + (NSUInteger)arc4random_uniform((uint32_t)19); 73 | 74 | for (NSUInteger i = 0; i < changeCount; i++) 75 | { 76 | NSUInteger oldIdx = (NSUInteger)arc4random_uniform((uint32_t)dst.count); 77 | NSUInteger newIdx; 78 | do { 79 | newIdx = (NSUInteger)arc4random_uniform((uint32_t)dst.count); 80 | } while (oldIdx == newIdx); 81 | 82 | if (DEBUG_THIS_METHOD) { 83 | NSLog(@"move: %llu -> %llu", (unsigned long long)oldIdx, (unsigned long long)newIdx); 84 | } 85 | NSString *key = dst[oldIdx]; 86 | 87 | [dst removeObjectAtIndex:oldIdx]; 88 | [dst insertObject:key atIndex:newIdx]; 89 | } 90 | 91 | // Does it halt ? 92 | NSArray *changes = [ZDCOrder estimateChangesetFrom:src to:dst hints:nil]; 93 | 94 | XCTAssert(changes.count >= 0); 95 | 96 | if (DEBUG_THIS_METHOD) { 97 | NSLog(@"-------------------------------------------------"); 98 | } 99 | }} 100 | } 101 | 102 | @end 103 | -------------------------------------------------------------------------------- /UnitTests/test_ZDCRecord.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import 7 | 8 | #import "ZDCRecord.h" 9 | 10 | #import "SimpleRecord.h" 11 | #import "ComplexRecord.h" 12 | 13 | @interface test_ZDCRecord : XCTestCase 14 | @end 15 | 16 | @implementation test_ZDCRecord 17 | 18 | - (void)test_undo 19 | { 20 | SimpleRecord *sr_a = nil; 21 | SimpleRecord *sr_b = nil; 22 | 23 | SimpleRecord *sr = [[SimpleRecord alloc] init]; 24 | 25 | sr.someString = @"abc123"; 26 | sr.someInteger = 42; 27 | 28 | [sr clearChangeTracking]; 29 | sr_a = [sr immutableCopy]; 30 | 31 | sr.someString = @"def456"; 32 | sr.someInteger = 23; 33 | 34 | NSDictionary *changeset_undo = [sr changeset]; 35 | sr_b = [sr immutableCopy]; 36 | 37 | NSError *error = nil; 38 | NSDictionary *changeset_redo = [sr undo:changeset_undo error:&error]; 39 | 40 | XCTAssert(error == nil); 41 | XCTAssert([sr isEqualToSimpleRecord:sr_a]); 42 | 43 | [sr undo:changeset_redo error:&error]; 44 | 45 | XCTAssert(error == nil); 46 | XCTAssert([sr isEqualToSimpleRecord:sr_b]); 47 | } 48 | 49 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 50 | #pragma mark Merge: Simple 51 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 52 | 53 | - (void)test_simpleMerge_1 54 | { 55 | NSError *error = nil; 56 | NSMutableArray *changesets = [NSMutableArray array]; 57 | 58 | SimpleRecord *localRecord = [[SimpleRecord alloc] init]; 59 | localRecord.someString = @"abc123"; 60 | localRecord.someInteger = 42; 61 | 62 | [localRecord clearChangeTracking]; 63 | SimpleRecord *cloudRecord = [localRecord copy]; 64 | 65 | { // local changes 66 | 67 | localRecord.someString = @"def456"; 68 | [changesets addObject:[localRecord changeset]]; 69 | } 70 | { // cloud changes 71 | 72 | cloudRecord.someInteger = 43; 73 | [cloudRecord makeImmutable]; 74 | } 75 | 76 | [localRecord mergeCloudVersion: cloudRecord 77 | withPendingChangesets: changesets 78 | error: &error]; 79 | 80 | XCTAssert([localRecord.someString isEqualToString:@"def456"]); 81 | XCTAssert(localRecord.someInteger == 43); 82 | } 83 | 84 | - (void)test_simpleMerge_2 85 | { 86 | NSError *error = nil; 87 | NSMutableArray *changesets = [NSMutableArray array]; 88 | 89 | SimpleRecord *localRecord = [[SimpleRecord alloc] init]; 90 | localRecord.someString = @"abc123"; 91 | localRecord.someInteger = 42; 92 | 93 | [localRecord clearChangeTracking]; 94 | SimpleRecord *cloudRecord = [localRecord copy]; 95 | 96 | { // local changes 97 | 98 | localRecord.someString = @"def456"; 99 | [changesets addObject:[localRecord changeset]]; 100 | } 101 | { // cloud changes 102 | 103 | cloudRecord.someString = @"xyz789"; 104 | cloudRecord.someInteger = 43; 105 | [cloudRecord makeImmutable]; 106 | } 107 | 108 | [localRecord mergeCloudVersion: cloudRecord 109 | withPendingChangesets: changesets 110 | error: &error]; 111 | 112 | XCTAssert([localRecord.someString isEqualToString:@"xyz789"]); 113 | XCTAssert(localRecord.someInteger == 43); 114 | } 115 | 116 | - (void)test_simpleMerge_3 117 | { 118 | NSError *error = nil; 119 | NSMutableArray *changesets = [NSMutableArray array]; 120 | 121 | SimpleRecord *localRecord = [[SimpleRecord alloc] init]; 122 | localRecord.someString = nil; 123 | localRecord.someInteger = 42; 124 | 125 | [localRecord clearChangeTracking]; 126 | SimpleRecord *cloudRecord = [localRecord copy]; 127 | 128 | { // local changes 129 | 130 | localRecord.someString = @"def456"; 131 | [changesets addObject:[localRecord changeset]]; 132 | } 133 | { // cloud changes 134 | 135 | cloudRecord.someString = @"xyz789"; 136 | cloudRecord.someInteger = 43; 137 | [cloudRecord makeImmutable]; 138 | } 139 | 140 | [localRecord mergeCloudVersion: cloudRecord 141 | withPendingChangesets: changesets 142 | error: &error]; 143 | 144 | XCTAssert([localRecord.someString isEqualToString:@"xyz789"]); 145 | XCTAssert(localRecord.someInteger == 43); 146 | } 147 | 148 | - (void)test_simpleMerge_4 149 | { 150 | NSError *error = nil; 151 | NSMutableArray *changesets = [NSMutableArray array]; 152 | 153 | SimpleRecord *localRecord = [[SimpleRecord alloc] init]; 154 | localRecord.someString = nil; 155 | localRecord.someInteger = 42; 156 | 157 | [localRecord clearChangeTracking]; 158 | SimpleRecord *cloudRecord = [localRecord copy]; 159 | 160 | { // local changes 161 | 162 | localRecord.someString = @"def456"; 163 | [changesets addObject:[localRecord changeset]]; 164 | } 165 | { // cloud changes 166 | 167 | cloudRecord.someInteger = 43; 168 | [cloudRecord makeImmutable]; 169 | } 170 | 171 | [localRecord mergeCloudVersion: cloudRecord 172 | withPendingChangesets: changesets 173 | error: &error]; 174 | 175 | XCTAssert([localRecord.someString isEqualToString:@"def456"]); 176 | XCTAssert(localRecord.someInteger == 43); 177 | } 178 | 179 | - (void)test_simpleMerge_5 180 | { 181 | NSError *error = nil; 182 | NSMutableArray *changesets = [NSMutableArray array]; 183 | 184 | SimpleRecord *localRecord = [[SimpleRecord alloc] init]; 185 | localRecord.someString = @"abc123"; 186 | localRecord.someInteger = 42; 187 | 188 | [localRecord clearChangeTracking]; 189 | SimpleRecord *cloudRecord = [localRecord copy]; 190 | 191 | { // local changes 192 | 193 | localRecord.someInteger = 43; 194 | [changesets addObject:[localRecord changeset]]; 195 | } 196 | { // cloud changes 197 | 198 | cloudRecord.someString = nil; 199 | [cloudRecord makeImmutable]; 200 | } 201 | 202 | [localRecord mergeCloudVersion: cloudRecord 203 | withPendingChangesets: changesets 204 | error: &error]; 205 | 206 | XCTAssert(localRecord.someString == nil); 207 | XCTAssert(localRecord.someInteger == 43); 208 | } 209 | 210 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 211 | #pragma mark Merge: Complex 212 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 213 | 214 | - (void)test_complexMerge_1 215 | { 216 | NSError *error = nil; 217 | NSMutableArray *changesets = [NSMutableArray array]; 218 | 219 | ComplexRecord *localRecord = [[ComplexRecord alloc] init]; 220 | localRecord.dict[@"dog"] = @"bark"; 221 | 222 | [localRecord clearChangeTracking]; 223 | ComplexRecord *cloudRecord = [localRecord copy]; 224 | 225 | { // local changes 226 | 227 | localRecord.someString = @"abc123"; 228 | localRecord.dict[@"cat"] = @"meow"; 229 | [changesets addObject:[localRecord changeset]]; 230 | } 231 | { // cloud changes 232 | 233 | cloudRecord.someInteger = 43; 234 | cloudRecord.dict[@"duck"] = @"quack"; 235 | [cloudRecord makeImmutable]; 236 | } 237 | 238 | [localRecord mergeCloudVersion: cloudRecord 239 | withPendingChangesets: changesets 240 | error: &error]; 241 | 242 | XCTAssert([localRecord.someString isEqualToString:@"abc123"]); 243 | XCTAssert(localRecord.someInteger == 43); 244 | 245 | XCTAssert([localRecord.dict[@"dog"] isEqualToString:@"bark"]); 246 | XCTAssert([localRecord.dict[@"cat"] isEqualToString:@"meow"]); 247 | XCTAssert([localRecord.dict[@"duck"] isEqualToString:@"quack"]); 248 | } 249 | 250 | @end 251 | -------------------------------------------------------------------------------- /UnitTests/test_ZDCSet.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import 7 | #import "ZDCSet.h" 8 | 9 | @interface test_ZDCSet : XCTestCase 10 | @end 11 | 12 | @implementation test_ZDCSet 13 | 14 | - (NSString *)randomLetters:(NSUInteger)length 15 | { 16 | NSString *alphabet = @"abcdefghijklmnopqrstuvwxyz"; 17 | NSUInteger alphabetLength = [alphabet length]; 18 | 19 | NSMutableString *result = [NSMutableString stringWithCapacity:length]; 20 | 21 | NSUInteger i; 22 | for (i = 0; i < length; i++) 23 | { 24 | unichar c = [alphabet characterAtIndex:(NSUInteger)arc4random_uniform((uint32_t)alphabetLength)]; 25 | 26 | [result appendFormat:@"%C", c]; 27 | } 28 | 29 | return result; 30 | } 31 | 32 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 33 | #pragma mark Fuzz 34 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 35 | 36 | - (void)test_undo_fuzz_everything 37 | { 38 | BOOL const DEBUG_THIS_METHOD = NO; 39 | 40 | for (NSUInteger round = 0; round < 1000; round++) { @autoreleasepool 41 | { 42 | ZDCSet *set_a = nil; 43 | ZDCSet *set_b = nil; 44 | 45 | ZDCSet *set = [[ZDCSet alloc] init]; 46 | 47 | // Start with an object that has a random number of objects [20 - 30) 48 | { 49 | NSUInteger startCount; 50 | if (DEBUG_THIS_METHOD) 51 | startCount = 5; 52 | else 53 | startCount = 20 + (NSUInteger)arc4random_uniform((uint32_t)10); 54 | 55 | for (NSUInteger i = 0; i < startCount; i++) 56 | { 57 | NSString *key = [self randomLetters:8]; 58 | 59 | [set addObject:key]; 60 | } 61 | } 62 | 63 | [set clearChangeTracking]; 64 | set_a = [set immutableCopy]; 65 | 66 | // Now make a random number of changes: [1 - 30) 67 | 68 | NSUInteger changeCount; 69 | if (DEBUG_THIS_METHOD) 70 | changeCount = 4; 71 | else 72 | changeCount = 1 + (NSUInteger)arc4random_uniform((uint32_t)29); 73 | 74 | for (NSUInteger i = 0; i < changeCount; i++) 75 | { 76 | uint32_t random = arc4random_uniform((uint32_t)2); 77 | 78 | if (random == 0) 79 | { 80 | // Add an item 81 | 82 | NSString *key = [self randomLetters:8]; 83 | 84 | if (DEBUG_THIS_METHOD) { 85 | NSLog(@"addObject: %@", key); 86 | } 87 | [set addObject:key]; 88 | } 89 | else if (random == 1) 90 | { 91 | // Remove an item 92 | 93 | NSUInteger idx = (NSUInteger)arc4random_uniform((uint32_t)set.count); 94 | 95 | NSString *key = nil; 96 | NSUInteger i = 0; 97 | for (NSString *obj in set) 98 | { 99 | if (i == idx) { 100 | key = obj; 101 | break; 102 | } 103 | i++; 104 | } 105 | 106 | if (DEBUG_THIS_METHOD) { 107 | NSLog(@"removeObject:%@", key); 108 | } 109 | [set removeObject:key]; 110 | } 111 | } 112 | 113 | NSDictionary *changeset_undo = [set changeset]; 114 | set_b = [set immutableCopy]; 115 | 116 | NSDictionary *changeset_redo = [set undo:changeset_undo error:nil]; // a <- b 117 | if (DEBUG_THIS_METHOD && ![set isEqualToSet:set_a]) { 118 | NSLog(@"It's going to FAIL"); 119 | } 120 | XCTAssert([set isEqualToSet:set_a]); 121 | 122 | [set undo:changeset_redo error:nil]; // a -> b 123 | if (DEBUG_THIS_METHOD && ![set isEqualToSet:set_b]) { 124 | NSLog(@"It's going to FAIL"); 125 | } 126 | XCTAssert([set isEqualToSet:set_b]); 127 | 128 | if (DEBUG_THIS_METHOD) { 129 | NSLog(@"-------------------------------------------------"); 130 | } 131 | }} 132 | } 133 | 134 | - (void)test_import_fuzz_everything 135 | { 136 | BOOL const DEBUG_THIS_METHOD = NO; 137 | 138 | for (NSUInteger round = 0; round < 1000; round++) { @autoreleasepool 139 | { 140 | ZDCSet *set_a = nil; 141 | ZDCSet *set_b = nil; 142 | NSMutableArray *changesets = [NSMutableArray array]; 143 | NSError *error = nil; 144 | 145 | ZDCSet *set = [[ZDCSet alloc] init]; 146 | 147 | // Start with an object that has a random number of objects [20 - 30) 148 | { 149 | NSUInteger startCount; 150 | if (DEBUG_THIS_METHOD) 151 | startCount = 5; 152 | else 153 | startCount = 20 + (NSUInteger)arc4random_uniform((uint32_t)10); 154 | 155 | for (NSUInteger i = 0; i < startCount; i++) 156 | { 157 | NSString *key = [self randomLetters:8]; 158 | 159 | [set addObject:key]; 160 | } 161 | } 162 | 163 | [set clearChangeTracking]; 164 | set_a = [set immutableCopy]; 165 | 166 | // Make a random number of changesets: [1 - 10) 167 | 168 | NSUInteger changesetCount; 169 | if (DEBUG_THIS_METHOD) 170 | changesetCount = 2; 171 | else 172 | changesetCount = 1 +(NSUInteger)arc4random_uniform((uint32_t)9); 173 | 174 | for (NSUInteger changesetIdx = 0; changesetIdx < changesetCount; changesetIdx++) 175 | { 176 | // Make a random number of changes: [1 - 30) 177 | 178 | NSUInteger changeCount; 179 | if (DEBUG_THIS_METHOD) 180 | changeCount = 2; 181 | else 182 | changeCount = 1 + (NSUInteger)arc4random_uniform((uint32_t)29); 183 | 184 | for (NSUInteger i = 0; i < changeCount; i++) 185 | { 186 | uint32_t random = arc4random_uniform((uint32_t)2); 187 | 188 | if (random == 0) 189 | { 190 | // Add an item 191 | 192 | NSString *key = [self randomLetters:8]; 193 | 194 | if (DEBUG_THIS_METHOD) { 195 | NSLog(@"addObject: %@", key); 196 | } 197 | [set addObject:key]; 198 | } 199 | else if (random == 1) 200 | { 201 | // Remove an item 202 | 203 | NSUInteger idx = (NSUInteger)arc4random_uniform((uint32_t)set.count); 204 | 205 | NSString *key = nil; 206 | NSUInteger i = 0; 207 | for (NSString *obj in set) 208 | { 209 | if (i == idx) { 210 | key = obj; 211 | break; 212 | } 213 | i++; 214 | } 215 | 216 | if (DEBUG_THIS_METHOD) { 217 | NSLog(@"removeObject: %@", key); 218 | } 219 | [set removeObject:key]; 220 | } 221 | } 222 | 223 | [changesets addObject:([set changeset] ?: @{})]; 224 | 225 | if (DEBUG_THIS_METHOD) { 226 | NSLog(@"********************"); 227 | } 228 | } 229 | 230 | set_b = [set immutableCopy]; 231 | 232 | error = [set importChangesets:changesets]; 233 | XCTAssert(error == nil); 234 | 235 | XCTAssert([set isEqualToSet:set_b]); 236 | 237 | NSDictionary *changeset_merged = [set changeset]; 238 | 239 | NSDictionary *changeset_redo = [set undo:changeset_merged error:&error]; 240 | XCTAssert(error == nil); 241 | if (DEBUG_THIS_METHOD && ![set isEqualToSet:set_a]) { 242 | NSLog(@"It's going to FAIL"); 243 | } 244 | XCTAssert([set isEqualToSet:set_a]); 245 | 246 | [set undo:changeset_redo error:&error]; 247 | XCTAssert(error == nil); 248 | if (DEBUG_THIS_METHOD && ![set isEqualToSet:set_b]) { 249 | NSLog(@"It's going to FAIL"); 250 | } 251 | XCTAssert([set isEqualToSet:set_b]); 252 | 253 | if (DEBUG_THIS_METHOD) { 254 | NSLog(@"-------------------------------------------------"); 255 | } 256 | }} 257 | } 258 | 259 | - (void)test_merge_fuzz_everything 260 | { 261 | BOOL const DEBUG_THIS_METHOD = NO; 262 | 263 | for (NSUInteger round = 0; round < 1000; round++) { @autoreleasepool 264 | { 265 | NSMutableArray *changesets = [NSMutableArray array]; 266 | 267 | ZDCSet *set = [[ZDCSet alloc] init]; 268 | 269 | // Start with an object that has a random number of objects [20 - 30) 270 | { 271 | NSUInteger startCount; 272 | if (DEBUG_THIS_METHOD) 273 | startCount = 5; 274 | else 275 | startCount = 20 + (NSUInteger)arc4random_uniform((uint32_t)10); 276 | 277 | for (NSUInteger i = 0; i < startCount; i++) 278 | { 279 | NSString *key = [self randomLetters:8]; 280 | 281 | [set addObject:key]; 282 | } 283 | } 284 | 285 | [set clearChangeTracking]; 286 | ZDCSet *set_cloud = [set immutableCopy]; // sanity check: don't allow modification (for now) 287 | 288 | // Make a random number of changesets: [1 - 10) 289 | 290 | NSUInteger changesetCount; 291 | if (DEBUG_THIS_METHOD) 292 | changesetCount = 2; 293 | else 294 | changesetCount = 1 +(NSUInteger)arc4random_uniform((uint32_t)9); 295 | 296 | for (NSUInteger changesetIdx = 0; changesetIdx < changesetCount; changesetIdx++) 297 | { 298 | // Make a random number of changes (to dict): [1 - 30) 299 | 300 | NSUInteger changeCount; 301 | if (DEBUG_THIS_METHOD) 302 | changeCount = 2; 303 | else 304 | changeCount = 1 + (NSUInteger)arc4random_uniform((uint32_t)29); 305 | 306 | for (NSUInteger i = 0; i < changeCount; i++) 307 | { 308 | uint32_t random = arc4random_uniform((uint32_t)2); 309 | 310 | if (random == 0) 311 | { 312 | // Add an item 313 | 314 | NSString *key = [self randomLetters:8]; 315 | 316 | if (DEBUG_THIS_METHOD) { 317 | NSLog(@"local: addObject: %@", key); 318 | } 319 | [set addObject:key]; 320 | } 321 | else if (random == 1) 322 | { 323 | // Remove an item 324 | 325 | NSUInteger idx = (NSUInteger)arc4random_uniform((uint32_t)set.count); 326 | 327 | NSString *key = nil; 328 | NSUInteger i = 0; 329 | for (NSString *obj in set) 330 | { 331 | if (i == idx) { 332 | key = obj; 333 | break; 334 | } 335 | i++; 336 | } 337 | 338 | if (DEBUG_THIS_METHOD) { 339 | NSLog(@"local: removeObject: %@", key); 340 | } 341 | [set removeObject:key]; 342 | } 343 | } 344 | 345 | [changesets addObject:([set changeset] ?: @{})]; 346 | 347 | if (DEBUG_THIS_METHOD) { 348 | NSLog(@"********************"); 349 | } 350 | } 351 | 352 | [set makeImmutable]; // sanity check: don't allow modification (for now) 353 | set_cloud = [set_cloud copy]; // sanity check: allow modification again 354 | 355 | { 356 | // Make a random number of changes (to dict_cloud): [1 - 30) 357 | 358 | NSUInteger changeCount; 359 | if (DEBUG_THIS_METHOD) 360 | changeCount = 2; 361 | else 362 | changeCount = 1 + (NSUInteger)arc4random_uniform((uint32_t)29); 363 | 364 | for (NSUInteger i = 0; i < changeCount; i++) 365 | { 366 | uint32_t random = arc4random_uniform((uint32_t)2); 367 | 368 | if (random == 0) 369 | { 370 | // Add an item 371 | 372 | NSString *key = [self randomLetters:8]; 373 | 374 | if (DEBUG_THIS_METHOD) { 375 | NSLog(@"cloud: addObject: %@", key); 376 | } 377 | [set_cloud addObject:key]; 378 | } 379 | else if (random == 1) 380 | { 381 | // Remove an item 382 | 383 | NSUInteger idx = (NSUInteger)arc4random_uniform((uint32_t)set.count); 384 | 385 | NSString *key = nil; 386 | NSUInteger i = 0; 387 | for (NSString *obj in set) 388 | { 389 | if (i == idx) { 390 | key = obj; 391 | break; 392 | } 393 | i++; 394 | } 395 | 396 | if (DEBUG_THIS_METHOD) { 397 | NSLog(@"cloud: removeObject: %@", key); 398 | } 399 | [set_cloud removeObject:key]; 400 | } 401 | } 402 | } 403 | 404 | set = [set copy]; // sanity check: allow modification again 405 | [set_cloud makeImmutable]; // sanity check: don't allow modification anymore 406 | 407 | ZDCSet *set_preMerge = [set immutableCopy]; 408 | 409 | NSError *error = nil; 410 | NSDictionary *redo = [set mergeCloudVersion:set_cloud withPendingChangesets:changesets error:&error]; 411 | 412 | if (DEBUG_THIS_METHOD && error) { 413 | NSLog(@"It's going to FAIL"); 414 | } 415 | XCTAssert(error == nil); 416 | 417 | [set undo:redo error:&error]; 418 | 419 | if (DEBUG_THIS_METHOD && error) { 420 | NSLog(@"It's going to FAIL"); 421 | } 422 | XCTAssert(error == nil); 423 | 424 | if (DEBUG_THIS_METHOD && ![set isEqualToSet:set_preMerge]) { 425 | NSLog(@"It's going to FAIL"); 426 | } 427 | XCTAssert([set isEqualToSet:set_preMerge]); 428 | 429 | if (DEBUG_THIS_METHOD) { 430 | NSLog(@"-------------------------------------------------"); 431 | } 432 | }} 433 | } 434 | 435 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 436 | #pragma mark Merge - Simple 437 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 438 | 439 | - (void)test_simpleMerge_1 440 | { 441 | NSError *error = nil; 442 | NSMutableArray *changesets = [NSMutableArray array]; 443 | 444 | ZDCSet *localSet = [[ZDCSet alloc] init]; 445 | [localSet addObject:@"abc123"]; 446 | [localSet addObject:@(42)]; 447 | 448 | [localSet clearChangeTracking]; 449 | ZDCSet *cloudSet = [localSet copy]; 450 | 451 | { // local changes 452 | 453 | [localSet removeObject:@"abc123"]; 454 | [localSet addObject:@"def456"]; 455 | [changesets addObject:[localSet changeset]]; 456 | } 457 | { // cloud changes 458 | 459 | [cloudSet removeObject:@(42)]; 460 | [cloudSet addObject:@(43)]; 461 | [cloudSet makeImmutable]; 462 | } 463 | 464 | [localSet mergeCloudVersion: cloudSet 465 | withPendingChangesets: changesets 466 | error: &error]; 467 | 468 | XCTAssert(![localSet containsObject:@"abc123"]); 469 | XCTAssert(![localSet containsObject:@(42)]); 470 | 471 | XCTAssert([localSet containsObject:@"def456"]); 472 | XCTAssert([localSet containsObject:@(43)]); 473 | } 474 | 475 | - (void)test_simpleMerge_2 476 | { 477 | NSError *error = nil; 478 | NSMutableArray *changesets = [NSMutableArray array]; 479 | 480 | ZDCSet *localSet = [[ZDCSet alloc] init]; 481 | [localSet addObject:@"abc123"]; 482 | [localSet addObject:@(42)]; 483 | 484 | [localSet clearChangeTracking]; 485 | ZDCSet *cloudSet = [localSet copy]; 486 | 487 | { // local changes 488 | 489 | [localSet removeObject:@"abc123"]; 490 | [localSet addObject:@"def456"]; 491 | [changesets addObject:[localSet changeset]]; 492 | } 493 | { // cloud changes 494 | 495 | [cloudSet removeObject:@"abc123"]; 496 | [cloudSet addObject:@"xyz789"]; 497 | [cloudSet removeObject:@(42)]; 498 | [cloudSet addObject:@(43)]; 499 | [cloudSet makeImmutable]; 500 | } 501 | 502 | [localSet mergeCloudVersion: cloudSet 503 | withPendingChangesets: changesets 504 | error: &error]; 505 | 506 | XCTAssert(![localSet containsObject:@"abc123"]); 507 | XCTAssert(![localSet containsObject:@(42)]); 508 | 509 | XCTAssert([localSet containsObject:@"def456"]); 510 | XCTAssert([localSet containsObject:@"xyz789"]); 511 | XCTAssert([localSet containsObject:@(43)]); 512 | } 513 | 514 | - (void)test_simpleMerge_3 515 | { 516 | NSError *error = nil; 517 | NSMutableArray *changesets = [NSMutableArray array]; 518 | 519 | ZDCSet *localSet = [[ZDCSet alloc] init]; 520 | [localSet addObject:@(42)]; 521 | 522 | [localSet clearChangeTracking]; 523 | ZDCSet *cloudSet = [localSet copy]; 524 | 525 | { // local changes 526 | 527 | [localSet addObject:@"def456"]; 528 | [changesets addObject:[localSet changeset]]; 529 | } 530 | { // cloud changes 531 | 532 | [cloudSet addObject:@"xyz789"]; 533 | [cloudSet removeObject:@(42)]; 534 | [cloudSet addObject:@(43)]; 535 | [cloudSet makeImmutable]; 536 | } 537 | 538 | [localSet mergeCloudVersion: cloudSet 539 | withPendingChangesets: changesets 540 | error: &error]; 541 | 542 | XCTAssert(![localSet containsObject:@(42)]); 543 | 544 | XCTAssert([localSet containsObject:@"def456"]); 545 | XCTAssert([localSet containsObject:@"xyz789"]); 546 | XCTAssert([localSet containsObject:@(43)]); 547 | } 548 | 549 | - (void)test_simpleMerge_4 550 | { 551 | NSError *error = nil; 552 | NSMutableArray *changesets = [NSMutableArray array]; 553 | 554 | ZDCSet *localSet = [[ZDCSet alloc] init]; 555 | [localSet addObject:@(42)]; 556 | 557 | [localSet clearChangeTracking]; 558 | ZDCSet *cloudSet = [localSet copy]; 559 | 560 | { // local changes 561 | 562 | [localSet addObject:@"def456"]; 563 | [changesets addObject:[localSet changeset]]; 564 | } 565 | { // cloud changes 566 | 567 | [cloudSet removeObject:@(42)]; 568 | [cloudSet addObject:@(43)]; 569 | [cloudSet makeImmutable]; 570 | } 571 | 572 | [localSet mergeCloudVersion: cloudSet 573 | withPendingChangesets: changesets 574 | error: &error]; 575 | 576 | XCTAssert(![localSet containsObject:@(42)]); 577 | 578 | XCTAssert([localSet containsObject:@"def456"]); 579 | XCTAssert([localSet containsObject:@(43)]); 580 | } 581 | 582 | - (void)test_simpleMerge_5 583 | { 584 | NSError *error = nil; 585 | NSMutableArray *changesets = [NSMutableArray array]; 586 | 587 | ZDCSet *localSet = [[ZDCSet alloc] init]; 588 | [localSet addObject:@"abc123"]; 589 | [localSet addObject:@(42)]; 590 | 591 | [localSet clearChangeTracking]; 592 | ZDCSet *cloudSet = [localSet copy]; 593 | 594 | { // local changes 595 | 596 | [localSet removeObject:@(42)]; 597 | [localSet addObject:@(43)]; 598 | [changesets addObject:[localSet changeset]]; 599 | } 600 | { // cloud changes 601 | 602 | [cloudSet removeObject:@"abc123"]; 603 | [cloudSet makeImmutable]; 604 | } 605 | 606 | [localSet mergeCloudVersion: cloudSet 607 | withPendingChangesets: changesets 608 | error: &error]; 609 | 610 | XCTAssert(![localSet containsObject:@(42)]); 611 | XCTAssert(![localSet containsObject:@"abc123"]); 612 | 613 | XCTAssert([localSet containsObject:@(43)]); 614 | } 615 | 616 | @end 617 | -------------------------------------------------------------------------------- /UnitTests/test_layered.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import 7 | 8 | #import "ZDCDictionary.h" 9 | #import "ZDCOrderedDictionary.h" 10 | 11 | #import "ComplexRecord.h" 12 | 13 | @interface test_layered : XCTestCase 14 | @end 15 | 16 | @implementation test_layered 17 | 18 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 19 | #pragma mark Dictionary of Dictionaries 20 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 21 | 22 | - (void)test_dictionary_dictionary_add 23 | { 24 | ZDCDictionary *dict_a = nil; 25 | ZDCDictionary *dict_b = nil; 26 | NSError *error = nil; 27 | 28 | // Dictionary of dictionaries 29 | 30 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 31 | 32 | dict[@"c"] = [[ZDCDictionary alloc] init]; 33 | dict[@"d"] = [[ZDCDictionary alloc] init]; 34 | 35 | dict_a = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 36 | [dict clearChangeTracking]; 37 | 38 | dict[@"c"][@"cat"] = @"meow"; 39 | dict[@"d"][@"dog"] = @"bark"; 40 | 41 | NSDictionary *changeset_undo = [dict changeset]; 42 | dict_b = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 43 | 44 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 45 | XCTAssert(error == nil); 46 | XCTAssert([dict isEqualToDictionary:dict_a]); 47 | 48 | [dict undo:changeset_redo error:&error]; 49 | XCTAssert(error == nil); 50 | XCTAssert([dict isEqualToDictionary:dict_b]); 51 | } 52 | 53 | - (void)test_dictionary_dictionary_remove 54 | { 55 | ZDCDictionary *dict_a = nil; 56 | ZDCDictionary *dict_b = nil; 57 | NSError *error = nil; 58 | 59 | // Dictionary of dictionaries 60 | 61 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 62 | 63 | dict[@"c"] = [[ZDCDictionary alloc] init]; 64 | dict[@"d"] = [[ZDCDictionary alloc] init]; 65 | 66 | dict[@"c"][@"cat"] = @"meow"; 67 | dict[@"d"][@"dog"] = @"bark"; 68 | 69 | dict_a = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 70 | [dict clearChangeTracking]; 71 | 72 | dict[@"c"][@"cat"] = nil; 73 | dict[@"d"][@"dog"] = nil; 74 | 75 | NSDictionary *changeset_undo = [dict changeset]; 76 | dict_b = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 77 | 78 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 79 | XCTAssert(error == nil); 80 | XCTAssert([dict isEqualToDictionary:dict_a]); 81 | 82 | [dict undo:changeset_redo error:&error]; 83 | XCTAssert(error == nil); 84 | XCTAssert([dict isEqualToDictionary:dict_b]); 85 | } 86 | 87 | - (void)test_dictionary_dictionary_modify 88 | { 89 | ZDCDictionary *dict_a = nil; 90 | ZDCDictionary *dict_b = nil; 91 | NSError *error = nil; 92 | 93 | // Dictionary of dictionaries 94 | 95 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 96 | 97 | dict[@"c"] = [[ZDCDictionary alloc] init]; 98 | dict[@"d"] = [[ZDCDictionary alloc] init]; 99 | 100 | dict[@"c"][@"cat"] = @"meow"; 101 | dict[@"d"][@"dog"] = @"bark"; 102 | 103 | dict_a = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 104 | [dict clearChangeTracking]; 105 | 106 | dict[@"c"][@"cat"] = @"hey, I'm a cat"; 107 | dict[@"d"][@"dog"] = @"ruff ruff"; 108 | 109 | NSDictionary *changeset_undo = [dict changeset]; 110 | dict_b = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 111 | 112 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 113 | XCTAssert(error == nil); 114 | XCTAssert([dict isEqualToDictionary:dict_a]); 115 | 116 | [dict undo:changeset_redo error:&error]; 117 | XCTAssert(error == nil); 118 | XCTAssert([dict isEqualToDictionary:dict_b]); 119 | } 120 | 121 | - (void)test_dictionary_dictionary_push 122 | { 123 | ZDCDictionary *dict_a = nil; 124 | ZDCDictionary *dict_b = nil; 125 | NSError *error = nil; 126 | 127 | // Dictionary of dictionaries 128 | 129 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 130 | 131 | dict[@"c"] = [[ZDCDictionary alloc] init]; 132 | dict[@"c"][@"cat"] = @"meow"; 133 | 134 | dict_a = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 135 | [dict clearChangeTracking]; 136 | 137 | dict[@"d"] = [[ZDCDictionary alloc] init]; 138 | dict[@"d"][@"dog"] = @"bark"; 139 | 140 | dict[@"c"][@"cat"] = @"hey, I'm a cat"; 141 | 142 | NSDictionary *changeset_undo = [dict changeset]; 143 | dict_b = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 144 | 145 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 146 | XCTAssert(error == nil); 147 | XCTAssert([dict isEqualToDictionary:dict_a]); 148 | 149 | [dict undo:changeset_redo error:&error]; 150 | XCTAssert(error == nil); 151 | XCTAssert([dict isEqualToDictionary:dict_b]); 152 | } 153 | 154 | - (void)test_dictionary_dictionary_pop 155 | { 156 | ZDCDictionary *dict_a = nil; 157 | ZDCDictionary *dict_b = nil; 158 | NSError *error = nil; 159 | 160 | // Dictionary of dictionaries 161 | 162 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 163 | 164 | dict[@"c"] = [[ZDCDictionary alloc] init]; 165 | dict[@"d"] = [[ZDCDictionary alloc] init]; 166 | 167 | dict[@"c"][@"cat"] = @"meow"; 168 | dict[@"d"][@"dog"] = @"bark"; 169 | 170 | dict_a = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 171 | [dict clearChangeTracking]; 172 | 173 | dict[@"d"] = nil; 174 | 175 | NSDictionary *changeset_undo = [dict changeset]; 176 | dict_b = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 177 | 178 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 179 | XCTAssert(error == nil); 180 | XCTAssert([dict isEqualToDictionary:dict_a]); 181 | 182 | [dict undo:changeset_redo error:&error]; 183 | XCTAssert(error == nil); 184 | XCTAssert([dict isEqualToDictionary:dict_b]); 185 | } 186 | 187 | - (void)test_dictionary_dictionary_swapin 188 | { 189 | ZDCDictionary *dict_a = nil; 190 | ZDCDictionary *dict_b = nil; 191 | NSError *error = nil; 192 | 193 | // Dictionary of dictionaries 194 | 195 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 196 | 197 | dict[@"c"] = [[ZDCDictionary alloc] init]; 198 | dict[@"c"][@"cat"] = @"meow"; 199 | 200 | dict[@"d"] = @"dog"; 201 | 202 | dict_a = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 203 | [dict clearChangeTracking]; 204 | 205 | dict[@"d"] = [[ZDCDictionary alloc] init]; 206 | dict[@"d"][@"dog"] = @"bark"; 207 | 208 | dict[@"c"][@"cat"] = @"hey, I'm a cat"; 209 | 210 | NSDictionary *changeset_undo = [dict changeset]; 211 | dict_b = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 212 | 213 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 214 | XCTAssert(error == nil); 215 | XCTAssert([dict isEqualToDictionary:dict_a]); 216 | 217 | [dict undo:changeset_redo error:&error]; 218 | XCTAssert(error == nil); 219 | XCTAssert([dict isEqualToDictionary:dict_b]); 220 | } 221 | 222 | - (void)test_dictionary_dictionary_swapout 223 | { 224 | ZDCDictionary *dict_a = nil; 225 | ZDCDictionary *dict_b = nil; 226 | NSError *error = nil; 227 | 228 | // Dictionary of dictionaries 229 | 230 | ZDCDictionary *dict = [[ZDCDictionary alloc] init]; 231 | 232 | dict[@"c"] = [[ZDCDictionary alloc] init]; 233 | dict[@"d"] = [[ZDCDictionary alloc] init]; 234 | 235 | dict[@"c"][@"cat"] = @"meow"; 236 | dict[@"d"][@"dog"] = @"bark"; 237 | 238 | dict_a = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 239 | [dict clearChangeTracking]; 240 | 241 | dict[@"d"] = @"dog"; 242 | 243 | NSDictionary *changeset_undo = [dict changeset]; 244 | dict_b = [[ZDCDictionary alloc] initWithDictionary:dict.rawDictionary copyItems:YES]; 245 | 246 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 247 | XCTAssert(error == nil); 248 | XCTAssert([dict isEqualToDictionary:dict_a]); 249 | 250 | [dict undo:changeset_redo error:&error]; 251 | XCTAssert(error == nil); 252 | XCTAssert([dict isEqualToDictionary:dict_b]); 253 | } 254 | 255 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 256 | #pragma mark OrderedDictionary of OrderedDictionaries 257 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 258 | 259 | - (void)test_orderedDictionary_orderedDictionary_add 260 | { 261 | ZDCOrderedDictionary *dict_a = nil; 262 | ZDCOrderedDictionary *dict_b = nil; 263 | NSError *error = nil; 264 | 265 | // Dictionary of dictionaries 266 | 267 | ZDCOrderedDictionary *dict = [[ZDCOrderedDictionary alloc] init]; 268 | 269 | dict[@"c"] = [[ZDCOrderedDictionary alloc] init]; 270 | dict[@"d"] = [[ZDCOrderedDictionary alloc] init]; 271 | 272 | dict_a = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 273 | [dict clearChangeTracking]; 274 | 275 | dict[@"c"][@"cat"] = @"meow"; 276 | dict[@"d"][@"dog"] = @"bark"; 277 | 278 | NSDictionary *changeset_undo = [dict changeset]; 279 | dict_b = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 280 | 281 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 282 | XCTAssert(error == nil); 283 | XCTAssert([dict isEqualToOrderedDictionary:dict_a]); 284 | 285 | [dict undo:changeset_redo error:&error]; 286 | XCTAssert(error == nil); 287 | XCTAssert([dict isEqualToOrderedDictionary:dict_b]); 288 | } 289 | 290 | - (void)test_orderedDictionary_orderedDictionary_remove 291 | { 292 | ZDCOrderedDictionary *dict_a = nil; 293 | ZDCOrderedDictionary *dict_b = nil; 294 | NSError *error = nil; 295 | 296 | // Dictionary of dictionaries 297 | 298 | ZDCOrderedDictionary *dict = [[ZDCOrderedDictionary alloc] init]; 299 | 300 | dict[@"c"] = [[ZDCOrderedDictionary alloc] init]; 301 | dict[@"d"] = [[ZDCOrderedDictionary alloc] init]; 302 | 303 | dict[@"c"][@"cat"] = @"meow"; 304 | dict[@"d"][@"dog"] = @"bark"; 305 | 306 | dict_a = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 307 | [dict clearChangeTracking]; 308 | 309 | dict[@"c"][@"cat"] = nil; 310 | dict[@"d"][@"dog"] = nil; 311 | 312 | NSDictionary *changeset_undo = [dict changeset]; 313 | dict_b = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 314 | 315 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 316 | XCTAssert(error == nil); 317 | XCTAssert([dict isEqualToOrderedDictionary:dict_a]); 318 | 319 | [dict undo:changeset_redo error:&error]; 320 | XCTAssert(error == nil); 321 | XCTAssert([dict isEqualToOrderedDictionary:dict_b]); 322 | } 323 | 324 | - (void)test_orderedDictionary_orderedDictionary_modify 325 | { 326 | ZDCOrderedDictionary *dict_a = nil; 327 | ZDCOrderedDictionary *dict_b = nil; 328 | NSError *error = nil; 329 | 330 | // Dictionary of dictionaries 331 | 332 | ZDCOrderedDictionary *dict = [[ZDCOrderedDictionary alloc] init]; 333 | 334 | dict[@"c"] = [[ZDCOrderedDictionary alloc] init]; 335 | dict[@"d"] = [[ZDCOrderedDictionary alloc] init]; 336 | 337 | dict[@"c"][@"cat"] = @"meow"; 338 | dict[@"d"][@"dog"] = @"bark"; 339 | 340 | dict_a = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 341 | [dict clearChangeTracking]; 342 | 343 | dict[@"c"][@"cat"] = @"hey, I'm a cat"; 344 | dict[@"d"][@"dog"] = @"ruff ruff"; 345 | 346 | NSDictionary *changeset_undo = [dict changeset]; 347 | dict_b = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 348 | 349 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 350 | XCTAssert(error == nil); 351 | XCTAssert([dict isEqualToOrderedDictionary:dict_a]); 352 | 353 | [dict undo:changeset_redo error:&error]; 354 | XCTAssert(error == nil); 355 | XCTAssert([dict isEqualToOrderedDictionary:dict_b]); 356 | } 357 | 358 | - (void)test_orderedDictionary_orderedDictionary_move 359 | { 360 | ZDCOrderedDictionary *dict_a = nil; 361 | ZDCOrderedDictionary *dict_b = nil; 362 | NSError *error = nil; 363 | 364 | // Dictionary of dictionaries 365 | 366 | ZDCOrderedDictionary *dict = [[ZDCOrderedDictionary alloc] init]; 367 | 368 | dict[@"c"] = [[ZDCOrderedDictionary alloc] init]; 369 | dict[@"d"] = [[ZDCOrderedDictionary alloc] init]; 370 | 371 | dict[@"c"][@"cat"] = @"meow"; 372 | dict[@"d"][@"dog"] = @"bark"; 373 | 374 | dict_a = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 375 | [dict clearChangeTracking]; 376 | 377 | [dict moveObjectAtIndex:0 toIndex:1]; 378 | 379 | NSDictionary *changeset_undo = [dict changeset]; 380 | dict_b = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 381 | 382 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 383 | XCTAssert(error == nil); 384 | XCTAssert([dict isEqualToOrderedDictionary:dict_a]); 385 | 386 | [dict undo:changeset_redo error:&error]; 387 | XCTAssert(error == nil); 388 | XCTAssert([dict isEqualToOrderedDictionary:dict_b]); 389 | } 390 | 391 | - (void)test_orderedDictionary_orderedDictionary_push 392 | { 393 | ZDCOrderedDictionary *dict_a = nil; 394 | ZDCOrderedDictionary *dict_b = nil; 395 | NSError *error = nil; 396 | 397 | // Dictionary of dictionaries 398 | 399 | ZDCOrderedDictionary *dict = [[ZDCOrderedDictionary alloc] init]; 400 | 401 | dict[@"c"] = [[ZDCOrderedDictionary alloc] init]; 402 | dict[@"c"][@"cat"] = @"meow"; 403 | 404 | dict_a = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 405 | [dict clearChangeTracking]; 406 | 407 | dict[@"d"] = [[ZDCOrderedDictionary alloc] init]; 408 | dict[@"d"][@"dog"] = @"bark"; 409 | 410 | dict[@"c"][@"cat"] = @"hey, I'm a cat"; 411 | 412 | NSDictionary *changeset_undo = [dict changeset]; 413 | dict_b = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 414 | 415 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 416 | XCTAssert(error == nil); 417 | XCTAssert([dict isEqualToOrderedDictionary:dict_a]); 418 | 419 | [dict undo:changeset_redo error:&error]; 420 | XCTAssert(error == nil); 421 | XCTAssert([dict isEqualToOrderedDictionary:dict_b]); 422 | } 423 | 424 | - (void)test_orderedDictionary_orderedDictionary_pop 425 | { 426 | ZDCOrderedDictionary *dict_a = nil; 427 | ZDCOrderedDictionary *dict_b = nil; 428 | NSError *error = nil; 429 | 430 | // Dictionary of dictionaries 431 | 432 | ZDCOrderedDictionary *dict = [[ZDCOrderedDictionary alloc] init]; 433 | 434 | dict[@"c"] = [[ZDCOrderedDictionary alloc] init]; 435 | dict[@"d"] = [[ZDCOrderedDictionary alloc] init]; 436 | 437 | dict[@"c"][@"cat"] = @"meow"; 438 | dict[@"d"][@"dog"] = @"bark"; 439 | 440 | dict_a = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 441 | [dict clearChangeTracking]; 442 | 443 | dict[@"d"] = nil; 444 | 445 | NSDictionary *changeset_undo = [dict changeset]; 446 | dict_b = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 447 | 448 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 449 | XCTAssert(error == nil); 450 | XCTAssert([dict isEqualToOrderedDictionary:dict_a]); 451 | 452 | [dict undo:changeset_redo error:&error]; 453 | XCTAssert(error == nil); 454 | XCTAssert([dict isEqualToOrderedDictionary:dict_b]); 455 | } 456 | 457 | - (void)test_orderedDictionary_orderedDictionary_swapin 458 | { 459 | ZDCOrderedDictionary *dict_a = nil; 460 | ZDCOrderedDictionary *dict_b = nil; 461 | NSError *error = nil; 462 | 463 | // Dictionary of dictionaries 464 | 465 | ZDCOrderedDictionary *dict = [[ZDCOrderedDictionary alloc] init]; 466 | 467 | dict[@"c"] = [[ZDCDictionary alloc] init]; 468 | dict[@"c"][@"cat"] = @"meow"; 469 | 470 | dict[@"d"] = @"dog"; 471 | 472 | dict_a = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 473 | [dict clearChangeTracking]; 474 | 475 | dict[@"d"] = [[ZDCOrderedDictionary alloc] init]; 476 | dict[@"d"][@"dog"] = @"bark"; 477 | 478 | dict[@"c"][@"cat"] = @"hey, I'm a cat"; 479 | 480 | NSDictionary *changeset_undo = [dict changeset]; 481 | dict_b = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 482 | 483 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 484 | XCTAssert(error == nil); 485 | XCTAssert([dict isEqualToOrderedDictionary:dict_a]); 486 | 487 | [dict undo:changeset_redo error:&error]; 488 | XCTAssert(error == nil); 489 | XCTAssert([dict isEqualToOrderedDictionary:dict_b]); 490 | } 491 | 492 | - (void)test_orderedDictionary_orderedDictionary_swapout 493 | { 494 | ZDCOrderedDictionary *dict_a = nil; 495 | ZDCOrderedDictionary *dict_b = nil; 496 | NSError *error = nil; 497 | 498 | // Dictionary of dictionaries 499 | 500 | ZDCOrderedDictionary *dict = [[ZDCOrderedDictionary alloc] init]; 501 | 502 | dict[@"c"] = [[ZDCOrderedDictionary alloc] init]; 503 | dict[@"d"] = [[ZDCOrderedDictionary alloc] init]; 504 | 505 | dict[@"c"][@"cat"] = @"meow"; 506 | dict[@"d"][@"dog"] = @"bark"; 507 | 508 | dict_a = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 509 | [dict clearChangeTracking]; 510 | 511 | dict[@"d"] = @"dog"; 512 | 513 | NSDictionary *changeset_undo = [dict changeset]; 514 | dict_b = [[ZDCOrderedDictionary alloc] initWithOrderedDictionary:dict copyItems:YES]; 515 | 516 | NSDictionary *changeset_redo = [dict undo:changeset_undo error:&error]; 517 | XCTAssert(error == nil); 518 | XCTAssert([dict isEqualToOrderedDictionary:dict_a]); 519 | 520 | [dict undo:changeset_redo error:&error]; 521 | XCTAssert(error == nil); 522 | XCTAssert([dict isEqualToOrderedDictionary:dict_b]); 523 | } 524 | 525 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 526 | #pragma mark ComplexRecord 527 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 528 | 529 | - (void)test_complexRecord 530 | { 531 | ComplexRecord *cr_a = nil; 532 | ComplexRecord *cr_b = nil; 533 | NSError *error = nil; 534 | 535 | ComplexRecord *cr = [[ComplexRecord alloc] init]; 536 | 537 | cr.someString = @"abc123"; 538 | cr.someInteger = 42; 539 | 540 | cr.dict[@"dog"] = @"bark"; 541 | cr.dict[@"cat"] = @"meow"; 542 | 543 | [cr.set addObject:@"foo"]; 544 | [cr.set addObject:@"bar"]; 545 | 546 | [cr clearChangeTracking]; 547 | cr_a = [cr immutableCopy]; 548 | 549 | cr.someString = @"def456"; 550 | cr.someInteger = 23; 551 | 552 | cr.dict[@"dog"] = @"ruff"; 553 | cr.dict[@"duck"] = @"quack"; 554 | cr.dict[@"cat"] = nil; 555 | 556 | [cr.set removeObject:@"bar"]; 557 | [cr.set addObject:@"buzz"]; 558 | 559 | NSDictionary *changeset_undo = [cr changeset]; 560 | cr_b = [cr immutableCopy]; 561 | 562 | NSDictionary *changeset_redo = [cr undo:changeset_undo error:&error]; 563 | 564 | XCTAssert(error == nil); 565 | XCTAssert([cr isEqualToComplexRecord:cr_a]); 566 | 567 | [cr undo:changeset_redo error:&error]; 568 | 569 | XCTAssert(error == nil); 570 | XCTAssert([cr isEqualToComplexRecord:cr_b]); 571 | } 572 | 573 | @end 574 | -------------------------------------------------------------------------------- /UnitTests_iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /UnitTests_macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /UnitTests_tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ZDCSyncable/Internal/ZDCNull.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | @import Foundation; 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | /** 11 | * ZDCNull is used internally to represent nil/null. 12 | * Do NOT use it directly. If you want to store null, then you should use NSNull. 13 | * 14 | * Why: 15 | * We needed our own special version because we needed a way to differentiate from NSNull. 16 | * 17 | * Where it's used: 18 | * In changeset dictionaries, ZDCNull is used as a placeholder to represent the absence of value. 19 | * For example, if the originalValue of an object is ZDCNull, this would mean the object was added. 20 | * 21 | * Notes: 22 | * ZDCNull is a singleton, so you can use `==` to do comparisons: if (obj == [ZDCNull null]) {...} 23 | * You cannot alloc/init an ZDCNull instance. (You can try, but it will throw an exception.) 24 | * This is true: [ZDCNull null] == [[ZDCNull null] copy]. 25 | * Also, deserialzing an ZDCNull will properly return the singleton. 26 | **/ 27 | NS_SWIFT_NAME(ZDCNull_ObjC) 28 | @interface ZDCNull : NSObject 29 | 30 | + (id)null; 31 | 32 | @end 33 | 34 | NS_ASSUME_NONNULL_END 35 | -------------------------------------------------------------------------------- /ZDCSyncable/Internal/ZDCNull.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCNull.h" 7 | 8 | 9 | @implementation ZDCNull 10 | 11 | static ZDCNull *singleton; 12 | 13 | + (void)initialize 14 | { 15 | static BOOL initialized = NO; 16 | if (!initialized) 17 | { 18 | initialized = YES; 19 | singleton = [[ZDCNull alloc] init]; 20 | } 21 | } 22 | 23 | + (id)null 24 | { 25 | return singleton; 26 | } 27 | 28 | - (instancetype)init 29 | { 30 | if (singleton != nil) 31 | { 32 | @throw [NSException exceptionWithName:@"ZDCNull" reason:@"Must use singleton via [ZDCNull null]" userInfo:nil]; 33 | } 34 | 35 | self = [super init]; 36 | return self; 37 | } 38 | 39 | - (instancetype)initWithCoder:(NSCoder *)decoder 40 | { 41 | return [ZDCNull null]; 42 | } 43 | 44 | - (void)encodeWithCoder:(NSCoder *)coder 45 | { 46 | // Nothing internal to encode. 47 | // NSCoder will record the class (ZDCNull) automatically. 48 | } 49 | 50 | - (id)copyWithZone:(NSZone *)zone 51 | { 52 | return self; // immutable singleton 53 | } 54 | 55 | - (NSString *)description 56 | { 57 | return @""; 58 | } 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /ZDCSyncable/Internal/ZDCRef.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | /** 11 | * ZDCRef is used within changesets. 12 | * 13 | * Where it's used: 14 | * In changeset dictionaries, ZDCRef is used as a placeholder to indicate the referenced 15 | * object conforms to the ZDCSyncable protocol, and has its own changeset. 16 | * 17 | * Notes: 18 | * ZDCRef is a singleton, so you can use `==` to do comparisons: if (obj == [ZDCRef ref]) {...} 19 | * You cannot alloc/init an ZDCRef instance. (You can try, but it will throw an exception.) 20 | * This is true: [ZDCRef ref] == [[ZDCRef ref] copy]. 21 | * Also, deserialzing an ZDCRef will properly return the singleton. 22 | */ 23 | NS_SWIFT_NAME(ZDCRef_ObjC) 24 | @interface ZDCRef : NSObject 25 | 26 | + (id)ref; 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /ZDCSyncable/Internal/ZDCRef.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCRef.h" 7 | 8 | @implementation ZDCRef 9 | 10 | static ZDCRef *singleton; 11 | 12 | + (void)initialize 13 | { 14 | static BOOL initialized = NO; 15 | if (!initialized) 16 | { 17 | initialized = YES; 18 | singleton = [[ZDCRef alloc] init]; 19 | } 20 | } 21 | 22 | + (id)ref 23 | { 24 | return singleton; 25 | } 26 | 27 | - (instancetype)init 28 | { 29 | if (singleton != nil) 30 | { 31 | @throw [NSException exceptionWithName:@"ZDCRef" reason:@"Must use singleton via [ZDCRef ref]" userInfo:nil]; 32 | } 33 | 34 | self = [super init]; 35 | return self; 36 | } 37 | 38 | - (instancetype)initWithCoder:(NSCoder *)decoder 39 | { 40 | return [ZDCRef ref]; 41 | } 42 | 43 | - (void)encodeWithCoder:(NSCoder *)coder 44 | { 45 | // Nothing internal to encode. 46 | // NSCoder will record the class (ZDCRef) automatically. 47 | } 48 | 49 | - (id)copyWithZone:(NSZone *)zone 50 | { 51 | return self; // immutable singleton 52 | } 53 | 54 | - (NSString *)description 55 | { 56 | return @""; 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /ZDCSyncable/Utilities/ZDCObjectSubclass.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCObject.h" 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | /** 11 | * Includes useful methods for subclasses. 12 | * 13 | * This includes method that subclasses may wish to override, 14 | * as well as common shared methods for creating errors or exceptions. 15 | */ 16 | @interface ZDCObject () 17 | 18 | #pragma mark Class configuration 19 | 20 | /** 21 | * Returns the set of properties that are "monitored" for changes. 22 | * 23 | * If a property is monitored, then: 24 | * - instances are configured to listen for changes to the property via KVO (key value observing) 25 | * - if the instance is marked as immutable, and a 'willChangeValueForKey:' notification is received, 26 | * then the instance will throw an exception 27 | * 28 | * By default, all properties are monitored for changes. 29 | * This includes all properties in the top-level class, and also all properties in subclasses. 30 | * 31 | * Subclasses may override this method if certain proprties don't need to be monitored. 32 | * For examples, if a property simply acts as a cached value (and doesn't therefore infer a mutation), 33 | * then the subclass may wish to override this method, and remove that property from the list. 34 | */ 35 | + (NSMutableSet *)monitoredProperties; 36 | 37 | /** 38 | * Returns the list of properties that are being monitored. 39 | * The default implementation caches the class configuration. 40 | */ 41 | - (NSSet *)monitoredProperties; 42 | 43 | /** 44 | * Returns YES if the given property is being monitored. 45 | * 46 | * Recall that if a property is being monitored: 47 | * - instances are configured to listen for changes to the property via KVO (key value observing) 48 | * - if the instance is marked as immutable, and a 'willChangeValueForKey' notification is received, 49 | * then the instance will throw an exception 50 | */ 51 | - (BOOL)isMonitoredProperty:(NSString *)localKey; 52 | 53 | #pragma make Copying 54 | 55 | /** 56 | * For complicated copying scenarios, such a nested deep copies. 57 | */ 58 | - (void)copyChangeTrackingTo:(id)another; 59 | 60 | #pragma mark Hooks 61 | 62 | /** 63 | * Subclasses can override this method to get notified of changes. 64 | * 65 | * @note This method is only called for monitoredProperties. 66 | */ 67 | - (void)_willChangeValueForKey:(NSString *)key; 68 | 69 | /** 70 | * Subclasses can override this method to get notified of changes. 71 | * 72 | * @note This method is only called for monitoredProperties. 73 | */ 74 | - (void)_didChangeValueForKey:(NSString *)key; 75 | 76 | #pragma mark Exceptions 77 | 78 | /** 79 | * Subclasses can use this method as a standard way of throwing immutable exceptions. 80 | */ 81 | - (NSException *)immutableException; 82 | 83 | /** 84 | * Subclasses can use this method as a standard way of throwing immutable exceptions. 85 | */ 86 | - (NSException *)immutableExceptionForKey:(nullable NSString *)key; 87 | 88 | #pragma mark Errors 89 | 90 | /** 91 | * Subclasses can use this method as a standard way of generating common errors. 92 | */ 93 | - (NSError *)hasChangesError; 94 | 95 | /** 96 | * Subclasses can use this method as a standard way of generating common errors. 97 | */ 98 | - (NSError *)malformedChangesetError; 99 | 100 | /** 101 | * Subclasses can use this method as a standard way of generating common errors. 102 | */ 103 | - (NSError *)mismatchedChangeset; 104 | 105 | /** 106 | * Subclasses can use this method as a standard way of generating common errors. 107 | */ 108 | - (NSError *)incorrectObjectClass; 109 | 110 | @end 111 | 112 | NS_ASSUME_NONNULL_END 113 | -------------------------------------------------------------------------------- /ZDCSyncable/Utilities/ZDCOrder.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | /** 11 | * Utility methods for estimating the changes made to an array. 12 | */ 13 | NS_SWIFT_NAME(ZDCOrder_ObjC) 14 | @interface ZDCOrder : NSObject 15 | 16 | /** 17 | * The ZDCSyncable protocol is focused on syncing changes with the cloud. 18 | * This gives us a focused set of constraints. 19 | * In particular, the cloud does NOT store an ordered list of every change that ever modified an object. 20 | * Instead it simply stores the current version of the object. 21 | * 22 | * This can be seen as a tradeoff. It minimizes cloud storage & bandwidth, 23 | * in exchange for losing a small degree of information concerning changes to an object. 24 | * 25 | * One difficulty we see with this tradeoff has to do with changes made to an ordered list. 26 | * For example, if a list is re-ordered, we don't know the exact items that were moved. 27 | * And it's not possible to calculate the information, 28 | * as multiple sets of changes could lead to the same end result. 29 | * 30 | * So our workaround is to estimate the changeset as best as possible. 31 | * This method performs that task using a simple deterministic algorithm 32 | * to arrive at a close-to-minimal changeset. 33 | * 34 | * @note The changeset is generally close-to-minimal, but not guaranteed to be the minimum. 35 | * If you're a math genius, you're welcome to try your hand at solving this problem. 36 | */ 37 | + (NSArray *)estimateChangesetFrom:(NSArray *)src 38 | to:(NSArray *)dst 39 | hints:(nullable NSSet *)hints; 40 | 41 | @end 42 | 43 | NS_ASSUME_NONNULL_END 44 | -------------------------------------------------------------------------------- /ZDCSyncable/Utilities/ZDCOrder.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCOrder.h" 7 | 8 | @implementation ZDCOrder 9 | 10 | /** 11 | * See header file for documentation. 12 | */ 13 | + (NSArray *)estimateChangesetFrom:(NSArray *)inSrc 14 | to:(NSArray *)dst 15 | hints:(NSSet *)hints 16 | { 17 | // Sanity checks 18 | 19 | NSUInteger const count = inSrc.count; 20 | if (count != dst.count) 21 | { 22 | // If this is NOT true, we're going to end up with an exception below anyway... 23 | 24 | @throw [self invalidArraysException:@"Cannot compare arrays of different lengths."]; 25 | return nil; 26 | } 27 | { 28 | // If this is NOT true, we're going to end up with an exception below anyway... 29 | 30 | BOOL mismatch = NO; 31 | NSMutableArray *dstCopy = [dst mutableCopy]; 32 | 33 | for (id obj in inSrc) 34 | { 35 | NSUInteger idx = [dstCopy indexOfObject:obj]; 36 | if (idx == NSNotFound) 37 | { 38 | mismatch = YES; 39 | break; 40 | } 41 | else 42 | { 43 | [dstCopy removeObjectAtIndex:idx]; 44 | } 45 | } 46 | 47 | if (mismatch) 48 | { 49 | @throw [self invalidArraysException:@"Cannot compare arrays with different sets of keys."]; 50 | return nil; 51 | } 52 | } 53 | 54 | NSMutableArray *loopSrc = [inSrc mutableCopy]; 55 | NSMutableArray *src = [NSMutableArray arrayWithCapacity:count]; 56 | 57 | NSMutableArray *result = [NSMutableArray array]; 58 | 59 | // Description of the problem: 60 | // 61 | // We're given the current sort order in the cloud, and the original sort order. 62 | // But we don't know exactly what changes were made by remote devices. 63 | // 64 | // (Although we do have some hints, based on items known to have been added). 65 | // 66 | // But given that multiple re-ordering paths can start a X and end at Y, 67 | // we cannot prove that the re-ordering we calculate is actually what occurred. 68 | // 69 | // The best we can hope for is along the lines of an "educated guess". 70 | 71 | if (hints.count > 0) 72 | { 73 | NSMutableDictionary *hints_idx = [NSMutableDictionary dictionaryWithCapacity:hints.count]; 74 | NSMutableArray *hints_order = [NSMutableArray arrayWithCapacity:hints.count]; 75 | 76 | for (id key in hints) 77 | { 78 | NSUInteger idx = [dst indexOfObject:key]; 79 | if (idx != NSNotFound) 80 | { 81 | hints_idx[key] = @(idx); 82 | [hints_order addObject:key]; 83 | 84 | idx = [loopSrc indexOfObject:key]; 85 | [loopSrc removeObjectAtIndex:idx]; 86 | } 87 | } 88 | 89 | [hints_order sortUsingComparator:^NSComparisonResult(NSString *key1, NSString *key2) { 90 | 91 | NSNumber *idx1 = hints_idx[key1]; 92 | NSNumber *idx2 = hints_idx[key2]; 93 | 94 | return [idx1 compare:idx2]; 95 | }]; 96 | 97 | for (id key in hints_order) 98 | { 99 | NSUInteger idx = [hints_idx[key] unsignedIntegerValue]; 100 | 101 | [loopSrc insertObject:key atIndex:idx]; 102 | [result addObject:key]; 103 | } 104 | } 105 | 106 | // Algorithm: 107 | // 108 | // 1. Compare src vs dst, moving from FIRST to LAST (index 0 to index last). 109 | // When a difference is discovered, change src by swapping the correct key into place, 110 | // and recording the swapped key in changes_firstToLast. 111 | // 112 | // 2. Compare src vs dst, moving from LAST to FIRST (index last to index 0). 113 | // When a difference is discovered, change src by swapping the correct key into place, 114 | // and recording the swapped key in changes_lastToFirst. 115 | // 116 | // 3. If the arrays match (changes_x.count == 0), you're done. 117 | // 118 | // 4. Otherwise, compare the counts of changes_firstToLast vs change_lastToFirst. 119 | // Pick the one with the shortest count. 120 | // And execute the first change in its list. 121 | // 122 | // 5. Repeat steps 1-4 until done. 123 | 124 | 125 | NSMutableArray *changes_firstToLast = [NSMutableArray array]; 126 | NSMutableArray *changes_lastToFirst = [NSMutableArray array]; 127 | 128 | NSUInteger idx_firstToLast_remove = 0; 129 | NSUInteger idx_firstToLast_insert = 0; 130 | 131 | NSUInteger idx_lastToFirst_remove = 0; 132 | NSUInteger idx_lastToFirst_insert = 0; 133 | 134 | NSUInteger i = 0; 135 | NSUInteger j = 0; 136 | BOOL done = NO; 137 | do { 138 | 139 | // Step 1: Compare: First to Last 140 | { 141 | [src setArray:loopSrc]; 142 | 143 | for (i = 0; i < dst.count; i++) 144 | { 145 | id key_src = src[i]; 146 | id key_dst = dst[i]; 147 | 148 | if (![key_src isEqual:key_dst]) 149 | { 150 | NSUInteger idx = [src indexOfObject:key_dst inRange:NSMakeRange(i+1, count-i-1)]; 151 | 152 | [src removeObjectAtIndex:idx]; 153 | [src insertObject:key_dst atIndex:i]; 154 | 155 | if (changes_firstToLast.count == 0) { 156 | idx_firstToLast_remove = idx; 157 | idx_firstToLast_insert = i; 158 | } 159 | 160 | [changes_firstToLast addObject:key_dst]; 161 | } 162 | } 163 | } 164 | 165 | // Step 2: Compare: Last to First 166 | // 167 | if (changes_firstToLast.count > 0) 168 | { 169 | [src setArray:loopSrc]; 170 | 171 | for (j = count; j > 0; j--) 172 | { 173 | i = j-1; 174 | 175 | id key_src = src[i]; 176 | id key_dst = dst[i]; 177 | 178 | if (![key_src isEqual:key_dst]) 179 | { 180 | NSUInteger idx = [src indexOfObject:key_dst inRange:NSMakeRange(0, i)]; 181 | 182 | [src removeObjectAtIndex:idx]; 183 | [src insertObject:key_dst atIndex:i]; 184 | 185 | if (changes_lastToFirst.count == 0) { 186 | idx_lastToFirst_remove = idx; 187 | idx_lastToFirst_insert = i; 188 | } 189 | 190 | [changes_lastToFirst addObject:key_dst]; 191 | } 192 | } 193 | } 194 | else // if (changes_firstToLast.count == 0) 195 | { 196 | done = YES; 197 | } 198 | 199 | if (!done) 200 | { 201 | if (changes_firstToLast.count <= changes_lastToFirst.count) 202 | { 203 | id key = changes_firstToLast[0]; 204 | [result addObject:key]; 205 | 206 | [loopSrc removeObjectAtIndex:idx_firstToLast_remove]; 207 | [loopSrc insertObject:key atIndex:idx_firstToLast_insert]; 208 | } 209 | else 210 | { 211 | id key = changes_lastToFirst[0]; 212 | [result addObject:key]; 213 | 214 | [loopSrc removeObjectAtIndex:idx_lastToFirst_remove]; 215 | [loopSrc insertObject:key atIndex:idx_lastToFirst_insert]; 216 | } 217 | 218 | [changes_firstToLast removeAllObjects]; 219 | [changes_lastToFirst removeAllObjects]; 220 | } 221 | 222 | } while (!done); 223 | 224 | return result; 225 | } 226 | 227 | + (NSException *)invalidArraysException:(NSString *)details 228 | { 229 | NSDictionary *userInfo = @{ 230 | NSLocalizedRecoverySuggestionErrorKey: details 231 | }; 232 | NSString *reason = @"Invalid arrays given as parameters."; 233 | 234 | return [NSException exceptionWithName:@"ZDCOrderException" reason:reason userInfo:userInfo]; 235 | } 236 | 237 | @end 238 | -------------------------------------------------------------------------------- /ZDCSyncable/ZDCArray.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCObject.h" 7 | #import "ZDCSyncable.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | /** 12 | * ZDCArray tracks changes to an array. 13 | * 14 | * It's designed act like a drop-in replacement for a mutable array. 15 | * 16 | * In addition to its core functionality, it also provides the following set of features: 17 | * - it can be made immutable (via `-[ZDCObject makeImmutable]` method) 18 | * - it implements the ZDCSyncable protocol and thus: 19 | * - it tracks all changes made to the dictionary, and can provide a changeset (which encodes the change info) 20 | * - it supports undo & redo 21 | * - it supports merge operations 22 | */ 23 | NS_SWIFT_NAME(ZDCArray_ObjC) 24 | @interface ZDCArray : ZDCObject 25 | 26 | /** 27 | * Creates an empty array. 28 | */ 29 | - (instancetype)init; 30 | 31 | /** 32 | * Creates a ZDCArray initialized by copying the given array. 33 | * 34 | * @note To initialize from another ZDCArray, you can either copy the original, 35 | * or use this method combined with `-[ZDCArray rawArray]` to provide the parameter. 36 | */ 37 | - (instancetype)initWithArray:(nullable NSArray *)array; 38 | 39 | /** 40 | * Creates a ZDCArray initialized by copying the given array. 41 | * 42 | * @note To initialize from another ZDCArray, you can either copy the original, 43 | * or use this method combined with `-[ZDCArray rawArray]` to provide the parameter. 44 | * 45 | * @param array 46 | * The set to copy. 47 | * 48 | * @param copyItems 49 | * If set to YES, the values will be copied when storing in the ZDCArray. 50 | * This means the values will need to support the NSCopying protocol. 51 | */ 52 | - (instancetype)initWithArray:(nullable NSArray *)array copyItems:(BOOL)copyItems; 53 | 54 | #pragma mark Raw 55 | 56 | /** 57 | * Returns a reference to the underlying NSArray that the ZDCArray instance is wrapping. 58 | * 59 | * @note The returned value is a copy of the underlying NSMutableArray. 60 | * Thus changes to the ZDCArray will not be reflected in the returned value. 61 | */ 62 | @property (nonatomic, copy, readonly) NSArray *rawArray; 63 | 64 | #pragma mark Reading 65 | 66 | /** 67 | * The number of items stored in the set. 68 | */ 69 | @property (nonatomic, readonly) NSUInteger count; 70 | 71 | /** 72 | * Returns the object stored at the given index. 73 | * 74 | * @important Raises an NSRangeException if index is out-of-bounds. 75 | */ 76 | - (ObjectType)objectAtIndex:(NSUInteger)idx; 77 | 78 | /** 79 | * Returns the object stored at the given index. 80 | * 81 | * Allows you to use syntax: 82 | * ``` 83 | * value = zdcArray[index] 84 | * ``` 85 | * 86 | * @important Raises an NSRangeException if index is out-of-bounds. 87 | */ 88 | - (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx; 89 | 90 | /** 91 | * Returns YES if the object is contained in the array. 92 | * 93 | * Starting at index 0, each element of the array is checked for equality with anObject 94 | * until a match is found or the end of the array is reached. 95 | * Objects are considered equal if isEqual: returns YES. 96 | */ 97 | - (BOOL)containsObject:(ObjectType)anObject; 98 | 99 | /** 100 | * Returns the index of the object within the array. 101 | * If not found in the array, returns NSNotFound. 102 | * 103 | * Starting at index 0, each element of the array is checked for equality with anObject 104 | * until a match is found or the end of the array is reached. 105 | * Objects are considered equal if isEqual: returns YES. 106 | */ 107 | - (NSUInteger)indexOfObject:(ObjectType)anObject; 108 | 109 | #pragma mark Writing 110 | 111 | /** 112 | * Adds the object to the end of the array. 113 | * 114 | * @important Raises an NSInvalidArgument exception if the object is nil. 115 | */ 116 | - (void)addObject:(ObjectType)object; 117 | 118 | /** 119 | * Inserts the object within the array at the given index. 120 | * 121 | * @important Raises an NSInvalidArgument exception if the object is nil. 122 | * @important Raises an NSRangeException if index is greater than the number of elements in the array. 123 | */ 124 | - (void)insertObject:(ObjectType)object atIndex:(NSUInteger)idx; 125 | 126 | /** 127 | * Inserts the object within the array at the given index. 128 | * 129 | * Allows you to use code syntax: 130 | * ``` 131 | * zdcArray[index] = value 132 | * ``` 133 | */ 134 | - (void)setObject:(ObjectType)obj atIndexedSubscript:(NSUInteger)idx; 135 | 136 | /** 137 | * Use this method when you only need to change an item's index. 138 | * 139 | * @param oldIndex 140 | * The current index of the item. 141 | * 142 | * @param newIndex 143 | * The index to use AFTER the index has been removed: 144 | * - Step 1: `[array removeObjectAtIndex:oldIndex]` 145 | * - Step 2: `[array insertObject:obj atIndex:]` 146 | * 147 | * First, this method is faster than removing the item & then re-adding it. 148 | * Second, this method will properly track the change (i.e. the intent to change the item's index). 149 | * Thus you'll benefit from proper syncing of this action. 150 | */ 151 | - (void)moveObjectAtIndex:(NSUInteger)oldIndex toIndex:(NSUInteger)newIndex; 152 | 153 | /** 154 | * Removes all occurrences in the array of a given object. 155 | * 156 | * This method determines a match by comparing anObject to the objects 157 | * in the receiver using the isEqual: method. If the array does not 158 | * contain anObject, the method has no effect (although it does incur 159 | * the overhead of searching the contents). 160 | */ 161 | - (void)removeObject:(ObjectType)object; 162 | 163 | /** 164 | * Removes the object from the array currently at the given index. 165 | */ 166 | - (void)removeObjectAtIndex:(NSUInteger)idx; 167 | 168 | /** 169 | * Removes all objects from the array. 170 | * Afterwards the array will be empty. 171 | */ 172 | - (void)removeAllObjects; 173 | 174 | #pragma mark Enumeration 175 | 176 | /** 177 | * Enumerates all objects in the array, 178 | * starting from index 0 and ending with the largest index in the array. 179 | */ 180 | - (void)enumerateObjectsUsingBlock:(void (^)(ObjectType obj, NSUInteger idx, BOOL *stop))block; 181 | 182 | /** 183 | * An enumerator object that lets you access each object in the array, 184 | * in order, from the element at the lowest index upwards. 185 | * 186 | * @note It is more efficient to use the fast enumeration protocol. 187 | */ 188 | - (NSEnumerator *)objectEnumerator; 189 | 190 | /** 191 | * Returns an enumerator that can be used to enumerate the objects in reverse order. 192 | */ 193 | - (NSEnumerator *)reverseObjectEnumerator; 194 | 195 | #pragma mark Equality 196 | 197 | /** 198 | * Returns YES if `another` is of class ZDCArray. 199 | * and the receiver & another contain the same objects in the same order. 200 | * 201 | * This method corresponds to `[NSArray isEqualToArray:]`. 202 | * 203 | * @note It does NOT take into account the changeset of either ZDCArray instance. 204 | */ 205 | - (BOOL)isEqual:(nullable id)another; 206 | 207 | /** 208 | * Returns YES if the receiver and `another` contain the same objects in the same order. 209 | * 210 | * This method corresponds to `[NSArray isEqualToArray:]`. 211 | * 212 | * @note It does NOT take into account the changeset of either ZDCArray instance. 213 | */ 214 | - (BOOL)isEqualToArray:(nullable ZDCArray *)another; 215 | 216 | @end 217 | 218 | NS_ASSUME_NONNULL_END 219 | -------------------------------------------------------------------------------- /ZDCSyncable/ZDCDictionary.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCObject.h" 7 | #import "ZDCSyncable.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | /** 12 | * ZDCDictionary tracks changes to a dictionary. 13 | * 14 | * It's designed to act like a drop-in replacement for a mutable dictionary. 15 | * 16 | * In addition to its core functionality, it also provides the following set of features: 17 | * - it can be made immutable (via `-[ZDCObject makeImmutable]` method) 18 | * - it implements the ZDCSyncable protocol and thus: 19 | * - it tracks all changes to the dictionary, and can provide a changeset (which encodes the changes info) 20 | * - it supports undo & redo 21 | * - it supports merge operations 22 | */ 23 | NS_SWIFT_NAME(ZDCDictionary_ObjC) 24 | @interface ZDCDictionary : ZDCObject 25 | 26 | /** 27 | * Creates an empty dictionary. 28 | */ 29 | - (instancetype)init; 30 | 31 | /** 32 | * Creates a dictionary that contains the {key, value} tuples from the given input. 33 | * 34 | * @note To initialize from another ZDCDictionary, you can either copy the original, 35 | * or use this method combined with `-[ZDCDictionary rawSet]` to provide the parameter. 36 | * 37 | * @param raw 38 | * The {key,value} tuples will be stored in the created ZDCDictionary. 39 | */ 40 | - (instancetype)initWithDictionary:(nullable NSDictionary *)raw; 41 | 42 | /** 43 | * Creates a dictionary that continas the {key, value} tuples from the given input. 44 | * 45 | * @note To initialize from another ZDCDictionary, you can either copy the original, 46 | * or use this method combined with `-[ZDCDictionary rawSet]` to provide the parameter. 47 | * 48 | * @param raw 49 | * The {key,value} tuples will be stored in the created ZDCDictionary. 50 | * 51 | * @param copyItems 52 | * If set to YES, the values will be copied when storing in the ZDCDictionary. 53 | * This means the values will need to support the NSCopying protocol. 54 | */ 55 | - (instancetype)initWithDictionary:(nullable NSDictionary *)raw copyItems:(BOOL)copyItems; 56 | 57 | #pragma mark Raw 58 | 59 | /** 60 | * Returns a reference to the underlying NSDictionary that the ZDCDictionary instance is wrapping. 61 | * 62 | * @note The returned value is a copy of the underlying NSMutableDictionary. 63 | * Thus changes to the ZDCDictionary will not be reflected in the returned value. 64 | */ 65 | @property (nonatomic, copy, readonly) NSDictionary *rawDictionary; 66 | 67 | #pragma mark Reading 68 | 69 | /** 70 | * The number of items stored in the dictionary. 71 | */ 72 | @property (nonatomic, readonly) NSUInteger count; 73 | 74 | /** 75 | * Returns the array of keys stored in the dictionary. 76 | */ 77 | - (NSArray *)allKeys; 78 | 79 | /** 80 | * Returns YES if there's a value stored in the dictionary for the given key. 81 | * This method is faster than calling `objectForKey:`, and then checking to see if the returned object is non-nil. 82 | */ 83 | - (BOOL)containsKey:(KeyType)key; 84 | 85 | /** 86 | * Returns the stored object for the given key. 87 | */ 88 | - (nullable ObjectType)objectForKey:(KeyType)key; 89 | 90 | /** 91 | * Returns the stored object for the given key. 92 | * 93 | * Allows you to use syntax: 94 | * ``` 95 | * value = zdcDict[key] 96 | * ``` 97 | */ 98 | - (nullable ObjectType)objectForKeyedSubscript:(KeyType)key; 99 | 100 | #pragma mark Writing 101 | 102 | /** 103 | * Stores the {key, value} tuple in the dictionary. 104 | * If there's already a value stored for the given key, the old value is replaced with the new value. 105 | */ 106 | - (void)setObject:(nullable ObjectType)object forKey:(KeyType)key; 107 | 108 | /** 109 | * Stores the {key, value} tuple in the dictionary. 110 | * If there's already a value stored for the given key, the old value is replaced with the new value. 111 | * 112 | * Allows you to use syntax: 113 | * ``` 114 | * zdcDict[key] = value 115 | * ``` 116 | */ 117 | - (void)setObject:(nullable ObjectType)object forKeyedSubscript:(KeyType)key; 118 | 119 | /** 120 | * Removes the {key, value} tuple from the dictionary if the key exists. 121 | * If the key doesn't exist, no changes are made. 122 | */ 123 | - (void)removeObjectForKey:(KeyType)key; 124 | 125 | /** 126 | * Removes all items from the dictionary matching the given list of keys. 127 | */ 128 | - (void)removeObjectsForKeys:(NSArray *)keys; 129 | 130 | /** 131 | * Removes all {key, value} tuples from the dictionary. 132 | */ 133 | - (void)removeAllObjects; 134 | 135 | #pragma mark Enumeration 136 | 137 | /** 138 | * Enumerates all keys in the dictionary with the given block. 139 | */ 140 | - (void)enumerateKeysUsingBlock:(void (^)(KeyType key, BOOL *stop))block; 141 | 142 | /** 143 | * Enumerates all {key, value} tuples in the dictionary with the given block. 144 | */ 145 | - (void)enumerateKeysAndObjectsUsingBlock:(void (^)(KeyType key, ObjectType obj, BOOL *stop))block; 146 | 147 | #pragma mark Equality 148 | 149 | /** 150 | * Returns YES if `another` is of class ZDCOrderedDictionary, 151 | * and the receiver & another contain the same set of {key, value} tuples. 152 | * 153 | * This method corresponds to `[NSDictionary isEqualToDictionary:]`. 154 | * 155 | * @note It does NOT take into account the changeset of either ZDCDictionary instance. 156 | */ 157 | - (BOOL)isEqual:(nullable id)another; 158 | 159 | /** 160 | * Returns YES if the receiver and `another` contain the same set of {key, value} tuples. 161 | * 162 | * This method corresponds to `[NSDictionary isEqualToDictionary:]`. 163 | * 164 | * @note It does NOT take into account the changeset of either ZDCDictionary instance. 165 | */ 166 | - (BOOL)isEqualToDictionary:(nullable ZDCDictionary *)another; 167 | 168 | @end 169 | 170 | NS_ASSUME_NONNULL_END 171 | -------------------------------------------------------------------------------- /ZDCSyncable/ZDCObject.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | @import Foundation; 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | /** 11 | * ZDCObject is a simple base class with a small, but very useful, set of functionality: 12 | * 13 | * - an object can be made immutable, via the `makeImmutable` method 14 | * - once immutable, attempts to change properties on the object will throw an exception 15 | * 16 | * You may even find it useful outside the context of syncing. 17 | * 18 | * A note on copying & mutability/immutability: 19 | * 20 | * Apple's container classes in objective-c implement a strict immutable/mutable separation. 21 | * For example, there is NSArray & NSMutableArray. 22 | * For our puposes, it is often inconvenient to follow the same design. 23 | * Additionally, doing so would make subclassing much more difficult. 24 | * For example, if we had a base class of `Car`, and a sub-class of `Tesla`, this would imply 25 | * we would have to create both `MutableCar` & `MutableTesla`. 26 | * The purpose of this class is to add optional immutability to mutable-by-default classes. 27 | * As such, it turns the copy/mutablCopy design on its head - instead we have copy/immutableCopy. 28 | * That is, since our classes are mutable-by-default, a copy returns a mutable version. 29 | * You can get an immutable version via immutableCopy. 30 | * And we purposefully do not implement mutableCopy to avoid confusion. 31 | */ 32 | NS_SWIFT_NAME(ZDCObject_ObjC) 33 | @interface ZDCObject : NSObject 34 | 35 | #pragma mark Immutability 36 | 37 | /** 38 | * Returns whether or not the object has been marked immutable. 39 | * Once immutable, attempts to alter the object will throw an exception. 40 | */ 41 | @property (nonatomic, readonly) BOOL isImmutable; 42 | 43 | /** 44 | * Marks the object as immutable. 45 | * Once immutable, attempts to alter the object will throw an exception. 46 | */ 47 | - (void)makeImmutable; 48 | 49 | /** 50 | * Shorthand for: [[obj copy] makeImmutable] 51 | * 52 | * It turns these 2 lines: 53 | * copy = [obj copy]; 54 | * [copy makeImmutable]; 55 | * 56 | * Into this one-liner: 57 | * copy = [obj immutableCopy]; 58 | */ 59 | - (instancetype)immutableCopy; 60 | 61 | 62 | #pragma mark Change Tracking 63 | 64 | /** 65 | * Returns whether or not there are any changes to the object. 66 | */ 67 | @property (nonatomic, readonly) BOOL hasChanges; 68 | 69 | /** 70 | * Resets the hasChanges property to false. 71 | * Use this to wipe the slate, and restart change tracking from the current state. 72 | */ 73 | - (void)clearChangeTracking; 74 | 75 | #pragma mark NSCoding Utilities 76 | 77 | /** 78 | * Apple strongly encourages you to use NSURL objects for storing local file references, 79 | * but encoding/decoding a fileURL is kinda broken. 80 | * 81 | * That is, code like this will appear to work, but will end up breaking things: 82 | * ``` 83 | * [coder encodeObject:myFileURL forKey:@"myFileURL"] // does NOT work! Broken! :( 84 | * myFileURL = [decoder decodeObjectForKey:@"myFileURL"]; // does NOT work! Broken! :( 85 | * ``` 86 | * 87 | * And this is because that file might get moved. 88 | * This is especially troublesome on iOS, where everytime your app is updated, iOS moves your app's root folder. 89 | * Which, in turn, breaks every single NSURL you encoded using the above technique. 90 | * 91 | * The solution is to use Apple's bookmark capability. 92 | * From their documentation on the topic: https://goo.gl/0Uqn5J 93 | * 94 | * If you want to save the location of a file persistently, use the bookmark capabilities of NSURL. 95 | * A bookmark is an opaque data structure, enclosed in an NSData object, that describes the location of a file. 96 | * Whereas path and file reference URLs are potentially fragile between launches of your app, 97 | * a bookmark can usually be used to re-create a URL to a file even in cases where the file was moved or renamed. 98 | * 99 | * These methods will "do the right thing" for you automatically. 100 | * They will first attempt to use the bookmark capabilities of NSURL. 101 | * If this fails because the file doesn't exist, the serializer will fallback to a hybrid binary plist system. 102 | * It will look for a parent directory that does exist, generate a bookmark of that, 103 | * and store the remainder as a relative path. 104 | * 105 | * Long story short, you can fix the problem by simply writing this instead: 106 | * ``` 107 | * [coder encodeObject:[self serializeFileURL:myFileURL] forKey:@"myFileURL"]; // works :) 108 | * myFileURL = [self deserializeFileURL:[decoder decodeObjectForKey:@"myFileURL"]]; // works :) 109 | * ``` 110 | */ 111 | - (nullable NSData *)serializeFileURL:(NSURL *)fileURL; 112 | 113 | /** 114 | * Performs the inverse of `serializeFileURL:`. See that method for more documentation. 115 | */ 116 | - (nullable NSURL *)deserializeFileURL:(NSData *)fileURLData; 117 | 118 | @end 119 | 120 | NS_ASSUME_NONNULL_END 121 | -------------------------------------------------------------------------------- /ZDCSyncable/ZDCObject.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCObject.h" 7 | #import "ZDCObjectSubclass.h" 8 | 9 | #import 10 | 11 | 12 | @implementation ZDCObject { 13 | @private 14 | 15 | void *observerContext; 16 | BOOL isImmutable; 17 | BOOL hasChanges; 18 | } 19 | 20 | /** 21 | * Make sure all your subclasses call this method ([super init]). 22 | */ 23 | - (instancetype)init 24 | { 25 | if ((self = [super init])) 26 | { 27 | // Turn on KVO for object. 28 | // We do this so we can get notified if the user is about to make changes to one of the object's properties. 29 | // 30 | // Don't worry, this doesn't create a retain cycle. 31 | // 32 | // Note: It's important use a unique observer context. 33 | // In the past, we saw crashes in iOS 11. 34 | // 35 | // https://forums.developer.apple.com/thread/70097 36 | // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOBasics.html 37 | 38 | observerContext = &observerContext; 39 | [self addObserver:self forKeyPath:@"isImmutable" options:0 context:observerContext]; 40 | } 41 | return self; 42 | } 43 | 44 | - (void)dealloc 45 | { 46 | if (observerContext) { 47 | [self removeObserver:self forKeyPath:@"isImmutable" context:observerContext]; 48 | } 49 | } 50 | 51 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 52 | #pragma mark NSCopying 53 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 54 | 55 | /** 56 | * In this example, all copies are automatically mutable. 57 | * So all you have to do in your code is something like this: 58 | * 59 | * [databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction]{ 60 | * 61 | * Car *car = [transaction objectForKey:carId inCollection:@"cars"]; 62 | * car = [car copy]; // make mutable copy 63 | * car.speed = newSpeed; 64 | * 65 | * [transaction setObject:car forKey:carId inCollection:@"cars"]; 66 | * }]; 67 | * 68 | * Which means all you have to do is implement the copyWithZone method in your model classes. 69 | **/ 70 | - (id)copyWithZone:(NSZone *)zone 71 | { 72 | // Subclasses should call this method via [super copyWithZone:zone]. 73 | // For example: 74 | // 75 | // MySubclass *copy = [super copyWithZone:zone]; 76 | // copy->ivar1 = [ivar1 copy]; 77 | // copy->ivar2 = ivar2; 78 | // return copy; 79 | 80 | ZDCObject *copy = [[[self class] alloc] init]; 81 | copy->isImmutable = NO; 82 | copy->hasChanges = self->hasChanges; 83 | 84 | return copy; 85 | } 86 | 87 | /** 88 | * For complicated copying scenarios, such as nested deep copies. 89 | * This method is declared in: ZDCObjectSubclass.h 90 | */ 91 | - (void)copyChangeTrackingTo:(id)another 92 | { 93 | if ([another isKindOfClass:[ZDCObject class]]) 94 | { 95 | __unsafe_unretained ZDCObject *copy = (ZDCObject *)another; 96 | if (!copy->isImmutable) 97 | { 98 | copy->hasChanges = self->hasChanges; 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * There's no need to override this method. 105 | * Just override copyWithZone: like usual for the NSCopying protocol. 106 | **/ 107 | - (instancetype)immutableCopy 108 | { 109 | typeof(self) copy = [self copy]; 110 | 111 | // This code is wrong: 112 | // copy->isImmutable = YES; 113 | // 114 | // Because the `makeImmutable` method may be overriden by subclasses, 115 | // and we need to go through this method for proper immutability. 116 | // 117 | [copy makeImmutable]; 118 | 119 | return copy; 120 | } 121 | 122 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 123 | #pragma mark Immutability 124 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 125 | 126 | @synthesize isImmutable = isImmutable; 127 | 128 | /** 129 | * See header file for description. 130 | */ 131 | - (void)makeImmutable 132 | { 133 | if (!isImmutable) 134 | { 135 | // Set immutable flag 136 | isImmutable = YES; 137 | } 138 | } 139 | 140 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 141 | #pragma mark Monitoring 142 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 143 | 144 | /** 145 | * See header file for description. 146 | */ 147 | - (BOOL)hasChanges 148 | { 149 | return hasChanges; 150 | } 151 | 152 | /** 153 | * See header file for description. 154 | */ 155 | - (void)clearChangeTracking 156 | { 157 | hasChanges = NO; 158 | 159 | // Implementation Thoughts: 160 | // 161 | // There are 2 possibilities here: 162 | // A.) [changedProperties removeAllObjects] 163 | // B.) changedProperties = nil 164 | // 165 | // If the object has been made immutable, then changedProperties shouldn't be needed anymore. 166 | // 167 | // if (isImmutable) { 168 | // changedProperties = nil; 169 | // } 170 | // else { 171 | // [changedProperties removeAllObjects]; 172 | // } 173 | } 174 | 175 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 176 | #pragma mark NSCoding Utilities 177 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 178 | 179 | static NSString *const kPlistKey_Version = @"version"; 180 | static NSString *const kPlistKey_BookmarkData = @"bookmarkData"; 181 | static NSString *const kPlistKey_PathComponents = @"pathComponents"; 182 | 183 | /** 184 | * See header file for description. 185 | */ 186 | - (NSData *)serializeFileURL:(NSURL *)fileURL 187 | { 188 | if (fileURL == nil) return nil; 189 | 190 | NSData *bookmarkData = [fileURL bookmarkDataWithOptions:NSURLBookmarkCreationSuitableForBookmarkFile 191 | includingResourceValuesForKeys:nil 192 | relativeToURL:nil 193 | error:NULL]; 194 | 195 | if (bookmarkData) { 196 | return bookmarkData; 197 | } 198 | 199 | // Failed to create bookmark data. 200 | // This is usually because the file doesn't exist. 201 | // As a backup plan, we're going to get a bookmark of the closest parent directory that does exist. 202 | // And combine it with the relative path after that point. 203 | 204 | if (!fileURL.isFileURL) { 205 | return nil; 206 | } 207 | 208 | NSMutableArray *pathComponents = [NSMutableArray arrayWithCapacity:2]; 209 | 210 | NSString *lastPathComponent = nil; 211 | NSURL *lastURL = nil; 212 | NSURL *parentURL = nil; 213 | 214 | lastURL = fileURL; 215 | 216 | lastPathComponent = [lastURL lastPathComponent]; 217 | if (lastPathComponent) 218 | [pathComponents addObject:lastPathComponent]; 219 | 220 | parentURL = [lastURL URLByDeletingLastPathComponent]; 221 | 222 | while (![parentURL isEqual:lastURL]) 223 | { 224 | bookmarkData = [parentURL bookmarkDataWithOptions:NSURLBookmarkCreationSuitableForBookmarkFile 225 | includingResourceValuesForKeys:nil 226 | relativeToURL:nil 227 | error:NULL]; 228 | 229 | if (bookmarkData) { 230 | break; 231 | } 232 | else 233 | { 234 | lastURL = parentURL; 235 | 236 | lastPathComponent = [lastURL lastPathComponent]; 237 | if (lastPathComponent) 238 | [pathComponents insertObject:lastPathComponent atIndex:0]; 239 | 240 | parentURL = [lastURL URLByDeletingLastPathComponent]; 241 | } 242 | } 243 | 244 | if (bookmarkData) 245 | { 246 | NSDictionary *plistDict = @{ 247 | kPlistKey_Version: @(1), 248 | kPlistKey_BookmarkData: bookmarkData, 249 | kPlistKey_PathComponents: pathComponents 250 | }; 251 | 252 | NSData *plistData = [NSPropertyListSerialization dataWithPropertyList:plistDict 253 | format:NSPropertyListBinaryFormat_v1_0 254 | options:0 255 | error:NULL]; 256 | return plistData; 257 | } 258 | 259 | return nil; 260 | } 261 | 262 | /** 263 | * See header file for description. 264 | */ 265 | - (NSURL *)deserializeFileURL:(NSData *)data 266 | { 267 | if (data.length == 0) return nil; 268 | 269 | const void *bytes = data.bytes; 270 | 271 | BOOL isBookmarkData = NO; 272 | BOOL isPlistData = NO; 273 | 274 | { 275 | NSData *magic = [@"book" dataUsingEncoding:NSASCIIStringEncoding]; 276 | if (data.length > magic.length) 277 | { 278 | isBookmarkData = (memcmp(bytes, magic.bytes, magic.length) == 0); 279 | } 280 | } 281 | 282 | if (!isBookmarkData) 283 | { 284 | NSData *magic = [@"bplist" dataUsingEncoding:NSASCIIStringEncoding]; 285 | if (data.length > magic.length) 286 | { 287 | isPlistData = (memcmp(bytes, magic.bytes, magic.length) == 0); 288 | } 289 | } 290 | 291 | BOOL isUnknown = !isBookmarkData && !isPlistData; 292 | 293 | if (isBookmarkData || isUnknown) 294 | { 295 | NSURL *url = 296 | [NSURL URLByResolvingBookmarkData:data 297 | options:NSURLBookmarkResolutionWithoutUI 298 | relativeToURL:nil 299 | bookmarkDataIsStale:NULL 300 | error:NULL]; 301 | 302 | if (url) { 303 | return url; 304 | } 305 | } 306 | 307 | if (isPlistData || isUnknown) 308 | { 309 | id plistObj = [NSPropertyListSerialization propertyListWithData:data 310 | options:NSPropertyListImmutable 311 | format:NULL 312 | error:NULL]; 313 | if ([plistObj isKindOfClass:[NSDictionary class]]) 314 | { 315 | NSDictionary *plistDict = (NSDictionary *)plistObj; 316 | 317 | id data = plistDict[kPlistKey_BookmarkData]; 318 | id comp = plistDict[kPlistKey_PathComponents]; 319 | 320 | if ([data isKindOfClass:[NSData class]] && [comp isKindOfClass:[NSArray class]]) 321 | { 322 | NSData *bookmarkData = (NSData *)data; 323 | NSArray *pathComponents = (NSArray *)comp; 324 | 325 | NSURL *url = [NSURL URLByResolvingBookmarkData:bookmarkData 326 | options:NSURLBookmarkResolutionWithoutUI 327 | relativeToURL:nil 328 | bookmarkDataIsStale:NULL 329 | error:NULL]; 330 | if (url) 331 | { 332 | NSString *path = [pathComponents componentsJoinedByString:@"/"]; 333 | 334 | return [[NSURL URLWithString:path relativeToURL:url] absoluteURL]; 335 | } 336 | } 337 | } 338 | } 339 | 340 | return nil; 341 | } 342 | 343 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 344 | #pragma mark Class Configuration 345 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 346 | 347 | /** 348 | * This method returns a list of all (known/static) properties that should be monitored. 349 | * That is, properties that should be considered immutable once the makeImmutable method has been invoked. 350 | * 351 | * It is designed to include all `@property` variables included in all subclasses. 352 | * Thus this method returns a list of all properties in each subclass in the hierarchy leading to "[self class]". 353 | * 354 | * However, this is not always exactly what you want. 355 | * For example, you may have properties which are simply used for caching: 356 | * 357 | * @property (nonatomic, strong, readwrite) UIImage *avatarImage; 358 | * @property (nonatomic, strong, readwrite) UIImage *cachedTransformedAvatarImage; 359 | * 360 | * In this example, you store the user's plain avatar image. 361 | * However, your code transforms the avatar in various ways for display in the UI. 362 | * So to reduce overhead, you'd like to cache these transformed images in the user object. 363 | * Thus the 'cachedTransformedAvatarImage' property doesn't actually mutate the user object. It's just a temp cache. 364 | * 365 | * So your subclass would override this method like so: 366 | * 367 | * + (NSMutableSet *)monitoredProperties 368 | * { 369 | * NSMutableSet *monitoredProperties = [super monitoredProperties]; 370 | * [monitoredProperties removeObject:NSStringFromSelector(@selector(cachedTransformedAvatarImage))]; 371 | * 372 | * return monitoredProperties; 373 | * } 374 | **/ 375 | + (NSMutableSet *)monitoredProperties 376 | { 377 | // Steps to override me (if needed): 378 | // 379 | // - Invoke [super monitoredProperties] 380 | // - Modify resulting mutable set 381 | // - Return modified set 382 | 383 | NSMutableSet *properties = nil; 384 | 385 | Class rootClass = [ZDCObject class]; 386 | Class subClass = [self class]; 387 | 388 | while (subClass != rootClass) 389 | { 390 | unsigned int count = 0; 391 | objc_property_t *propertyList = class_copyPropertyList(subClass, &count); 392 | if (propertyList) 393 | { 394 | if (properties == nil) 395 | properties = [NSMutableSet setWithCapacity:count]; 396 | 397 | for (unsigned int i = 0; i < count; i++) 398 | { 399 | const char *name = property_getName(propertyList[i]); 400 | NSString *property = [NSString stringWithUTF8String:name]; 401 | 402 | [properties addObject:property]; 403 | } 404 | 405 | free(propertyList); 406 | } 407 | 408 | subClass = [subClass superclass]; 409 | } 410 | 411 | // For some reason, we have to remove common NSObject stuff. 412 | // Even though, theoretically, these should only be listed within the NSObject subClass... 413 | // For some bizarre reason, objc is including them in other classes. 414 | // 415 | // So now we have to manually remove them. 416 | 417 | NSArray *fixup_NSObject = @[ 418 | @"superclass", @"hash", @"description", @"debugDescription" 419 | ]; 420 | 421 | for (NSString *property in fixup_NSObject) 422 | { 423 | [properties removeObject:property]; 424 | } 425 | 426 | // We also need to remove common ZDCObject stuff. 427 | // Again, these should be only listed in ZDCObject. 428 | // But again, objc confuses us, and will list them in other classes. 429 | 430 | NSArray *fixup_ZDCObject = @[ 431 | @"isImmutable", @"hasChanges" 432 | ]; 433 | 434 | for (NSString *property in fixup_ZDCObject) 435 | { 436 | [properties removeObject:property]; 437 | } 438 | 439 | if (properties) 440 | return properties; 441 | else 442 | return [NSMutableSet setWithCapacity:0]; 443 | } 444 | 445 | /** 446 | * Generally you should NOT override this method. 447 | * Just override the class version of this method (above). 448 | **/ 449 | - (NSSet *)monitoredProperties 450 | { 451 | NSSet *cached = objc_getAssociatedObject([self class], _cmd); 452 | if (cached) return cached; 453 | 454 | NSSet *monitoredProperties = [[[self class] monitoredProperties] copy]; 455 | 456 | objc_setAssociatedObject([self class], _cmd, monitoredProperties, OBJC_ASSOCIATION_RETAIN); 457 | return monitoredProperties; 458 | } 459 | 460 | /** 461 | * Override this method if your class includes 'dynamic' monitored properties. 462 | * That is, properties that should be monitored, but don't have dedicated '@property' declarations. 463 | * 464 | * Important: 465 | * If a property (localKey) is not included in the 'monitoredProperties' set, 466 | * then the class will be unable to automatically register for KVO notifications concerning the value. 467 | * This means that you MUST manually invoke [self willChangeValueForKey:] & [self didChangeValueForKey:], 468 | * in order to run the code for the corresponding methods in this class. 469 | **/ 470 | - (BOOL)isMonitoredProperty:(NSString *)localKey 471 | { 472 | return [self.monitoredProperties containsObject:localKey]; 473 | } 474 | 475 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 476 | #pragma mark KVO 477 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 478 | 479 | + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key 480 | { 481 | if ([key isEqualToString:@"isImmutable"]) 482 | return YES; 483 | else 484 | return [super automaticallyNotifiesObserversForKey:key]; 485 | } 486 | 487 | + (NSSet *)keyPathsForValuesAffectingIsImmutable 488 | { 489 | // In order for the KVO magic to work, we specify that the isImmutable property is dependent 490 | // upon all other properties in the class that should become immutable. 491 | // 492 | // The code below ** attempts ** to do this automatically. 493 | // It does so by creating a list of all the properties in the class. 494 | // 495 | // Obviously this will not work for every situation. 496 | // In particular: 497 | // 498 | // - if you have custom setter methods that aren't specified as properties 499 | // - if you have other custom methods that modify the object 500 | // 501 | // To cover these edge cases, simply add code like the following at the beginning of such methods: 502 | // 503 | // - (void)recalculateFoo 504 | // { 505 | // NSString *const key = NSStringFromSelector(@selector(foo)); 506 | // [self willChangeValueForKey:key]; 507 | // 508 | // ... normal code that modifies `foo` ivar ... 509 | // 510 | // [self didChangeValueForKey:key]; 511 | // } 512 | 513 | return [self monitoredProperties]; 514 | } 515 | 516 | - (void)observeValueForKeyPath:(NSString *)keyPath 517 | ofObject:(id)object 518 | change:(NSDictionary *)change 519 | context:(void *)context 520 | { 521 | // Nothing to do (but method is required to exist) 522 | } 523 | 524 | - (void)willChangeValueForKey:(NSString *)key 525 | { 526 | if ([self isMonitoredProperty:key]) 527 | { 528 | if (isImmutable) 529 | { 530 | @throw [self immutableExceptionForKey:key]; 531 | } 532 | 533 | [self _willChangeValueForKey:key]; 534 | } 535 | 536 | [super willChangeValueForKey:key]; 537 | } 538 | 539 | - (void)_willChangeValueForKey:(NSString *)key 540 | { 541 | // Subclass hook 542 | } 543 | 544 | - (void)didChangeValueForKey:(NSString *)key 545 | { 546 | if ([self isMonitoredProperty:key]) 547 | { 548 | if (!hasChanges) { 549 | hasChanges = YES; 550 | } 551 | 552 | [self _didChangeValueForKey:key]; 553 | } 554 | 555 | [super didChangeValueForKey:key]; 556 | } 557 | 558 | - (void)_didChangeValueForKey:(NSString *)key 559 | { 560 | // Subclass hook 561 | } 562 | 563 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 564 | #pragma mark Exceptions 565 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 566 | 567 | - (NSException *)immutableException 568 | { 569 | return [self immutableExceptionForKey:nil]; 570 | } 571 | 572 | - (NSException *)immutableExceptionForKey:(nullable NSString *)key 573 | { 574 | NSString *reason; 575 | if (key) { 576 | reason = [NSString stringWithFormat: 577 | @"Attempting to mutate immutable object. Class = %@, property = %@", NSStringFromClass([self class]), key]; 578 | } 579 | else { 580 | reason = [NSString stringWithFormat: 581 | @"Attempting to mutate immutable object. Class = %@", NSStringFromClass([self class])]; 582 | } 583 | 584 | NSDictionary *userInfo = @{ NSLocalizedRecoverySuggestionErrorKey: 585 | @"To make modifications you should create a copy via [object copy]." 586 | @" You may then make changes to the copy."}; 587 | 588 | return [NSException exceptionWithName:@"ZDCObjectException" reason:reason userInfo:userInfo]; 589 | } 590 | 591 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 592 | #pragma mark Errors 593 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 594 | 595 | - (NSError *)hasChangesError 596 | { 597 | NSDictionary *userInfo = @{ 598 | NSLocalizedDescriptionKey: 599 | @"The object has unsaved changes, and the requested operation only works on a clean object." 600 | }; 601 | 602 | return [NSError errorWithDomain:NSStringFromClass([self class]) code:100 userInfo:userInfo]; 603 | } 604 | 605 | - (NSError *)malformedChangesetError 606 | { 607 | NSDictionary *userInfo = @{ 608 | NSLocalizedDescriptionKey: 609 | @"The changeset is malformed. " 610 | }; 611 | 612 | return [NSError errorWithDomain:NSStringFromClass([self class]) code:101 userInfo:userInfo]; 613 | } 614 | 615 | - (NSError *)mismatchedChangeset 616 | { 617 | NSDictionary *userInfo = @{ 618 | NSLocalizedDescriptionKey: 619 | @"The changeset appears to be mismatched." 620 | @" It does not line-up properly with the current state of the object." 621 | }; 622 | 623 | return [NSError errorWithDomain:NSStringFromClass([self class]) code:102 userInfo:userInfo]; 624 | } 625 | 626 | - (NSError *)incorrectObjectClass 627 | { 628 | NSDictionary *userInfo = @{ 629 | NSLocalizedDescriptionKey: 630 | [NSString stringWithFormat: 631 | @"Unable to merge cloudVersion. Not proper class. Expected: %@", NSStringFromClass([self class])] 632 | }; 633 | 634 | return [NSError errorWithDomain:NSStringFromClass([self class]) code:103 userInfo:userInfo]; 635 | } 636 | 637 | @end 638 | -------------------------------------------------------------------------------- /ZDCSyncable/ZDCOrderedDictionary.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCObject.h" 7 | #import "ZDCSyncable.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | /** 12 | * ZDCOrderedDictionary tracks changes made to an ordered dictionary. 13 | * 14 | * An ordered dictionary is a combination between a dictionary & an array. 15 | * That is, it provides an ordered set of {key, value} pairs. 16 | * 17 | * In addition to its core functionality, it also provides the following set of features: 18 | * - it can be made immutable (via `-[ZDCObject makeImmutable]` method) 19 | * - it implements the ZDCSyncable protocol and thus: 20 | * - it tracks all changes and can provide a changeset (which encodes the change info) 21 | * - it supports undo & redo 22 | * - it supports merge operations 23 | */ 24 | NS_SWIFT_NAME(ZDCOrderedDictionary_ObjC) 25 | @interface ZDCOrderedDictionary : ZDCObject 26 | 27 | /** 28 | * Creates an empty orderedDictionary. 29 | */ 30 | - (instancetype)init; 31 | 32 | /** 33 | * Creates a dictionary that contains the {key, value} tuples from the given input. 34 | * There's no particular order associated with the items. 35 | * 36 | * @param raw 37 | * The {key,value} tuples will be stored in the created orderedDictionary. 38 | */ 39 | - (instancetype)initWithDictionary:(nullable NSDictionary *)raw; 40 | 41 | /** 42 | * Creates a dictionary that contains the {key, value} tuples from the given input. 43 | * There's no particular order associated with the items. 44 | * 45 | * @param raw 46 | * The {key,value} tuples will be stored in the created orderedDictionary. 47 | * 48 | * @param copyItems 49 | * If set to YES, the values will be copied when storing in the ZDCDictionary. 50 | * This means the values will need to support the NSCopying protocol. 51 | */ 52 | - (instancetype)initWithDictionary:(nullable NSDictionary *)raw copyItems:(BOOL)copyItems; 53 | 54 | /** 55 | * Creates a new orderedDictionary by copying to given one. 56 | * 57 | * @param another 58 | * The source orderedDictionary to copy. Both the tuples & order are copied. 59 | */ 60 | - (instancetype)initWithOrderedDictionary:(nullable ZDCOrderedDictionary *)another; 61 | 62 | /** 63 | * Creates a new orderedDictionary by copying to given one. 64 | * 65 | * @param another 66 | * The source orderedDictionary to copy. Both the tuples & order are copied. 67 | * 68 | * @param flag 69 | * If set to YES, the values will be copied when storing in the ZDCDictionary. 70 | * This means the values will need to support the NSCopying protocol. 71 | */ 72 | - (instancetype)initWithOrderedDictionary:(nullable ZDCOrderedDictionary *)another 73 | copyItems:(BOOL)flag; 74 | 75 | #pragma mark Raw 76 | 77 | /** 78 | * Returns a reference to the underlying NSDictionary that the ZDCOrderedDictionary instance is wrapping. 79 | * 80 | * @note The returned value is a copy of the underlying NSMutableDictionary. 81 | * Thus changes to the ZDCOrderedDictionary will not be reflected in the returned value. 82 | */ 83 | @property (nonatomic, copy, readonly) NSDictionary *rawDictionary; 84 | 85 | /** 86 | * Returns a reference to the underlying NSArray that the ZDCDictionary instance is wrapping. 87 | * 88 | * @note The returned value is a copy of the underlying NSMutableArray. 89 | * Thus changes to the ZDCOrderedDictionary will not be reflected in the returned value. 90 | */ 91 | @property (nonatomic, copy, readonly) NSArray *rawOrder; 92 | 93 | #pragma mark Reading 94 | 95 | /** 96 | * The number of items stored in the ordered dictionary. 97 | */ 98 | @property (nonatomic, readonly) NSUInteger count; 99 | 100 | /** 101 | * Returns the first item in the orderedDictionary. 102 | * If the orderedDictionary is empty, returns nil. 103 | * 104 | * @note This method returns the value component of the {key, value} tuple. 105 | */ 106 | @property (nonatomic, readonly, nullable) ObjectType firstObject; 107 | 108 | /** 109 | * Returns the last item in the orderedDictionary. 110 | * If the orderedDictionary is empty, returns nil. 111 | * 112 | * @note This method returns the value component of the {key, value} tuple. 113 | */ 114 | @property (nonatomic, readonly, nullable) ObjectType lastObject; 115 | 116 | /** 117 | * Returns the (ordered) array of keys stored in the dictionary. 118 | * 119 | * @note This method is equivalent to the `rawOrder` method. 120 | */ 121 | - (NSArray *)allKeys; 122 | 123 | /** 124 | * Returns YES if there's a value stored in the dictionary for the given key. 125 | * This method is faster than calling `objectForKey:`, and then checking to see if the returned object is non-nil. 126 | */ 127 | - (BOOL)containsKey:(KeyType)key; 128 | 129 | /** 130 | * Returns the index of key within the orderedDictionary. 131 | * If the key isn't present, returns NSNotFound. 132 | */ 133 | - (NSUInteger)indexForKey:(KeyType)key; 134 | 135 | /** 136 | * Returns the stored object for the given key. 137 | */ 138 | - (nullable ObjectType)objectForKey:(KeyType)key; 139 | 140 | /** 141 | * Returns the key at the given index. 142 | * Throws an exception if the given index is out-of-bounds. 143 | */ 144 | - (KeyType)keyAtIndex:(NSUInteger)idx; 145 | 146 | /** 147 | * Returns the object at the given index. 148 | * Throws an exception if the given index is out-of-bounds. 149 | */ 150 | - (ObjectType)objectAtIndex:(NSUInteger)idx; 151 | 152 | /** 153 | * Returns the stored object for the given key. 154 | * 155 | * Allows you to use code syntax: 156 | * ``` 157 | * value = orderedDictionary[key] 158 | * ``` 159 | */ 160 | - (nullable ObjectType)objectForKeyedSubscript:(KeyType)key; 161 | 162 | /** 163 | * Allows you to use code syntax: 164 | * ``` 165 | * value = orderedDictionary[index] 166 | * ``` 167 | * 168 | * @note This method returns the value component of the {key, value} tuple. 169 | */ 170 | - (nullable ObjectType)objectAtIndexedSubscript:(NSUInteger)idx; 171 | 172 | #pragma mark Writing 173 | 174 | /** 175 | * Sets the value for the key, replacing any previous value that may have existed. 176 | * If the item is being added, it's automatically added to the end of the array. 177 | */ 178 | - (void)setObject:(nullable ObjectType)object forKey:(KeyType)key; 179 | 180 | /** 181 | * Sets the value for the key, replacing any previous value that may have existed. 182 | * If the item is being added, it's automatically added to the end of the array. 183 | * 184 | * Allows you to use code syntax: 185 | * ``` 186 | * orderedDictionary[key] = value 187 | * ``` 188 | */ 189 | - (void)setObject:(nullable ObjectType)object forKeyedSubscript:(KeyType)key; 190 | 191 | /** 192 | * This method works the same as `setObject:forKey:`, except that it will return the index of the object. 193 | * If the object was added, it will return `count-1`, since the item was added to the end of the array. 194 | * If the key already existed, it will return its current index in the array (old value is replaced with new value). 195 | * 196 | * Returns NSNotFound if you attempt an illegal operation such as passing a nil object or a nil key. 197 | **/ 198 | - (NSUInteger)addObject:(ObjectType)object forKey:(KeyType)key; 199 | 200 | /** 201 | * This method works similarly to a `setObject:forKey:` operation with one difference: 202 | * - If the object was added (did NOT previously exist) it will be inserted at the given index, 203 | * and that index is returned. 204 | * - If the item already existed, it will return its current index in the array. 205 | * 206 | * If the given index is out-of-bounds, it will be ignored, and the item will be added at the end of the array. 207 | * Returns NSNotFound if you attempt an illegal operation such as passing a nil object or a nil key. 208 | */ 209 | - (NSUInteger)insertObject:(ObjectType)object forKey:(KeyType)key atIndex:(NSUInteger)index; 210 | 211 | /** 212 | * Use this method when you only need to change an item's index. 213 | * 214 | * @param oldIndex 215 | * The current index of the item. 216 | * 217 | * @param newIndex 218 | * The index to use AFTER the index has been removed: 219 | * - Step 1: `[array removeObjectAtIndex:oldIndex]` 220 | * - Step 2: `[array insertObject:obj atIndex:]` 221 | * 222 | * First, this method is faster than removing the item & then re-adding it. 223 | * Second, this method will properly track the change (i.e. the intent to change the item's index). 224 | * Thus you'll benefit from proper syncing of this action. 225 | */ 226 | - (void)moveObjectAtIndex:(NSUInteger)oldIndex toIndex:(NSUInteger)newIndex; 227 | 228 | /** 229 | * Removes the {key, value} tuple from the orderedDictionary if the key exists. 230 | * If the key doesn't exist, no changes are made. 231 | */ 232 | - (void)removeObjectForKey:(KeyType)key; 233 | 234 | /** 235 | * Removes all items from the dictionary matching the given list of keys. 236 | */ 237 | - (void)removeObjectsForKeys:(NSArray *)keys; 238 | 239 | /** 240 | * Removes the {key, value} tuple at the given index. 241 | * Does nothing if the index is out-of-bounds (no exception is thrown). 242 | */ 243 | - (void)removeObjectAtIndex:(NSUInteger)idx; 244 | 245 | #pragma mark Enumeration 246 | 247 | /** 248 | * Enumerates the keys in the ordered dictionary. 249 | * The enumeration is done in order, from first (index 0) to last. 250 | */ 251 | - (void)enumerateKeysUsingBlock:(void (^)(KeyType key, NSUInteger idx, BOOL *stop))block; 252 | 253 | /** 254 | * Enumerates the {key, value} tuples in the ordered dictionary. 255 | * The enumeration is done in order, from first (index 0) to last. 256 | */ 257 | - (void)enumerateKeysAndObjectsUsingBlock:(void (^)(KeyType key, ObjectType obj, NSUInteger idx, BOOL *stop))block; 258 | 259 | #pragma mark Equality 260 | 261 | /** 262 | * Returns YES if `another` is of class ZDCOrderedDictionary, 263 | * and receiver & another contain the same {key, value} tuples in the same order. 264 | * 265 | * @note It does NOT take into account the changeset of either ZDCDictionary instance. 266 | */ 267 | - (BOOL)isEqual:(nullable id)another; 268 | 269 | /** 270 | * Returns YES if receiver & another contain the same {key, value} tuples in the same order. 271 | * 272 | * @note It does NOT take into account the changeset of either ZDCDictionary instance. 273 | */ 274 | - (BOOL)isEqualToOrderedDictionary:(nullable ZDCOrderedDictionary *)another; 275 | 276 | @end 277 | 278 | NS_ASSUME_NONNULL_END 279 | -------------------------------------------------------------------------------- /ZDCSyncable/ZDCOrderedSet.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCObject.h" 7 | #import "ZDCSyncable.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | /** 12 | * ZDCOrderedSet tracks changes made to an ordered set. 13 | * 14 | * It's designed to act like a drop-in replacement for NSOrderedSet. 15 | * 16 | * In addition to its core functionality, it also provides the following set of features: 17 | * - instances can be made immutable (via `-[ZDCObject makeImmutable]` method) 18 | * - it implements the ZDCSyncable protocol and thus: 19 | * - it tracks all changes and can provide a changeset (which encodes the changes) 20 | * - it supports undo & redo 21 | * - it supports merge operations 22 | **/ 23 | NS_SWIFT_NAME(ZDCOrderedSet_ObjC) 24 | @interface ZDCOrderedSet : ZDCObject 25 | 26 | /** 27 | * Creates an empty ordered set. 28 | */ 29 | - (instancetype)init; 30 | 31 | /** 32 | * Creates an ordered set initialized from the given array. 33 | */ 34 | - (instancetype)initWithArray:(nullable NSArray *)array; 35 | 36 | /** 37 | * Creates an ordered set initialized from the given array. 38 | * 39 | * @param array 40 | * The array to initialize the set with. 41 | * 42 | * @param copyItems 43 | * If set to YES, the values will be copied when storing in the ZDCOrderedSet. 44 | * This means the values will need to support the NSCopying protocol. 45 | */ 46 | - (instancetype)initWithArray:(nullable NSArray *)array copyItems:(BOOL)copyItems; 47 | 48 | /** 49 | * Creates an ordered set initialized from the given set. 50 | * The order is indeterminate. 51 | */ 52 | - (instancetype)initWithSet:(nullable NSSet *)set; 53 | 54 | /** 55 | * Creates an ordered set initialized from the given set. 56 | * The order is indeterminate. 57 | * 58 | * @param set 59 | * The source to initialize the ordered set with. 60 | * 61 | * @param copyItems 62 | * If set to YES, the values will be copied when storing in the ZDCOrderedSet. 63 | * This means the values will need to support the NSCopying protocol. 64 | */ 65 | - (instancetype)initWithSet:(nullable NSSet *)set copyItems:(BOOL)copyItems; 66 | 67 | /** 68 | * Creates a ZDCOrderedSet instance initialized by copying the given NSOrderedSet. 69 | */ 70 | - (instancetype)initWithOrderedSet:(nullable NSOrderedSet *)orderedSet; 71 | 72 | /** 73 | * Creates a ZDCOrderedSet instance initialized by copying the given NSOrderedSet. 74 | * 75 | * @param orderedSet 76 | * The source to initialize the ZDCOrderedSet with. 77 | * 78 | * @param copyItems 79 | * If set to YES, the values will be copied when storing in the ZDCOrderedSet. 80 | * This means the values will need to support the NSCopying protocol. 81 | */ 82 | - (instancetype)initWithOrderedSet:(nullable NSOrderedSet *)orderedSet copyItems:(BOOL)copyItems; 83 | 84 | #pragma mark Raw 85 | 86 | /** 87 | * Returns a reference to the underlying NSOrderedSet that the ZDCOrderedSet instance is wrapping. 88 | * 89 | * @note The returned value is a copy of the underlying NSMutableOrderedSet. 90 | * Thus changes to the ZDCOrderedSet will not be reflected in the returned value. 91 | */ 92 | @property (nonatomic, copy, readonly) NSOrderedSet *rawOrderedSet; 93 | 94 | #pragma mark Reading 95 | 96 | /** 97 | * The number of items stored in the set. 98 | */ 99 | @property (nonatomic, readonly) NSUInteger count; 100 | 101 | /** 102 | * Returns the first item (at index 0) in the orderedSet. 103 | * If the orderedSet is empty, returns nil. 104 | */ 105 | @property (nonatomic, readonly, nullable) ObjectType firstObject; 106 | 107 | /** 108 | * Returns the last item (with largest index) in the orderedSet. 109 | * If the orderedSet is empty, returns nil. 110 | */ 111 | @property (nonatomic, readonly, nullable) ObjectType lastObject; 112 | 113 | /** 114 | * Returns a Boolean value that indicates whether a given object is present in the ordered set. 115 | */ 116 | - (BOOL)containsObject:(ObjectType)object; 117 | 118 | /** 119 | * Returns the object stored at the given index. 120 | * 121 | * @important Raises an NSRangeException if index is out-of-bounds. 122 | */ 123 | - (ObjectType)objectAtIndex:(NSUInteger)idx; 124 | 125 | /** 126 | * Returns the object stored at the given index. 127 | * 128 | * Allows you to use syntax: 129 | * ``` 130 | * value = zdcArray[index] 131 | * ``` 132 | * 133 | * @important Raises an NSRangeException if index is out-of-bounds. 134 | */ 135 | - (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx; 136 | 137 | /** 138 | * Returns the index of the object within the array. 139 | * If not found in the array, returns NSNotFound. 140 | */ 141 | - (NSUInteger)indexOfObject:(ObjectType)object; 142 | 143 | #pragma mark Writing 144 | 145 | /** 146 | * Appends a given object to the end of the mutable ordered set, if it is not already a member. 147 | */ 148 | - (void)addObject:(ObjectType)object; 149 | 150 | /** 151 | * Inserts the given object at the specified index of the mutable ordered set, if it is not already a member. 152 | * 153 | * @important Raises an NSRangeException if idx is greater than the number of elements in the mutable ordered set. 154 | */ 155 | - (void)insertObject:(ObjectType)object atIndex:(NSUInteger)idx; 156 | 157 | /** 158 | * Replaces the given object at the specified index of the mutable ordered set. 159 | * 160 | * @important Raises an NSRangeException if idx is greater than the number of elements in the mutable ordered set. 161 | */ 162 | - (void)setObject:(ObjectType)obj atIndexedSubscript:(NSUInteger)idx; 163 | 164 | /** 165 | * Use this method when you only need to change an item's index. 166 | * 167 | * @param oldIndex 168 | * The current index of the item. 169 | * 170 | * @param newIndex 171 | * The index to use AFTER the index has been removed: 172 | * - Step 1: `[array removeObjectAtIndex:oldIndex]` 173 | * - Step 2: `[array insertObject:obj atIndex:]` 174 | * 175 | * First, this method is faster than removing the item & then re-adding it. 176 | * Second, this method will properly track the change (i.e. the intent to change the item's index). 177 | * Thus you'll benefit from proper syncing of this action. 178 | */ 179 | - (void)moveObjectAtIndex:(NSUInteger)oldIndex toIndex:(NSUInteger)newIndex; 180 | 181 | /** 182 | * Removes a given object from the mutable ordered set (if it exists). 183 | */ 184 | - (void)removeObject:(ObjectType)object; 185 | 186 | /** 187 | * Removes a the object at the specified index from the mutable ordered set. 188 | */ 189 | - (void)removeObjectAtIndex:(NSUInteger)idx; 190 | 191 | /** 192 | * Removes all objects from the ordered set. 193 | * Afterwards the set will be empty. 194 | */ 195 | - (void)removeAllObjects; 196 | 197 | #pragma mark Enumeration 198 | 199 | /** 200 | * Enumerates all objects in the ordered set, 201 | * starting from index 0 and ending with the largest index. 202 | */ 203 | - (void)enumerateObjectsUsingBlock:(void (^)(ObjectType obj, NSUInteger idx, BOOL *stop))block; 204 | 205 | /** 206 | * An enumerator object that lets you access each object in the ordered set, 207 | * in order, from the element at the lowest index upwards. 208 | * 209 | * @note It is more efficient to use the fast enumeration protocol. 210 | */ 211 | - (NSEnumerator *)objectEnumerator; 212 | 213 | /** 214 | * Returns an enumerator that can be used to enumerate the objects in reverse order. 215 | */ 216 | - (NSEnumerator *)reverseObjectEnumerator; 217 | 218 | #pragma mark Equality 219 | 220 | /** 221 | * Returns YES if `another` is of class ZDCOrderedSet, 222 | * and the receiver & another contain the same objects in the same order. 223 | * 224 | * This method corresponds to `[NSOrderedSet isEqualToOrderedSet:]`. 225 | * 226 | * @note It does NOT take into account the changeset of either ZDCOrderedSet instance. 227 | */ 228 | - (BOOL)isEqual:(nullable id)another; 229 | 230 | /** 231 | * Returns YES if the receiver and `another` contain the same objects in the same order. 232 | * 233 | * This method corresponds to `[NSOrderedSet isEqualToOrderedSet:]`. 234 | * 235 | * @note It does NOT take into account the changeset of either ZDCOrderedSet instance. 236 | */ 237 | - (BOOL)isEqualToOrderedSet:(nullable ZDCOrderedSet *)another; 238 | 239 | @end 240 | 241 | NS_ASSUME_NONNULL_END 242 | -------------------------------------------------------------------------------- /ZDCSyncable/ZDCRecord.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCObject.h" 7 | #import "ZDCSyncable.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | /** 12 | * The ZDCRecord class is designed to be subclassed. 13 | * 14 | * It provides the following set of features for your subclass: 15 | * - instances can be made immutable (via `-[ZDCObject makeImmutable]` method) 16 | * - it implements the ZDCSyncable protocol and thus: 17 | * - it tracks all changes and can provide a changeset (which encodes the changes info) 18 | * - it supports undo & redo 19 | * - it supports merge operations 20 | */ 21 | NS_SWIFT_NAME(ZDCRecord_ObjC) 22 | @interface ZDCRecord : ZDCObject 23 | 24 | // 25 | // SUBCLASS ME ! 26 | // 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /ZDCSyncable/ZDCRecord.m: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCRecord.h" 7 | #import "ZDCObjectSubclass.h" 8 | #import "ZDCNull.h" 9 | #import "ZDCRef.h" 10 | 11 | // Changeset Keys 12 | // 13 | static NSString *const kChangeset_refs = @"refs"; 14 | static NSString *const kChangeset_values = @"values"; 15 | 16 | 17 | @implementation ZDCRecord { 18 | 19 | NSMutableDictionary *originalValues; 20 | } 21 | 22 | - (nonnull id)copyWithZone:(nullable NSZone *)zone 23 | { 24 | ZDCRecord *copy = [super copyWithZone:zone]; // [ZDCObject copyWithZone:] 25 | 26 | copy->originalValues = [self->originalValues mutableCopy]; 27 | 28 | return copy; 29 | } 30 | 31 | /** 32 | * For complicated copying scenarios, such as nested deep copies. 33 | * This method is declared in: ZDCObjectSubclass.h 34 | */ 35 | - (void)copyChangeTrackingTo:(id)another 36 | { 37 | if ([another isKindOfClass:[ZDCRecord class]]) 38 | { 39 | __unsafe_unretained ZDCRecord *copy = (ZDCRecord *)another; 40 | if (!copy.isImmutable) 41 | { 42 | copy->originalValues = [self->originalValues mutableCopy]; 43 | 44 | [super copyChangeTrackingTo:another]; 45 | } 46 | } 47 | } 48 | 49 | - (void)enumeratePropertiesWithBlock:(void (^)(NSString *propertyName, id _Nullable obj, BOOL *stop))block 50 | { 51 | for (NSString *propertyName in self.monitoredProperties) 52 | { 53 | id value = [self valueForKey:propertyName]; 54 | 55 | BOOL stop = NO; 56 | block(propertyName, value, &stop); 57 | 58 | if (stop) break; 59 | } 60 | } 61 | 62 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 63 | #pragma mark ZDCObject Overrides 64 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 65 | 66 | - (void)makeImmutable 67 | { 68 | [super makeImmutable]; 69 | 70 | [self enumeratePropertiesWithBlock:^(NSString *propertyName, id _Nullable obj, BOOL *stop) { 71 | 72 | if ([obj isKindOfClass:[ZDCObject class]]) 73 | { 74 | [(ZDCObject *)obj makeImmutable]; 75 | } 76 | }]; 77 | } 78 | 79 | - (BOOL)hasChanges 80 | { 81 | if ([super hasChanges]) return YES; 82 | 83 | if (originalValues.count > 0) return YES; 84 | 85 | __block BOOL hasChanges = NO; 86 | [self enumeratePropertiesWithBlock:^(NSString *propertyName, id _Nullable obj, BOOL *stop) { 87 | 88 | if ([obj isKindOfClass:[ZDCObject class]]) 89 | { 90 | if ([(ZDCObject *)obj hasChanges]) 91 | { 92 | hasChanges = YES; 93 | *stop = YES; 94 | } 95 | } 96 | }]; 97 | 98 | return hasChanges; 99 | } 100 | 101 | - (void)_willChangeValueForKey:(NSString *)key 102 | { 103 | if (originalValues == nil) { 104 | originalValues = [[NSMutableDictionary alloc] init]; 105 | } 106 | 107 | if (originalValues[key] == nil) 108 | { 109 | id originalValue = [self valueForKey:key]; 110 | if (originalValue) { 111 | originalValues[key] = originalValue; 112 | } 113 | else { 114 | originalValues[key] = [ZDCNull null]; 115 | } 116 | } 117 | } 118 | 119 | - (void)clearChangeTracking 120 | { 121 | [super clearChangeTracking]; 122 | 123 | // Implementation Thoughts: 124 | // 125 | // There are 2 possibilities here: 126 | // A.) [changedProperties removeAllObjects] 127 | // B.) changedProperties = nil 128 | // 129 | // If the object has been made immutable, then changedProperties shouldn't be needed anymore. 130 | // 131 | if (self.isImmutable) { 132 | originalValues = nil; 133 | } 134 | else { 135 | [originalValues removeAllObjects]; 136 | } 137 | 138 | [self enumeratePropertiesWithBlock:^(NSString *propertyName, id _Nullable obj, BOOL *stop) { 139 | 140 | if ([obj isKindOfClass:[ZDCObject class]]) 141 | { 142 | [(ZDCObject *)obj clearChangeTracking]; 143 | } 144 | }]; 145 | } 146 | 147 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 148 | #pragma mark ZDCSyncable 149 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 150 | 151 | - (nullable NSDictionary *)_changeset 152 | { 153 | if (![self hasChanges]) return nil; 154 | 155 | NSMutableDictionary *changeset = [NSMutableDictionary dictionaryWithCapacity:2]; 156 | 157 | // changeset: { 158 | // refs: { 159 | // key: changeset, ... 160 | // }, 161 | // ... 162 | // } 163 | 164 | __block NSMutableDictionary *refs = nil; 165 | 166 | void (^AddRef)(id, NSDictionary*) = ^(id key, NSDictionary *obj_changeset) { 167 | 168 | if (refs == nil) { 169 | refs = [[NSMutableDictionary alloc] init]; 170 | changeset[kChangeset_refs] = refs; 171 | } 172 | 173 | refs[key] = obj_changeset; 174 | }; 175 | 176 | [self enumeratePropertiesWithBlock:^(NSString *key, id _Nullable obj, BOOL *stop) { 177 | 178 | if ([obj conformsToProtocol:@protocol(ZDCSyncable)]) 179 | { 180 | id originalValue = self->originalValues[key]; 181 | 182 | // Several possibilities: 183 | // 184 | // - If obj was added, then originalValue will be ZDCNull. 185 | // If this is the case, we should not add to refs. 186 | // 187 | // - If obj was swapped out, then originalValue will be some other obj. 188 | // If this is the case, we should not add to refs. 189 | // 190 | // - If obj was simply modified, then originalValue wll be the same as obj. 191 | // And only then should we add a changeset to refs. 192 | 193 | BOOL wasAdded = (originalValue == [ZDCNull null]); 194 | BOOL wasSwapped = originalValue && (originalValue != obj); 195 | 196 | if (!wasAdded && !wasSwapped) 197 | { 198 | NSDictionary *obj_changeset = [(id)obj peakChangeset]; 199 | if (obj_changeset == nil) 200 | { 201 | BOOL wasModified = originalValue != nil; 202 | if (wasModified) { 203 | obj_changeset = @{}; 204 | } 205 | } 206 | 207 | if (obj_changeset) { 208 | AddRef(key, obj_changeset); 209 | } 210 | } 211 | } 212 | }]; 213 | 214 | if (originalValues.count > 0) 215 | { 216 | // changeset: { 217 | // values: { 218 | // key: oldValue, ... 219 | // }, 220 | // ... 221 | // } 222 | 223 | NSMutableDictionary *values = [NSMutableDictionary dictionaryWithCapacity:originalValues.count]; 224 | 225 | [originalValues enumerateKeysAndObjectsUsingBlock:^(id key, id originalValue, BOOL *stop) { 226 | 227 | if (refs[key]) { 228 | values[key] = [ZDCRef ref]; 229 | } 230 | else if ([originalValue conformsToProtocol:@protocol(NSCopying)]) { 231 | values[key] = [originalValue copy]; 232 | } 233 | else { 234 | values[key] = originalValue; 235 | } 236 | }]; 237 | 238 | changeset[kChangeset_values] = values; 239 | } 240 | 241 | return changeset; 242 | } 243 | 244 | /** 245 | * See ZDCSyncable.h for method description. 246 | */ 247 | - (nullable NSDictionary *)changeset 248 | { 249 | NSDictionary *changeset = [self _changeset]; 250 | [self clearChangeTracking]; 251 | 252 | return changeset; 253 | } 254 | 255 | /** 256 | * See ZDCSyncable.h for method description. 257 | */ 258 | - (nullable NSDictionary *)peakChangeset 259 | { 260 | return [self _changeset]; 261 | } 262 | 263 | - (BOOL)isMalformedChangeset:(NSDictionary *)changeset 264 | { 265 | if (changeset.count == 0) { 266 | return NO; 267 | } 268 | 269 | // changeset: { 270 | // refs: { 271 | // : , ... 272 | // }, 273 | // values: { 274 | // : , ... 275 | // } 276 | // } 277 | 278 | { // refs 279 | 280 | NSDictionary *changeset_refs = changeset[kChangeset_refs]; 281 | if (changeset_refs) 282 | { 283 | if (![changeset_refs isKindOfClass:[NSDictionary class]]) { 284 | return YES; 285 | } 286 | 287 | for (id obj in [(NSDictionary *)changeset_refs objectEnumerator]) 288 | { 289 | if (![obj isKindOfClass:[NSDictionary class]]) { 290 | return YES; 291 | } 292 | } 293 | } 294 | } 295 | { // values 296 | 297 | NSDictionary *changeset_values = changeset[kChangeset_values]; 298 | if (changeset_values) 299 | { 300 | if (![changeset_values isKindOfClass:[NSDictionary class]]) { 301 | return YES; 302 | } 303 | 304 | for (id key in (NSDictionary *)changeset_values) 305 | { 306 | if (![key isKindOfClass:[NSString class]]) { 307 | return YES; 308 | } 309 | } 310 | } 311 | } 312 | 313 | // Looks good (not malformed) 314 | return NO; 315 | } 316 | 317 | - (NSError *)_undo:(NSDictionary *)changeset 318 | { 319 | // Important: `isMalformedChangeset:` must be called before invoking this method. 320 | 321 | NSDictionary *changeset_refs = changeset[kChangeset_refs]; 322 | if (changeset_refs.count > 0) 323 | { 324 | for (id key in changeset_refs) 325 | { 326 | NSDictionary *obj_changeset = changeset_refs[key]; 327 | 328 | id obj = [self valueForKey:key]; 329 | 330 | if ([obj conformsToProtocol:@protocol(ZDCSyncable)]) 331 | { 332 | NSError *error = [obj performUndo:obj_changeset]; 333 | if (error) 334 | { 335 | return error; 336 | } 337 | } 338 | else 339 | { 340 | return [self mismatchedChangeset]; 341 | } 342 | } 343 | } 344 | 345 | NSDictionary *changeset_values = changeset[kChangeset_values]; 346 | if (changeset_values.count > 0) 347 | { 348 | for (id key in changeset_values) 349 | { 350 | id oldValue = changeset_values[key]; 351 | 352 | if (oldValue == [ZDCNull null]) { 353 | [self setValue:nil forKey:key]; 354 | } 355 | else if (oldValue != [ZDCRef ref]) { 356 | [self setValue:oldValue forKey:key]; 357 | } 358 | } 359 | } 360 | 361 | return nil; 362 | } 363 | 364 | /** 365 | * See ZDCSyncable.h for method description. 366 | */ 367 | - (nullable NSDictionary *)undo:(NSDictionary *)changeset error:(NSError **)errPtr 368 | { 369 | NSError *error = [self performUndo:changeset]; 370 | if (error) 371 | { 372 | if (errPtr) *errPtr = error; 373 | return nil; 374 | } 375 | else 376 | { 377 | // Undo successful - generate redo changeset 378 | NSDictionary *reverseChangeset = [self changeset]; 379 | 380 | if (errPtr) *errPtr = nil; 381 | return (reverseChangeset ?: @{}); // don't return nil without error 382 | } 383 | } 384 | 385 | /** 386 | * See ZDCSyncable.h for method description. 387 | */ 388 | - (nullable NSError *)performUndo:(NSDictionary *)changeset 389 | { 390 | if (self.isImmutable) { 391 | @throw [self immutableException]; 392 | } 393 | 394 | if ([self hasChanges]) 395 | { 396 | // You cannot invoke this method if the object currently has changes. 397 | // The code doesn't know what you want to happen. 398 | // Are you asking us to throw away the current changes ? 399 | // Are you expecting us to magically merge everything ? 400 | return [self hasChangesError]; 401 | } 402 | 403 | if ([self isMalformedChangeset:changeset]) 404 | { 405 | return [self malformedChangesetError]; 406 | } 407 | 408 | NSError *error = [self _undo:changeset]; 409 | if (error) 410 | { 411 | // Abandon botched undo attempt - revert to original state 412 | [self rollback]; 413 | } 414 | 415 | return error; 416 | } 417 | 418 | /** 419 | * See ZDCSyncable.h for method description. 420 | */ 421 | - (void)rollback 422 | { 423 | NSDictionary *changeset = [self changeset]; 424 | if (changeset) 425 | { 426 | [self undo:changeset error:nil]; 427 | } 428 | } 429 | 430 | /** 431 | * See ZDCSyncable.h for method description. 432 | */ 433 | - (nullable NSDictionary *)mergeChangesets:(NSArray *)orderedChangesets 434 | error:(NSError *_Nullable *_Nullable)errPtr 435 | { 436 | NSError *error = [self importChangesets:orderedChangesets]; 437 | if (error) 438 | { 439 | if (errPtr) *errPtr = error; 440 | return nil; 441 | } 442 | else 443 | { 444 | NSDictionary *mergedChangeset = [self changeset]; 445 | 446 | if (errPtr) *errPtr = nil; 447 | return (mergedChangeset ?: @{}); // don't return nil without error 448 | } 449 | } 450 | 451 | /** 452 | * See ZDCSyncable.h for method description. 453 | */ 454 | - (nullable NSError *)importChangesets:(NSArray *)orderedChangesets 455 | { 456 | if (self.isImmutable) { 457 | @throw [self immutableException]; 458 | } 459 | 460 | if ([self hasChanges]) 461 | { 462 | // You cannot invoke this method if the object currently has changes. 463 | // The code doesn't know what you want to happen. 464 | // Are you asking us to throw away the current changes ? 465 | // Are you expecting us to magically merge everything ? 466 | return [self hasChangesError]; 467 | } 468 | 469 | // Check for malformed changesets. 470 | // It's better to detect this early on, before we start modifying the object. 471 | // 472 | for (NSDictionary *changeset in orderedChangesets) 473 | { 474 | if ([self isMalformedChangeset:changeset]) 475 | { 476 | return [self malformedChangesetError]; 477 | } 478 | } 479 | 480 | if (orderedChangesets.count == 0) { 481 | return nil; 482 | } 483 | 484 | NSError *result_error = nil; 485 | NSMutableArray *changesets_redo = [NSMutableArray arrayWithCapacity:orderedChangesets.count]; 486 | 487 | for (NSDictionary *changeset in [orderedChangesets reverseObjectEnumerator]) 488 | { 489 | result_error = [self _undo:changeset]; 490 | if (result_error) 491 | { 492 | // Abort botched attempt - Revert to original state (before current `_undo:`) 493 | [self rollback]; 494 | 495 | // We still need to revert previous `_undo:` calls 496 | break; 497 | } 498 | else 499 | { 500 | NSDictionary *redo = [self changeset]; 501 | if (redo) { 502 | [changesets_redo addObject:redo]; 503 | } 504 | } 505 | } 506 | 507 | for (NSDictionary *redo in [changesets_redo reverseObjectEnumerator]) 508 | { 509 | NSError *error = [self _undo:redo]; 510 | if (error) 511 | { 512 | // Not much we can do here - we're in a bad state 513 | if (result_error == nil) { 514 | result_error = error; 515 | } 516 | break; 517 | } 518 | } 519 | 520 | return result_error; 521 | } 522 | 523 | /** 524 | * See ZDCSyncable.h for method description. 525 | */ 526 | - (nullable NSDictionary *)mergeCloudVersion:(id)inCloudVersion 527 | withPendingChangesets:(NSArray *)pendingChangesets 528 | error:(NSError **)errPtr 529 | { 530 | if (self.isImmutable) { 531 | @throw [self immutableException]; 532 | } 533 | 534 | if ([self hasChanges]) 535 | { 536 | // You cannot invoke this method if the object currently has changes. 537 | // The code doesn't know what you want to happen. 538 | // Are you asking us to throw away the current changes ? 539 | // Are you expecting us to magically merge everything ? 540 | if (errPtr) *errPtr = [self hasChangesError]; 541 | return nil; 542 | } 543 | 544 | // Check for malformed changesets. 545 | // It's better to detect this early on, before we start modifying the object. 546 | // 547 | for (NSDictionary *changeset in pendingChangesets) 548 | { 549 | if ([self isMalformedChangeset:changeset]) 550 | { 551 | if (errPtr) *errPtr = [self malformedChangesetError]; 552 | return nil; 553 | } 554 | } 555 | 556 | if (![inCloudVersion isKindOfClass:[self class]]) 557 | { 558 | if (errPtr) *errPtr = [self incorrectObjectClass]; 559 | return nil; 560 | } 561 | ZDCRecord *cloudVersion = (ZDCRecord *)inCloudVersion; 562 | 563 | // Step 1 of 4: 564 | // 565 | // We need to determine which keys have been changed locally, and what the original versions were. 566 | // We'll need this information when comparing to the cloudVersion. 567 | 568 | NSMutableDictionary *merged_originalValues = [NSMutableDictionary dictionary]; 569 | 570 | for (NSDictionary *changeset in pendingChangesets) 571 | { 572 | NSDictionary *changeset_originalValues = changeset[kChangeset_values]; 573 | 574 | [changeset_originalValues enumerateKeysAndObjectsUsingBlock: 575 | ^(NSString *key, id oldValue, BOOL *stop) 576 | { 577 | if (merged_originalValues[key] == nil) 578 | { 579 | merged_originalValues[key] = oldValue; 580 | } 581 | }]; 582 | } 583 | 584 | // Step 2 of 4: 585 | // 586 | // Next, we're going to enumerate what values are in the cloud. 587 | // This will tell us what was added & modified by remote devices. 588 | 589 | BOOL (^IsEqualOrBothNil)(id, id) = ^BOOL (id _Nullable objA, id _Nullable objB) { 590 | 591 | if (objA == nil) 592 | { 593 | return (objB == nil); 594 | } 595 | else if (objB == nil) 596 | { 597 | return NO; 598 | } 599 | else 600 | { 601 | return [objA isEqual:objB]; 602 | } 603 | }; 604 | 605 | [cloudVersion enumeratePropertiesWithBlock:^(NSString *key, id cloudValue, BOOL *stop) { 606 | 607 | id currentLocalValue = [self valueForKey:key]; 608 | id originalLocalValue = merged_originalValues[key]; 609 | 610 | BOOL modifiedValueLocally = (originalLocalValue != nil); 611 | if (originalLocalValue == [ZDCNull null]) { 612 | originalLocalValue = nil; 613 | } 614 | 615 | if (!modifiedValueLocally && 616 | [currentLocalValue conformsToProtocol:@protocol(ZDCSyncable)] && 617 | [cloudValue conformsToProtocol:@protocol(ZDCSyncable)]) 618 | { 619 | // continue - handled by refs 620 | return; // from block 621 | } 622 | 623 | BOOL mergeRemoteValue = NO; 624 | 625 | if (!IsEqualOrBothNil(cloudValue, currentLocalValue)) // remote & (current) local values differ 626 | { 627 | if (modifiedValueLocally) 628 | { 629 | if (IsEqualOrBothNil(cloudValue, originalLocalValue)) { 630 | // modified by local only 631 | } 632 | else { 633 | mergeRemoteValue = YES; // added/modified by local & remote - remote wins 634 | } 635 | } 636 | else // we have not modified the value locally 637 | { 638 | mergeRemoteValue = YES; // added/modified by remote 639 | } 640 | } 641 | else // remote & local values match 642 | { 643 | if (modifiedValueLocally) // we've modified the value locally 644 | { 645 | // Possible future optimization. 646 | // There's no need to push this particular change since cloud already has it. 647 | } 648 | } 649 | 650 | if (mergeRemoteValue) 651 | { 652 | [self setValue:cloudValue forKey:key]; 653 | } 654 | }]; 655 | 656 | // Step 3 of 4: 657 | // 658 | // Next we need to determine if any values were deleted by remote devices. 659 | { 660 | NSMutableSet *baseKeys = [self.monitoredProperties mutableCopy]; 661 | 662 | [merged_originalValues enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { 663 | 664 | if (obj == [ZDCNull null]) // Null => we added this tuple. 665 | [baseKeys removeObject:key]; // So it's not part of the set the cloud is expected to have. 666 | else 667 | [baseKeys addObject:key]; // For items that we may have deleted (no longer in [self allKeys]) 668 | }]; 669 | 670 | for (NSString *key in baseKeys) 671 | { 672 | id remoteValue = [cloudVersion valueForKey:key]; 673 | if (remoteValue == nil) 674 | { 675 | // The remote key/value pair was deleted 676 | 677 | [self setValue:nil forKey:key]; 678 | } 679 | } 680 | } 681 | 682 | // Step 4 of 4: 683 | // 684 | // Merge the ZDCSyncable properties 685 | 686 | NSMutableSet *refs = [NSMutableSet set]; 687 | 688 | for (NSDictionary *changeset in pendingChangesets) 689 | { 690 | NSDictionary *changeset_refs = changeset[kChangeset_refs]; 691 | 692 | for (NSString *key in changeset_refs) 693 | { 694 | if (merged_originalValues[key] == nil) 695 | { 696 | [refs addObject:key]; 697 | } 698 | } 699 | } 700 | 701 | NSError *err = nil; 702 | 703 | for (NSString *key in refs) 704 | { 705 | id localRef = [self valueForKey:key]; 706 | id cloudRef = [cloudVersion valueForKey:key]; 707 | 708 | if ([localRef conformsToProtocol:@protocol(ZDCSyncable)] && 709 | [cloudRef conformsToProtocol:@protocol(ZDCSyncable)]) 710 | { 711 | NSMutableArray *pendingChangesets_ref = [NSMutableArray arrayWithCapacity:pendingChangesets.count]; 712 | 713 | for (NSDictionary *changeset in pendingChangesets) 714 | { 715 | NSDictionary *changeset_refs = changeset[kChangeset_refs]; 716 | NSDictionary *changeset_ref = changeset_refs[key]; 717 | 718 | if (changeset_ref) 719 | { 720 | [pendingChangesets_ref addObject:changeset_ref]; 721 | } 722 | } 723 | 724 | NSError *subMergeErr = nil; 725 | [localRef mergeCloudVersion: cloudRef 726 | withPendingChangesets: pendingChangesets_ref 727 | error: &subMergeErr]; 728 | 729 | if (subMergeErr && !err) { 730 | err = subMergeErr; 731 | } 732 | } 733 | } 734 | 735 | if (errPtr) *errPtr = err; 736 | if (err) { 737 | return nil; 738 | } 739 | else { 740 | return ([self changeset] ?: @{}); 741 | } 742 | } 743 | 744 | @end 745 | -------------------------------------------------------------------------------- /ZDCSyncable/ZDCSet.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import "ZDCObject.h" 7 | #import "ZDCSyncable.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | /** 12 | * ZDCSet tracks changes to a set. 13 | * 14 | * It's designed act like a mutable set. 15 | * 16 | * In addition to its core functionality, it also provides the following set of features: 17 | * - it can be made immutable (via `-[ZDCObject makeImmutable]` method) 18 | * - it implements the ZDCSyncable protocol and thus: 19 | * - it tracks all changes and can provide a changeset (which encodes the changes info) 20 | * - it supports undo & redo 21 | * - it supports merge operations 22 | */ 23 | NS_SWIFT_NAME(ZDCSet_ObjC) 24 | @interface ZDCSet : ZDCObject 25 | 26 | /** 27 | * Creates an empty set 28 | */ 29 | - (instancetype)init; 30 | 31 | /** 32 | * Creates a set initialized with the elements from the given array. 33 | */ 34 | - (instancetype)initWithArray:(nullable NSArray *)array; 35 | 36 | /** 37 | * Creates a set initialized with the elements from the given array. 38 | * 39 | * @param array 40 | * The array to initialize the set with. 41 | * 42 | * @param copyItems 43 | * If set to YES, the values will be copied when storing in the ZDCDictionary. 44 | * This means the values will need to support the NSCopying protocol. 45 | */ 46 | - (instancetype)initWithArray:(nullable NSArray *)array copyItems:(BOOL)copyItems; 47 | 48 | /** 49 | * Creates a new set by copying the given set. 50 | * 51 | * @note To initialize from another ZDCSet, you can either copy the original, 52 | * or use this method combined with `-[ZDCSet rawSet]` to provide the parameter. 53 | */ 54 | - (instancetype)initWithSet:(nullable NSSet *)set; 55 | 56 | /** 57 | * Creates a new set by copying the given set. 58 | * 59 | * @note To initialize from another ZDCSet, you can either copy the original, 60 | * or use this method combined with `-[ZDCSet rawSet]` to provide the parameter. 61 | * 62 | * @param set 63 | * The set to copy. 64 | * 65 | * @param copyItems 66 | * If set to YES, the values will be copied when storing in the ZDCDictionary. 67 | * This means the values will need to support the NSCopying protocol. 68 | */ 69 | - (instancetype)initWithSet:(nullable NSSet *)set copyItems:(BOOL)copyItems; 70 | 71 | #pragma mark Raw 72 | 73 | /** 74 | * Returns a reference to the underlying NSSet that the ZDCSet instance is wrapping. 75 | * 76 | * @note The returned value is a copy of the underlying NSMutableSet. 77 | * Thus changes to the ZDCSet will not be reflected in the returned value. 78 | */ 79 | @property (nonatomic, copy, readonly) NSSet *rawSet; 80 | 81 | #pragma mark Reading 82 | 83 | /** 84 | * The number of items stored in the set. 85 | */ 86 | @property (nonatomic, readonly) NSUInteger count; 87 | 88 | /** 89 | * Returns YES if the object is included in the set. 90 | */ 91 | - (BOOL)containsObject:(ObjectType)anObject; 92 | 93 | #pragma mark Writing 94 | 95 | /** 96 | * Adds the given object to the set (if it's not already included). 97 | */ 98 | - (void)addObject:(ObjectType)object; 99 | 100 | /** 101 | * Removes the given object from the set (if it's currently included). 102 | */ 103 | - (void)removeObject:(ObjectType)object; 104 | 105 | /** 106 | * Removes all objects from the set. 107 | * Afterwards the set will be empty. 108 | */ 109 | - (void)removeAllObjects; 110 | 111 | #pragma mark Enumeration 112 | 113 | /** 114 | * Enumerates the objects within the set. 115 | * The enumeration is performed in no particular order. 116 | */ 117 | - (void)enumerateObjectsUsingBlock:(void (^)(ObjectType obj, BOOL *stop))block; 118 | 119 | #pragma mark Equality 120 | 121 | /** 122 | * Returns YES if `another` is of class ZDCSet, 123 | * and the receiver & another contain the same objects. 124 | * 125 | * This method corresponds to `[NSSet isEqualToSet:]`. 126 | * 127 | * @note It does NOT take into account the changeset of either ZDCSet instance. 128 | */ 129 | - (BOOL)isEqual:(nullable id)another; 130 | 131 | /** 132 | * Returns YES if the receiver and `another` contain the same objects. 133 | * 134 | * This method corresponds to `[NSSet isEqualToSet:]`. 135 | * 136 | * @note It does NOT take into account the changeset of either ZDCSet instance. 137 | */ 138 | - (BOOL)isEqualToSet:(nullable ZDCSet *)another; 139 | 140 | @end 141 | 142 | NS_ASSUME_NONNULL_END 143 | -------------------------------------------------------------------------------- /ZDCSyncable/ZDCSyncable.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | @import Foundation; 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | /** 11 | * The ZDCSyncable protocol defines the common methods for: 12 | * - tracking changes 13 | * - performing undo & redo 14 | * - merging changes from external sources 15 | */ 16 | NS_SWIFT_NAME(ZDCSyncable_ObjC) 17 | @protocol ZDCSyncable 18 | 19 | /** 20 | * Returns whether or not there are any changes to the object. 21 | */ 22 | @property (nonatomic, readonly) BOOL hasChanges; 23 | 24 | /** 25 | * Resets the hasChanges property to false, and clears all internal change tracking information. 26 | * Use this to wipe the slate, and restart change tracking from the current state. 27 | */ 28 | - (void)clearChangeTracking; 29 | 30 | /** 31 | * Returns a changeset that contains information about changes that were made to the object. 32 | * 33 | * This changeset can then be used to undo the changes (via the `undo::` method). 34 | * If syncing the object to the cloud, this changeset may be needed to properly merge local & remote changes. 35 | * 36 | * The changeset will inclue all changes since the last time either 37 | * `changeset` or `[ZDCObject clearChangeTracking]` was called. 38 | * 39 | * @note 40 | * This method is the equivalent of calling `peakChangeset` 41 | * followed by `[ZDCObject clearChangeTracking]`. 42 | * 43 | * @note 44 | * If you simply want to know if an object has changes, use the `[ZDCObject hasChanges]` property. 45 | * 46 | * @return 47 | * A changeset dictionary, or nil if there are no changes. 48 | */ 49 | - (nullable NSDictionary *)changeset; 50 | 51 | /** 52 | * Returns the current changeset without clearing the changes from the object. 53 | * This is primarily used for debugging. 54 | * 55 | * @note 56 | * If you simply want to know if an object has changes, use the `[ZDCObject hasChanges]` property. 57 | * 58 | * @return 59 | * A changeset dictionary, or nil if there are no changes. 60 | */ 61 | - (nullable NSDictionary *)peakChangeset; 62 | 63 | /** 64 | * Moves the state of the object backwards in time, undoing the changes represented in the changeset. 65 | * 66 | * If an error occurs when attempting to undo the changes, then the undo attempt is aborted, 67 | * and the previous state of the object will be restored. 68 | * 69 | * @note 70 | * This method is the equivalent of calling `performUndo:` 71 | * followed by `changeset`, and returning that changeset. 72 | * 73 | * @param changeset 74 | * A valid changeset previously returned via the `changeset` method. 75 | * 76 | * @param errPtr 77 | * If an error occurs, the error can be returned via this parameter. 78 | * 79 | * @return 80 | * A changeset dictionary if the undo was successful (which can be used to redo the changes). 81 | * Otherwise returns nil, and sets the errPtr to an error object explaining what went wrong. 82 | */ 83 | - (nullable NSDictionary *)undo:(NSDictionary *)changeset error:(NSError *_Nullable *_Nullable)errPtr; 84 | 85 | /** 86 | * Moves the state of the object backwards in time, undoing the changes represented in the changeset. 87 | * 88 | * If an error occurs when attempting to undo the changes, then the undo attempt is aborted, 89 | * and the previous state of the object will be restored. 90 | * 91 | * @param changeset 92 | * A valid changeset previously returned via the `changeset` method. 93 | * 94 | * @return 95 | * Returns nil on success, otherwise returns an error explaining what went wrong. 96 | */ 97 | - (nullable NSError *)performUndo:(NSDictionary *)changeset; 98 | 99 | /** 100 | * Performs an undo for all changes that have occurred since the last time either 101 | * `changeset` or `[ZDCObject clearChangeTracking]` was called. 102 | */ 103 | - (void)rollback; 104 | 105 | /** 106 | * This method is used to merge multiple changesets. 107 | * 108 | * You pass in an ordered list of changesets, and when the method completes: 109 | * - the state of the object is the same as it was before 110 | * - a changeset is returned which represents a consolidated version of the given list 111 | * 112 | * @note 113 | * This method is the equivalent of calling `importChangesets:` 114 | * followed by `changeset`, and returning that changeset. 115 | * 116 | * @param orderedChangesets 117 | * An ordered list of changesets, with oldest at index 0. 118 | * 119 | * @param errPtr 120 | * If an error occurs, the error can be returned via this parameter. 121 | * 122 | * @return 123 | * On success, returns a changeset dictionary which represents a consolidated version of the given list. 124 | * Otherwise returns nil, and sets the errPtr to an error object explaining what went wrong. 125 | */ 126 | - (nullable NSDictionary *)mergeChangesets:(NSArray *)orderedChangesets 127 | error:(NSError *_Nullable *_Nullable)errPtr; 128 | 129 | /** 130 | * This method is used to merge multiple changesets. 131 | * 132 | * You pass in an ordered list of changesets, and when the method completes: 133 | * - the state of the object is the same as it was before 134 | * - but calling `hasChanges` will now return YES 135 | * - and calling `changeset` will now return a merged changeset 136 | * 137 | * @param orderedChangesets 138 | * An ordered list of changesets, with oldest at index 0. 139 | */ 140 | - (nullable NSError *)importChangesets:(NSArray *)orderedChangesets; 141 | 142 | /** 143 | * @return 144 | * On success, returns a changeset dictionary that can be used to undo the changes. 145 | * On failure, returns nil and sets the errPtr. 146 | */ 147 | - (nullable NSDictionary *)mergeCloudVersion:(id)cloudVersion 148 | withPendingChangesets:(nullable NSArray *)pendingChangesets 149 | error:(NSError *_Nullable *_Nullable)errPtr; 150 | 151 | @end 152 | 153 | NS_ASSUME_NONNULL_END 154 | -------------------------------------------------------------------------------- /ZDCSyncable/ZDCSyncableObjC.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ZDCSyncable 3 | * https://github.com/4th-ATechnologies/ZDCSyncable 4 | **/ 5 | 6 | #import 7 | 8 | //! Project version number 9 | FOUNDATION_EXPORT double ZDCSyncableObjCVersionNumber; 10 | 11 | //! Project version string 12 | FOUNDATION_EXPORT const unsigned char ZDCSyncableObjCVersionString[]; 13 | 14 | // In this header, you should import all the public headers of your 15 | // framework using statements like #import 16 | 17 | #import "ZDCSyncable.h" 18 | 19 | #import "ZDCObject.h" 20 | #import "ZDCRecord.h" 21 | #import "ZDCDictionary.h" 22 | #import "ZDCOrderedDictionary.h" 23 | #import "ZDCSet.h" 24 | #import "ZDCOrderedSet.h" 25 | #import "ZDCArray.h" 26 | 27 | #import "ZDCObjectSubclass.h" 28 | #import "ZDCOrder.h" 29 | -------------------------------------------------------------------------------- /ZDCSyncableObjC.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ZDCSyncableObjC" 3 | s.version = "1.0.1" 4 | s.summary = "Undo, redo & merge capabilities for plain objects in Objective-C." 5 | s.homepage = "https://github.com/4th-ATechnologies/ZDCSyncableObjC" 6 | s.license = 'MIT' 7 | 8 | s.author = { 9 | "Robbie Hanson" => "robbiehanson@deusty.com" 10 | } 11 | s.source = { 12 | :git => "https://github.com/4th-ATechnologies/ZDCSyncableObjC.git", 13 | :tag => s.version.to_s 14 | } 15 | 16 | s.osx.deployment_target = '10.10' 17 | s.ios.deployment_target = '10.0' 18 | s.tvos.deployment_target = '10.0' 19 | 20 | s.source_files = 'ZDCSyncable/*.{h,m}', 'ZDCSyncable/{Internal,Utilities}/*.{h,m}' 21 | s.private_header_files = 'ZDCSyncable/Internal/*.h' 22 | 23 | end 24 | -------------------------------------------------------------------------------- /ZDCSyncableObjC.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ZDCSyncableObjC.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ZDCSyncable_iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /ZDCSyncable_macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2019 4th-A Technologies. All rights reserved. 23 | 24 | 25 | -------------------------------------------------------------------------------- /ZDCSyncable_tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | --------------------------------------------------------------------------------