)key;
122 |
123 | #pragma mark ENUMERATION
124 |
125 | - (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block;
126 | - (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id key, id obj, BOOL *stop))block;
127 |
128 | #pragma mark KEYS & VALUES
129 |
130 | - (NSArray *)allKeys;
131 | - (NSArray *)allValues;
132 |
133 | @end
134 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | PlistModel
2 | ==========
3 |
4 | A Class For Easily Interacting With Plists as Objects via Automatically Set Properties
5 |
6 | Version 1.0.0
7 | Last Updated: 20 May 2014
8 |
9 | ###What is it, and why do I need it?
10 |
11 | PlistModel was created to have interaction with Plists be as simple and pleasant as possible. Sometimes a project requires persistance that just begs to be stored in a Plist. Whether you need to generate mutable Plists dynamically, or read Plists from the main bundle, this class makes interaction simple and painless.
12 |
13 | ###Features
14 |
15 | - Automatically populates Plist values into matching properties at runtime.
16 | - Works with bundled Plists, or creates new ones automatically
17 | - Automatically saves
18 | - Background methods to keep UI snappy
19 | - Smart saving only writes files if dirty
20 |
21 | ###Set Up - Using a Custom Plist included in Bundle
22 |
23 | ####Step 1: Set up your Plist
24 |
25 | In `CustomModel.plist`
26 |
27 |
28 | 
29 |
30 |
31 | ####Step 2: Add Corresponding Properties to Subclass
32 |
33 | In `CustomModel.h`
34 |
35 | ```ObjC
36 | #import "PlistModel.h"
37 |
38 | @interface CustomModel : PlistModel
39 |
40 | @property (strong, nonatomic) NSString * stringPropertyKey;
41 | @property (strong, nonatomic) NSDate * datePropertyKey;
42 | @property (strong, nonatomic) NSArray * arrayPropertyKey;
43 | @property (strong, nonatomic) NSDictionary * dictionaryPropertyKey;
44 |
45 | @property int intPropertyKey;
46 | @property BOOL boolPropertyKey;
47 | @property float floatPropertyKey;
48 |
49 | @end
50 | ```
51 |
52 | The logic that connects Plist keys to properties is case insensitive. Do not duplicate keys in your Plist or this may cause errors.
53 |
54 | ####Step 3: Load and Use Plist
55 |
56 | ```ObjC
57 | [CustomModel plistNamed:@"CustomModel" inBackgroundWithBlock:^(PlistModel *plistModel) {
58 |
59 | // Get our custom model from return block
60 | CustomModel * customModel = (CustomModel *)plistModel;
61 |
62 | NSLog(@"\n");
63 | NSLog(@"** CustomModel.plist **");
64 | NSLog(@"CM:StringProperty: %@", customModel.stringPropertyKey);
65 | NSLog(@"CM:DateProperty: %@", customModel.datePropertyKey);
66 | NSLog(@"CM:ArrayProperty: %@", customModel.arrayPropertyKey);
67 | NSLog(@"CM:DictionaryProperty: %@", customModel.dictionaryPropertyKey);
68 | NSLog(@"CM:IntProperty: %i", customModel.intPropertyKey);
69 | NSLog(@"CM:BoolProperty: %@", customModel.boolPropertyKey ? @"YES" : @"NO");
70 | NSLog(@"CM:FloatProperty: %f", customModel.floatPropertyKey);
71 | NSLog(@"\n");
72 |
73 | }];
74 | ```
75 |
76 | The properties are automatically populated at runtime without any additional code. Running in background is optional, but loading files from the directory can sometimes be an expensive operation. Background methods are suggested.
77 |
78 | ###Set Up - Dynamically Created Plist - **MUTABLE**
79 |
80 | ####Step 1: Declare properties you'd like to use in .h
81 |
82 | In `DynamicModel.h`
83 |
84 | ```ObjC
85 | #import "PlistModel.h"
86 |
87 | @interface DynamicModel : PlistModel
88 |
89 | @property (strong, nonatomic) NSString *name;
90 | @property int counter;
91 |
92 | @end
93 |
94 | ```
95 |
96 | ####Step 2: Interact with your Plist:
97 |
98 | ```ObjC
99 | [DynamicModel plistNamed:@"DynamicModel" inBackgroundWithBlock:^(PlistModel *plistModel) {
100 | DynamicModel * dynamicModel = (DynamicModel *)plistModel;
101 | // Will be null on first run
102 | NSLog(@"DynamicModel.name = %@", dynamicModel.name);
103 | NSLog(@"Counter: %i", dynamicModel.counter);
104 | dynamicModel.name = @"Hello World!";
105 | dynamicModel.counter++;
106 | NSLog(@"DynamicModel: %@", dynamicModel);
107 | }];
108 | ```
109 |
110 | If no Plist already exists at the specified name, a new one will be created automatically. PlistModel will save in the background automatically on `dealloc` or you can call save explicitly using `saveInBackgroundWithBlock`.
111 |
112 | ###Set Up: Using the Default Info.plist
113 |
114 | ####Step 1: Call PlistModel w/o Subclassing
115 |
116 | ```ObjC
117 | [PlistModel plistNamed:@"Info" inBackgroundWithBlock:^(PlistModel *plistModel) {
118 | NSLog(@"\n\n\n");
119 | NSLog(@"** Info.plist **");
120 | NSLog(@"Development Region: %@", plistModel.CFBundleDevelopmentRegion);
121 | NSLog(@"Version: %@", plistModel.CFBundleVersion);
122 | NSLog(@"Application requires iPhone environment? %@", plistModel.LSRequiresIPhoneOS ? @"YES" : @"NO");
123 | // Etc ... (see PlistModel.h for full list)
124 | NSLog(@"\n\n\n\n");
125 | }];
126 | ```
127 |
128 | You can find more available properties in `PlistModel.h`
129 |
130 | ##Dynamic Keys
131 |
132 | You can also interact with PlistModel as if it is a mutableDictionary for keys that you might not know ahead of time and thus can't set as properties. For these situations, you can use:
133 |
134 | ```ObjC
135 | instanceOfPlistModel[@"dynamicKey"] = @"dynamicValue";
136 | NSString * dynamicValue = instanceOfPlistModel[@"dynamicKey"];
137 | ```
138 |
139 | ###NOTE:
140 |
141 | 1. Working with values this way will be a touch slower than working with properties.
142 | 2. Keys are case insensitive which means `instanceOfPlistModel[@"foo"]` and `instanceOfPlistModel[@"fOo"]` and `instanceOfPlistModel[@"FOO"]` will all ultimately point to the same address.
143 |
144 |
145 |
146 |
147 |
148 |
--------------------------------------------------------------------------------
/PlistModel.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 801E13931912146700F282BA /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 801E13921912146700F282BA /* Foundation.framework */; };
11 | 801E13951912146700F282BA /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 801E13941912146700F282BA /* CoreGraphics.framework */; };
12 | 801E13971912146700F282BA /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 801E13961912146700F282BA /* UIKit.framework */; };
13 | 801E139D1912146700F282BA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 801E139B1912146700F282BA /* InfoPlist.strings */; };
14 | 801E139F1912146700F282BA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 801E139E1912146700F282BA /* main.m */; };
15 | 801E13A31912146700F282BA /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 801E13A21912146700F282BA /* AppDelegate.m */; };
16 | 801E13A61912146700F282BA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 801E13A41912146700F282BA /* Main.storyboard */; };
17 | 801E13A91912146700F282BA /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 801E13A81912146700F282BA /* ViewController.m */; };
18 | 801E13AB1912146700F282BA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 801E13AA1912146700F282BA /* Images.xcassets */; };
19 | 801E13B21912146700F282BA /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 801E13B11912146700F282BA /* XCTest.framework */; };
20 | 801E13B31912146700F282BA /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 801E13921912146700F282BA /* Foundation.framework */; };
21 | 801E13B41912146700F282BA /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 801E13961912146700F282BA /* UIKit.framework */; };
22 | 801E13BC1912146700F282BA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 801E13BA1912146700F282BA /* InfoPlist.strings */; };
23 | 801E13BE1912146700F282BA /* PlistModelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 801E13BD1912146700F282BA /* PlistModelTests.m */; };
24 | 801E13CA191214CC00F282BA /* PlistModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 801E13C9191214CC00F282BA /* PlistModel.m */; };
25 | 801E13D5191216DB00F282BA /* CustomModel.plist in Resources */ = {isa = PBXBuildFile; fileRef = 801E13D4191216DB00F282BA /* CustomModel.plist */; };
26 | 801E13D819121A2600F282BA /* CustomModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 801E13D719121A2600F282BA /* CustomModel.m */; };
27 | 80AAF3F2192AFBAF00C4FA0C /* DynamicModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 80AAF3F1192AFBAF00C4FA0C /* DynamicModel.m */; };
28 | 80AAF3F5192C312E00C4FA0C /* PlistExample.png in Resources */ = {isa = PBXBuildFile; fileRef = 80AAF3F4192C312E00C4FA0C /* PlistExample.png */; };
29 | /* End PBXBuildFile section */
30 |
31 | /* Begin PBXContainerItemProxy section */
32 | 801E13B51912146700F282BA /* PBXContainerItemProxy */ = {
33 | isa = PBXContainerItemProxy;
34 | containerPortal = 801E13871912146700F282BA /* Project object */;
35 | proxyType = 1;
36 | remoteGlobalIDString = 801E138E1912146700F282BA;
37 | remoteInfo = PlistModel;
38 | };
39 | /* End PBXContainerItemProxy section */
40 |
41 | /* Begin PBXFileReference section */
42 | 801E138F1912146700F282BA /* PlistModel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PlistModel.app; sourceTree = BUILT_PRODUCTS_DIR; };
43 | 801E13921912146700F282BA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
44 | 801E13941912146700F282BA /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
45 | 801E13961912146700F282BA /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
46 | 801E139A1912146700F282BA /* PlistModel-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "PlistModel-Info.plist"; sourceTree = ""; };
47 | 801E139C1912146700F282BA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
48 | 801E139E1912146700F282BA /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
49 | 801E13A01912146700F282BA /* PlistModel-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PlistModel-Prefix.pch"; sourceTree = ""; };
50 | 801E13A11912146700F282BA /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
51 | 801E13A21912146700F282BA /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
52 | 801E13A51912146700F282BA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
53 | 801E13A71912146700F282BA /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; };
54 | 801E13A81912146700F282BA /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; };
55 | 801E13AA1912146700F282BA /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; };
56 | 801E13B01912146700F282BA /* PlistModelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PlistModelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
57 | 801E13B11912146700F282BA /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
58 | 801E13B91912146700F282BA /* PlistModelTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "PlistModelTests-Info.plist"; sourceTree = ""; };
59 | 801E13BB1912146700F282BA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
60 | 801E13BD1912146700F282BA /* PlistModelTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlistModelTests.m; sourceTree = ""; };
61 | 801E13C8191214CC00F282BA /* PlistModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlistModel.h; sourceTree = ""; };
62 | 801E13C9191214CC00F282BA /* PlistModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlistModel.m; sourceTree = ""; };
63 | 801E13D4191216DB00F282BA /* CustomModel.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = CustomModel.plist; sourceTree = ""; };
64 | 801E13D619121A2600F282BA /* CustomModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomModel.h; sourceTree = ""; };
65 | 801E13D719121A2600F282BA /* CustomModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomModel.m; sourceTree = ""; };
66 | 80AAF3F0192AFBAF00C4FA0C /* DynamicModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DynamicModel.h; sourceTree = ""; };
67 | 80AAF3F1192AFBAF00C4FA0C /* DynamicModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DynamicModel.m; sourceTree = ""; };
68 | 80AAF3F4192C312E00C4FA0C /* PlistExample.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = PlistExample.png; sourceTree = ""; };
69 | /* End PBXFileReference section */
70 |
71 | /* Begin PBXFrameworksBuildPhase section */
72 | 801E138C1912146700F282BA /* Frameworks */ = {
73 | isa = PBXFrameworksBuildPhase;
74 | buildActionMask = 2147483647;
75 | files = (
76 | 801E13951912146700F282BA /* CoreGraphics.framework in Frameworks */,
77 | 801E13971912146700F282BA /* UIKit.framework in Frameworks */,
78 | 801E13931912146700F282BA /* Foundation.framework in Frameworks */,
79 | );
80 | runOnlyForDeploymentPostprocessing = 0;
81 | };
82 | 801E13AD1912146700F282BA /* Frameworks */ = {
83 | isa = PBXFrameworksBuildPhase;
84 | buildActionMask = 2147483647;
85 | files = (
86 | 801E13B21912146700F282BA /* XCTest.framework in Frameworks */,
87 | 801E13B41912146700F282BA /* UIKit.framework in Frameworks */,
88 | 801E13B31912146700F282BA /* Foundation.framework in Frameworks */,
89 | );
90 | runOnlyForDeploymentPostprocessing = 0;
91 | };
92 | /* End PBXFrameworksBuildPhase section */
93 |
94 | /* Begin PBXGroup section */
95 | 801E13861912146700F282BA = {
96 | isa = PBXGroup;
97 | children = (
98 | 801E13981912146700F282BA /* PlistModel */,
99 | 801E13B71912146700F282BA /* PlistModelTests */,
100 | 801E13911912146700F282BA /* Frameworks */,
101 | 801E13901912146700F282BA /* Products */,
102 | );
103 | sourceTree = "";
104 | };
105 | 801E13901912146700F282BA /* Products */ = {
106 | isa = PBXGroup;
107 | children = (
108 | 801E138F1912146700F282BA /* PlistModel.app */,
109 | 801E13B01912146700F282BA /* PlistModelTests.xctest */,
110 | );
111 | name = Products;
112 | sourceTree = "";
113 | };
114 | 801E13911912146700F282BA /* Frameworks */ = {
115 | isa = PBXGroup;
116 | children = (
117 | 801E13921912146700F282BA /* Foundation.framework */,
118 | 801E13941912146700F282BA /* CoreGraphics.framework */,
119 | 801E13961912146700F282BA /* UIKit.framework */,
120 | 801E13B11912146700F282BA /* XCTest.framework */,
121 | );
122 | name = Frameworks;
123 | sourceTree = "";
124 | };
125 | 801E13981912146700F282BA /* PlistModel */ = {
126 | isa = PBXGroup;
127 | children = (
128 | 80AAF3F3192C312E00C4FA0C /* Images */,
129 | 801E13C7191214CC00F282BA /* PlistModel */,
130 | 801E13CB1912154E00F282BA /* CustomModel */,
131 | 80AAF3EF192AFB1B00C4FA0C /* DynamicModel */,
132 | 801E13A11912146700F282BA /* AppDelegate.h */,
133 | 801E13A21912146700F282BA /* AppDelegate.m */,
134 | 801E13A41912146700F282BA /* Main.storyboard */,
135 | 801E13A71912146700F282BA /* ViewController.h */,
136 | 801E13A81912146700F282BA /* ViewController.m */,
137 | 801E13AA1912146700F282BA /* Images.xcassets */,
138 | 801E13991912146700F282BA /* Supporting Files */,
139 | );
140 | path = PlistModel;
141 | sourceTree = "";
142 | };
143 | 801E13991912146700F282BA /* Supporting Files */ = {
144 | isa = PBXGroup;
145 | children = (
146 | 801E139A1912146700F282BA /* PlistModel-Info.plist */,
147 | 801E139B1912146700F282BA /* InfoPlist.strings */,
148 | 801E139E1912146700F282BA /* main.m */,
149 | 801E13A01912146700F282BA /* PlistModel-Prefix.pch */,
150 | );
151 | name = "Supporting Files";
152 | sourceTree = "";
153 | };
154 | 801E13B71912146700F282BA /* PlistModelTests */ = {
155 | isa = PBXGroup;
156 | children = (
157 | 801E13BD1912146700F282BA /* PlistModelTests.m */,
158 | 801E13B81912146700F282BA /* Supporting Files */,
159 | );
160 | path = PlistModelTests;
161 | sourceTree = "";
162 | };
163 | 801E13B81912146700F282BA /* Supporting Files */ = {
164 | isa = PBXGroup;
165 | children = (
166 | 801E13B91912146700F282BA /* PlistModelTests-Info.plist */,
167 | 801E13BA1912146700F282BA /* InfoPlist.strings */,
168 | );
169 | name = "Supporting Files";
170 | sourceTree = "";
171 | };
172 | 801E13C7191214CC00F282BA /* PlistModel */ = {
173 | isa = PBXGroup;
174 | children = (
175 | 801E13C8191214CC00F282BA /* PlistModel.h */,
176 | 801E13C9191214CC00F282BA /* PlistModel.m */,
177 | );
178 | path = PlistModel;
179 | sourceTree = "";
180 | };
181 | 801E13CB1912154E00F282BA /* CustomModel */ = {
182 | isa = PBXGroup;
183 | children = (
184 | 801E13D619121A2600F282BA /* CustomModel.h */,
185 | 801E13D719121A2600F282BA /* CustomModel.m */,
186 | 801E13D4191216DB00F282BA /* CustomModel.plist */,
187 | );
188 | name = CustomModel;
189 | sourceTree = "";
190 | };
191 | 80AAF3EF192AFB1B00C4FA0C /* DynamicModel */ = {
192 | isa = PBXGroup;
193 | children = (
194 | 80AAF3F0192AFBAF00C4FA0C /* DynamicModel.h */,
195 | 80AAF3F1192AFBAF00C4FA0C /* DynamicModel.m */,
196 | );
197 | name = DynamicModel;
198 | sourceTree = "";
199 | };
200 | 80AAF3F3192C312E00C4FA0C /* Images */ = {
201 | isa = PBXGroup;
202 | children = (
203 | 80AAF3F4192C312E00C4FA0C /* PlistExample.png */,
204 | );
205 | path = Images;
206 | sourceTree = "";
207 | };
208 | /* End PBXGroup section */
209 |
210 | /* Begin PBXNativeTarget section */
211 | 801E138E1912146700F282BA /* PlistModel */ = {
212 | isa = PBXNativeTarget;
213 | buildConfigurationList = 801E13C11912146800F282BA /* Build configuration list for PBXNativeTarget "PlistModel" */;
214 | buildPhases = (
215 | 801E138B1912146700F282BA /* Sources */,
216 | 801E138C1912146700F282BA /* Frameworks */,
217 | 801E138D1912146700F282BA /* Resources */,
218 | );
219 | buildRules = (
220 | );
221 | dependencies = (
222 | );
223 | name = PlistModel;
224 | productName = PlistModel;
225 | productReference = 801E138F1912146700F282BA /* PlistModel.app */;
226 | productType = "com.apple.product-type.application";
227 | };
228 | 801E13AF1912146700F282BA /* PlistModelTests */ = {
229 | isa = PBXNativeTarget;
230 | buildConfigurationList = 801E13C41912146800F282BA /* Build configuration list for PBXNativeTarget "PlistModelTests" */;
231 | buildPhases = (
232 | 801E13AC1912146700F282BA /* Sources */,
233 | 801E13AD1912146700F282BA /* Frameworks */,
234 | 801E13AE1912146700F282BA /* Resources */,
235 | );
236 | buildRules = (
237 | );
238 | dependencies = (
239 | 801E13B61912146700F282BA /* PBXTargetDependency */,
240 | );
241 | name = PlistModelTests;
242 | productName = PlistModelTests;
243 | productReference = 801E13B01912146700F282BA /* PlistModelTests.xctest */;
244 | productType = "com.apple.product-type.bundle.unit-test";
245 | };
246 | /* End PBXNativeTarget section */
247 |
248 | /* Begin PBXProject section */
249 | 801E13871912146700F282BA /* Project object */ = {
250 | isa = PBXProject;
251 | attributes = {
252 | LastUpgradeCheck = 0510;
253 | ORGANIZATIONNAME = "Logan Wright";
254 | TargetAttributes = {
255 | 801E13AF1912146700F282BA = {
256 | TestTargetID = 801E138E1912146700F282BA;
257 | };
258 | };
259 | };
260 | buildConfigurationList = 801E138A1912146700F282BA /* Build configuration list for PBXProject "PlistModel" */;
261 | compatibilityVersion = "Xcode 3.2";
262 | developmentRegion = English;
263 | hasScannedForEncodings = 0;
264 | knownRegions = (
265 | en,
266 | Base,
267 | );
268 | mainGroup = 801E13861912146700F282BA;
269 | productRefGroup = 801E13901912146700F282BA /* Products */;
270 | projectDirPath = "";
271 | projectRoot = "";
272 | targets = (
273 | 801E138E1912146700F282BA /* PlistModel */,
274 | 801E13AF1912146700F282BA /* PlistModelTests */,
275 | );
276 | };
277 | /* End PBXProject section */
278 |
279 | /* Begin PBXResourcesBuildPhase section */
280 | 801E138D1912146700F282BA /* Resources */ = {
281 | isa = PBXResourcesBuildPhase;
282 | buildActionMask = 2147483647;
283 | files = (
284 | 801E13D5191216DB00F282BA /* CustomModel.plist in Resources */,
285 | 801E13AB1912146700F282BA /* Images.xcassets in Resources */,
286 | 80AAF3F5192C312E00C4FA0C /* PlistExample.png in Resources */,
287 | 801E139D1912146700F282BA /* InfoPlist.strings in Resources */,
288 | 801E13A61912146700F282BA /* Main.storyboard in Resources */,
289 | );
290 | runOnlyForDeploymentPostprocessing = 0;
291 | };
292 | 801E13AE1912146700F282BA /* Resources */ = {
293 | isa = PBXResourcesBuildPhase;
294 | buildActionMask = 2147483647;
295 | files = (
296 | 801E13BC1912146700F282BA /* InfoPlist.strings in Resources */,
297 | );
298 | runOnlyForDeploymentPostprocessing = 0;
299 | };
300 | /* End PBXResourcesBuildPhase section */
301 |
302 | /* Begin PBXSourcesBuildPhase section */
303 | 801E138B1912146700F282BA /* Sources */ = {
304 | isa = PBXSourcesBuildPhase;
305 | buildActionMask = 2147483647;
306 | files = (
307 | 801E13CA191214CC00F282BA /* PlistModel.m in Sources */,
308 | 80AAF3F2192AFBAF00C4FA0C /* DynamicModel.m in Sources */,
309 | 801E13A91912146700F282BA /* ViewController.m in Sources */,
310 | 801E13A31912146700F282BA /* AppDelegate.m in Sources */,
311 | 801E13D819121A2600F282BA /* CustomModel.m in Sources */,
312 | 801E139F1912146700F282BA /* main.m in Sources */,
313 | );
314 | runOnlyForDeploymentPostprocessing = 0;
315 | };
316 | 801E13AC1912146700F282BA /* Sources */ = {
317 | isa = PBXSourcesBuildPhase;
318 | buildActionMask = 2147483647;
319 | files = (
320 | 801E13BE1912146700F282BA /* PlistModelTests.m in Sources */,
321 | );
322 | runOnlyForDeploymentPostprocessing = 0;
323 | };
324 | /* End PBXSourcesBuildPhase section */
325 |
326 | /* Begin PBXTargetDependency section */
327 | 801E13B61912146700F282BA /* PBXTargetDependency */ = {
328 | isa = PBXTargetDependency;
329 | target = 801E138E1912146700F282BA /* PlistModel */;
330 | targetProxy = 801E13B51912146700F282BA /* PBXContainerItemProxy */;
331 | };
332 | /* End PBXTargetDependency section */
333 |
334 | /* Begin PBXVariantGroup section */
335 | 801E139B1912146700F282BA /* InfoPlist.strings */ = {
336 | isa = PBXVariantGroup;
337 | children = (
338 | 801E139C1912146700F282BA /* en */,
339 | );
340 | name = InfoPlist.strings;
341 | sourceTree = "";
342 | };
343 | 801E13A41912146700F282BA /* Main.storyboard */ = {
344 | isa = PBXVariantGroup;
345 | children = (
346 | 801E13A51912146700F282BA /* Base */,
347 | );
348 | name = Main.storyboard;
349 | sourceTree = "";
350 | };
351 | 801E13BA1912146700F282BA /* InfoPlist.strings */ = {
352 | isa = PBXVariantGroup;
353 | children = (
354 | 801E13BB1912146700F282BA /* en */,
355 | );
356 | name = InfoPlist.strings;
357 | sourceTree = "";
358 | };
359 | /* End PBXVariantGroup section */
360 |
361 | /* Begin XCBuildConfiguration section */
362 | 801E13BF1912146800F282BA /* Debug */ = {
363 | isa = XCBuildConfiguration;
364 | buildSettings = {
365 | ALWAYS_SEARCH_USER_PATHS = NO;
366 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
367 | CLANG_CXX_LIBRARY = "libc++";
368 | CLANG_ENABLE_MODULES = YES;
369 | CLANG_ENABLE_OBJC_ARC = YES;
370 | CLANG_WARN_BOOL_CONVERSION = YES;
371 | CLANG_WARN_CONSTANT_CONVERSION = YES;
372 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
373 | CLANG_WARN_EMPTY_BODY = YES;
374 | CLANG_WARN_ENUM_CONVERSION = YES;
375 | CLANG_WARN_INT_CONVERSION = YES;
376 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
377 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
378 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
379 | COPY_PHASE_STRIP = NO;
380 | GCC_C_LANGUAGE_STANDARD = gnu99;
381 | GCC_DYNAMIC_NO_PIC = NO;
382 | GCC_OPTIMIZATION_LEVEL = 0;
383 | GCC_PREPROCESSOR_DEFINITIONS = (
384 | "DEBUG=1",
385 | "$(inherited)",
386 | );
387 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
388 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
389 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
390 | GCC_WARN_UNDECLARED_SELECTOR = YES;
391 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
392 | GCC_WARN_UNUSED_FUNCTION = YES;
393 | GCC_WARN_UNUSED_VARIABLE = YES;
394 | IPHONEOS_DEPLOYMENT_TARGET = 7.1;
395 | ONLY_ACTIVE_ARCH = YES;
396 | SDKROOT = iphoneos;
397 | };
398 | name = Debug;
399 | };
400 | 801E13C01912146800F282BA /* Release */ = {
401 | isa = XCBuildConfiguration;
402 | buildSettings = {
403 | ALWAYS_SEARCH_USER_PATHS = NO;
404 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
405 | CLANG_CXX_LIBRARY = "libc++";
406 | CLANG_ENABLE_MODULES = YES;
407 | CLANG_ENABLE_OBJC_ARC = YES;
408 | CLANG_WARN_BOOL_CONVERSION = YES;
409 | CLANG_WARN_CONSTANT_CONVERSION = YES;
410 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
411 | CLANG_WARN_EMPTY_BODY = YES;
412 | CLANG_WARN_ENUM_CONVERSION = YES;
413 | CLANG_WARN_INT_CONVERSION = YES;
414 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
415 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
416 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
417 | COPY_PHASE_STRIP = YES;
418 | ENABLE_NS_ASSERTIONS = NO;
419 | GCC_C_LANGUAGE_STANDARD = gnu99;
420 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
421 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
422 | GCC_WARN_UNDECLARED_SELECTOR = YES;
423 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
424 | GCC_WARN_UNUSED_FUNCTION = YES;
425 | GCC_WARN_UNUSED_VARIABLE = YES;
426 | IPHONEOS_DEPLOYMENT_TARGET = 7.1;
427 | SDKROOT = iphoneos;
428 | VALIDATE_PRODUCT = YES;
429 | };
430 | name = Release;
431 | };
432 | 801E13C21912146800F282BA /* Debug */ = {
433 | isa = XCBuildConfiguration;
434 | buildSettings = {
435 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
436 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
437 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
438 | GCC_PREFIX_HEADER = "PlistModel/PlistModel-Prefix.pch";
439 | INFOPLIST_FILE = "PlistModel/PlistModel-Info.plist";
440 | PRODUCT_NAME = "$(TARGET_NAME)";
441 | WRAPPER_EXTENSION = app;
442 | };
443 | name = Debug;
444 | };
445 | 801E13C31912146800F282BA /* Release */ = {
446 | isa = XCBuildConfiguration;
447 | buildSettings = {
448 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
449 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
450 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
451 | GCC_PREFIX_HEADER = "PlistModel/PlistModel-Prefix.pch";
452 | INFOPLIST_FILE = "PlistModel/PlistModel-Info.plist";
453 | PRODUCT_NAME = "$(TARGET_NAME)";
454 | WRAPPER_EXTENSION = app;
455 | };
456 | name = Release;
457 | };
458 | 801E13C51912146800F282BA /* Debug */ = {
459 | isa = XCBuildConfiguration;
460 | buildSettings = {
461 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/PlistModel.app/PlistModel";
462 | FRAMEWORK_SEARCH_PATHS = (
463 | "$(SDKROOT)/Developer/Library/Frameworks",
464 | "$(inherited)",
465 | "$(DEVELOPER_FRAMEWORKS_DIR)",
466 | );
467 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
468 | GCC_PREFIX_HEADER = "PlistModel/PlistModel-Prefix.pch";
469 | GCC_PREPROCESSOR_DEFINITIONS = (
470 | "DEBUG=1",
471 | "$(inherited)",
472 | );
473 | INFOPLIST_FILE = "PlistModelTests/PlistModelTests-Info.plist";
474 | PRODUCT_NAME = "$(TARGET_NAME)";
475 | TEST_HOST = "$(BUNDLE_LOADER)";
476 | WRAPPER_EXTENSION = xctest;
477 | };
478 | name = Debug;
479 | };
480 | 801E13C61912146800F282BA /* Release */ = {
481 | isa = XCBuildConfiguration;
482 | buildSettings = {
483 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/PlistModel.app/PlistModel";
484 | FRAMEWORK_SEARCH_PATHS = (
485 | "$(SDKROOT)/Developer/Library/Frameworks",
486 | "$(inherited)",
487 | "$(DEVELOPER_FRAMEWORKS_DIR)",
488 | );
489 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
490 | GCC_PREFIX_HEADER = "PlistModel/PlistModel-Prefix.pch";
491 | INFOPLIST_FILE = "PlistModelTests/PlistModelTests-Info.plist";
492 | PRODUCT_NAME = "$(TARGET_NAME)";
493 | TEST_HOST = "$(BUNDLE_LOADER)";
494 | WRAPPER_EXTENSION = xctest;
495 | };
496 | name = Release;
497 | };
498 | /* End XCBuildConfiguration section */
499 |
500 | /* Begin XCConfigurationList section */
501 | 801E138A1912146700F282BA /* Build configuration list for PBXProject "PlistModel" */ = {
502 | isa = XCConfigurationList;
503 | buildConfigurations = (
504 | 801E13BF1912146800F282BA /* Debug */,
505 | 801E13C01912146800F282BA /* Release */,
506 | );
507 | defaultConfigurationIsVisible = 0;
508 | defaultConfigurationName = Release;
509 | };
510 | 801E13C11912146800F282BA /* Build configuration list for PBXNativeTarget "PlistModel" */ = {
511 | isa = XCConfigurationList;
512 | buildConfigurations = (
513 | 801E13C21912146800F282BA /* Debug */,
514 | 801E13C31912146800F282BA /* Release */,
515 | );
516 | defaultConfigurationIsVisible = 0;
517 | defaultConfigurationName = Release;
518 | };
519 | 801E13C41912146800F282BA /* Build configuration list for PBXNativeTarget "PlistModelTests" */ = {
520 | isa = XCConfigurationList;
521 | buildConfigurations = (
522 | 801E13C51912146800F282BA /* Debug */,
523 | 801E13C61912146800F282BA /* Release */,
524 | );
525 | defaultConfigurationIsVisible = 0;
526 | defaultConfigurationName = Release;
527 | };
528 | /* End XCConfigurationList section */
529 | };
530 | rootObject = 801E13871912146700F282BA /* Project object */;
531 | }
532 |
--------------------------------------------------------------------------------
/PlistModel/PlistModel/PlistModel.m:
--------------------------------------------------------------------------------
1 | //
2 | // PlistModel.m
3 | // PlistModel
4 | //
5 | // Created by Logan Wright on 4/29/14.
6 | // Copyright (c) 2014 Logan Wright. All rights reserved.
7 | //
8 |
9 | /*
10 | Mozilla Public License
11 | Version 2.0
12 | */
13 |
14 | #import "PlistModel.h"
15 | #import
16 |
17 | @interface PlistModel ()
18 |
19 | /*!
20 | The actual representation of our Plist
21 | */
22 | @property (strong, nonatomic) NSMutableDictionary * backingDictionary;
23 |
24 | /*!
25 | The name of our Plist
26 | */
27 | @property (strong, nonatomic) NSString * plistName;
28 | /*!
29 | The path to our Plist in directory or bundle
30 | */
31 | @property (strong, nonatomic) NSString * plistPath;
32 |
33 | /*!
34 | The keyPaths self is currently observing on backingDictionary
35 | */
36 | @property (strong, nonatomic) NSMutableSet * observingKeyPaths;
37 |
38 | /*!
39 | The names of all properties included in class.
40 | */
41 | @property (strong, nonatomic) NSMutableArray * propertyNames;
42 |
43 | /*!
44 | Bundled Plists are immutable, we will use this to save time later (set during 'configurePath')
45 | */
46 | @property BOOL isBundledPlist;
47 |
48 | @end
49 |
50 | @implementation PlistModel
51 |
52 | @synthesize isDirty = _isDirty;
53 |
54 | #pragma mark INITIALIZERS
55 |
56 | + (instancetype) plistNamed:(NSString *)plistName {
57 | return [[self alloc]initWithPlistName:plistName];
58 | }
59 |
60 | + (void) plistNamed:(NSString *)plistName inBackgroundWithBlock:(void(^)(PlistModel * plistModel))completion {
61 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
62 |
63 | PlistModel * newModel = [[self alloc]initWithPlistName:plistName];
64 |
65 | dispatch_sync(dispatch_get_main_queue(), ^{
66 | completion(newModel);
67 | });
68 |
69 | });
70 | }
71 |
72 | - (instancetype)init {
73 | self = [super init];
74 | if (self) {
75 |
76 | // See if init was called properly, if not, we'll default to a plist with the class name
77 | if (!_plistName) {
78 |
79 | // If no plist is named, set it to class name:
80 | _plistName = NSStringFromClass(self.class);
81 |
82 | if ([_plistName isEqual:@"PlistModel"]) {
83 | // If Class is plistModel, then is not subclassed. Set to "Info"
84 | _plistName = @"Info";
85 | }
86 |
87 | }
88 |
89 | // To make sure everything is set properly
90 | self = [self initWithPlistName:_plistName];
91 |
92 | }
93 | return self;
94 | }
95 |
96 | - (instancetype) initWithPlistName:(NSString *)plistName {
97 | self = [super init];
98 | if (self) {
99 |
100 | /*
101 | MUST BE EXECUTED IN THIS ORDER!
102 | */
103 |
104 | // Step 1: Establish out plistName
105 | _plistName = plistName;
106 |
107 | // Step 2: Set our Path
108 | [self configurePath];
109 |
110 | // Step 3: Set our properties as Keys in _propertyNames
111 | [self configurePropertyNames];
112 |
113 | // Step 4: Fetch PLIST & set to our backing dictionary
114 | [self configureBackingDictionary];
115 |
116 | // Step 5: Set Properties from PlistDictionary (_backingDictionary) & populate corresponding dictionaryKeys with their property in _propertyNames
117 | [self populateProperties];
118 |
119 | // Step 6: Start observing
120 | /*
121 | We will only observe _backingDictionary because all properties are eventually updated in the dictionary. In this way we can always know if there is a core change before saving. We must add KVO observers in `setObject` to assure interaction w/ keys is not overlooked.
122 |
123 | getPlist(above) will set _isBundledPlist property, should be set at this point
124 | */
125 | if (!_isBundledPlist) {
126 | // Don't observe bundled plists because they're immutable. Dirty is irrelevant.
127 | _observingKeyPaths = [NSMutableSet setWithArray:_backingDictionary.allKeys];
128 | [_observingKeyPaths enumerateObjectsUsingBlock:^(NSString * keyPath, BOOL *stop) {
129 | [_backingDictionary addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
130 | }];
131 | }
132 |
133 | }
134 | return self;
135 | }
136 |
137 | #pragma mark INIT HELPERS
138 |
139 | - (void) configurePath {
140 | NSString *path = [[NSBundle mainBundle] pathForResource:_plistName ofType: @"plist"];
141 |
142 | if (path) {
143 | _isBundledPlist = YES;
144 | }
145 | else {
146 |
147 | // There isn't already a plist, make one
148 | NSString * appendedPlistName = [NSString stringWithFormat:@"%@.plist", _plistName];
149 |
150 | // Fetch out plist & set to new path
151 | NSArray *pathArray;
152 | NSString *documentsDirectory;
153 | #if TARGET_OS_IPHONE
154 | pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
155 | documentsDirectory = [pathArray objectAtIndex:0];
156 | path = [documentsDirectory stringByAppendingPathComponent:appendedPlistName];
157 | #elif TARGET_OS_MAC
158 | NSString *name = [[NSBundle mainBundle] infoDictionary][(NSString *)kCFBundleNameKey];
159 | pathArray = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
160 | documentsDirectory = [pathArray objectAtIndex:0];
161 | NSString *directoryPath = [documentsDirectory stringByAppendingPathComponent:name];
162 | NSFileManager *fileManager = [NSFileManager defaultManager];
163 | BOOL isDir = NO;
164 | if (![fileManager fileExistsAtPath:directoryPath isDirectory:&isDir]) {
165 | NSError *err;
166 | [fileManager createDirectoryAtPath:directoryPath withIntermediateDirectories:NO attributes:nil error:&err];
167 | }
168 | path = [directoryPath stringByAppendingPathComponent:appendedPlistName];
169 | #endif
170 | }
171 | _plistPath = path;
172 | }
173 |
174 | - (void) configurePropertyNames {
175 | _propertyNames = [NSMutableArray array];
176 |
177 | // Fetch Properties
178 | unsigned count;
179 | objc_property_t *properties = class_copyPropertyList([self class], &count);
180 |
181 | // Set the properties to not be included in dictionary
182 | NSArray * propertyNamesToBlock = @[@"backingDictionary",
183 | @"plistName",
184 | @"observingKeyPaths",
185 | @"isDirty",
186 | @"isBundledPlist",
187 | @"propertyNames",
188 | @"plistPath"];
189 |
190 | // Parse Out Properties
191 | for (int i = 0; i < count; i++) {
192 | objc_property_t property = properties[i];
193 | const char * name = property_getName(property);
194 | // NSLog(@"Name: %s", name);
195 | NSString *stringName = [NSString stringWithUTF8String:name];
196 |
197 | // Ignore these properties
198 | if ([propertyNamesToBlock containsObject:stringName]) {
199 | // Block these properties
200 | continue;
201 | }
202 |
203 | // Check if READONLY
204 | const char * attributes = property_getAttributes(property);
205 | NSString * attributeString = [NSString stringWithUTF8String:attributes];
206 | NSArray * attributesArray = [attributeString componentsSeparatedByString:@","];
207 | if ([attributesArray containsObject:@"R"]) {
208 | // is ReadOnly
209 | NSLog(@"Properties can NOT be readonly to work properly. %s will not be set", name);
210 | }
211 | else {
212 | NSString * propertyName = [NSString stringWithUTF8String:name];
213 | [_propertyNames addObject:propertyName];
214 | }
215 | }
216 |
217 | // Free our properties
218 | free(properties);
219 | }
220 |
221 | - (void) configureBackingDictionary {
222 | // Check to see if there's a Plist included in the main bundle
223 | NSString * path = _plistPath;
224 |
225 | // Get Plist
226 | NSMutableDictionary *plist;
227 | #if TARGET_OS_IPHONE
228 | plist = [[NSMutableDictionary alloc]initWithContentsOfFile:path];
229 | #elif TARGET_OS_MAC
230 | NSData *plistData = [NSData dataWithContentsOfFile:path];
231 | if (plistData) {
232 | plist = [NSKeyedUnarchiver unarchiveObjectWithData:plistData];
233 | }
234 | #endif
235 |
236 | // Return -- If null, return empty, do not return null
237 | _backingDictionary = (plist) ? plist : [NSMutableDictionary dictionary];
238 | }
239 |
240 | - (void) populateProperties {
241 | [_propertyNames enumerateObjectsUsingBlock:^(NSString * propertyName, NSUInteger idx, BOOL *stop) {
242 | [self setPropertyFromDictionaryValueWithName:propertyName];
243 | }];
244 | }
245 |
246 | #pragma mark DEALLOC
247 |
248 | - (void) dealloc {
249 |
250 | // Bundled Plists are immutable ... return
251 | if (_isBundledPlist) {
252 | return;
253 | }
254 | else {
255 |
256 | // Update Dictionary Before We Compare Dirty (WILL SET VIA KVO)
257 | [self synchronizePropertiesToDictionary];
258 |
259 | // AFTER setting objects to dictionary from properties so we can know if dirty
260 | [self removeKVO];
261 |
262 | // Save
263 | if (_isDirty) {
264 | [self writeDictionaryInBackground:_backingDictionary toPath:_plistPath withCompletion:nil];
265 | }
266 | }
267 |
268 | }
269 |
270 | - (void) removeKVO {
271 | [_observingKeyPaths enumerateObjectsUsingBlock:^(NSString * keyPath, BOOL *stop) {
272 | [_backingDictionary removeObserver:self forKeyPath:keyPath];
273 | }];
274 | }
275 |
276 | #pragma mark SAVE & WRITE TO FILE
277 |
278 | - (void) writeDictionaryInBackground:(NSDictionary *)dictionary toPath:(NSString *)path withCompletion:(void(^)(BOOL successful))completion {
279 |
280 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
281 |
282 | // Prepare Package
283 | BOOL successful = NO;
284 |
285 | // Attempt Write
286 | #if TARGET_OS_IPHONE
287 | successful = [dictionary writeToFile:path atomically:YES];
288 | #elif TARGET_OS_MAC
289 | NSData *data = [NSKeyedArchiver archivedDataWithRootObject:dictionary];
290 | successful = [[NSFileManager defaultManager]createFileAtPath:path contents:data attributes:nil];
291 | #endif
292 | if (completion) {
293 | dispatch_sync(dispatch_get_main_queue(), ^{
294 | completion(successful);
295 | });
296 | }
297 | else {
298 | // No completion block
299 | }
300 |
301 | });
302 | }
303 |
304 | - (BOOL) save {
305 |
306 | // Synchronize
307 | [self synchronizePropertiesToDictionary];
308 |
309 | // Prep Return Package
310 | BOOL successful = NO;
311 |
312 | // Write
313 | if (_isDirty) {
314 | successful = [_backingDictionary writeToFile:_plistPath atomically:YES];
315 | if (successful) _isDirty = NO;
316 | }
317 | else {
318 | successful = YES;
319 | }
320 |
321 | return successful;
322 | }
323 |
324 | - (void) saveInBackgroundWithCompletion:(void(^)(BOOL successful))completion {
325 |
326 | // Prepare Package
327 | BOOL successful = NO;
328 |
329 | // Bundled Plists are immutable, don't save (on real devices)
330 | if (_isBundledPlist) {
331 | if (completion) {
332 | NSLog(@"Bundled Plists are immutable on a RealDevice, New values will not save!");
333 | successful = NO;
334 | completion(successful);
335 | }
336 | return;
337 | }
338 |
339 | // Update dictionary to reflect values set via properties. Will set _isDirty via KVO
340 | [self synchronizePropertiesToDictionary];
341 |
342 | // Save if dirty
343 | if (_isDirty) {
344 |
345 | __weak typeof(self) weakSelf = self;
346 |
347 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
348 |
349 | __strong typeof(weakSelf) strongSelf = weakSelf;
350 |
351 | if (strongSelf) {
352 |
353 | // Prepare Package
354 | BOOL successful = NO;
355 |
356 | // Write to Path
357 | successful = [strongSelf.backingDictionary writeToFile:strongSelf.plistPath atomically:YES];
358 |
359 | // Reset dirty - We need to access directly because of readOnly status
360 | if (successful) strongSelf->_isDirty = NO;
361 |
362 | if (completion) {
363 | // Completion on Main Queue
364 | dispatch_sync(dispatch_get_main_queue(), ^{
365 | completion(successful);
366 | });
367 | }
368 |
369 | }
370 | });
371 | }
372 | else if (completion) {
373 | // Object is clean, run completion if it exists
374 | successful = YES;
375 | completion(successful);
376 | }
377 | else {
378 | // clean w/ no completion
379 | }
380 | }
381 |
382 | #pragma mark SELECTOR ARGUMENT / RETURN TYPE METHODS
383 |
384 | - (const char *) returnTypeOfSelector:(SEL)selector {
385 | NSMethodSignature * sig = [self methodSignatureForSelector:selector];
386 | return [sig methodReturnType];
387 | }
388 |
389 | - (const char *) typeOfArgumentForSelector:(SEL)selector atIndex:(int)index {
390 | NSMethodSignature * sig = [self methodSignatureForSelector:selector];
391 |
392 | if (index < sig.numberOfArguments) {
393 | // Index 0 is object, Index 1 is the selector itself, arguments start at Index 2
394 | const char * argType = [sig getArgumentTypeAtIndex:index];
395 | return argType;
396 | }
397 | else {
398 | NSLog(@"Index out of range of arguments");
399 | return nil;
400 | }
401 | }
402 |
403 | #pragma mark SELECTORS & PROPERTIES
404 |
405 | - (SEL) setterSelectorForPropertyName:(NSString *)propertyName {
406 |
407 | /*
408 | Because apple automatically generates setters to "setPropertyName:", we can use that and return the first argument to get the type of property it is. That way, we can set it to our plist values. Custom setters will cause problems.
409 | */
410 |
411 | // Make our first letter capitalized - Using this because `capitalizedString` causes issues with camelCase => Camelcase
412 | NSString * capitalizedPropertyName = [propertyName stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[propertyName substringToIndex:1] capitalizedString]];
413 |
414 | // The name of our auto synthesized setter | Custom setters will cause issues
415 | NSString * methodString = [NSString stringWithFormat:@"set%@:", capitalizedPropertyName];
416 |
417 | // Set our Selector
418 | SEL propertySetterSelector = NSSelectorFromString(methodString);
419 |
420 | // Return it
421 | return propertySetterSelector;
422 | }
423 |
424 | - (SEL) getterSelectorForPropertyName:(NSString *)propertyName {
425 |
426 | // AutoSynthesized Getters are just the property name
427 | return NSSelectorFromString(propertyName);
428 | }
429 |
430 | #pragma mark SYNCHRONYZING DICTIONARY AND PROPERTIES
431 |
432 | - (void) synchronizePropertiesToDictionary {
433 |
434 | // So we don't have to check it every time
435 | BOOL isInfo = [_plistName isEqualToString:@"Info"];
436 |
437 | // Set our properties to the dictionary before we write it
438 | for (NSString * propertyName in _propertyNames) {
439 |
440 | // Check if we're using an Info.plist model
441 | if (!isInfo) {
442 | // If not Info.plist, don't set this variable. The other properties won't be set because the can be null, but because it's a BOOL, it will set a default 0 and show NO. This means that any custom plist will have this property added;
443 | if ([propertyName isEqualToString:@"LSRequiresIPhoneOS"]) {
444 | continue;
445 | }
446 | }
447 |
448 | // Make sure our dictionary is set to latest property value
449 | [self setDictionaryValueFromPropertyWithName:propertyName];
450 | }
451 |
452 | }
453 | /*!
454 | Set the dictionary value from the property value
455 | */
456 | - (void) setDictionaryValueFromPropertyWithName:(NSString *)propertyName {
457 |
458 | SEL propertyGetterSelector = [self getterSelectorForPropertyName:propertyName];
459 |
460 | const char * returnType = [self returnTypeOfSelector:propertyGetterSelector];
461 |
462 | if ([self respondsToSelector:propertyGetterSelector]) {
463 |
464 | // Get object from our dictionary
465 | // strcmp(str1, str2)
466 | // 0 if same
467 | // A value greater than zero indicates that the first character that does not match has a greater value in str1 than in str2;
468 | // And a value less than zero indicates the opposite.
469 |
470 | // Set our implementation
471 | IMP imp = [self methodForSelector:propertyGetterSelector];
472 |
473 | // Get object to set
474 | id objectToSet;
475 |
476 | // Set to property
477 | if (strcmp(returnType, @encode(id)) == 0) {
478 | //NSLog(@"Is Object");
479 | id (*func)(id, SEL) = (void *)imp;
480 | objectToSet = func(self, propertyGetterSelector);
481 | }
482 | else if (strcmp(returnType, @encode(BOOL)) == 0) {
483 | //NSLog(@"Is Bool");
484 | BOOL (*func)(id, SEL) = (void *)imp;
485 | objectToSet = @(func(self, propertyGetterSelector));
486 | }
487 | else if (strcmp(returnType, @encode(int)) == 0) {
488 | //NSLog(@"Is Int");
489 | int (*func)(id, SEL) = (void *)imp;
490 | objectToSet = @(func(self, propertyGetterSelector));
491 | }
492 | else if (strcmp(returnType, @encode(float)) == 0) {
493 | //NSLog(@"Is Float");
494 | float (*func)(id, SEL) = (void *)imp;
495 | objectToSet = @(func(self, propertyGetterSelector));
496 |
497 | }
498 | else if (strcmp(returnType, @encode(double)) == 0) {
499 | //NSLog(@"Is Double");
500 | double (*func)(id, SEL) = (void *)imp;
501 | objectToSet = @(func(self, propertyGetterSelector));
502 | }
503 |
504 | if (objectToSet) {
505 | // self[propertyName] = object;
506 | [self setObject:objectToSet forKey:propertyName];
507 | }
508 | else {
509 | [self removeObjectForKey:propertyName];
510 | }
511 | }
512 | }
513 |
514 | - (void) setPropertyFromDictionaryValueWithName:(NSString *)propertyName {
515 |
516 | // Default
517 | __block NSString * dictionaryKey = propertyName;
518 |
519 | // If propertyName isn't contained, double check to see if key exists case insensitive
520 | if (!_backingDictionary[propertyName]) {
521 | /*
522 | If dictionary value doesn't exist, do case insensitive to check for correctKey
523 | */
524 | [_backingDictionary.allKeys enumerateObjectsUsingBlock:^(NSString * key, NSUInteger idx, BOOL *stop) {
525 | if ([key caseInsensitiveCompare:propertyName] == NSOrderedSame) {
526 | dictionaryKey = key;
527 | *stop = YES;
528 | }
529 | }];
530 |
531 | }
532 |
533 | // Get our setter from our string
534 | SEL propertySetterSelector = [self setterSelectorForPropertyName:propertyName];
535 |
536 | // Make sure it exists as a property
537 | if ([self respondsToSelector:propertySetterSelector]) {
538 |
539 | // Index 0 is object, Index 1 is the selector: arguments start at Index 2
540 | const char * typeOfProperty = [self typeOfArgumentForSelector:propertySetterSelector atIndex:2];
541 | // Set our implementation
542 | IMP imp = [self methodForSelector:propertySetterSelector];
543 |
544 | if (_backingDictionary[dictionaryKey]) {
545 |
546 | // Get object from our dictionary
547 | id objectFromDictionaryForProperty = _backingDictionary[dictionaryKey];
548 |
549 | // strcmp(str1, str2)
550 | // 0 if same
551 | // A value greater than zero indicates that the first character that does not match has a greater value in str1 than in str2;
552 | // And a value less than zero indicates the opposite.
553 |
554 | // Set PlistValue to property
555 | if (strcmp(typeOfProperty, @encode(id)) == 0) {
556 | // NSLog(@"Is Object");
557 | void (*func)(id, SEL, id) = (void *)imp;
558 | func(self, propertySetterSelector, objectFromDictionaryForProperty);
559 | }
560 | else if (strcmp(typeOfProperty, @encode(BOOL)) == 0) {
561 | // NSLog(@"Is Bool");
562 | void (*func)(id, SEL, BOOL) = (void *)imp;
563 | func(self, propertySetterSelector, [objectFromDictionaryForProperty boolValue]);
564 | }
565 | else if (strcmp(typeOfProperty, @encode(int)) == 0) {
566 | // NSLog(@"Is Int");
567 | void (*func)(id, SEL, int) = (void *)imp;
568 | func(self, propertySetterSelector, [objectFromDictionaryForProperty intValue]);
569 | }
570 | else if (strcmp(typeOfProperty, @encode(float)) == 0) {
571 | // NSLog(@"Is Float");
572 | void (*func)(id, SEL, float) = (void *)imp;
573 | func(self, propertySetterSelector, [objectFromDictionaryForProperty floatValue]);
574 | }
575 | else if (strcmp(typeOfProperty, @encode(double)) == 0) {
576 | // NSLog(@"Is Double");
577 | void (*func)(id, SEL, double) = (void *)imp;
578 | func(self, propertySetterSelector, [objectFromDictionaryForProperty doubleValue]);
579 | }
580 |
581 | }
582 | else {
583 |
584 | // strcmp(str1, str2)
585 | // 0 if same
586 | // A value greater than zero indicates that the first character that does not match has a greater value in str1 than in str2;
587 | // And a value less than zero indicates the opposite.
588 |
589 | // Set our implementation
590 | IMP imp = [self methodForSelector:propertySetterSelector];
591 |
592 | // Set PlistValue to property
593 | if (strcmp(typeOfProperty, @encode(id)) == 0) {
594 | //NSLog(@"Is Object");
595 | void (*func)(id, SEL, id) = (void *)imp;
596 | func(self, propertySetterSelector, [NSNull new]);
597 | }
598 | else if (strcmp(typeOfProperty, @encode(BOOL)) == 0) {
599 | //NSLog(@"Is Bool");
600 | void (*func)(id, SEL, BOOL) = (void *)imp;
601 | func(self, propertySetterSelector, NO);
602 | }
603 | else if (strcmp(typeOfProperty, @encode(int)) == 0) {
604 | //NSLog(@"Is Int");
605 | void (*func)(id, SEL, int) = (void *)imp;
606 | func(self, propertySetterSelector, 0);
607 | }
608 | else if (strcmp(typeOfProperty, @encode(float)) == 0) {
609 | //NSLog(@"Is Float");
610 | void (*func)(id, SEL, float) = (void *)imp;
611 | func(self, propertySetterSelector, 0);
612 | }
613 | else if (strcmp(typeOfProperty, @encode(double)) == 0) {
614 | //NSLog(@"Is Double");
615 | void (*func)(id, SEL, double) = (void *)imp;
616 | func(self, propertySetterSelector, 0);
617 | }
618 | }
619 | }
620 | }
621 |
622 | #pragma mark LITERALS SUPPORT
623 |
624 | - (id)objectForKeyedSubscript:(id)key {
625 | return [self objectForKey:key];
626 | }
627 |
628 | - (void)setObject:(id)obj forKeyedSubscript:(id )key {
629 | [self setObject:obj forKey:key];
630 | }
631 |
632 | #pragma mark NSMutableDictionary Subclass Like Interaction -- NECESSARY!
633 |
634 | - (void) setObject:(id)anObject forKey:(id)aKey {
635 |
636 | if ([[(id)aKey class]isSubclassOfClass:[NSString class]]) {
637 |
638 | if (_isBundledPlist) {
639 | // Bundled plists are immutable
640 | return;
641 | }
642 |
643 | __block NSString *blockKey = (NSString *)aKey;
644 | __block NSString *blockPropertyName;
645 |
646 | // Check if key matches property, if it does, sync to property value. Properties take priority
647 | [_propertyNames enumerateObjectsUsingBlock:^(NSString *propertyName, NSUInteger idx, BOOL *stop) {
648 | if ([propertyName caseInsensitiveCompare:blockKey] == NSOrderedSame) {
649 | // key matches property
650 | blockPropertyName = propertyName;
651 | *stop = YES;
652 | }
653 | }];
654 |
655 |
656 |
657 | // Check to see if there's already a key matching our current key
658 | if (!_backingDictionary[blockKey]) {
659 | /*
660 | If dictionary value doesn't exist, do case insensitive to check for correctKey
661 | */
662 | [_backingDictionary.allKeys enumerateObjectsUsingBlock:^(NSString * key, NSUInteger idx, BOOL *stop) {
663 | if ([key caseInsensitiveCompare:blockKey] == NSOrderedSame) {
664 | blockKey = key;
665 | *stop = YES;
666 | }
667 | }];
668 |
669 | }
670 |
671 | // We must observe this key before we set it, if we aren't already, otherwise, will not trigger dirty!
672 | if (![_observingKeyPaths containsObject:blockKey]) {
673 | [_observingKeyPaths addObject:blockKey];
674 | [_backingDictionary addObserver:self forKeyPath:blockKey options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
675 | }
676 |
677 | // Set the object to our background dictionary
678 | _backingDictionary[blockKey] = anObject;
679 |
680 | // Update our property -- Just to keep everything synced
681 | [self setPropertyFromDictionaryValueWithName:blockPropertyName];
682 | }
683 | else {
684 | NSLog(@"Error - Unable to add Object: PlistModel can only take strings as keys");
685 | }
686 |
687 | }
688 |
689 | - (void) removeObjectForKey:(id)aKey {
690 |
691 | if ([[(id)aKey class]isSubclassOfClass:[NSString class]]) {
692 |
693 | if (_isBundledPlist) {
694 | // Bundled plists are immutable
695 | return;
696 | }
697 |
698 | __block NSString *blockKey = (NSString *)aKey;
699 | __block NSString *blockPropertyName;
700 |
701 | // Check if key matches property, if it does, sync to property value. Properties take priority
702 | [_propertyNames enumerateObjectsUsingBlock:^(NSString *propertyName, NSUInteger idx, BOOL *stop) {
703 | if ([propertyName caseInsensitiveCompare:blockKey] == NSOrderedSame) {
704 | // key matches property
705 | blockPropertyName = propertyName;
706 | *stop = YES;
707 | }
708 | }];
709 |
710 |
711 |
712 | // Check to see if there's already a key matching our current key
713 | if (!_backingDictionary[blockKey]) {
714 | /*
715 | If dictionary value doesn't exist, do case insensitive to check for correctKey
716 | */
717 | [_backingDictionary.allKeys enumerateObjectsUsingBlock:^(NSString * key, NSUInteger idx, BOOL *stop) {
718 | if ([key caseInsensitiveCompare:blockKey] == NSOrderedSame) {
719 | blockKey = key;
720 | *stop = YES;
721 | }
722 | }];
723 |
724 | }
725 |
726 | // Remove object from background dictionary
727 | [_backingDictionary removeObjectForKey:blockKey];
728 |
729 | /*
730 | I don't remove KVO observers here because, I don't think it's immediately necessary. I will remove all observers on dealloc, and it's possible the user will still use this key to add objects.
731 | */
732 |
733 | // Update our property -- Just to keep everything synced
734 | [self setPropertyFromDictionaryValueWithName:blockPropertyName];
735 | }
736 | else {
737 | NSLog(@"Error - Unable to remove Object: Plist Model can only take strings as keys");
738 | }
739 | }
740 |
741 | - (NSUInteger) count {
742 | return _backingDictionary.count;
743 | }
744 |
745 | - (id)objectForKey:(id)aKey {
746 |
747 | __block NSString *blockKey = aKey;
748 |
749 | // Check if key matches property, if it does, sync to property value. Properties take priority
750 | [_propertyNames enumerateObjectsUsingBlock:^(NSString *propertyName, NSUInteger idx, BOOL *stop) {
751 | if ([propertyName caseInsensitiveCompare:blockKey] == NSOrderedSame) {
752 | // key matches property, must sync - Properties take priority
753 | [self setDictionaryValueFromPropertyWithName:propertyName];
754 | *stop = YES;
755 | }
756 | }];
757 |
758 | // If propertyName isn't contained, double check to see if key exists case insensitive
759 | if (!_backingDictionary[blockKey]) {
760 | /*
761 | If dictionary value doesn't exist, do case insensitive to check for correctKey
762 | */
763 | [_backingDictionary.allKeys enumerateObjectsUsingBlock:^(NSString * key, NSUInteger idx, BOOL *stop) {
764 | if ([key caseInsensitiveCompare:aKey] == NSOrderedSame) {
765 | blockKey = key;
766 | *stop = YES;
767 | }
768 | }];
769 |
770 | }
771 |
772 | // Return
773 | return _backingDictionary[blockKey];
774 | }
775 |
776 | - (NSEnumerator *)keyEnumerator {
777 | return [_backingDictionary keyEnumerator];
778 | }
779 |
780 | #pragma mark ENUMERATION
781 |
782 | - (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block {
783 |
784 | [self synchronizePropertiesToDictionary];
785 |
786 | [_backingDictionary enumerateKeysAndObjectsUsingBlock:block];
787 | }
788 | - (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id key, id obj, BOOL *stop))block {
789 |
790 | [self synchronizePropertiesToDictionary];
791 |
792 | [_backingDictionary enumerateKeysAndObjectsWithOptions:opts usingBlock:block];
793 | }
794 | #pragma mark KEYS & VALUES
795 |
796 | - (NSArray *)allKeys {
797 |
798 | [self synchronizePropertiesToDictionary];
799 |
800 | return _backingDictionary.allKeys;
801 | }
802 | - (NSArray *)allValues {
803 |
804 | [self synchronizePropertiesToDictionary];
805 |
806 | return _backingDictionary.allValues;
807 | }
808 |
809 | #pragma mark KVO OBSERVING
810 |
811 | - (void) observeValueForKeyPath:(NSString *)keyPath
812 | ofObject:(id)object
813 | change:(NSDictionary *)change
814 | context:(void *)context {
815 |
816 | // If it's already dirty, don't bother
817 | if (!_isDirty) {
818 | if (![change[@"new"]isEqual:change[@"old"]]) {
819 | // NewValue, We are now dirty
820 | _isDirty = YES;
821 | }
822 | }
823 | }
824 |
825 | #pragma mark IS DIRTY GETTER
826 |
827 | - (BOOL) isDirty {
828 |
829 | /*
830 | Within self always use _isDirty or self->isDirty. This method is only for external access if the user wants to check if Dirty
831 | */
832 |
833 | // Will update dictionary to current values (KVO WILL TRIGGER _isDirty) this way user gets latest value
834 | [self synchronizePropertiesToDictionary];
835 |
836 | // Set when synchronized
837 | return _isDirty;
838 | }
839 |
840 | #pragma mark DESCRIPTION
841 |
842 | - (NSString *) description {
843 |
844 | // Sync our properties so it will print the appropriate values
845 | [self synchronizePropertiesToDictionary];
846 |
847 | // Print dictionary
848 | return [_backingDictionary description];
849 | }
850 |
851 | @end
852 |
--------------------------------------------------------------------------------