-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathUserDataManager.m
More file actions
421 lines (360 loc) · 16.2 KB
/
UserDataManager.m
File metadata and controls
421 lines (360 loc) · 16.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
//
// UserDataManager.m
// iVerb
//
// Created by Max on 16/04/2017.
//
//
#import "UserDataManager.h"
#import "ManagedObjectContext.h"
#import "Playlist.h"
#import "Verb.h"
#import "QuizResult.h"
#import "UserDataEvent.h"
#import "Playlist+additions.h"
#import "Verb+additions.h"
#import "NSDate+addition.h"
#import "NSManagedObject+addition.h"
@implementation Playlist (infinitives)
- (NSArray<NSString *> *)infinitives
{
return [(NSSet *)[self.verbs valueForKey:SelectorName(infinitif)] allObjects] ?: @[];
}
@end
@implementation QuizResult (score)
+ (NSDateFormatter *)keyValueStoreFormatter
{
NSDateFormatter * formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"dd/MM/yy HH:mm:ss";
return formatter;
}
- (NSInteger)score
{
return self.rightResponses.integerValue;
}
- (NSInteger)total
{
return (self.rightResponses.integerValue + self.wrongResponses.integerValue);
}
@end
static UserDataManager * __defaultManager = nil;
NSString * const KeyValueStoreListsKey = @"lists"; // Value is of `KVSLists` type
NSString * const KeyValueStoreBookmarksKey = @"bookmarks"; // Value is of `KVSBookmarks` type
NSString * const KeyValueStoreNotesKey = @"notes"; // Value is of `KVSNotes` type
/// An array of dictionary with all playlists:
/// `@{ "name": playlist name, "verbs": @[ infinitives ], "quiz": { "16/04/17 12:37:00": "6/8" } }`
typedef DictionaryTy(String, Object) KVSList;
typedef NSArray <KVSList *> KVSLists;
/// An array of infinitif for each bookmarked verbs: `@[ infinitif ]`
typedef ArrayTy(String) KVSBookmarks;
/// A dictionary with all notes: `@{ verb infinitif : note content }`
typedef DictionaryTy(String, String) KVSNotes;
NSString * const UserDataEventsKey = @"userDataEvents";
@interface UserDataManager ()
/// Non-shared local user defaults; contains notes (`note_[infinitif]`), last selected playlist (`UserDefaultsLastUsedPlaylistKey`) and verb displayed count `UserDefaultsVerbPopularitiesKey`
@property (nonatomic, strong) NSUserDefaults * userDefaults;
/// iCloud store; contains lists, bookmarks and notes
/// @see `KeyValueStoreXXXKey` keys and `KVSXXX` types for more details on format.
@property (nonatomic, strong) NSUbiquitousKeyValueStore * keyValueStore;
/// Local Core Data's managed object context; contains lists, verbs, quotes and quiz results
@property (nonatomic, strong) NSManagedObjectContext * managedObjectContext;
/// Remove old events (more than 2 weeks) that should not be applied on iCloud synchronisation merge.
/// This also clean up saved events if iCloud sync is disabled to avoid growing user default storage.
- (void)removeStaleEvents;
@end
@implementation UserDataManager
+ (instancetype)defaultManager
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
__defaultManager = [[UserDataManager alloc] init];
});
return __defaultManager;
}
- (instancetype)init
{
if ((self = [super init])) {
_userDefaults = [NSUserDefaults standardUserDefaults]; // Synchronization not needed to get it up to date
_keyValueStore = [NSUbiquitousKeyValueStore defaultStore];
_managedObjectContext = [ManagedObjectContext sharedContext];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateExternalStore:)
name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
object:nil];
[self startObservingEventNotifications];
[_keyValueStore synchronize];
}
return self;
}
- (void)removeStaleEvents
{
NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults];
NSMutableArray <UserDataEvent *> * events = [NSMutableArray arrayWithCapacity:10];
for (NSData * eventData in [userDefaults arrayForKey:UserDataEventsKey]) {
UserDataEvent * const event = [NSKeyedUnarchiver unarchiveObjectWithData:eventData];
if (event) [events addObject:event];
}
// Remove events older than 2 weeks
NSDate * const date = [NSDate dateWithTimeIntervalSinceNow:-2*7*24*60*60/*2 weeks*/];
[events filterUsingPredicate:[NSPredicate predicateWithFormat:@"%K > %@", NSStringFromSelector(@selector(timestamp)), date]];
// Remove duplicates
NSSet <NSString *> * const eventDescriptionsToKeep = [[NSSet setWithArray:events] valueForKey:NSStringFromSelector(@selector(description))];
[events filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UserDataEvent * event, NSDictionary * bindings) {
return [eventDescriptionsToKeep member:event.description]; // Check equality on pointer
}]];
NSMutableArray <NSData *> * eventDatas = [NSMutableArray arrayWithCapacity:events.count];
for (UserDataEvent * event in events)
[eventDatas addObject:[NSKeyedArchiver archivedDataWithRootObject:event]];
[userDefaults setObject:eventDatas forKey:UserDataEventsKey]; // ???: Should be saved as dict with timestamp as key (more efficient)?
}
- (NSArray <NSString *> *)eventNotificationNames
{
return @[ PlaylistDidCreateNotification, PlaylistWillDeleteNotification, PlaylistDidUpdateNameNotification,
PlaylistDidAddVerbNotification, PlaylistDidRemoveVerbNotification,
VerbDidUpdateNoteNotification, VerbDidRemoveNoteNotification ];
}
- (void)startObservingEventNotifications
{
for (NSString * name in self.eventNotificationNames)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveEvent:) name:name object:nil];
}
- (void)stopObservingEventNotifications
{
for (NSString * name in self.eventNotificationNames)
[[NSNotificationCenter defaultCenter] removeObserver:self name:name object:nil];
}
- (void)saveEvent:(NSNotification *)notification
{
Playlist * playlist = ([notification.object isKindOfClass:Playlist.class]) ? notification.object : nil;
if ([playlist isHistoryPlaylist])
return ;
UserDataEvent* (^createEventFromNotification)(NSNotification *) = ^UserDataEvent*(NSNotification * notification){
if /**/ ([notification.name isEqualToString:PlaylistDidCreateNotification]) {
assert(playlist);
return [[UDPlaylistCreateEvent alloc] initWithPlaylist:playlist];
}
else if ([notification.name isEqualToString:PlaylistWillDeleteNotification]) {
assert(playlist);
return [[UDPlaylistDeleteEvent alloc] initWithPlaylist:playlist];
}
else if ([notification.name isEqualToString:PlaylistDidUpdateNameNotification]) {
assert(playlist);
UDPlaylistRenameEvent * event = [[UDPlaylistRenameEvent alloc] initWithPlaylist:playlist];
assert(event.name);
event.oldName = notification.userInfo[@"oldName"];
assert(event.oldName);
return event;
}
else if ([notification.name isEqualToString:PlaylistDidAddVerbNotification]) {
assert(playlist);
UDPlaylistAddVerbEvent * event = [[UDPlaylistAddVerbEvent alloc] initWithPlaylist:playlist];
Verb * const verb = notification.userInfo[@"verb"];
assert(verb);
event.infinitif = verb.infinitif;
return event;
}
else if ([notification.name isEqualToString:PlaylistDidRemoveVerbNotification]) {
assert(playlist);
UDPlaylistRemoveVerbEvent * event = [[UDPlaylistRemoveVerbEvent alloc] initWithPlaylist:playlist];
Verb * const verb = notification.userInfo[@"verb"];
assert(verb);
event.infinitif = verb.infinitif;
return event;
}
else if ([notification.name isEqualToString:VerbDidUpdateNoteNotification]) {
UDVerbAddNoteEvent * event = [[UDVerbAddNoteEvent alloc] init];
Verb * const verb = notification.object;
event.infinitif = verb.infinitif;
event.note = verb.note;
return event;
}
else if ([notification.name isEqualToString:VerbDidRemoveNoteNotification]) {
UDVerbRemoveNoteEvent * event = [[UDVerbRemoveNoteEvent alloc] init];
Verb * const verb = notification.object;
event.infinitif = verb.infinitif;
return event;
}
assert(false);
};
UserDataEvent * event = createEventFromNotification(notification);
NSDebugLog(@"Saving user data event: %@", event);
// Save event to user defaults
NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults];
NSMutableArray <NSData *> * eventDatas = ([userDefaults arrayForKey:UserDataEventsKey] ?: @[]).mutableCopy;
[eventDatas addObject:[NSKeyedArchiver archivedDataWithRootObject:event]];
[userDefaults setObject:eventDatas forKey:UserDataEventsKey];
}
- (void)updateExternalStore:(NSNotification *)notification
{
NSNumber * const changeReason = notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey];
if (!changeReason)
return ;
BOOL hasChanges = ((changeReason.integerValue == NSUbiquitousKeyValueStoreServerChange) ||
(changeReason.integerValue == NSUbiquitousKeyValueStoreInitialSyncChange));
if (hasChanges) {
NSArray * const changedKeys = notification.userInfo[NSUbiquitousKeyValueStoreChangedKeysKey];
for (NSString * key in changedKeys) {
if ([key isEqualToString:KeyValueStoreListsKey]) {
KVSLists * lists = (KVSLists *)[_keyValueStore arrayForKey:key];
if (!lists)
continue;
// Create/delete local playlists
NSMutableArray <NSString *> * names = [NSMutableArray arrayWithCapacity:lists.count];
for (KVSList * list in lists) [names addObject:(NSString *)list[@"name"]];
for (Playlist * playlist in [Playlist userPlaylists]) {
if (![names containsObject:playlist.name]) // Removed playlist
[_managedObjectContext deleteObject:playlist];
}
for (NSString * name in names) {
if (![Playlist playlistWithName:name]) // New playlist
[Playlist insertPlaylistWithName:name inManagedObjectContext:_managedObjectContext];
}
[_managedObjectContext save:nil];
// Update playlists
for (KVSList * list in lists) {
NSString * const name = (NSString *)list[@"name"];
Playlist * playlist = [Playlist playlistWithName:name];
assert(playlist);
// Update verbs
Array(String) infinitives = (Array(String))list[@"verbs"];
NSArray <Verb *> * updatedVerbs = [Verb verbsWithInfinitives:infinitives];
playlist.verbs = [NSSet setWithArray:updatedVerbs];
NSMutableSet <QuizResult *> * quizResults = [playlist mutableSetValueForKey:SelectorName(quizResults)];
// Update quiz
Dictionary(String, String) results = (Dictionary(String, String))list[@"quiz"];
for (NSString * dateString in results) { // `quiz = @{ @"16/04/2017" : @"6/8" }`
NSDate * const date = [[QuizResult keyValueStoreFormatter] dateFromString:dateString];
NSString * resultString = results[dateString];
long score = 0, total = 0;
if (sscanf([resultString cStringUsingEncoding:NSUTF8StringEncoding], "%ld/%ld", &score, &total) == 2) {
BOOL existingFound = NO;
for (QuizResult * result in playlist.quizResults) {
if ((existingFound |= (ABS([date timeIntervalSinceDate:result.date]) < 5 &&
result.score == score && result.total == total) ))
break;
}
// If no matching (i.e. same day, score and total) quiz result, insert the one from iCloud
if (!existingFound) {
QuizResult * quizResult = [QuizResult instanciateInContext:_managedObjectContext];
quizResult.date = date;
quizResult.rightResponses = @(score);
quizResult.wrongResponses = @(total - score);
[quizResults addObject:quizResult];
}
}
}
}
[_managedObjectContext save:nil];
}
else if ([key isEqualToString:KeyValueStoreBookmarksKey]) {
KVSBookmarks * bookmarks = (KVSBookmarks *)[_keyValueStore arrayForKey:key];
if (!bookmarks)
continue;
// Update bookmarks
Playlist * playlist = [Playlist bookmarksPlaylist];
NSMutableSet <Verb *> * verbs = [playlist mutableSetValueForKey:SelectorName(verbs)];
[verbs removeAllObjects];
[verbs addObjectsFromArray:[Verb verbsWithInfinitives:bookmarks]];
[_managedObjectContext save:nil];
}
else if ([key isEqualToString:KeyValueStoreNotesKey]) {
KVSNotes * notes = (KVSNotes *)[_keyValueStore dictionaryForKey:key];
if (!notes)
continue;
// Update User Defaults notes
for (NSString * infinitif in notes) {
NSString * const key = [NSString stringWithFormat:@"note_%@", infinitif];
[_userDefaults setObject:notes[infinitif] forKey:key];
}
}
}
// Re-apply events
[self stopObservingEventNotifications];
[self removeStaleEvents];
NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults];
NSMutableArray <UserDataEvent *> * events = [NSMutableArray arrayWithCapacity:10];
for (NSData * eventData in [userDefaults arrayForKey:UserDataEventsKey]) {
UserDataEvent * const event = [NSKeyedUnarchiver unarchiveObjectWithData:eventData];
if (event) [events addObject:event];
}
[events sortUsingDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:SelectorName(timestamp)
ascending:YES] ]];
for (UserDataEvent * anEvent in events) {
if /**/ ([anEvent isKindOfClass:UDPlaylistCreateEvent.class]) {
UDPlaylistCreateEvent * event = (UDPlaylistCreateEvent *)anEvent;
NSString * const name = event.name;
if (!event.playlist && name)
[Playlist insertPlaylistWithName:name
inManagedObjectContext:_managedObjectContext];
}
else if ([anEvent isKindOfClass:UDPlaylistDeleteEvent.class]) {
UDPlaylistEvent * event = (UDPlaylistDeleteEvent *)anEvent;
if (event.playlist)
[_managedObjectContext deleteObject:event.playlist];
}
else if ([anEvent isKindOfClass:UDPlaylistRenameEvent.class]) {
UDPlaylistRenameEvent * event = (UDPlaylistRenameEvent *)anEvent;
Playlist * const playlist = [Playlist playlistWithName:event.oldName];
playlist.name = event.name;
}
else if ([anEvent isKindOfClass:UDPlaylistAddVerbEvent.class]) {
UDPlaylistAddVerbEvent * event = (UDPlaylistAddVerbEvent *)anEvent;
[event.playlist addVerb:[Verb verbWithInfinitif:event.infinitif]];
}
else if ([anEvent isKindOfClass:UDPlaylistRemoveVerbEvent.class]) {
UDPlaylistRemoveVerbEvent * event = (UDPlaylistRemoveVerbEvent *)anEvent;
[event.playlist removeVerb:[Verb verbWithInfinitif:event.infinitif]];
}
else if ([anEvent isKindOfClass:UDVerbAddNoteEvent.class]) {
UDVerbAddNoteEvent * event = (UDVerbAddNoteEvent *)anEvent;
[Verb verbWithInfinitif:event.infinitif].note = event.note;
}
else if ([anEvent isKindOfClass:UDVerbRemoveNoteEvent.class]) {
UDVerbRemoveNoteEvent * event = (UDVerbRemoveNoteEvent *)anEvent;
[Verb verbWithInfinitif:event.infinitif].note = nil;
}
}
if ([_managedObjectContext save:nil])
[userDefaults removeObjectForKey:UserDataEventsKey];
[self startObservingEventNotifications];
[[NSNotificationCenter defaultCenter] postNotificationName:PlaylistDidUpdatedNotification object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsDidChangeNotification object:nil];
}
}
- (BOOL)synchronize
{
[self removeStaleEvents];
NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass(Verb.class)];
request.predicate = [NSPredicate predicateWithValue:YES];
NSArray <Verb *> * allVerbs = (NSArray *)[_managedObjectContext executeFetchRequest:request error:nil];
// Notes
MDictionary(String, String) notes = [NSMutableDictionary dictionaryWithCapacity:30];
for (NSString * infinitif in [allVerbs valueForKey:SelectorName(infinitif)]) {
NSString * const key = [NSString stringWithFormat:@"note_%@", infinitif];
NSString * note = [_userDefaults stringForKey:key];
if (note)
notes[infinitif] = note;
}
[_keyValueStore setDictionary:notes forKey:KeyValueStoreNotesKey];
// Bookmarks
Array(String) bookmarks = [Playlist bookmarksPlaylist].infinitives;
[_keyValueStore setArray:bookmarks forKey:KeyValueStoreBookmarksKey];
// Playlists
NSMutableArray <Dictionary(String, Object)> * lists = [NSMutableArray arrayWithCapacity:5];
for (Playlist * playlist in [Playlist userPlaylists]) {
MDictionary(String, String) results = [NSMutableDictionary dictionaryWithCapacity:playlist.quizResults.count];
for (QuizResult * result in playlist.quizResults) {
NSString * const key = [[QuizResult keyValueStoreFormatter] stringFromDate:result.date];
results[key] = [NSString stringWithFormat:@"%ld/%ld", (long)result.score, (long)result.total];
}
[lists addObject:@{ @"name" : playlist.name,
@"verbs": playlist.infinitives,
@"quiz" : results }];
}
[_keyValueStore setArray:lists forKey:KeyValueStoreListsKey];
return [_keyValueStore synchronize];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end