diff --git a/plugin.xml b/plugin.xml index e732ee2..2aa0f5e 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,58 +1,69 @@ - - Apple Watch - - Telerik - - AppleWatch plugin for your Cordova project. You build the UI with JS, so no XCode native code hacking required. - - AppleWatch - - MIT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + Apple Watch + + Telerik + + AppleWatch plugin for your Cordova project. You build the UI with JS, so no XCode native code hacking required. + + AppleWatch + + MIT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ios/app/AppDelegate+applewatch.h b/src/ios/app/AppDelegate+applewatch.h index ef9e12e..868606a 100644 --- a/src/ios/app/AppDelegate+applewatch.h +++ b/src/ios/app/AppDelegate+applewatch.h @@ -1,6 +1,7 @@ #import "AppDelegate.h" @interface AppDelegate (applewatch) -- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply; + +- (void) callJavascriptFunctionWhenAvailable:(NSString*)function; @end \ No newline at end of file diff --git a/src/ios/app/AppDelegate+applewatch.m b/src/ios/app/AppDelegate+applewatch.m index c0048f6..30e1f72 100644 --- a/src/ios/app/AppDelegate+applewatch.m +++ b/src/ios/app/AppDelegate+applewatch.m @@ -5,56 +5,17 @@ @implementation AppDelegate (applewatch) -- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply { - // requesting a bit of background processing time, useful when the app was killed instead of running in the background - __block UIBackgroundTaskIdentifier watchKitHandler; - watchKitHandler = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"backgroundTask" - expirationHandler:^{ - watchKitHandler = UIBackgroundTaskInvalid; - }]; - dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)NSEC_PER_SEC * 30 ), dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{ - [[UIApplication sharedApplication] endBackgroundTask:watchKitHandler]; - } ); - - NSString* jsFunction = [userInfo objectForKey:@"action"]; - - NSLog(@"In handleWatchKitExtensionRequest, jsFunction: %@", jsFunction); - - NSString* params; - // TODO 'onVoted' should not be hardcoded! - if ([jsFunction isEqualToString:@"onVoted"]) { - if ([[userInfo objectForKey:@"params"] isKindOfClass:[NSData class]]) { - // animated gif - params = [NSString stringWithFormat:@"{'type':'base64img', 'data':'data:image/gif;base64,%@'}", [[userInfo objectForKey:@"params"] base64EncodedStringWithOptions:0]]; - } else { - // text label, emoji, dictated text - params = [NSString stringWithFormat:@"{'type':'text', 'data':'%@'}", [userInfo objectForKey:@"params"]]; - } - } else { - params = [NSString stringWithFormat:@"'%@'", [userInfo objectForKey:@"params"]]; - } - - NSString* result = [NSString stringWithFormat:@"%@(%@)", jsFunction, params == nil ? @"" : params]; - [self callJavascriptFunctionWhenAvailable:result]; - - // no need to wait as data is passed back async - reply(@{}); -} - // check every x seconds for the phone app to be ready, or stop from glance.didDeactivate // TODO stop after x tries - (void) callJavascriptFunctionWhenAvailable:(NSString*)function { - AppleWatch *appleWatch = [self.viewController getCommandInstance:@"AppleWatch"]; - if (appleWatch.initDone) { - [appleWatch.webView stringByEvaluatingJavaScriptFromString:function]; - } else { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 80 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ - [self callJavascriptFunctionWhenAvailable:function]; - }); - } -} - -- (void)applicationDidBecomeActive:(UIApplication *)application { + AppleWatch *appleWatch = [self.viewController getCommandInstance:@"AppleWatch"]; + if (appleWatch.initDone) { + [appleWatch.webView stringByEvaluatingJavaScriptFromString:function]; + } else { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 80 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + [self callJavascriptFunctionWhenAvailable:function]; + }); + } } @end \ No newline at end of file diff --git a/src/ios/app/AppleWatch.m b/src/ios/app/AppleWatch.m index f708cc0..eef3ed0 100644 --- a/src/ios/app/AppleWatch.m +++ b/src/ios/app/AppleWatch.m @@ -1,7 +1,8 @@ #import "Cordova/CDV.h" #import "Cordova/CDVViewController.h" #import "AppleWatch.h" -#import "MMWormhole.h" +#import "MMWormholeUmbrella.h" +#import "AppDelegate+applewatch.h" static NSString *const AWPlugin_Page_Glance = @"Glance"; static NSString *const AWPlugin_Page_AppMain = @"AppMain"; @@ -9,7 +10,11 @@ @interface AppleWatch () -@property (nonatomic, strong) MMWormhole* wormhole; +@property (nonatomic, strong) MMWormhole *wormhole; + +#if ( defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 ) +@property (nonatomic, strong) MMWormholeSession *watchConnectivityListeningWormhole; +#endif @end @@ -17,25 +22,81 @@ @implementation AppleWatch - (void) init:(CDVInvokedUrlCommand*)command { NSString *appGroup = [NSString stringWithFormat:@"group.%@", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"]]; - + +#if ( defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 ) + self.watchConnectivityListeningWormhole = MMWormholeSession.sharedListeningSession; + // Make sure we are activating the listening wormhole so that it will receive new messages from + // the WatchConnectivity framework. + [self.watchConnectivityListeningWormhole activateSessionListening]; + // Initialize the wormhole using the WatchConnectivity framework's application context transiting type + self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:appGroup + optionalDirectory:@"wormhole" + transitingType:MMWormholeTransitingTypeSessionMessage]; + [self.watchConnectivityListeningWormhole listenForMessageWithIdentifier:@"action" listener:^(id _Nullable messageObject) { + if (UIApplication.sharedApplication.applicationState == UIApplicationStateBackground) { + // requesting a bit of background processing time, useful when the app was killed instead of running in the background + __block UIBackgroundTaskIdentifier watchKitHandler; + watchKitHandler = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"backgroundTask" + expirationHandler:^{ + watchKitHandler = UIBackgroundTaskInvalid; + }]; + dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)NSEC_PER_SEC * 30 ), dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{ + [[UIApplication sharedApplication] endBackgroundTask:watchKitHandler]; + }); + } + + id actionPayload = [messageObject objectForKey:@"action"]; + NSString* jsFunction = nil; + if ([actionPayload isKindOfClass:NSData.class]) { + jsFunction = [[NSString alloc] initWithData:actionPayload encoding:NSUTF8StringEncoding]; + } else if ([actionPayload isKindOfClass:NSString.class]) { + jsFunction = actionPayload; + } else { + return ; + } + + NSLog(@"In handleWatchKitExtensionRequest, jsFunction: %@", jsFunction); + + NSString* params; + // TODO 'onVoted' should not be hardcoded! + if ([jsFunction isEqualToString:@"onVoted"]) { + if ([[messageObject objectForKey:@"params"] isKindOfClass:[NSData class]]) { + // animated gif + params = [NSString stringWithFormat:@"{'type':'base64img', 'data':'data:image/gif;base64,%@'}", [[messageObject objectForKey:@"params"] base64EncodedStringWithOptions:0]]; + } else { + // text label, emoji, dictated text + params = [NSString stringWithFormat:@"{'type':'text', 'data':'%@'}", [messageObject objectForKey:@"params"]]; + } + } else { + params = [NSString stringWithFormat:@"'%@'", [messageObject objectForKey:@"params"]]; + } + + NSString* result = [NSString stringWithFormat:@"%@(%@)", jsFunction, params == nil ? @"" : params]; + [UIApplication.sharedApplication.delegate performSelector:@selector(callJavascriptFunctionWhenAvailable:) withObject:result]; + + }]; + +#else + // Initialize the wormhole self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:appGroup optionalDirectory:@"wormhole"]; - +#endif + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; - + // make sure the app is awake dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 500 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ - self.initDone = YES; // after a little timeout perhaps? + self.initDone = YES; // after a little timeout perhaps? }); } - (void) registerNotifications:(CDVInvokedUrlCommand*)command; { CDVPluginResult* pluginResult = nil; - + UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert; UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; - + if ([[UIApplication sharedApplication] currentUserNotificationSettings].types != UIUserNotificationTypeNone) { pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:(true)]; @@ -44,111 +105,111 @@ - (void) registerNotifications:(CDVInvokedUrlCommand*)command; { pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsBool:(false)]; } - + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } - (void) loadGlance:(CDVInvokedUrlCommand*)command { - [self sendMessage:command forPage:AWPlugin_Page_Glance]; + [self sendMessage:command forPage:AWPlugin_Page_Glance]; } - (void) loadAppMain:(CDVInvokedUrlCommand*)command { - [self sendMessage:command forPage:AWPlugin_Page_AppMain]; + [self sendMessage:command forPage:AWPlugin_Page_AppMain]; } - (void) loadAppDetail:(CDVInvokedUrlCommand*)command { - [self sendMessage:command forPage:AWPlugin_Page_AppDetail]; + [self sendMessage:command forPage:AWPlugin_Page_AppDetail]; } - (void) sendMessage:(CDVInvokedUrlCommand*)command { - NSDictionary *args = [command.arguments objectAtIndex:0]; - NSMutableDictionary *dic = [args objectForKey:@"payload"]; - - NSString *pageID = [dic objectForKey:@"id"]; - if (pageID == nil) { - [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"id is mandatory"] callbackId:command.callbackId]; - return; - } - - [self sendMessage:command forPage:pageID]; + NSDictionary *args = [command.arguments objectAtIndex:0]; + NSMutableDictionary *dic = [args objectForKey:@"payload"]; + + NSString *pageID = [dic objectForKey:@"id"]; + if (pageID == nil) { + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"id is mandatory"] callbackId:command.callbackId]; + return; + } + + [self sendMessage:command forPage:pageID]; } - (void) sendMessage:(CDVInvokedUrlCommand*)command forPage:(NSString*)pageID { - NSDictionary *args = [command.arguments objectAtIndex:0]; - NSMutableDictionary *dic = [args objectForKey:@"payload"]; - - NSString* queueName = [@"fromjstowatchapp-" stringByAppendingString:pageID]; - - // TODO add support for non-local images by downloading them here from the interwebs - [self bundleImage:[dic valueForKey:@"image"] withCallbackId:command.callbackId]; - [self bundleImage:[dic valueForKey:@"image2"] withCallbackId:command.callbackId]; - NSDictionary* table = [dic valueForKey:@"table"]; - if (table != nil) { - NSArray *rows = [table valueForKey:@"rows"]; - for (NSInteger i = 0; i < rows.count; i++) { - NSMutableDictionary* rowDef = rows[i]; - [self bundleImage:[rowDef valueForKey:@"imageLeft"] withCallbackId:command.callbackId]; - [self bundleImage:[rowDef valueForKey:@"imageRight"] withCallbackId:command.callbackId]; - [self bundleImage:[rowDef valueForKey:@"col1image"] withCallbackId:command.callbackId]; - [self bundleImage:[rowDef valueForKey:@"col2image"] withCallbackId:command.callbackId]; + NSDictionary *args = [command.arguments objectAtIndex:0]; + NSMutableDictionary *dic = [args objectForKey:@"payload"]; + + NSString* queueName = [@"fromjstowatchapp-" stringByAppendingString:pageID]; + + // TODO add support for non-local images by downloading them here from the interwebs + [self bundleImage:[dic valueForKey:@"image"] withCallbackId:command.callbackId]; + [self bundleImage:[dic valueForKey:@"image2"] withCallbackId:command.callbackId]; + NSDictionary* table = [dic valueForKey:@"table"]; + if (table != nil) { + NSArray *rows = [table valueForKey:@"rows"]; + for (NSInteger i = 0; i < rows.count; i++) { + NSMutableDictionary* rowDef = rows[i]; + [self bundleImage:[rowDef valueForKey:@"imageLeft"] withCallbackId:command.callbackId]; + [self bundleImage:[rowDef valueForKey:@"imageRight"] withCallbackId:command.callbackId]; + [self bundleImage:[rowDef valueForKey:@"col1image"] withCallbackId:command.callbackId]; + [self bundleImage:[rowDef valueForKey:@"col2image"] withCallbackId:command.callbackId]; + } } - } - - [self.wormhole passMessageObject:dic identifier:queueName]; - [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; + + [self.wormhole passMessageObject:dic identifier:queueName]; + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; } - (void) bundleImage:(NSMutableDictionary*)imageDic withCallbackId:(NSString*)callbackId { - if (imageDic != nil) { - NSString *imageSrc = [imageDic valueForKey:@"src"]; - NSString *imageData = [imageDic valueForKey:@"data"]; - - // no longer need this - [imageDic removeObjectForKey:@"data"]; - - if (imageSrc == nil && imageData == nil) { - [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"please provide a src or data attribute for image"] callbackId:callbackId]; - } else { - UIImage *image; - - if (imageData != nil) { - // more forgiving decoding of base 64 strings than NSData#initWithBase64EncodedString - NSData *encodedData = [NSData dataWithContentsOfURL: [NSURL URLWithString: imageData]]; - image = [UIImage imageWithData:encodedData]; - } else { - // NOTE: this expects a path like 'www/img/img.png' - image = [UIImage imageNamed:imageSrc]; - } - - // create NSData so it can be passed through the wormhole - NSData *imageDataObject = UIImagePNGRepresentation(image); - [imageDic setObject:imageDataObject forKey:@"src"]; + if (imageDic != nil) { + NSString *imageSrc = [imageDic valueForKey:@"src"]; + NSString *imageData = [imageDic valueForKey:@"data"]; + + // no longer need this + [imageDic removeObjectForKey:@"data"]; + + if (imageSrc == nil && imageData == nil) { + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"please provide a src or data attribute for image"] callbackId:callbackId]; + } else { + UIImage *image; + + if (imageData != nil) { + // more forgiving decoding of base 64 strings than NSData#initWithBase64EncodedString + NSData *encodedData = [NSData dataWithContentsOfURL: [NSURL URLWithString: imageData]]; + image = [UIImage imageWithData:encodedData]; + } else { + // NOTE: this expects a path like 'www/img/img.png' + image = [UIImage imageNamed:imageSrc]; + } + + // create NSData so it can be passed through the wormhole + NSData *imageDataObject = UIImagePNGRepresentation(image); + [imageDic setObject:imageDataObject forKey:@"src"]; + } } - } } - (void) navigateToAppDetail:(CDVInvokedUrlCommand*)command { - [self navigate:command toPage:AWPlugin_Page_AppDetail]; + [self navigate:command toPage:AWPlugin_Page_AppDetail]; } - (void) navigateToAppMain:(CDVInvokedUrlCommand*)command { - [self navigate:command toPage:AWPlugin_Page_AppMain]; + [self navigate:command toPage:AWPlugin_Page_AppMain]; } - (void) navigate:(CDVInvokedUrlCommand*)command toPage:(NSString*)id { - NSDictionary *args = [command.arguments objectAtIndex:0]; - NSMutableDictionary *dic = [args objectForKey:@"payload"]; - if (dic == nil) { - dic = [[NSMutableDictionary alloc] init]; - } - if ([dic valueForKey:@"id"] == nil) { - [dic setObject:id forKey:@"id"]; - } - - NSString* queueName = @"fromjstowatchapp-navigation"; - - [self.wormhole passMessageObject:dic identifier:queueName]; - [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; + NSDictionary *args = [command.arguments objectAtIndex:0]; + NSMutableDictionary *dic = [args objectForKey:@"payload"]; + if (dic == nil) { + dic = [[NSMutableDictionary alloc] init]; + } + if ([dic valueForKey:@"id"] == nil) { + [dic setObject:id forKey:@"id"]; + } + + NSString* queueName = @"fromjstowatchapp-navigation"; + + [self.wormhole passMessageObject:dic identifier:queueName]; + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; } // TODO: this doesn't work in the sim, so perhaps return an error in that case (see calendar plugin) @@ -156,30 +217,30 @@ - (void) navigate:(CDVInvokedUrlCommand*)command toPage:(NSString*)id { - (void) sendNotification:(CDVInvokedUrlCommand*)command; { NSMutableDictionary *args = [command.arguments objectAtIndex:0]; - + UILocalNotification *localNotification = [[UILocalNotification alloc] init]; - + if ([localNotification respondsToSelector:@selector(alertTitle)]) { localNotification.alertTitle = [args objectForKey:@"title"]; } - + if ([localNotification respondsToSelector:@selector(category)]) { localNotification.category = [args objectForKey:@"category"]; } - + localNotification.alertBody = [args objectForKey:@"body"]; localNotification.applicationIconBadgeNumber = [[args objectForKey:@"badge"] intValue]; - + // enable this if you want a delay // NSDate *in5seconds = [NSDate dateWithTimeIntervalSinceNow:5]; // localNotification.fireDate = in5seconds; // [NSDate date]; localNotification.fireDate = [NSDate date]; localNotification.soundName = UILocalNotificationDefaultSoundName; - + [[UIApplication sharedApplication] scheduleLocalNotification:localNotification]; - + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; } @end diff --git a/src/ios/hooks/ab/add-targets.js b/src/ios/hooks/ab/add-targets.js index 9c8acc5..da1fdaa 100644 --- a/src/ios/hooks/ab/add-targets.js +++ b/src/ios/hooks/ab/add-targets.js @@ -9,14 +9,17 @@ var fs = require('fs'), watchKitApp = 'watchkitapp', watchKitExtension = 'watchkitextension', embedAppExtensions = 'Embed App Extensions', + embedWatchContent = 'Embed Watch Content', interfaceStoryBoard = 'Interface.storyboard', base = 'Base', baseLproj = 'Base.lproj', commentKeyRegex = /_comment$/; -module.exports = function (context) { +module.exports = function(context) { var projectRoot = context.opts.projectRoot, - xcodeProjectPath = fs.readdirSync(projectRoot).filter(function (file) { return ~file.indexOf('.xcodeproj') && fs.statSync(path.join(projectRoot, file)).isDirectory(); })[0], + xcodeProjectPath = fs.readdirSync(projectRoot).filter(function(file) { + return ~file.indexOf('.xcodeproj') && fs.statSync(path.join(projectRoot, file)).isDirectory(); + })[0], cordovaProjectName = xcodeProjectPath.slice(0, -'.xcodeproj'.length), projectPlistPath = path.join(projectRoot, cordovaProjectName, util.format('%s-Info.plist', cordovaProjectName)), projectPlistJson = plist.parse(fs.readFileSync(projectPlistPath, 'utf8')), @@ -34,29 +37,24 @@ module.exports = function (context) { pbxProjectSection = pbxProject.pbxProjectSection(), watchKitAppDisplayName = '"' + displayName + ' WatchKit App"', watchKitExtensionDisplayName = '"' + displayName + ' WatchKit Extension"', - watchKitAppFilename = displayName + ' WatchKit App.app', + watchKitAppFileName = displayName + ' WatchKit App.app', watchKitExtensionAppexFileName = displayName + ' WatchKit Extension.appex', - extEntitlementsFileName = "watchkitextension.entitlements", - extEntitlementsSourceFile = path.join(resourcesPath, extEntitlementsFileName), - extEntitlementsTargetFile = path.join(projectRoot, cordovaProjectName, extEntitlementsFileName), - placeHolderValues = [ - { - placeHolder: '__DISPLAY_NAME__', - value: displayName - }, - { - placeHolder: '__APP_IDENTIFIER__', - value: projectPlistJson['CFBundleIdentifier'] - }, - { - placeHolder: '__BUNDLE_SHORT_VERSION_STRING__', - value: projectPlistJson['CFBundleShortVersionString'] - }, - { - placeHolder: '__BUNDLE_VERSION__', - value: projectPlistJson['CFBundleVersion'] - } - ], + extEntitlementsFileName = "watchkitextension.entitlements", + extEntitlementsSourceFile = path.join(resourcesPath, extEntitlementsFileName), + extEntitlementsTargetFile = path.join(projectRoot, cordovaProjectName, extEntitlementsFileName), + placeHolderValues = [{ + placeHolder: '__DISPLAY_NAME__', + value: displayName + }, { + placeHolder: '__APP_IDENTIFIER__', + value: projectPlistJson['CFBundleIdentifier'] + }, { + placeHolder: '__BUNDLE_SHORT_VERSION_STRING__', + value: projectPlistJson['CFBundleShortVersionString'] + }, { + placeHolder: '__BUNDLE_VERSION__', + value: projectPlistJson['CFBundleVersion'] + }], i = 0; console.log('Copying contents of WatchKit App'); @@ -64,7 +62,7 @@ module.exports = function (context) { console.log('Copying contents of WatchKit Extensions'); wrench.copyDirSyncRecursive(watchKitExtensionPath, projectWatchKitExtensionPath); console.log('Copying entitlements file for WatchKit Extensions'); - fs.writeFileSync(extEntitlementsTargetFile, fs.readFileSync(extEntitlementsSourceFile)); + fs.writeFileSync(extEntitlementsTargetFile, fs.readFileSync(extEntitlementsSourceFile)); console.log('Modifying Plist files'); wkcommon.replacePlaceholdersInPlist(watchKitAppPlistFilePath, placeHolderValues); @@ -88,7 +86,7 @@ module.exports = function (context) { for (i = 0; i < projectWatchKitExtensionPathContents.length; i++) { watchExtArray.push(path.join(projectWatchKitExtensionPath, projectWatchKitExtensionPathContents[i])); } - + // Special case Interface.storyboard // Interface.storyboard is the only file so far // which is inconsistent between the PBXBuildFile and PBXFileReference sections @@ -110,18 +108,24 @@ module.exports = function (context) { pbxProject.addToPbxBuildFileSection(storyBoardBuildFile); pbxProject.addToPbxFileReferenceSection(storyBoardReferenceFile); // --------------------------------------- - + console.log('App Phase GUID'); var watchKitAppBuildPhase = pbxProject.addBuildPhase(watchAppArray, 'PBXResourcesBuildPhase', watchKitApp); // Add the storyboard - watchKitAppBuildPhase.buildPhase.files.push({ value: storyBoardBuildFile.uuid, comment: interfaceStoryBoard }); + watchKitAppBuildPhase.buildPhase.files.push({ + value: storyBoardBuildFile.uuid, + comment: interfaceStoryBoard + }); console.log('Extension Phase GUID'); var watchKitExtensionSourcesBuildPhase = pbxProject.addBuildPhase(watchExtArray, 'PBXSourcesBuildPhase', watchKitExtension); var watchKitAppPbxGroup = pbxProject.addPbxGroup(watchAppArray, watchKitAppDisplayName, watchKitAppDisplayName); // Add the storyboard - watchKitAppPbxGroup.pbxGroup.children.push({ value: storyBoardBuildFile.fileRef, comment: interfaceStoryBoard }); + watchKitAppPbxGroup.pbxGroup.children.push({ + value: storyBoardBuildFile.fileRef, + comment: interfaceStoryBoard + }); var watchKitExtensionPbxGroup = pbxProject.addPbxGroup(watchExtArray, watchKitExtensionDisplayName, watchKitExtensionDisplayName); @@ -130,7 +134,10 @@ module.exports = function (context) { pbxProject.hash.project.objects['PBXVariantGroup'] = pbxProject.hash.project.objects['PBXVariantGroup'] || {}; pbxProject.hash.project.objects['PBXVariantGroup'][storyBoardBuildFile.fileRef] = { isa: 'PBXVariantGroup', - children: [{ value: storyBoardReferenceFile.fileRef, comment: storyBoardReferenceFile.name }], + children: [{ + value: storyBoardReferenceFile.fileRef, + comment: storyBoardReferenceFile.name + }], name: interfaceStoryBoard, sourceTree: '""' }; @@ -145,7 +152,9 @@ module.exports = function (context) { basename: watchKitExtensionAppexFileName, path: watchKitExtensionAppexFileName, sourceTree: 'BUILT_PRODUCTS_DIR', - settings: { ATTRIBUTES: ['RemoveHeadersOnCopy'] } + settings: { + ATTRIBUTES: ['RemoveHeadersOnCopy'] + } }; pbxProject.addToPbxFileReferenceSection(watchKitExtensionAppexFile); @@ -156,8 +165,8 @@ module.exports = function (context) { fileRef: pbxProject.generateUuid(), explicitFileType: 'wrapper.application', includeInIndex: 0, - basename: watchKitAppFilename, - path: watchKitAppFilename, + basename: watchKitAppFileName, + path: watchKitAppFileName, sourceTree: 'BUILT_PRODUCTS_DIR' }; @@ -166,55 +175,94 @@ module.exports = function (context) { var watchKitExtensionResourcesBuildPhase = pbxProject.addBuildPhase([watchKitAppFile.path], 'PBXResourcesBuildPhase', watchKitExtension + ' Resources'); - pbxProject.pbxGroupByName('CustomTemplate').children.push({ value: watchKitAppPbxGroup.uuid, comment: watchKitAppDisplayName }, { value: watchKitExtensionPbxGroup.uuid, comment: watchKitExtensionDisplayName }); - pbxProject.pbxGroupByName('Products').children.push({ value: watchKitAppFile.fileRef, comment: watchKitAppFilename }, { value: watchKitExtensionAppexFile.fileRef, comment: watchKitExtensionAppexFileName }); + pbxProject.pbxGroupByName('CustomTemplate').children.push({ + value: watchKitAppPbxGroup.uuid, + comment: watchKitAppDisplayName + }, { + value: watchKitExtensionPbxGroup.uuid, + comment: watchKitExtensionDisplayName + }); + pbxProject.pbxGroupByName('Products').children.push({ + value: watchKitAppFile.fileRef, + comment: watchKitAppFileName + }, { + value: watchKitExtensionAppexFile.fileRef, + comment: watchKitExtensionAppexFileName + }); - var watchKitFrameworkBuildPhase = wkext.addFrameworks(pbxProject, watchKitExtension, projectRelativePluginDirPath); - var watchKitExtensionNativeTargetGuid = wkext.addTarget(pbxProject, - { + var watchKitAppPlistJson = plist.parse(fs.readFileSync(watchKitAppPlistFilePath, 'utf8')), + watchKitExtensionPlistJson = plist.parse(fs.readFileSync(watchKitExtensionPlistFilePath, 'utf8')); + watchKitFrameworkBuildPhase = wkext.addFrameworks(pbxProject, watchKitExtension, projectRelativePluginDirPath); + var watchKitExtensionNativeTargetGuid = wkext.addTarget(pbxProject, { plistFilePath: watchKitExtensionPlistFilePath, displayName: watchKitExtensionDisplayName, projectPluginDir: projectRelativePluginDirPath, buildPhase: watchKitFrameworkBuildPhase, sourcesBuildPhase: watchKitExtensionSourcesBuildPhase, - resourcesBuildPhase: watchKitExtensionResourcesBuildPhase, + // resourcesBuildPhase: watchKitExtensionResourcesBuildPhase, productReference: watchKitExtensionAppexFile.fileRef, productReference_comment: watchKitExtensionAppexFileName, - }); + }, + watchKitExtensionPlistJson['CFBundleIdentifier']); + + var pbxNativeTargetSection = pbxProject.pbxNativeTarget(); + var mainAppTargetGuid = getUuidByComment(cordovaProjectName, pbxNativeTargetSection); + // WARNING: Workaround to correctly add libmmwormhole-watchos.a to extension target only. + pbxProject.removeFromPbxFrameworksBuildPhase({ + target: mainAppTargetGuid.uuid, + basename: 'libMMWormhole-watchos.a', + group: 'Frameworks' + }); - var watchKitAppNativeTargetGuid = wkapp.addTarget(pbxProject, - { + var watchKitAppNativeTargetGuid = wkapp.addTarget(pbxProject, { plistFilePath: watchKitAppPlistFilePath, displayName: watchKitAppDisplayName, bundleDisplayName: displayName, buildPhase: watchKitAppBuildPhase, productReference: watchKitAppFile.fileRef, - productReference_comment: watchKitAppFilename - }); - - var pbxNativeTargetSection = pbxProject.pbxNativeTarget(); + productReference_comment: watchKitAppFileName + }, + watchKitAppPlistJson['CFBundleIdentifier']); console.log('Adding both targets to the main project'); var pbxProjectKey = pbxProject.hash.project.rootObject; - pbxProjectSection[pbxProjectKey].targets.push({ value: watchKitAppNativeTargetGuid, comment: watchKitAppDisplayName }, { value: watchKitExtensionNativeTargetGuid, comment: watchKitExtensionDisplayName }); + pbxProjectSection[pbxProjectKey].targets.push({ + value: watchKitAppNativeTargetGuid, + comment: watchKitAppDisplayName + }, { + value: watchKitExtensionNativeTargetGuid, + comment: watchKitExtensionDisplayName + }); var mainAppTarget = pbxProject.pbxTargetByName(cordovaProjectName); - var mainAppTargetGuid = getUuidByComment(cordovaProjectName, pbxNativeTargetSection); + console.log('Add Embed Watch Content Build Phase'); + pbxProject.hash.project.objects['PBXCopyFilesBuildPhase'] = pbxProject.hash.project.objects['PBXCopyFilesBuildPhase'] || {}; + var embedWatchContentBuildPhase = pbxProject.addBuildPhase([watchKitAppFileName], 'PBXCopyFilesBuildPhase', embedWatchContent); + embedWatchContentBuildPhase.buildPhase['dstPath'] = '"$(CONTENTS_FOLDER_PATH)/Watch"'; + embedWatchContentBuildPhase.buildPhase['dstSubfolderSpec'] = 16; + mainAppTarget.buildPhases.push({ + value: embedWatchContentBuildPhase.uuid, + comment: embedWatchContent + }); + + var watchAppTarget = pbxProject.pbxTargetByName(watchKitAppDisplayName); console.log('Add Embed App Extensions Build Phase'); pbxProject.hash.project.objects['PBXCopyFilesBuildPhase'] = pbxProject.hash.project.objects['PBXCopyFilesBuildPhase'] || {}; var embedAppExtensionsBuildPhase = pbxProject.addBuildPhase([watchKitExtensionAppexFileName], 'PBXCopyFilesBuildPhase', embedAppExtensions); embedAppExtensionsBuildPhase.buildPhase['dstPath'] = '""'; embedAppExtensionsBuildPhase.buildPhase['dstSubfolderSpec'] = 13; - mainAppTarget.buildPhases.push({ value: embedAppExtensionsBuildPhase.uuid, comment: embedAppExtensions }); + watchAppTarget.buildPhases.push({ + value: embedAppExtensionsBuildPhase.uuid, + comment: embedAppExtensions + }); console.log('Adding dependencies'); - console.log('WatchKit Extension depends on WatchKit App'); - pbxProject.addTargetDependency(watchKitExtensionNativeTargetGuid, [watchKitAppNativeTargetGuid]); + console.log('WatchKit App depends on WatchKit App Extension'); + pbxProject.addTargetDependency(watchKitAppNativeTargetGuid, [watchKitExtensionNativeTargetGuid]); - console.log('Original App depends on WatchKit Extension'); - pbxProject.addTargetDependency(mainAppTargetGuid, [watchKitExtensionNativeTargetGuid]); + console.log('Original App depends on WatchKit App'); + pbxProject.addTargetDependency(mainAppTargetGuid, [watchKitAppNativeTargetGuid]); - var buffer = pbxProject.writeSync(); projectFile.write(); console.log('pbxProject was modified successfully'); } diff --git a/src/ios/hooks/ab/watchkit-app.js b/src/ios/hooks/ab/watchkit-app.js index 6a60e75..a0b728d 100644 --- a/src/ios/hooks/ab/watchkit-app.js +++ b/src/ios/hooks/ab/watchkit-app.js @@ -4,56 +4,54 @@ var path = require('path'), release = 'Release', watchKitApp = 'WatchKit App'; -function addWatchkitAppTarget(pbxProject, prop) { +function addWatchkitAppTarget(pbxProject, prop, bundleIdentifier) { console.log('Adding WatchKit App XCConfigurationList'); var watchKitAppConfigurations = [{ isa: 'XCBuildConfiguration', buildSettings: { ASSETCATALOG_COMPILER_APPICON_NAME: 'AppIcon', - GCC_PREPROCESSOR_DEFINITIONS: [ - '"DEBUG=1"', - '"$(inherited)"', - ], IBSC_MODULE: prop.bundleDisplayName + '_WatchKit_Extension', INFOPLIST_FILE: wkcommon.quoteString(prop.plistFilePath), - IPHONEOS_DEPLOYMENT_TARGET: 8.2, //Hardcoding this - problems may arise + PRODUCT_BUNDLE_IDENTIFIER: bundleIdentifier, PRODUCT_NAME: '"${TARGET_NAME}"', - PROVISIONING_PROFILE: '"${PROVISION_WATCHKITAPP}"', - SKIP_INSTALL: 'NO', - TARGETED_DEVICE_FAMILY: 4, //Hardcoding this also - '"TARGETED_DEVICE_FAMILY[sdk=iphonesimulator*]"': '"1,4"', // According to apple documentation 1 is for iPhone/iPad and 2 is for iPad - I'm guessing 4 is for AppleWatch - COPY_PHASE_STRIP: 'NO', - MTL_ENABLE_DEBUG_INFO: 'NO' + PROVISIONING_PROFILE: '"${PROVISION_WATCHKITAPP}"', + SDKROOT: 'watchos', + SKIP_INSTALL: 'YES', + TARGETED_DEVICE_FAMILY: 4, + WATCHOS_DEPLOYMENT_TARGET: '2.0' }, name: debug, }, { - isa: 'XCBuildConfiguration', - buildSettings: { - ASSETCATALOG_COMPILER_APPICON_NAME: 'AppIcon', - IBSC_MODULE: prop.bundleDisplayName + '_WatchKit_Extension', - INFOPLIST_FILE: wkcommon.quoteString(prop.plistFilePath), - IPHONEOS_DEPLOYMENT_TARGET: 8.2, //Hardcoding this - problems may arise - PRODUCT_NAME: '"${TARGET_NAME}"', - PROVISIONING_PROFILE: '"${PROVISION_WATCHKITAPP}"', - SKIP_INSTALL: 'NO', - TARGETED_DEVICE_FAMILY: 4, //Hardcoding this also - '"TARGETED_DEVICE_FAMILY[sdk=iphonesimulator*]"': '"1,4"', // According to apple documentation 1 is for iPhone/iPad and 2 is for iPad - I'm guessing 4 is for AppleWatch - COPY_PHASE_STRIP: 'NO' - }, - name: release, - }]; + isa: 'XCBuildConfiguration', + buildSettings: { + ASSETCATALOG_COMPILER_APPICON_NAME: 'AppIcon', + IBSC_MODULE: prop.bundleDisplayName + '_WatchKit_Extension', + INFOPLIST_FILE: wkcommon.quoteString(prop.plistFilePath), + PRODUCT_BUNDLE_IDENTIFIER: bundleIdentifier, + PRODUCT_NAME: '"${TARGET_NAME}"', + PROVISIONING_PROFILE: '"${PROVISION_WATCHKITAPP}"', + SDKROOT: 'watchos', + SKIP_INSTALL: 'NO', + TARGETED_DEVICE_FAMILY: 4, + WATCHOS_DEPLOYMENT_TARGET: '2.0' + }, + name: release, + }]; var watchKitAppXCConfigurations = pbxProject.addXCConfigurationList(watchKitAppConfigurations, 'Release', 'Build configuration list for PBXNativeTarget ' + prop.displayName); return wkcommon.addNativeTarget(pbxProject, { buildConfiguration: watchKitAppXCConfigurations, - buildPhases: [{ value: prop.buildPhase.uuid, comment: watchKitApp }], + buildPhases: [{ + value: prop.buildPhase.uuid, + comment: watchKitApp + }], buildRules: [], dependencies: [], productName: prop.displayName, productReference: prop.productReference, productReference_comment: prop.productReference_comment, - productType: '"com.apple.product-type.application.watchapp"' + productType: '"com.apple.product-type.application.watchapp2"' }); } diff --git a/src/ios/hooks/ab/watchkit-ext.js b/src/ios/hooks/ab/watchkit-ext.js index da5e198..7f531ad 100644 --- a/src/ios/hooks/ab/watchkit-ext.js +++ b/src/ios/hooks/ab/watchkit-ext.js @@ -6,69 +6,73 @@ var path = require('path'), function addWatchkitExtensionFrameworks(pbxProject, watchKitExtension, projectPluginDir) { console.log('Add WatchKit.Framework'); - var watchKitFramework = pbxProject.addFramework('System/Library/Frameworks/WatchKit.framework'); + var watchConnectivityFramework = pbxProject.addFramework('System/Library/Frameworks/WatchConnectivity.framework'); var coreLocationFramework = pbxProject.addFramework('System/Library/Frameworks/CoreLocation.framework'); - var libmmwormholeLib = pbxProject.addFramework(path.join(projectPluginDir, 'libmmwormhole.a')); - return pbxProject.addBuildPhase( - [watchKitFramework.path, coreLocationFramework.path, libmmwormholeLib.path], - 'PBXFrameworksBuildPhase', - watchKitExtension + ' Frameworks'); + var libmmwormholeLib = pbxProject.addFramework(path.join(projectPluginDir, 'libMMWormhole-watchos.a')); + + return pbxProject.addBuildPhase([watchConnectivityFramework.path, coreLocationFramework.path, libmmwormholeLib.path], + 'PBXFrameworksBuildPhase', watchKitExtension + ' Frameworks'); } -function addWatchkitExtensionTarget(pbxProject, prop) { +function addWatchkitExtensionTarget(pbxProject, prop, bundleIdentifier) { console.log('Adding WatchKit Extension XCConfigurationList'); var INHERITED = '"$(inherited)"', librarySearchPath = [INHERITED, '"\\"$(SRCROOT)/' + prop.projectPluginDir + '\\""'], watchKitExtensionConfigurations = [{ isa: 'XCBuildConfiguration', buildSettings: { - GCC_PREPROCESSOR_DEFINITIONS: [ - '"DEBUG=1"', - INHERITED, - ], INFOPLIST_FILE: wkcommon.quoteString(prop.plistFilePath), LD_RUNPATH_SEARCH_PATHS: '"$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"', + LIBRARY_SEARCH_PATHS: librarySearchPath, + PRODUCT_BUNDLE_IDENTIFIER: bundleIdentifier, PRODUCT_NAME: '"${TARGET_NAME}"', - CODE_SIGN_ENTITLEMENTS: '"${ENTITLEMENTS_WATCHKITEXTENSION}"', - PROVISIONING_PROFILE: '"${PROVISION_WATCHKITEXTENSION}"', - IPHONEOS_DEPLOYMENT_TARGET: 8.2, //Hardcoding this - problems may arise - SKIP_INSTALL: 'YES', - COPY_PHASE_STRIP: 'NO', - GCC_OPTIMIZATION_LEVEL: 0, - LIBRARY_SEARCH_PATHS: librarySearchPath + PROVISIONING_PROFILE: '"${PROVISION_WATCHKITEXTENSION}"', + SDKROOT: 'watchos', + SKIP_INSTALL: 'NO', + TARGETED_DEVICE_FAMILY: 4, + WATCHOS_DEPLOYMENT_TARGET: '2.0', + CODE_SIGN_ENTITLEMENTS: '"${ENTITLEMENTS_WATCHKITEXTENSION}"', + OTHER_LDFLAGS: ['-weak_framework', 'UIKit'] }, name: debug, }, { - isa: 'XCBuildConfiguration', - buildSettings: { - INFOPLIST_FILE: wkcommon.quoteString(prop.plistFilePath), - LD_RUNPATH_SEARCH_PATHS: '"$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"', - PRODUCT_NAME: '"${TARGET_NAME}"', - CODE_SIGN_ENTITLEMENTS: '"${ENTITLEMENTS_WATCHKITEXTENSION}"', - PROVISIONING_PROFILE: '"${PROVISION_WATCHKITEXTENSION}"', - IPHONEOS_DEPLOYMENT_TARGET: 8.2, //Hardcoding this - problems may arise - SKIP_INSTALL: 'YES', - COPY_PHASE_STRIP: 'NO', - VALIDATE_PRODUCT: 'YES', - LIBRARY_SEARCH_PATHS: librarySearchPath - }, - name: release, - } - ]; + isa: 'XCBuildConfiguration', + buildSettings: { + INFOPLIST_FILE: wkcommon.quoteString(prop.plistFilePath), + LD_RUNPATH_SEARCH_PATHS: '"$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"', + LIBRARY_SEARCH_PATHS: librarySearchPath, + PRODUCT_BUNDLE_IDENTIFIER: bundleIdentifier, + PRODUCT_NAME: '"${TARGET_NAME}"', + PROVISIONING_PROFILE: '"${PROVISION_WATCHKITEXTENSION}"', + SDKROOT: 'watchos', + SKIP_INSTALL: 'NO', + TARGETED_DEVICE_FAMILY: 4, + WATCHOS_DEPLOYMENT_TARGET: '2.0', + OTHER_LDFLAGS: ['-weak_framework', 'UIKit'] + }, + name: release, + }]; var watchKitExtensionXCConfigurations = pbxProject.addXCConfigurationList(watchKitExtensionConfigurations, 'Release', 'Build configuration list for PBXNativeTarget ' + prop.displayName); return wkcommon.addNativeTarget(pbxProject, { buildConfiguration: watchKitExtensionXCConfigurations, - buildPhases: [{ value: prop.sourcesBuildPhase.uuid, comment: watchKitExtension }, - { value: prop.resourcesBuildPhase.uuid, comment: 'Resources' }, - { value: prop.buildPhase.uuid, comment: 'Frameworks' }], + buildPhases: [{ + value: prop.sourcesBuildPhase.uuid, + comment: watchKitExtension + }, + // { value: prop.resourcesBuildPhase.uuid, comment: 'Resources' }, + { + value: prop.buildPhase.uuid, + comment: 'Frameworks' + } + ], buildRules: [], dependencies: [], productName: prop.displayName, productReference: prop.productReference, productReference_comment: prop.productReference_comment, - productType: '"com.apple.product-type.watchkit-extension"' + productType: '"com.apple.product-type.watchkit2-extension"' }); } diff --git a/src/ios/lib/headers/MMWormhole.h b/src/ios/lib/headers/MMWormhole.h index c126413..b56d0b8 100644 --- a/src/ios/lib/headers/MMWormhole.h +++ b/src/ios/lib/headers/MMWormhole.h @@ -23,6 +23,28 @@ #import +#import "MMWormholeCoordinatedFileTransiting.h" +#import "MMWormholeFileTransiting.h" + +#if ( defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 ) +#import "MMWormholeSessionContextTransiting.h" +#import "MMWormholeSessionFileTransiting.h" +#import "MMWormholeSessionMessageTransiting.h" +#endif + +#import "MMWormholeTransiting.h" + +typedef NS_ENUM(NSInteger, MMWormholeTransitingType) { + MMWormholeTransitingTypeFile = 0, + MMWormholeTransitingTypeCoordinatedFile, + MMWormholeTransitingTypeSessionContext, + MMWormholeTransitingTypeSessionMessage, + MMWormholeTransitingTypeSessionFile +}; + + +NS_ASSUME_NONNULL_BEGIN + /** This class creates a wormhole between a containing iOS application and an extension. The wormhole is meant to be used to pass data or commands back and forth between the two locations. The effect @@ -63,7 +85,23 @@ should use it's own set of identifiers to associate with it's messages back to the application. Passing messages to the same identifier from two locations should be done only at your own risk. */ -@interface MMWormhole : NSObject +@interface MMWormhole : NSObject + +/** + The wormhole messenger is an object that conforms to the MMWormholeTransiting protocol. By default + this object will be set to a default implementation of this protocol which handles archiving and + unarchiving the message to the shared app group in a file named after the identifier of the + message. + + Users of this class may create their own implementation of the MMWormholeTransiting protocol to use + for the purpose of defining the means by which messages transit the wormhole. You could use this to + change the way that MMWormhole stores messages as files, to read and write messages to a database, + or otherwise be notified in other ways when messages are changed. + + @warning While changing this property is optional, the value of the wormhole messenger should + not be nil and is required for the class to work. + */ +@property (nonatomic, strong) id wormholeMessenger; /** Designated Initializer. This method must be called with an application group identifier that will @@ -74,8 +112,24 @@ @param directory An optional directory to read/write messages */ -- (instancetype)initWithApplicationGroupIdentifier:(NSString *)identifier - optionalDirectory:(NSString *)directory NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithApplicationGroupIdentifier:(nullable NSString *)identifier + optionalDirectory:(nullable NSString *)directory NS_DESIGNATED_INITIALIZER; + +/** + Optional Initializer. This method is provided for convenience while creating MMWormhole instances + with custom message transiting options. By default MMWormhole will use the + MMWormholeTransitingTypeFile option when creating a Wormhole, however, this method can be used to + easily choose a different transiting class at initialization time. You can always initialize a + different class that implements the MMWormholeTransiting class later and replace the Wormhole's + 'wormholeMessenger' property to change the transiting type at a later time. + + @param identifier An application group identifier + @param directory An optional directory to read/write messages + @param transitingType A type of wormhole message transiting that will be used for message passing. +*/ +- (instancetype)initWithApplicationGroupIdentifier:(nullable NSString *)identifier + optionalDirectory:(nullable NSString *)directory + transitingType:(MMWormholeTransitingType)transitingType; /** This method passes a message object associated with a given identifier. This is the primary means @@ -87,25 +141,26 @@ listener for a "finished changing" message to let the other side know it's safe to read the contents of your message. - @param messageobject The message object to be passed + @param messageObject The message object to be passed. + This object may be nil. In this case only a notification is posted. @param identifier The identifier for the message */ -- (void)passMessageObject:(id )messageObject - identifier:(NSString *)identifier; +- (void)passMessageObject:(nullable id )messageObject + identifier:(nullable NSString *)identifier; /** This method returns the value of a message with a specific identifier as an object. @param identifier The identifier for the message */ -- (id)messageWithIdentifier:(NSString *)identifier; +- (nullable id)messageWithIdentifier:(nullable NSString *)identifier; /** This method clears the contents of a specific message with a given identifier. @param identifier The identifier for the message */ -- (void)clearMessageContentsForIdentifier:(NSString *)identifier; +- (void)clearMessageContentsForIdentifier:(nullable NSString *)identifier; /** This method clears the contents of your optional message directory to give you a clean state. @@ -127,8 +182,8 @@ @param listener A listener block called with the messageObject parameter when a notification is observed. */ -- (void)listenForMessageWithIdentifier:(NSString *)identifier - listener:(void (^)(id messageObject))listener; +- (void)listenForMessageWithIdentifier:(nullable NSString *)identifier + listener:(nullable void (^)(__nullable id messageObject))listener; /** This method stops listening for change notifications for a given message identifier. @@ -138,6 +193,8 @@ @param identifier The identifier for the message */ -- (void)stopListeningForMessageWithIdentifier:(NSString *)identifier; +- (void)stopListeningForMessageWithIdentifier:(nullable NSString *)identifier; @end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/lib/headers/MMWormholeCoordinatedFileTransiting.h b/src/ios/lib/headers/MMWormholeCoordinatedFileTransiting.h new file mode 100644 index 0000000..7e50f0f --- /dev/null +++ b/src/ios/lib/headers/MMWormholeCoordinatedFileTransiting.h @@ -0,0 +1,33 @@ +// +// MMWormholeCoordinatedFileTransiting.h +// +// Copyright (c) 2015 Mutual Mobile (http://www.mutualmobile.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MMWormholeFileTransiting.h" + +/** + This class inherits from the default implementation of the MMWormholeTransiting protocol + and implements message transiting in a similar way but using NSFileCoordinator for its file + reading and writing. + */ +@interface MMWormholeCoordinatedFileTransiting : MMWormholeFileTransiting + +@end diff --git a/src/ios/lib/headers/MMWormholeFileTransiting.h b/src/ios/lib/headers/MMWormholeFileTransiting.h new file mode 100644 index 0000000..7f1f0ee --- /dev/null +++ b/src/ios/lib/headers/MMWormholeFileTransiting.h @@ -0,0 +1,77 @@ +// +// MMWormholeFileTransiting.h +// +// Copyright (c) 2015 Mutual Mobile (http://www.mutualmobile.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MMWormholeTransiting.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + This class is a default implementation of the MMWormholeTransiting protocol that implements + message transiting by archiving and unarchiving messages that are written and read to files on + disk in an optional directory in the given app group. This default implementation has a relatively + naive implementation of file writing, and simply uses the built in NSData file operations. + + This class is able to be subclassed to provide slightly different file reading and writing behavior + while still maintaining the logic for naming a file within the given directory and app group. + */ +@interface MMWormholeFileTransiting : NSObject + +/** + Designated Initializer. This method must be called with an application group identifier that will + be used to contain passed messages. It is also recommended that you include a directory name for + messages to be read and written, but this parameter is optional. + + @param identifier An application group identifier + @param directory An optional directory to read/write messages + */ +- (instancetype)initWithApplicationGroupIdentifier:(nullable NSString *)identifier + optionalDirectory:(nullable NSString *)directory NS_DESIGNATED_INITIALIZER; + +/** + The File Manager associated with this transiting implementation. You can use this property for + implementing your own variant of file transiting that needs to customize where and how files are + stored. + */ +@property (nonatomic, strong, readonly) NSFileManager *fileManager; + +/** + This method returns the full file path for the message passing directory, including the optional + directory passed in the designated initializer. Subclasses can use this method to provide custom + implementations. + + @return The full path to the message passing directory. + */ +- (nullable NSString *)messagePassingDirectoryPath; + +/** + This method returns the full file path for the file associated with the given message identifier. + It includes the optional directory passed in the designated initializer if there is one. Subclasses + can use this method to provide custom implementations. + + @return The full path to the file associated with the given message identifier. + */ +- (nullable NSString *)filePathForIdentifier:(nullable NSString *)identifier; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/ios/lib/headers/MMWormholeSession.h b/src/ios/lib/headers/MMWormholeSession.h new file mode 100644 index 0000000..29df612 --- /dev/null +++ b/src/ios/lib/headers/MMWormholeSession.h @@ -0,0 +1,56 @@ +// +// MMWormholeSession.h +// +// Copyright (c) 2015 Mutual Mobile (http://www.mutualmobile.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MMWormhole.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MMWormholeSession : MMWormhole + +/** + This method returns a specific instance of MMWormholeSession that should be used for listening. You + may create your own instances of MMWormholeSession for sending messages, but this is the only object + that will be able to receive messages. + + The reason for this is that MMWormholeSession is based on the WCSession class that is part of the + WatchConnectivity framework provided by Apple, and WCSession is itself a singleton with a single + delegate. Therefore, to receive callbacks, only one MMWormholeSession object may register itself + as a listener. + */ ++ (instancetype)sharedListeningSession; + +/** + This method should be called after all of your initial listeners have been set and you are ready to + begin listening for messages. There are likely some listeners that your application requires to be + active so that it won't miss critical messages. You should set up these listeners before calling + this method so that any already queued messages will be delivered immediately when you activate the + session. Any listeners you set up after calling this method may miss messages that were already + queued and waiting to be delivered. + */ +- (void)activateSessionListening; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/src/ios/lib/headers/MMWormholeSessionContextTransiting.h b/src/ios/lib/headers/MMWormholeSessionContextTransiting.h new file mode 100644 index 0000000..a1efb58 --- /dev/null +++ b/src/ios/lib/headers/MMWormholeSessionContextTransiting.h @@ -0,0 +1,44 @@ +// +// MMWormholeSessionContextTransiting.h +// +// Copyright (c) 2015 Mutual Mobile (http://www.mutualmobile.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MMWormholeFileTransiting.h" + +/** + This class provides support for the WatchConnectivity framework's Application Context message + reading and writing ability. This class will pass it's messages directly via the + -updateApplicationContext method, and read message values from application context. + + This class also uses a local mutable dictionary for maintaining a more consistent version of your + wormhole-based application context. The contents of the local dictionary are merged with the + application context for passing messages. Clearing message contents on a wormhole using this + transiting implementation will clear both the applicationContext as well as the local mutable + dictionary. + + @discussion This class should be treated as the default MMWormholeTransiting implementation for + applications wanting to leverage the WatchConnectivity framework within MMWormhole. The application + context provides the best of both real time message passing and baked in state persistence for + setting up your UI. + */ +@interface MMWormholeSessionContextTransiting : MMWormholeFileTransiting + +@end diff --git a/src/ios/lib/headers/MMWormholeSessionFileTransiting.h b/src/ios/lib/headers/MMWormholeSessionFileTransiting.h new file mode 100644 index 0000000..67f008b --- /dev/null +++ b/src/ios/lib/headers/MMWormholeSessionFileTransiting.h @@ -0,0 +1,47 @@ +// +// MMWormholeSessionFileTransiting.h +// +// Copyright (c) 2015 Mutual Mobile (http://www.mutualmobile.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MMWormholeFileTransiting.h" + +/** + This class provides support for the WatchConnectivity framework's file transfer ability. This class + will behave very similar to the MMWormholeFileTransiting implementation, meaning it will archive + messages to disk as files and send them via the WatchConnectivity framework's -transferFile API. + + @warning In order for your Wormhole to support reading the contents of transferred file messages, + you will need to set this object as the 'wormholeMessenger' property on the + [MMWormholeSession sharedListeningSession]. The reason for this is that the sharedListeningSession + requires a configured application group and optional file directory in order to know where to save + received files. If you don't set this object as the wormholeMessenger you will still be notified + when your receive files, but you will be responsible for storing the contents yourself and they + won't be persisted for you. + + @discussion This class should only be used in very specific circumstances. Typically speaking, if + you find yourself needing to use the WatchConnectivity framework's file transfer APIs you will best + be served by using the WatchConnectivity framework directly and bypassing MMWormhole. This class + is provided as a basic implementation for simple use cases and isn't intended to be the core of + your file based message transfer system. + */ +@interface MMWormholeSessionFileTransiting : MMWormholeFileTransiting + +@end diff --git a/src/ios/lib/headers/MMWormholeSessionMessageTransiting.h b/src/ios/lib/headers/MMWormholeSessionMessageTransiting.h new file mode 100644 index 0000000..ff058c4 --- /dev/null +++ b/src/ios/lib/headers/MMWormholeSessionMessageTransiting.h @@ -0,0 +1,48 @@ +// +// MMWormholeSessionMessageTransiting.h +// +// Copyright (c) 2015 Mutual Mobile (http://www.mutualmobile.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#import "MMWormholeFileTransiting.h" + +/** + This class provides support for the WatchConnectivity framework's real time message passing ability. + This is the only version of the MMWormholeSessionTransiting system that will be able to wake up + your iPhone app in the background. As such, this class has a very specific purpose. It also means + that it will most likely be used only by your Apple Watch app, or in very very specific instances + by your iPhone app. Typically, your iPhone app will want to use the + MMWormholeSessionContextTransiting option instead. + + @warning Waking up the iPhone app from a Watch app is an expensive operation. You should only use + this transiting implementation when this action is required for your application. Otherwise you are + better served by using the MMWormholeSessionContextTransiting implementation. + + @warning This transiting implementation does not support reading message contents because real time + messages are delivered once and not persisted. + + @discussion This class should be used in cases where your Apple Watch app needs to ensure your + iPhone app is running to receive a message and take some action on it. One example of this would be + to start background location tracking or audio. + */ +@interface MMWormholeSessionMessageTransiting : MMWormholeFileTransiting + +@end diff --git a/src/ios/lib/headers/MMWormholeTransiting.h b/src/ios/lib/headers/MMWormholeTransiting.h new file mode 100644 index 0000000..29b1278 --- /dev/null +++ b/src/ios/lib/headers/MMWormholeTransiting.h @@ -0,0 +1,79 @@ +// +// MMWormholeTransiting.h +// +// Copyright (c) 2015 Mutual Mobile (http://www.mutualmobile.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + This protocol defines the public interface for classes wishing to support the transiting of data + between two sides of the wormhole. Transiting is defined as passage between two points, and in this + case it involves both the reading and writing of messages as well as the deletion of message + contents. + */ +@protocol MMWormholeTransiting + +/** + This method is responsible for writing a given message object in a persisted format for a given + identifier. The method should return YES if the message was successfully saved. The message object + may be nil, in which case YES should also be returned. Returning YES from this method results in a + notification being fired which will trigger the corresponding listener block for the given + identifier. + + @param messageObject The message object to be passed. + This object may be nil. In this the method should return YES. + @param identifier The identifier for the message + @return YES indicating that a notification should be sent and NO otherwise + */ +- (BOOL)writeMessageObject:(nullable id)messageObject forIdentifier:(NSString *)identifier; + +/** + This method is responsible for reading and returning the contents of a given message. It should + understand the structure of messages saved by the implementation of the above writeMessageObject + method and be able to read those messages and return their contents. + + @param identifier The identifier for the message + */ +- (nullable id)messageObjectForIdentifier:(nullable NSString *)identifier; + +/** + This method should clear the persisted contents of a specific message with a given identifier. + + @param identifier The identifier for the message + */ +- (void)deleteContentForIdentifier:(nullable NSString *)identifier; + +/** + This method should clear the contents of all messages passed to the wormhole. + */ +- (void)deleteContentForAllMessages; + +@end + +@protocol MMWormholeTransitingDelegate + +- (void)notifyListenerForMessageWithIdentifier:(nullable NSString *)identifier message:(nullable id)message; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/src/ios/lib/headers/MMWormholeUmbrella.h b/src/ios/lib/headers/MMWormholeUmbrella.h new file mode 100644 index 0000000..d750d22 --- /dev/null +++ b/src/ios/lib/headers/MMWormholeUmbrella.h @@ -0,0 +1,20 @@ +// +// MMWormholeUmbrella.h +// MMWormhole +// +// Created by Toma Popov on 11/17/15. +// Copyright © 2015 Toma Popov. All rights reserved. +// + +#import "MMWormhole.h" +#import "MMWormholeCoordinatedFileTransiting.h" +#import "MMWormholeFileTransiting.h" + +#if ((defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000) || (defined(TARGET_OS_WATCH) && TARGET_OS_WATCH == 1)) +#import "MMWormholeSession.h" +#import "MMWormholeSessionContextTransiting.h" +#import "MMWormholeSessionFileTransiting.h" +#import "MMWormholeSessionMessageTransiting.h" +#endif + +#import "MMWormholeTransiting.h" diff --git a/src/ios/lib/libMMWormhole-ios.a b/src/ios/lib/libMMWormhole-ios.a new file mode 100644 index 0000000..11cc7d3 Binary files /dev/null and b/src/ios/lib/libMMWormhole-ios.a differ diff --git a/src/ios/lib/libMMWormhole-watchos.a b/src/ios/lib/libMMWormhole-watchos.a new file mode 100644 index 0000000..a416105 Binary files /dev/null and b/src/ios/lib/libMMWormhole-watchos.a differ diff --git a/src/ios/lib/libmmwormhole.a b/src/ios/lib/libmmwormhole.a deleted file mode 100644 index 1ea8c57..0000000 Binary files a/src/ios/lib/libmmwormhole.a and /dev/null differ diff --git a/src/ios/resources/watchkitapp/Info.plist b/src/ios/resources/watchkitapp/Info.plist index 440f236..ca4dcbf 100644 --- a/src/ios/resources/watchkitapp/Info.plist +++ b/src/ios/resources/watchkitapp/Info.plist @@ -1,39 +1 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - __DISPLAY_NAME__ - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - __APP_IDENTIFIER__.watchkitapp - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - __BUNDLE_SHORT_VERSION_STRING__ - CFBundleSignature - ???? - CFBundleVersion - __BUNDLE_VERSION__ - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - - WKCompanionAppBundleIdentifier - __APP_IDENTIFIER__ - WKWatchKitApp - - UIDeviceFamily - - 4 - - - + CFBundleDevelopmentRegion en CFBundleDisplayName __DISPLAY_NAME__ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier __APP_IDENTIFIER__.watchkitapp CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString __BUNDLE_SHORT_VERSION_STRING__ CFBundleSignature ???? CFBundleVersion __BUNDLE_VERSION__ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown WKCompanionAppBundleIdentifier __APP_IDENTIFIER__ WKWatchKitApp UIDeviceFamily 4 \ No newline at end of file diff --git a/src/ios/resources/watchkitextension/Info.plist b/src/ios/resources/watchkitextension/Info.plist index 0cab7cb..58cb406 100644 --- a/src/ios/resources/watchkitextension/Info.plist +++ b/src/ios/resources/watchkitextension/Info.plist @@ -1,38 +1 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - __DISPLAY_NAME__ WatchKit Extension - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - __APP_IDENTIFIER__.watchkitextension - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - XPC! - CFBundleShortVersionString - __BUNDLE_SHORT_VERSION_STRING__ - CFBundleSignature - ???? - CFBundleVersion - __BUNDLE_VERSION__ - NSExtension - - NSExtensionAttributes - - WKAppBundleIdentifier - __APP_IDENTIFIER__.watchkitapp - - NSExtensionPointIdentifier - com.apple.watchkit - - RemoteInterfacePrincipalClass - InterfaceController - - + CFBundleDevelopmentRegion en CFBundleDisplayName __DISPLAY_NAME__ WatchKit Extension CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier __APP_IDENTIFIER__.watchkitapp.watchkitextension CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString __BUNDLE_SHORT_VERSION_STRING__ CFBundleSignature ???? CFBundleVersion __BUNDLE_VERSION__ NSExtension NSExtensionAttributes WKAppBundleIdentifier __APP_IDENTIFIER__.watchkitapp NSExtensionPointIdentifier com.apple.watchkit RemoteInterfacePrincipalClass InterfaceController \ No newline at end of file diff --git a/src/ios/resources/watchkitextension/InterfaceController.h b/src/ios/resources/watchkitextension/InterfaceController.h index 2132e83..3c2f981 100644 --- a/src/ios/resources/watchkitextension/InterfaceController.h +++ b/src/ios/resources/watchkitextension/InterfaceController.h @@ -2,6 +2,4 @@ @interface InterfaceController : ParentInterfaceController -@property (nonatomic, strong) MMWormhole *navwormhole; - @end diff --git a/src/ios/resources/watchkitextension/InterfaceController.m b/src/ios/resources/watchkitextension/InterfaceController.m index de63d64..64a982f 100644 --- a/src/ios/resources/watchkitextension/InterfaceController.m +++ b/src/ios/resources/watchkitextension/InterfaceController.m @@ -1,38 +1,24 @@ #import "InterfaceController.h" #import "WatchKitHelper.h" - +#import "WormholeManager.h" @implementation InterfaceController -- (instancetype)init { - id obj = [super init]; - - self.navwormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:[super getAppGroup] - optionalDirectory:@"wormhole"]; - - NSString *wormholeIdentifier = @"fromjstowatchapp-navigation"; - [self.navwormhole listenForMessageWithIdentifier:wormholeIdentifier listener:^(id messageObject) { - [self pushControllerWithName:[messageObject valueForKey:@"id"] context:messageObject]; - }]; - - return obj; -} - - (NSString*) getPageID { - return @"AppMain"; + return @"AppMain"; } // When the app is launched from a notification. // If launched from app icon in notification UI, identifier will be empty. // Note that (only) the default interfacecontroller will receive this message. - (void)handleActionWithIdentifier:(NSString *)identifier forRemoteNotification:(NSDictionary *)remoteNotification { - [WatchKitHelper openParent:@"applewatch.callback.onRemoteNotification" withParams:identifier]; + [WatchKitHelper openParent:@"applewatch.callback.onRemoteNotification" withParams:identifier]; } // When the app is launched from a notification. // If launched from app icon in notification UI, identifier will be empty // Note that (only) the default interfacecontroller will receive this message. - (void)handleActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)localNotification { - [WatchKitHelper openParent:@"applewatch.callback.onLocalNotification" withParams:identifier]; + [WatchKitHelper openParent:@"applewatch.callback.onLocalNotification" withParams:identifier]; } diff --git a/src/ios/resources/watchkitextension/ParentInterfaceController.h b/src/ios/resources/watchkitextension/ParentInterfaceController.h index e4db3f6..32390f7 100644 --- a/src/ios/resources/watchkitextension/ParentInterfaceController.h +++ b/src/ios/resources/watchkitextension/ParentInterfaceController.h @@ -2,11 +2,11 @@ #import #import "WatchKitHelper.h" #import "WatchKitUIHelper.h" -#import "MMWormhole.h" - +#import @interface ParentInterfaceController : WKInterfaceController @property (nonatomic, strong) MMWormhole *wormhole; +@property (nonatomic, strong) MMWormholeSession *listeningWormhole; @property (weak, nonatomic) IBOutlet WKInterfaceGroup *wrapper; @@ -45,6 +45,4 @@ @property (retain, nonatomic) NSString *contextMenuButton3Callback; @property (retain, nonatomic) NSString *contextMenuButton4Callback; -- (NSString*) getAppGroup; - @end diff --git a/src/ios/resources/watchkitextension/ParentInterfaceController.m b/src/ios/resources/watchkitextension/ParentInterfaceController.m index 36b1307..e5b445c 100644 --- a/src/ios/resources/watchkitextension/ParentInterfaceController.m +++ b/src/ios/resources/watchkitextension/ParentInterfaceController.m @@ -1,191 +1,198 @@ #import "ParentInterfaceController.h" +#import "WormholeManager.h" + +NSString * const kNavigationIdentifier = @"fromjstowatchapp-navigation"; @implementation ParentInterfaceController - (void)awakeWithContext:(id)context { - [super awakeWithContext:context]; - - // hide everything initially - [self hideAllWidgets]; - - // Initialize the wormhole - self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:[self getAppGroup] - optionalDirectory:@"wormhole"]; + [super awakeWithContext:context]; + // hide everything initially + [self hideAllWidgets]; } // poor man's abstract method implementation - (NSString*) getPageID { - @throw [NSException exceptionWithName:NSInternalInconsistencyException - reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] - userInfo:nil]; + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; } -- (NSString*) getAppGroup { - NSString *appGroup = [NSString stringWithFormat:@"group.%@", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"]]; - return [appGroup stringByReplacingOccurrencesOfString:@".watchkitextension" withString:@""]; +- (NSString *)fromJsToWatchAppId { + return [@"fromjstowatchapp-" stringByAppendingString:[self getPageID]]; } - (id)contextForSegueWithIdentifier:(NSString*)segueIdentifier { - // here we can pass data to the next page's awakeWithContext method - return nil; + // here we can pass data to the next page's awakeWithContext method + return nil; } - (void)table:(WKInterfaceTable *)table didSelectRowAtIndex:(NSInteger)rowIndex { - if (self.tableCallback) { - [WatchKitHelper openParent:self.tableCallback withParams:[@(rowIndex) stringValue]]; - } else { - [WatchKitHelper logError:@"A table row was selected, but no callback was specified"]; - } + if (self.tableCallback) { + [WatchKitHelper openParent:self.tableCallback withParams:[@(rowIndex) stringValue]]; + + // Updating the listeners resolves a navigation issue. + [WormholeManager.sharedInstance.listeningWormhole stopListeningForMessageWithIdentifier:kNavigationIdentifier]; + [WormholeManager.sharedInstance.listeningWormhole listenForMessageWithIdentifier:kNavigationIdentifier listener:^(id messageObject) { + [self pushControllerWithName:[messageObject valueForKey:@"id"] context:messageObject]; + }]; + } else { + [WatchKitHelper logError:@"A table row was selected, but no callback was specified"]; + } } - (void) hideAllWidgets { - [self.label setHidden:YES]; - [self.label2 setHidden:YES]; - [self.table setHidden:YES]; - [self.image setHidden:YES]; - [self.switch1 setHidden:YES]; - [self.switch2 setHidden:YES]; - [self.map setHidden:YES]; - [self.sliderGroup setHidden:YES]; - [self.slider setHidden:YES]; - [self.sliderLabel setHidden:YES]; - [self.userInputButton setHidden:YES]; - [self.actionButton setHidden:YES]; - [self.pushNavButton setHidden:YES]; - [self.modalNavButton setHidden:YES]; + [self.label setHidden:YES]; + [self.label2 setHidden:YES]; + [self.table setHidden:YES]; + [self.image setHidden:YES]; + [self.switch1 setHidden:YES]; + [self.switch2 setHidden:YES]; + [self.map setHidden:YES]; + [self.sliderGroup setHidden:YES]; + [self.slider setHidden:YES]; + [self.sliderLabel setHidden:YES]; + [self.userInputButton setHidden:YES]; + [self.actionButton setHidden:YES]; + [self.pushNavButton setHidden:YES]; + [self.modalNavButton setHidden:YES]; } - (void) buildUI:(NSDictionary*)messageObject { - // the page title, in case we're not showing the root page - if ([messageObject valueForKey:@"title"] != nil) { - [self setTitle:[messageObject valueForKey:@"title"]]; - } else { - [self setTitle:nil]; - } - - // a menu, triggered by a force touch - if ([messageObject valueForKey:@"contextMenu"] != nil) { - [self clearAllMenuItems]; - NSArray *items = [[messageObject valueForKey:@"contextMenu"] valueForKey:@"items"]; - if (items == nil || items.count == 0) { - [WatchKitHelper logError:@"A 'contextMenu' was specified, but we can't find an 'items' array. The menu will not be rendered."]; + // the page title, in case we're not showing the root page + if ([messageObject valueForKey:@"title"] != nil) { + [self setTitle:[messageObject valueForKey:@"title"]]; } else { - self.contextMenuButton1Callback = [self addContextMenuItem:items forItemAtIndex:0 performSelector:@selector(contextMenuButton1Action)]; - self.contextMenuButton2Callback = [self addContextMenuItem:items forItemAtIndex:1 performSelector:@selector(contextMenuButton2Action)]; - self.contextMenuButton3Callback = [self addContextMenuItem:items forItemAtIndex:2 performSelector:@selector(contextMenuButton3Action)]; - self.contextMenuButton4Callback = [self addContextMenuItem:items forItemAtIndex:3 performSelector:@selector(contextMenuButton4Action)]; + [self setTitle:nil]; + } + + // a menu, triggered by a force touch + if ([messageObject valueForKey:@"contextMenu"] != nil) { + [self clearAllMenuItems]; + NSArray *items = [[messageObject valueForKey:@"contextMenu"] valueForKey:@"items"]; + if (items == nil || items.count == 0) { + [WatchKitHelper logError:@"A 'contextMenu' was specified, but we can't find an 'items' array. The menu will not be rendered."]; + } else { + self.contextMenuButton1Callback = [self addContextMenuItem:items forItemAtIndex:0 performSelector:@selector(contextMenuButton1Action)]; + self.contextMenuButton2Callback = [self addContextMenuItem:items forItemAtIndex:1 performSelector:@selector(contextMenuButton2Action)]; + self.contextMenuButton3Callback = [self addContextMenuItem:items forItemAtIndex:2 performSelector:@selector(contextMenuButton3Action)]; + self.contextMenuButton4Callback = [self addContextMenuItem:items forItemAtIndex:3 performSelector:@selector(contextMenuButton4Action)]; + } } - } - - [WatchKitUIHelper setGroup:self.wrapper fromDic:[messageObject valueForKey:@"group"]]; - [WatchKitUIHelper setLabel:self.label fromDic:[messageObject valueForKey:@"label"]]; - [WatchKitUIHelper setLabel:self.label2 fromDic:[messageObject valueForKey:@"label2"]]; - [WatchKitUIHelper setImage:self.image fromDic:[messageObject valueForKey:@"image"]]; - [WatchKitUIHelper setMap:self.map fromDic:[messageObject valueForKey:@"map"]]; - self.tableCallback = [WatchKitUIHelper setTable:self.table fromDic:[messageObject valueForKey:@"table"]]; - self.switchCallback = [WatchKitUIHelper setSwitch:self.switch1 fromDic:[messageObject valueForKey:@"switch"]]; - self.switch2Callback = [WatchKitUIHelper setSwitch:self.switch2 fromDic:[messageObject valueForKey:@"switch2"]]; - self.sliderCallback = [WatchKitUIHelper setSlider:self.slider withLabel:self.sliderLabel inGroup:self.sliderGroup fromDic:[messageObject valueForKey:@"slider"]]; - self.pushNavButtonCallback = [WatchKitUIHelper setButtonWithCallback:self.pushNavButton fromDic:[messageObject valueForKey:@"pushNavButton"]]; - self.modalNavButtonCallback = [WatchKitUIHelper setButtonWithCallback:self.modalNavButton fromDic:[messageObject valueForKey:@"modalNavButton"]]; - self.actionButtonCallback = [WatchKitUIHelper setButtonWithCallback:self.actionButton fromDic:[messageObject valueForKey:@"actionButton"]]; - self.userInputButtonDic = [WatchKitUIHelper setUserInputButton:self.userInputButton fromDic:[messageObject valueForKey:@"userInputButton"]]; + + [WatchKitUIHelper setGroup:self.wrapper fromDic:[messageObject valueForKey:@"group"]]; + [WatchKitUIHelper setLabel:self.label fromDic:[messageObject valueForKey:@"label"]]; + [WatchKitUIHelper setLabel:self.label2 fromDic:[messageObject valueForKey:@"label2"]]; + [WatchKitUIHelper setImage:self.image fromDic:[messageObject valueForKey:@"image"]]; + [WatchKitUIHelper setMap:self.map fromDic:[messageObject valueForKey:@"map"]]; + self.tableCallback = [WatchKitUIHelper setTable:self.table fromDic:[messageObject valueForKey:@"table"]]; + self.switchCallback = [WatchKitUIHelper setSwitch:self.switch1 fromDic:[messageObject valueForKey:@"switch"]]; + self.switch2Callback = [WatchKitUIHelper setSwitch:self.switch2 fromDic:[messageObject valueForKey:@"switch2"]]; + self.sliderCallback = [WatchKitUIHelper setSlider:self.slider withLabel:self.sliderLabel inGroup:self.sliderGroup fromDic:[messageObject valueForKey:@"slider"]]; + self.pushNavButtonCallback = [WatchKitUIHelper setButtonWithCallback:self.pushNavButton fromDic:[messageObject valueForKey:@"pushNavButton"]]; + self.modalNavButtonCallback = [WatchKitUIHelper setButtonWithCallback:self.modalNavButton fromDic:[messageObject valueForKey:@"modalNavButton"]]; + self.actionButtonCallback = [WatchKitUIHelper setButtonWithCallback:self.actionButton fromDic:[messageObject valueForKey:@"actionButton"]]; + self.userInputButtonDic = [WatchKitUIHelper setUserInputButton:self.userInputButton fromDic:[messageObject valueForKey:@"userInputButton"]]; } - (NSString*) addContextMenuItem:(NSArray*)items forItemAtIndex:(int)index performSelector:(SEL)selector { - if (items.count <= index) { - return nil; - } - NSDictionary* item = [items objectAtIndex:index]; - WKMenuItemIcon icon = [WatchKitUIHelper WKMenuItemIconFromString:[item valueForKey:@"iconNamed"]]; - [self addMenuItemWithItemIcon:icon title:[item valueForKey:@"title"] action:selector]; - return [item valueForKey:@"callback"]; + if (items.count <= index) { + return nil; + } + NSDictionary* item = [items objectAtIndex:index]; + WKMenuItemIcon icon = [WatchKitUIHelper WKMenuItemIconFromString:[item valueForKey:@"iconNamed"]]; + [self addMenuItemWithItemIcon:icon title:[item valueForKey:@"title"] action:selector]; + return [item valueForKey:@"callback"]; } - (IBAction)contextMenuButton1Action { - [WatchKitHelper openParent:self.contextMenuButton1Callback]; + [WatchKitHelper openParent:self.contextMenuButton1Callback]; } - (IBAction)contextMenuButton2Action { - [WatchKitHelper openParent:self.contextMenuButton2Callback]; + [WatchKitHelper openParent:self.contextMenuButton2Callback]; } - (IBAction)contextMenuButton3Action { - [WatchKitHelper openParent:self.contextMenuButton3Callback]; + [WatchKitHelper openParent:self.contextMenuButton3Callback]; } - (IBAction)contextMenuButton4Action { - [WatchKitHelper openParent:self.contextMenuButton4Callback]; + [WatchKitHelper openParent:self.contextMenuButton4Callback]; } - (IBAction)switchAction:(BOOL)on { - if (self.switchCallback) { - [WatchKitHelper openParent:self.switchCallback withParams:@(on ? "true" : "false")]; - } else { - [WatchKitHelper logError:@"No callback specified for switch"]; - } + if (self.switchCallback) { + [WatchKitHelper openParent:self.switchCallback withParams:@(on ? "true" : "false")]; + } else { + [WatchKitHelper logError:@"No callback specified for switch"]; + } } - (IBAction)switch2Action:(BOOL)on { - if (self.switch2Callback) { - [WatchKitHelper openParent:self.switch2Callback withParams:@(on ? "true" : "false")]; - } + if (self.switch2Callback) { + [WatchKitHelper openParent:self.switch2Callback withParams:@(on ? "true" : "false")]; + } } - (IBAction)sliderAction:(float)value { - NSString *str = [NSString stringWithFormat:@"%.f", value]; - [self.sliderLabel setText:str]; - [WatchKitHelper openParent:self.sliderCallback withParams:str]; + NSString *str = [NSString stringWithFormat:@"%.f", value]; + [self.sliderLabel setText:str]; + [WatchKitHelper openParent:self.sliderCallback withParams:str]; } - (IBAction)actionButtonAction { - [WatchKitHelper openParent:self.actionButtonCallback]; + [WatchKitHelper openParent:self.actionButtonCallback]; } - (IBAction)userInputButtonAction { - NSArray *suggestionsDef = [self.userInputButtonDic valueForKey:@"suggestions"]; - NSMutableArray *suggestions = [NSMutableArray arrayWithCapacity:suggestionsDef.count]; - for (NSInteger i = 0; i < suggestionsDef.count; i++) { - [suggestions addObject:suggestionsDef[i]]; - } - long inputMode; - NSString *inputModeDef = [self.userInputButtonDic valueForKey:@"inputMode"]; - if ([inputModeDef isEqualToString:@"WKTextInputModeAllowAnimatedEmoji"]) { - inputMode = WKTextInputModeAllowAnimatedEmoji; - } else if ([inputModeDef isEqualToString:@"WKTextInputModeAllowEmoji"]) { - inputMode = WKTextInputModeAllowEmoji; - } else { - inputMode = WKTextInputModePlain; - } - [self presentTextInputControllerWithSuggestions:suggestions allowedInputMode:inputMode completion:^(NSArray *results) { - // will be nil if dismissed - if (results != nil) { - // results is usually a String, but can be NSData (emoji image), see http://www.fiveminutewatchkit.com/blog/2015/3/15/how-to-get-text-input-from-the-user - [WatchKitHelper openParent:[self.userInputButtonDic objectForKey:@"callback"] withParams:results[0]]; + NSArray *suggestionsDef = [self.userInputButtonDic valueForKey:@"suggestions"]; + NSMutableArray *suggestions = [NSMutableArray arrayWithCapacity:suggestionsDef.count]; + for (NSInteger i = 0; i < suggestionsDef.count; i++) { + [suggestions addObject:suggestionsDef[i]]; } - }]; + long inputMode; + NSString *inputModeDef = [self.userInputButtonDic valueForKey:@"inputMode"]; + if ([inputModeDef isEqualToString:@"WKTextInputModeAllowAnimatedEmoji"]) { + inputMode = WKTextInputModeAllowAnimatedEmoji; + } else if ([inputModeDef isEqualToString:@"WKTextInputModeAllowEmoji"]) { + inputMode = WKTextInputModeAllowEmoji; + } else { + inputMode = WKTextInputModePlain; + } + [self presentTextInputControllerWithSuggestions:suggestions allowedInputMode:inputMode completion:^(NSArray *results) { + // will be nil if dismissed + if (results != nil) { + // results is usually a String, but can be NSData (emoji image), see http://www.fiveminutewatchkit.com/blog/2015/3/15/how-to-get-text-input-from-the-user + [WatchKitHelper openParent:[self.userInputButtonDic objectForKey:@"callback"] withParams:results[0]]; + } + }]; } // This method is called when watch view controller is about to be visible to the user - (void)willActivate { - [super willActivate]; - - NSString *wormholeIdentifier = [@"fromjstowatchapp-" stringByAppendingString:[self getPageID]]; - [self.wormhole listenForMessageWithIdentifier:wormholeIdentifier listener:^(id messageObject) { - [self buildUI:messageObject]; - }]; - - [WatchKitHelper openParent:[NSString stringWithFormat:@"applewatch.callback.onLoad%@Request", [self getPageID]]]; + [super willActivate]; + + [WormholeManager.sharedInstance listenForMessageWithIdentifier:self.fromJsToWatchAppId listener:^(id messageObject) { + [self buildUI:messageObject]; + }]; + [WormholeManager.sharedInstance.listeningWormhole listenForMessageWithIdentifier:kNavigationIdentifier listener:^(id messageObject) { + [self pushControllerWithName:[messageObject valueForKey:@"id"] context:messageObject]; + }]; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Execute just once otherwise we risk to get into a loop. + [WatchKitHelper openParent:[NSString stringWithFormat:@"applewatch.callback.onLoad%@Request", [self getPageID]]]; + }); } // This method is called when watch view controller is no longer visible - (void)didDeactivate { - [super didDeactivate]; - // be a good citizen.. btw, doing this here seems ugly but it's better than forgetting to do it in a subclass and leaving a mess - if (![@"main" isEqualToString:[self getPageID]]) { - NSString *wormholeIdentifier = [@"fromjstowatchapp-" stringByAppendingString:[self getPageID]]; - [self.wormhole stopListeningForMessageWithIdentifier:wormholeIdentifier]; - } + [super didDeactivate]; + // be a good citizen.. btw, doing this here seems ugly but it's better than forgetting to do it in a subclass and leaving a mess + + [WormholeManager.sharedInstance.listeningWormhole stopListeningForMessageWithIdentifier:kNavigationIdentifier]; + [WormholeManager.sharedInstance.listeningWormhole stopListeningForMessageWithIdentifier:self.fromJsToWatchAppId]; } @end \ No newline at end of file diff --git a/src/ios/resources/watchkitextension/WatchKitHelper.h b/src/ios/resources/watchkitextension/WatchKitHelper.h index 7dea79e..e60a3e5 100644 --- a/src/ios/resources/watchkitextension/WatchKitHelper.h +++ b/src/ios/resources/watchkitextension/WatchKitHelper.h @@ -1,4 +1,4 @@ -#import +#import @interface WatchKitHelper : NSObject diff --git a/src/ios/resources/watchkitextension/WatchKitHelper.m b/src/ios/resources/watchkitextension/WatchKitHelper.m index 3f6d761..49ef606 100644 --- a/src/ios/resources/watchkitextension/WatchKitHelper.m +++ b/src/ios/resources/watchkitextension/WatchKitHelper.m @@ -1,18 +1,23 @@ #import "WatchKitHelper.h" +#import "WormholeManager.h" +#import + +NSString * const kActionKey = @"action"; +NSString * const kParamsKey = @"params"; @implementation WatchKitHelper + (void) openParent:(NSString*)action { - [WKInterfaceController openParentApplication:@{@"action" : action} reply:nil]; + [WormholeManager.sharedInstance passMessageObject:@{kActionKey:[action dataUsingEncoding:NSUTF8StringEncoding]} identifier:kActionKey]; } + (void) openParent:(NSString*)action withParams:(NSString*)params { - [WKInterfaceController openParentApplication:@{@"action" : action, @"params" : params} reply:nil]; + [WormholeManager.sharedInstance passMessageObject:@{kActionKey:action, kParamsKey:params} identifier:kActionKey]; } + (void) logError:(NSString*) message { - NSLog(@"Error: %@", message); - [self openParent:@"applewatch.callback.onError" withParams:message]; + NSLog(@"Error: %@", message); + [self openParent:@"applewatch.callback.onError" withParams:message]; } -@end +@end \ No newline at end of file diff --git a/src/ios/resources/watchkitextension/WatchKitUIHelper.h b/src/ios/resources/watchkitextension/WatchKitUIHelper.h index 9396102..8ce2fb6 100644 --- a/src/ios/resources/watchkitextension/WatchKitUIHelper.h +++ b/src/ios/resources/watchkitextension/WatchKitUIHelper.h @@ -1,5 +1,6 @@ #import #import +#import #import "WatchKitHelper.h" #import "OneColumnSelectableRowType.h" #import "OneColumnRowType.h" diff --git a/src/ios/resources/watchkitextension/WatchKitUIHelper.m b/src/ios/resources/watchkitextension/WatchKitUIHelper.m index 4503103..31cdcfd 100644 --- a/src/ios/resources/watchkitextension/WatchKitUIHelper.m +++ b/src/ios/resources/watchkitextension/WatchKitUIHelper.m @@ -3,311 +3,311 @@ @implementation WatchKitUIHelper + (NSAttributedString*) getAttributedStringFrom:(NSDictionary*)dic { - NSString* value = [dic valueForKey:@"value"]; - if (value == nil) { - value = @""; - [WatchKitHelper logError:@"No 'value' specified, using '' as default"]; - } - NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:value]; - - NSString *hexColor = [dic valueForKey:@"color"]; - // default white - UIColor *color = hexColor == nil ? [UIColor whiteColor] : [self colorFromHexString:hexColor]; - [attrString addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, value.length)]; - - NSDictionary *fontdef = [dic valueForKey:@"font"]; - if (fontdef != nil && [fontdef valueForKey:@"size"] != nil) { - //NSString *fontface = [fontdef valueForKey:@"face"]; - NSNumber *fontsize = [fontdef valueForKey:@"size"]; - //if (fontface == nil && fontsize != nil) { - // fontface = @"HelveticaNeue-Bold"; // default (set defaults in JS?) - //} - int fsize = 11; - if (fontsize != nil) { - fsize = fontsize.intValue; + NSString* value = [dic valueForKey:@"value"]; + if (value == nil) { + value = @""; + [WatchKitHelper logError:@"No 'value' specified, using '' as default"]; } -// UIFont *font = [UIFont fontWithName:fontface size:fsize]; - UIFont *font = [UIFont systemFontOfSize:fsize]; - if (font != nil) { - // range can be used to apply the style to a substring of the label - [attrString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, value.length)]; + NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:value]; + + NSString *hexColor = [dic valueForKey:@"color"]; + // default white + UIColor *color = hexColor == nil ? [UIColor whiteColor] : [self colorFromHexString:hexColor]; + [attrString addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, value.length)]; + + NSDictionary *fontdef = [dic valueForKey:@"font"]; + if (fontdef != nil && [fontdef valueForKey:@"size"] != nil) { + //NSString *fontface = [fontdef valueForKey:@"face"]; + NSNumber *fontsize = [fontdef valueForKey:@"size"]; + //if (fontface == nil && fontsize != nil) { + // fontface = @"HelveticaNeue-Bold"; // default (set defaults in JS?) + //} + int fsize = 11; + if (fontsize != nil) { + fsize = fontsize.intValue; + } + // UIFont *font = [UIFont fontWithName:fontface size:fsize]; + UIFont *font = [UIFont systemFontOfSize:fsize]; + if (font != nil) { + // range can be used to apply the style to a substring of the label + [attrString addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, value.length)]; + } } - } - return attrString; + return attrString; } + (void) setLabel:(WKInterfaceLabel*)label fromDic:(NSDictionary*)dic { - if (dic == nil) { - [label setHidden:YES]; - } else { - [label setAttributedText:[self getAttributedStringFrom:dic]]; - [self setCommonPropertiesAndShow:label fromDic:dic]; - } + if (dic == nil) { + [label setHidden:YES]; + } else { + [label setAttributedText:[self getAttributedStringFrom:dic]]; + [self setCommonPropertiesAndShow:label fromDic:dic]; + } } + (void) setGroup:(WKInterfaceGroup*)group fromDic:(NSDictionary*)dic { - NSString *hexColor = [dic valueForKey:@"backgroundColor"]; - if (hexColor != nil) { - [group setBackgroundColor:[self colorFromHexString:hexColor]]; - } - - NSString *namedBackground = [dic valueForKey:@"backgroundImageNamed"]; - if (namedBackground != nil) { - [group setBackgroundImageNamed:namedBackground]; - } - - NSNumber *cornerRadius = [dic valueForKey:@"cornerRadius"]; - if (cornerRadius != nil) { - [group setCornerRadius:[cornerRadius floatValue]]; - } - - [self setCommonPropertiesAndShow:group fromDic:dic]; + NSString *hexColor = [dic valueForKey:@"backgroundColor"]; + if (hexColor != nil) { + [group setBackgroundColor:[self colorFromHexString:hexColor]]; + } + + NSString *namedBackground = [dic valueForKey:@"backgroundImageNamed"]; + if (namedBackground != nil) { + [group setBackgroundImageNamed:namedBackground]; + } + + NSNumber *cornerRadius = [dic valueForKey:@"cornerRadius"]; + if (cornerRadius != nil) { + [group setCornerRadius:[cornerRadius floatValue]]; + } + + [self setCommonPropertiesAndShow:group fromDic:dic]; } + (void) setImage:(WKInterfaceImage*)image fromDic:(NSDictionary*)dic { - if (dic == nil) { - [image setHidden:YES]; - } else { - NSString *namedSrc = [dic valueForKey:@"namedSrc"]; - - if (namedSrc != nil) { - [image setImageNamed:namedSrc]; + if (dic == nil) { + [image setHidden:YES]; } else { - // This implementation seems to use a cached version which gets lost between nav from glance to app, - // not a problem if developers use different images, but we can't expect that to be the case. -// [image setImageData:[dic valueForKey:@"src"]]; - - // so we're using this instead.. a bit less efficient but more failsafe - UIImage *img = [UIImage imageWithData:[dic valueForKey:@"src"]]; - [image setImage:img]; + NSString *namedSrc = [dic valueForKey:@"namedSrc"]; + + if (namedSrc != nil) { + [image setImageNamed:namedSrc]; + } else { + // This implementation seems to use a cached version which gets lost between nav from glance to app, + // not a problem if developers use different images, but we can't expect that to be the case. + // [image setImageData:[dic valueForKey:@"src"]]; + + // so we're using this instead.. a bit less efficient but more failsafe + UIImage *img = [UIImage imageWithData:[dic valueForKey:@"src"]]; + [image setImage:img]; + } + + [self setCommonPropertiesAndShow:image fromDic:dic]; } - - [self setCommonPropertiesAndShow:image fromDic:dic]; - } } + (void) setMap:(WKInterfaceMap*)map fromDic:(NSDictionary*)dic { - if (dic == nil) { - [map setHidden:YES]; - } else { - CLLocationCoordinate2D coordinate = [self makeCoordinate:[dic valueForKey:@"center"]]; - NSNumber *zoom = [dic valueForKey:@"zoom"]; - if (zoom == nil) { - [WatchKitHelper logError:@"No 'zoom' specified, using '0.1' as default"]; - zoom = [NSNumber numberWithFloat:0.1]; - } - MKCoordinateSpan span = MKCoordinateSpanMake([zoom floatValue], [zoom floatValue]); - MKCoordinateRegion region = MKCoordinateRegionMake(coordinate, span); - [map setRegion:region]; - - [map removeAllAnnotations]; - NSArray *annotations = [dic valueForKey:@"annotations"]; - for (int i = 0; i < annotations.count; i++) { - NSDictionary* anDef = annotations[i]; - [map addAnnotation:[self makeCoordinate:anDef] withPinColor:[self WKInterfaceMapPinColorFromString:[anDef valueForKey:@"pinColor"]]]; + if (dic == nil) { + [map setHidden:YES]; + } else { + CLLocationCoordinate2D coordinate = [self makeCoordinate:[dic valueForKey:@"center"]]; + NSNumber *zoom = [dic valueForKey:@"zoom"]; + if (zoom == nil) { + [WatchKitHelper logError:@"No 'zoom' specified, using '0.1' as default"]; + zoom = [NSNumber numberWithFloat:0.1]; + } + MKCoordinateSpan span = MKCoordinateSpanMake([zoom floatValue], [zoom floatValue]); + MKCoordinateRegion region = MKCoordinateRegionMake(coordinate, span); + [map setRegion:region]; + + [map removeAllAnnotations]; + NSArray *annotations = [dic valueForKey:@"annotations"]; + for (int i = 0; i < annotations.count; i++) { + NSDictionary* anDef = annotations[i]; + [map addAnnotation:[self makeCoordinate:anDef] withPinColor:[self WKInterfaceMapPinColorFromString:[anDef valueForKey:@"pinColor"]]]; + } + [self setCommonPropertiesAndShow:map fromDic:dic]; } - [self setCommonPropertiesAndShow:map fromDic:dic]; - } } + (CLLocationCoordinate2D) makeCoordinate:(NSDictionary*) dic { - NSNumber *lat = [dic valueForKey:@"lat"]; - NSNumber *lng = [dic valueForKey:@"lng"]; - if (lat == nil || lng == nil) { - [WatchKitHelper logError:@"Please specify 'lat' and 'lng', using defaults (0) which is an odd place btw."]; - } - return CLLocationCoordinate2DMake([lat floatValue], [lng floatValue]); + NSNumber *lat = [dic valueForKey:@"lat"]; + NSNumber *lng = [dic valueForKey:@"lng"]; + if (lat == nil || lng == nil) { + [WatchKitHelper logError:@"Please specify 'lat' and 'lng', using defaults (0) which is an odd place btw."]; + } + return CLLocationCoordinate2DMake([lat floatValue], [lng floatValue]); } + (void) setButton:(WKInterfaceButton*)button fromDic:(NSDictionary*)dic { - if (dic == nil) { - [button setHidden:YES]; - } else { - [button setAttributedTitle:[self getAttributedStringFrom:[dic valueForKey:@"title"]]]; - - NSString *hexColor = [dic valueForKey:@"backgroundColor"]; - if (hexColor != nil) { - UIColor *theColor = [self colorFromHexString:hexColor]; - [button setBackgroundColor:theColor]; + if (dic == nil) { + [button setHidden:YES]; + } else { + [button setAttributedTitle:[self getAttributedStringFrom:[dic valueForKey:@"title"]]]; + + NSString *hexColor = [dic valueForKey:@"backgroundColor"]; + if (hexColor != nil) { + UIColor *theColor = [self colorFromHexString:hexColor]; + [button setBackgroundColor:theColor]; + } + [self setCommonPropertiesAndShow:button fromDic:dic]; } - [self setCommonPropertiesAndShow:button fromDic:dic]; - } } + (NSString*) setButtonWithCallback:(WKInterfaceButton*)button fromDic:(NSDictionary*)dic { - [self setButton:button fromDic:dic]; - return dic == nil ? nil : [dic valueForKey:@"callback"]; + [self setButton:button fromDic:dic]; + return dic == nil ? nil : [dic valueForKey:@"callback"]; } + (NSDictionary*) setUserInputButton:(WKInterfaceButton*)button fromDic:(NSDictionary*)dic { - [self setButton:button fromDic:dic]; - return dic; + [self setButton:button fromDic:dic]; + return dic; } + (NSString*) setSwitch:(WKInterfaceSwitch*)switch1 fromDic:(NSDictionary*)dic { - if (dic == nil) { - [switch1 setHidden:YES]; - return nil; - } else { - [switch1 setTitle:[dic valueForKey:@"title"]]; - NSNumber *on = (NSNumber *)[dic objectForKey: @"on"]; - [switch1 setOn:[on boolValue]]; - - NSString *hexColor = [dic valueForKey:@"color"]; - if (hexColor != nil) { - UIColor *theColor = [self colorFromHexString:hexColor]; - [switch1 setColor:theColor]; + if (dic == nil) { + [switch1 setHidden:YES]; + return nil; + } else { + [switch1 setTitle:[dic valueForKey:@"title"]]; + NSNumber *on = (NSNumber *)[dic objectForKey: @"on"]; + [switch1 setOn:[on boolValue]]; + + NSString *hexColor = [dic valueForKey:@"color"]; + if (hexColor != nil) { + UIColor *theColor = [self colorFromHexString:hexColor]; + [switch1 setColor:theColor]; + } + [self setCommonPropertiesAndShow:switch1 fromDic:dic]; + return [dic valueForKey:@"callback"]; } - [self setCommonPropertiesAndShow:switch1 fromDic:dic]; - return [dic valueForKey:@"callback"]; - } } + (NSString*) setSlider:(WKInterfaceSlider*)slider withLabel:(WKInterfaceLabel*)label inGroup:(WKInterfaceGroup*)group fromDic:(NSDictionary*)dic { - if (dic == nil) { - [group setHidden:YES]; - [slider setHidden:YES]; - [label setHidden:YES]; - return nil; - } else { - NSNumber* value = [dic objectForKey: @"value"]; - [slider setValue:[value floatValue]]; - - NSNumber* steps = [dic objectForKey: @"steps"]; - [slider setNumberOfSteps:[steps integerValue]]; - - NSString *hexColor = [dic valueForKey:@"color"]; - if (hexColor != nil) { - UIColor *theColor = [self colorFromHexString:hexColor]; - [slider setColor:theColor]; - } - - NSNumber *hideValue = (NSNumber *)[dic objectForKey: @"hideValue"]; - if (![hideValue boolValue]) { - [label setHidden:NO]; - [label setText:[NSString stringWithFormat:@"%.f", [value floatValue]]]; + if (dic == nil) { + [group setHidden:YES]; + [slider setHidden:YES]; + [label setHidden:YES]; + return nil; + } else { + NSNumber* value = [dic objectForKey: @"value"]; + [slider setValue:[value floatValue]]; + + NSNumber* steps = [dic objectForKey: @"steps"]; + [slider setNumberOfSteps:[steps integerValue]]; + + NSString *hexColor = [dic valueForKey:@"color"]; + if (hexColor != nil) { + UIColor *theColor = [self colorFromHexString:hexColor]; + [slider setColor:theColor]; + } + + NSNumber *hideValue = (NSNumber *)[dic objectForKey: @"hideValue"]; + if (![hideValue boolValue]) { + [label setHidden:NO]; + [label setText:[NSString stringWithFormat:@"%.f", [value floatValue]]]; + } + + [self setCommonPropertiesAndShow:slider fromDic:dic]; + [group setHidden:NO]; + return [dic valueForKey:@"callback"]; } - - [self setCommonPropertiesAndShow:slider fromDic:dic]; - [group setHidden:NO]; - return [dic valueForKey:@"callback"]; - } } + (NSString*) setTable:(WKInterfaceTable*)table fromDic:(NSDictionary*)dic { - if (dic == nil) { - [table setHidden:YES]; - return nil; - } else { - NSMutableArray *rowTypes = [NSMutableArray arrayWithCapacity:2]; - NSArray *rows = [dic valueForKey:@"rows"]; - for (NSInteger i = 0; i < rows.count; i++) { - NSDictionary* rowDef = rows[i]; - NSString* rowType = [rowDef objectForKey:@"type"]; - [rowTypes addObject:rowType]; - } - [table setRowTypes:rowTypes]; - - for (NSInteger i = 0; i < rows.count; i++) { - NSDictionary* rowDef = rows[i]; - - if ([rowTypes[i] isEqualToString:@"OneColumnSelectableRowType"]) { - OneColumnSelectableRowType* row = [table rowControllerAtIndex:i]; - [self setLabel:row.label fromDic:[rowDef objectForKey:@"label"]]; - [self setGroup:row.group fromDic:[rowDef objectForKey:@"group"]]; - [self setImage:row.imageLeft fromDic:[rowDef objectForKey:@"imageLeft"]]; - [self setImage:row.imageRight fromDic:[rowDef objectForKey:@"imageRight"]]; - - } else if ([rowTypes[i] isEqualToString:@"OneColumnRowType"]) { - OneColumnRowType* row = [table rowControllerAtIndex:i]; - [self setLabel:row.label fromDic:[rowDef objectForKey:@"label"]]; - [self setGroup:row.group fromDic:[rowDef objectForKey:@"group"]]; - [self setImage:row.imageLeft fromDic:[rowDef objectForKey:@"imageLeft"]]; - [self setImage:row.imageRight fromDic:[rowDef objectForKey:@"imageRight"]]; - - } else if ([rowTypes[i] isEqualToString:@"TwoColumnsRowType"]) { - TwoColumnsRowType* row = [table rowControllerAtIndex:i]; - [self setLabel:row.col1label fromDic:[rowDef objectForKey:@"col1label"]]; - [self setLabel:row.col2label fromDic:[rowDef objectForKey:@"col2label"]]; - [self setImage:row.col1image fromDic:[rowDef objectForKey:@"col1image"]]; - [self setImage:row.col2image fromDic:[rowDef objectForKey:@"col2image"]]; - [self setGroup:row.group fromDic:[rowDef objectForKey:@"group"]]; - } + if (dic == nil) { + [table setHidden:YES]; + return nil; + } else { + NSMutableArray *rowTypes = [NSMutableArray arrayWithCapacity:2]; + NSArray *rows = [dic valueForKey:@"rows"]; + for (NSInteger i = 0; i < rows.count; i++) { + NSDictionary* rowDef = rows[i]; + NSString* rowType = [rowDef objectForKey:@"type"]; + [rowTypes addObject:rowType]; + } + [table setRowTypes:rowTypes]; + + for (NSInteger i = 0; i < rows.count; i++) { + NSDictionary* rowDef = rows[i]; + + if ([rowTypes[i] isEqualToString:@"OneColumnSelectableRowType"]) { + OneColumnSelectableRowType* row = [table rowControllerAtIndex:i]; + [self setLabel:row.label fromDic:[rowDef objectForKey:@"label"]]; + [self setGroup:row.group fromDic:[rowDef objectForKey:@"group"]]; + [self setImage:row.imageLeft fromDic:[rowDef objectForKey:@"imageLeft"]]; + [self setImage:row.imageRight fromDic:[rowDef objectForKey:@"imageRight"]]; + + } else if ([rowTypes[i] isEqualToString:@"OneColumnRowType"]) { + OneColumnRowType* row = [table rowControllerAtIndex:i]; + [self setLabel:row.label fromDic:[rowDef objectForKey:@"label"]]; + [self setGroup:row.group fromDic:[rowDef objectForKey:@"group"]]; + [self setImage:row.imageLeft fromDic:[rowDef objectForKey:@"imageLeft"]]; + [self setImage:row.imageRight fromDic:[rowDef objectForKey:@"imageRight"]]; + + } else if ([rowTypes[i] isEqualToString:@"TwoColumnsRowType"]) { + TwoColumnsRowType* row = [table rowControllerAtIndex:i]; + [self setLabel:row.col1label fromDic:[rowDef objectForKey:@"col1label"]]; + [self setLabel:row.col2label fromDic:[rowDef objectForKey:@"col2label"]]; + [self setImage:row.col1image fromDic:[rowDef objectForKey:@"col1image"]]; + [self setImage:row.col2image fromDic:[rowDef objectForKey:@"col2image"]]; + [self setGroup:row.group fromDic:[rowDef objectForKey:@"group"]]; + } + } + [self setCommonPropertiesAndShow:table fromDic:dic]; + return [dic valueForKey:@"callback"]; } - [self setCommonPropertiesAndShow:table fromDic:dic]; - return [dic valueForKey:@"callback"]; - } } // Assumes input like "#00FF00" (#RRGGBB). Will be black on bogus input + (UIColor *)colorFromHexString:(NSString *)hexString { - unsigned rgbValue = 0; - NSScanner *scanner = [NSScanner scannerWithString:hexString]; - [scanner setScanLocation:1]; // bypass '#' character - [scanner scanHexInt:&rgbValue]; - return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; + unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + [scanner setScanLocation:1]; // bypass '#' character + [scanner scanHexInt:&rgbValue]; + return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; } #pragma st00pid ObjC enums + (WKMenuItemIcon) WKMenuItemIconFromString:(NSString*)str { - if ([str isEqualToString:@"accept"]) return WKMenuItemIconAccept; - else if ([str isEqualToString:@"add"]) return WKMenuItemIconAdd; - else if ([str isEqualToString:@"block"]) return WKMenuItemIconBlock; - else if ([str isEqualToString:@"decline"]) return WKMenuItemIconDecline; - else if ([str isEqualToString:@"info"]) return WKMenuItemIconInfo; - else if ([str isEqualToString:@"maybe"]) return WKMenuItemIconMaybe; - else if ([str isEqualToString:@"more"]) return WKMenuItemIconMore; - else if ([str isEqualToString:@"mute"]) return WKMenuItemIconMute; - else if ([str isEqualToString:@"pause"]) return WKMenuItemIconPause; - else if ([str isEqualToString:@"play"]) return WKMenuItemIconPlay; - else if ([str isEqualToString:@"repeat"]) return WKMenuItemIconRepeat; - else if ([str isEqualToString:@"resume"]) return WKMenuItemIconResume; - else if ([str isEqualToString:@"share"]) return WKMenuItemIconShare; - else if ([str isEqualToString:@"shuffle"]) return WKMenuItemIconShuffle; - else if ([str isEqualToString:@"speaker"]) return WKMenuItemIconSpeaker; - else if ([str isEqualToString:@"trash"]) return WKMenuItemIconTrash; - else { - [WatchKitHelper logError:@"No 'iconNamed' specified, using 'info' by default"]; - return WKMenuItemIconInfo; - } + if ([str isEqualToString:@"accept"]) return WKMenuItemIconAccept; + else if ([str isEqualToString:@"add"]) return WKMenuItemIconAdd; + else if ([str isEqualToString:@"block"]) return WKMenuItemIconBlock; + else if ([str isEqualToString:@"decline"]) return WKMenuItemIconDecline; + else if ([str isEqualToString:@"info"]) return WKMenuItemIconInfo; + else if ([str isEqualToString:@"maybe"]) return WKMenuItemIconMaybe; + else if ([str isEqualToString:@"more"]) return WKMenuItemIconMore; + else if ([str isEqualToString:@"mute"]) return WKMenuItemIconMute; + else if ([str isEqualToString:@"pause"]) return WKMenuItemIconPause; + else if ([str isEqualToString:@"play"]) return WKMenuItemIconPlay; + else if ([str isEqualToString:@"repeat"]) return WKMenuItemIconRepeat; + else if ([str isEqualToString:@"resume"]) return WKMenuItemIconResume; + else if ([str isEqualToString:@"share"]) return WKMenuItemIconShare; + else if ([str isEqualToString:@"shuffle"]) return WKMenuItemIconShuffle; + else if ([str isEqualToString:@"speaker"]) return WKMenuItemIconSpeaker; + else if ([str isEqualToString:@"trash"]) return WKMenuItemIconTrash; + else { + [WatchKitHelper logError:@"No 'iconNamed' specified, using 'info' by default"]; + return WKMenuItemIconInfo; + } } + (WKInterfaceMapPinColor) WKInterfaceMapPinColorFromString:(NSString*)str { - if ([str isEqualToString:@"green"]) return WKInterfaceMapPinColorGreen; - else if ([str isEqualToString:@"purple"]) return WKInterfaceMapPinColorPurple; - else if ([str isEqualToString:@"red"]) return WKInterfaceMapPinColorRed; - else { - [WatchKitHelper logError:@"No 'pinColor' specified, using 'red' by default"]; - return WKInterfaceMapPinColorRed; - } + if ([str isEqualToString:@"green"]) return WKInterfaceMapPinColorGreen; + else if ([str isEqualToString:@"purple"]) return WKInterfaceMapPinColorPurple; + else if ([str isEqualToString:@"red"]) return WKInterfaceMapPinColorRed; + else { + [WatchKitHelper logError:@"No 'pinColor' specified, using 'red' by default"]; + return WKInterfaceMapPinColorRed; + } } # pragma common methods in WKInterfaceObject + (void) setCommonPropertiesAndShow:(WKInterfaceObject*) obj fromDic:(NSDictionary*)dic { - [self setAlpha:obj fromDic:dic]; - [self setWidth:obj fromDic:dic]; - [self setHeight:obj fromDic:dic]; - - [obj setHidden:NO]; + [self setAlpha:obj fromDic:dic]; + [self setWidth:obj fromDic:dic]; + [self setHeight:obj fromDic:dic]; + + [obj setHidden:NO]; } + (void) setAlpha:(WKInterfaceObject*) obj fromDic:(NSDictionary*)dic { - NSNumber *alpha = [dic valueForKey:@"alpha"]; - [obj setAlpha:alpha == nil ? 1 /* full opaque (solid) */ : alpha.doubleValue]; + NSNumber *alpha = [dic valueForKey:@"alpha"]; + [obj setAlpha:alpha == nil ? 1 /* full opaque (solid) */ : alpha.doubleValue]; } + (void) setWidth:(WKInterfaceObject*) obj fromDic:(NSDictionary*)dic { - NSNumber *width = [dic valueForKey:@"width"]; - if (width != nil) { - [obj setWidth:width.doubleValue]; - } + NSNumber *width = [dic valueForKey:@"width"]; + if (width != nil) { + [obj setWidth:width.doubleValue]; + } } + (void) setHeight:(WKInterfaceObject*) obj fromDic:(NSDictionary*)dic { - NSNumber *height = [dic valueForKey:@"height"]; - if (height != nil) { - [obj setHeight:height.doubleValue]; - } + NSNumber *height = [dic valueForKey:@"height"]; + if (height != nil) { + [obj setHeight:height.doubleValue]; + } } @end \ No newline at end of file diff --git a/src/ios/resources/watchkitextension/WormholeManager.h b/src/ios/resources/watchkitextension/WormholeManager.h new file mode 100644 index 0000000..9474ee1 --- /dev/null +++ b/src/ios/resources/watchkitextension/WormholeManager.h @@ -0,0 +1,25 @@ +// +// WormholeManager.h +// MMWormhole +// +// Created by Toma Popov on 12/29/15. +// Copyright © 2015 Conrad Stoll. All rights reserved. +// + +#import +#import "MMWormholeUmbrella.h" + +@interface WormholeManager : NSObject + ++ (instancetype)sharedInstance; + +- (void)listenForMessageWithIdentifier:(NSString *)identifier + listener:(void (^)(id messageObject))listener; + +- (void)passMessageObject:(id )messageObject + identifier:(NSString *)identifier; + +@property (nonatomic, strong, readonly) MMWormhole *wormhole; +@property (nonatomic, strong, readonly) MMWormholeSession *listeningWormhole; + +@end diff --git a/src/ios/resources/watchkitextension/WormholeManager.m b/src/ios/resources/watchkitextension/WormholeManager.m new file mode 100644 index 0000000..2f5c990 --- /dev/null +++ b/src/ios/resources/watchkitextension/WormholeManager.m @@ -0,0 +1,76 @@ +// +// WormholeManager.m +// MMWormhole +// +// Created by Toma Popov on 12/29/15. +// Copyright © 2015 Conrad Stoll. All rights reserved. +// + +#import "WormholeManager.h" + +@interface WormholeManager () + +@property (nonatomic, strong) MMWormhole *wormhole; +@property (nonatomic, strong) MMWormholeSession *listeningWormhole; + +@end + +@implementation WormholeManager + +//MARK: Constructors + +static WormholeManager *_sharedInstance; + ++ (instancetype)sharedInstance { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (!_sharedInstance) { + _sharedInstance = WormholeManager.new; + // Initialize the wormhole + _sharedInstance.listeningWormhole = MMWormholeSession.sharedListeningSession; + // Make sure we are activating the listening wormhole so that it will receive new messages from + // the WatchConnectivity framework. + [_sharedInstance.listeningWormhole activateSessionListening]; + _sharedInstance.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:_sharedInstance.getAppGroup + optionalDirectory:@"wormhole" + transitingType:MMWormholeTransitingTypeSessionMessage]; + [_sharedInstance.listeningWormhole clearAllMessageContents]; + [_sharedInstance.wormhole clearAllMessageContents]; + [_sharedInstance.wormhole.wormholeMessenger deleteContentForIdentifier:@"fromjstowatchapp-navigation"]; + [_sharedInstance.wormhole.wormholeMessenger deleteContentForAllMessages]; + + [[NSNotificationCenter defaultCenter] removeObserver:_sharedInstance.listeningWormhole]; + + CFNotificationCenterRef const center = CFNotificationCenterGetDarwinNotifyCenter(); + CFNotificationCenterRemoveEveryObserver(center, (__bridge const void *)(_sharedInstance.listeningWormhole)); + + [[NSNotificationCenter defaultCenter] removeObserver:_sharedInstance.wormhole]; + + CFNotificationCenterRemoveEveryObserver(center, (__bridge const void *)(_sharedInstance.wormhole)); + + } + }); + + return _sharedInstance; +} + +//MARK: Public methods + +- (void)listenForMessageWithIdentifier:(NSString *)identifier + listener:(void (^)(id messageObject))listener { + [_sharedInstance.listeningWormhole listenForMessageWithIdentifier:identifier listener:listener]; +} + +- (void)passMessageObject:(id )messageObject + identifier:(NSString *)identifier { + [_sharedInstance.wormhole passMessageObject:messageObject identifier:identifier]; +} + +//MARK: Utilities + +- (NSString*)getAppGroup { + NSString *appGroup = [NSString stringWithFormat:@"group.%@", [NSBundle.mainBundle.infoDictionary objectForKey:@"CFBundleIdentifier"]]; + return [appGroup stringByReplacingOccurrencesOfString:@".watchkitapp.watchkitextension" withString:@""]; +} + +@end