deep-linking-sample/Apps/Wikipedia/WMF Framework/WMFCrossProcessCoreDataSynchronizer.m
Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

163 lines
5.5 KiB
Objective-C

#import <WMF/WMFCrossProcessCoreDataSynchronizer.h>
#include <notify.h>
#import <WMF/WMF-Swift.h>
#import <CoreData/CoreData.h>
@interface WMFCrossProcessCoreDataSynchronizer () {
dispatch_semaphore_t _semaphore;
int _token;
}
@property (nonatomic, copy) NSString *identifier;
@property (nonatomic, copy) NSURL *containerURL;
@end
static uint64_t bundleHash() {
static dispatch_once_t onceToken;
static uint64_t bundleHash;
dispatch_once(&onceToken, ^{
bundleHash = (uint64_t)[[[NSBundle mainBundle] bundleIdentifier] hash];
});
return bundleHash;
}
@implementation WMFCrossProcessCoreDataSynchronizer
- (instancetype)initWithIdentifier:(NSString *)identifier storageDirectory:(NSURL *)directoryURL {
self = [super init];
if (self) {
_semaphore = dispatch_semaphore_create(1);
self.containerURL = directoryURL;
self.identifier = identifier;
}
return self;
}
- (void)dealloc {
[self stop];
}
- (void)startSynchronizingContexts:(NSArray<NSManagedObjectContext *> *)contexts {
if (!self.identifier) {
DDLogError(@"missing channel name");
return;
}
const char *name = [self.identifier UTF8String];
for (NSManagedObjectContext *context in contexts) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextDidSave:) name:NSManagedObjectContextDidSaveNotification object:context];
}
@weakify(self)
notify_register_dispatch(name, &_token, dispatch_get_main_queue(), ^(int token) {
@strongify(self)
uint64_t state;
notify_get_state(token, &state);
BOOL isExternal = state != bundleHash();
if (isExternal) {
[self readCrossProcessCoreDataNotificationWithState:state intoContexts:contexts];
}
});
}
- (void)stop {
if (_token != 0) {
notify_cancel(_token);
}
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Writing Changes from this Process
- (void)contextDidSave:(NSNotification *)note {
[self writeCrossProcessCoreDataNotification:note];
}
- (void)writeCrossProcessCoreDataNotification:(NSNotification *)note {
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSDictionary *userInfo = note.userInfo;
if (!userInfo) {
return;
}
uint64_t state = bundleHash();
NSDictionary *archiveableUserInfo = [self archivableNotificationUserInfoForUserInfo:userInfo];
NSError *archiveError = nil;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:archiveableUserInfo requiringSecureCoding:NO error:&archiveError];
if (archiveError) {
DDLogError(@"Error archiving cross process changes: %@", archiveError);
return;
}
NSURL *fileURL = [self archivedChangesFileURLWithState:state];
[data writeToURL:fileURL atomically:YES];
const char *name = [self.identifier UTF8String];
notify_set_state(_token, state);
notify_post(name);
dispatch_semaphore_signal(_semaphore);
}
#pragma mark - Reading changes from other processes
- (void)readCrossProcessCoreDataNotificationWithState:(uint64_t)state intoContexts:(NSArray<NSManagedObjectContext *> *)contexts {
NSURL *fileURL = [self archivedChangesFileURLWithState:state];
NSError *unarchiveError = nil;
NSDictionary *userInfo = [self unarchivedDictionaryFromFileURL:fileURL error:&unarchiveError];
if (unarchiveError) {
DDLogError(@"Error unarchiving cross process core data notification: %@", unarchiveError);
return;
}
[NSManagedObjectContext mergeChangesFromRemoteContextSave:userInfo intoContexts:contexts];
}
#pragma mark - Notification Archive Utitlities
- (NSDictionary *)unarchivedDictionaryFromFileURL:(NSURL *)fileURL error:(NSError **)error {
NSData *data = [NSData dataWithContentsOfURL:fileURL];
NSSet *allowedClasses = [NSSet setWithArray:[NSSecureUnarchiveFromDataTransformer allowedTopLevelClasses]];
return [NSKeyedUnarchiver unarchivedObjectOfClasses:allowedClasses fromData:data error:error];
}
- (NSURL *)archivedChangesFileURLWithState:(uint64_t)state {
NSString *fileName = [NSString stringWithFormat:@"%llu.%@.changes", state, self.identifier];
return [self.containerURL URLByAppendingPathComponent:fileName isDirectory:NO];
}
- (nullable id)archiveableNotificationValueForValue:(id)value {
if ([value isKindOfClass:[NSManagedObject class]]) {
return [[value objectID] URIRepresentation];
} else if ([value isKindOfClass:[NSManagedObjectID class]]) {
return [value URIRepresentation];
} else if ([value isKindOfClass:[NSSet class]] || [value isKindOfClass:[NSArray class]]) {
return [value wmf_map:^id(id obj) {
return [self archiveableNotificationValueForValue:obj];
}];
} else if ([value conformsToProtocol:@protocol(NSCoding)]) {
return value;
} else {
return nil;
}
}
- (NSDictionary *)archivableNotificationUserInfoForUserInfo:(NSDictionary *)userInfo {
NSMutableDictionary *archiveableUserInfo = [NSMutableDictionary dictionaryWithCapacity:userInfo.count];
NSArray *allKeys = userInfo.allKeys;
for (NSString *key in allKeys) {
id value = userInfo[key];
if ([value isKindOfClass:[NSDictionary class]]) {
value = [self archivableNotificationUserInfoForUserInfo:value];
} else {
value = [self archiveableNotificationValueForValue:value];
}
if (value) {
archiveableUserInfo[key] = value;
}
}
return archiveableUserInfo;
}
@end