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
163 lines
5.5 KiB
Objective-C
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
|