<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[watchkit - Curtis Herbert]]></title><description><![CDATA[watchkit - Curtis Herbert]]></description><link>https://blog.curtisherbert.com/</link><image><url>https://blog.curtisherbert.com/favicon.png</url><title>watchkit - Curtis Herbert</title><link>https://blog.curtisherbert.com/</link></image><generator>Ghost 4.32</generator><lastBuildDate>Sat, 16 Nov 2024 19:48:11 GMT</lastBuildDate><atom:link href="https://blog.curtisherbert.com/tag/watchkit/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[WatchKit App Automated Build Numbers]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>I wrote <a href="https://blog.curtisherbert.com/automated-build-numbers/">previously</a> about how I set up automated build numbers that were source-control friendly and didn&apos;t require a build server.</p>
<p>Unfortunately WatchKit apps (unlike WatchKit extensions) don&apos;t have build phases exposed in Xcode, so things get tricky.</p>
<p>After a month of trying I couldn&apos;</p>]]></description><link>https://blog.curtisherbert.com/watchkit-app-versioning/</link><guid isPermaLink="false">597e0854b3f09c242028539d</guid><category><![CDATA[watchkit]]></category><category><![CDATA[xcode]]></category><dc:creator><![CDATA[Curtis herbert]]></dc:creator><pubDate>Sat, 11 Apr 2015 03:42:46 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>I wrote <a href="https://blog.curtisherbert.com/automated-build-numbers/">previously</a> about how I set up automated build numbers that were source-control friendly and didn&apos;t require a build server.</p>
<p>Unfortunately WatchKit apps (unlike WatchKit extensions) don&apos;t have build phases exposed in Xcode, so things get tricky.</p>
<p>After a month of trying I couldn&apos;t find a way to get automated numbering working through build scripts in the project itself. It always resulted in code signing errors as the best you could do with phases was change the number after the fact. I ended up having to fall back to using <code>agvtool</code> during release builds to manually bump (or at least using <a href="https://fastlane.tools">Fastlane</a>, automatically bump) build numbers.</p>
<p>Then a friend pointed me in the right direction.</p>
<h2 id="manuallyeditingpbxprojforfunandprofit">Manually editing .pbxproj for fun and profit</h2>
<p>While the UI isn&apos;t exposed, you can still add build phases to the project file for a watchkit app.</p>
<p><mark>Please back up your project first. You&apos;ll be manually editing the Xcode project file here.</mark></p>
<p>In your app&apos;s folder right click the project file and show package contents. Then open <code>project.pbxproj</code> in your favorite text editor.</p>
<p><strong>Step 1:</strong> We need to add a new build phase manually. I&apos;d recommend you complete my previous tutorial first so you have existing phases for automating your build number that you can just duplicate.</p>
<p>Search the file for &quot;CFBundleVersion&quot;, part of the script from the last tutorial. You should see it in a block like this left over from my last tutorial:</p>
<pre><code>81B5AC951B0A816900E3CD52 /* Build Number */ = {
  isa = PBXShellScriptBuildPhase;
  buildActionMask = 2147483647;
  files = (
  );
  inputPaths = (
  );
  name = &quot;Build Number&quot;;
  outputPaths = (
  );
  runOnlyForDeploymentPostprocessing = 0;
  shellPath = /bin/sh;
  shellScript = &quot;git=`sh /etc/profile; which git`\nbranch_name=`$git symbolic-ref HEAD | sed -e &apos;s,.*/\\\\(.*\\\\),\\\\1,&apos;`\ngit_count=`$git rev-list $branch_name |wc -l | sed &apos;s/^ *//;s/ *$//&apos;`\nsimple_branch_name=`$git rev-parse --abbrev-ref HEAD`\n\nbuild_number=\&quot;$git_count\&quot;\n\nplist=\&quot;${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\&quot;\ndsym_plist=\&quot;${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\&quot;\n\n/usr/libexec/PlistBuddy -c \&quot;Set :CFBundleVersion $build_number\&quot; \&quot;$plist\&quot;\nif [ -f \&quot;$DSYM_INFO_PLIST\&quot; ] ; then\n/usr/libexec/PlistBuddy -c \&quot;Set :CFBundleVersion $build_number\&quot; \&quot;$dsym_plist\&quot;\nfi&quot;;
};
</code></pre>
<p>Copy and paste that block to duplicate it right below the first one, and then change the ID (<code>81B5AC951B0A816900E3CD52</code> in my case) to something unique. I just changed the ending 2 to a 1. Do a quick search with the new ID to make sure you didn&apos;t accidentally pick an ID that&apos;s already been used elsewhere in the file.</p>
<p>Remember this ID you just made, we need it for step 2.</p>
<p><strong>Step 2:</strong> We need to add a new build phase to the watchkit app. Do a search on the file for &quot;com.apple.product-type.application.watchapp&quot; to find the config used by the watchkit app. It&apos;ll look like this:</p>
<pre><code>81B0136E1A6ABD5C0080C8C5 /* WatchKit App */ = {
	isa = PBXNativeTarget;
	buildConfigurationList = 81B013841A6ABD5C0080C8C5 /* Build configuration list for PBXNativeTarget &quot;WatchKit App&quot; */;
	buildPhases = (
		81B0136D1A6ABD5C0080C8C5 /* Resources */,
	);
	buildRules = (
	);
	dependencies = (
	);
	name = &quot;WatchKit App&quot;;
	productName = &quot;Slopes WatchKit App&quot;;
	productReference = 81B0136F1A6ABD5C0080C8C5 /* WatchKit App.app */;
	productType = &quot;com.apple.product-type.application.watchapp&quot;;
};
</code></pre>
<p>Add a new line to the build phases using the ID of the phase you created from the last step.</p>
<pre><code>buildPhases = (
  81B5AC951B0A816900E3CD51 /* Build Number */,
  81B0136D1A6ABD5C0080C8C5 /* Resources */,
);
</code></pre>
<p>At this point you can save and run and have automated build numbers working for your watchkit app too, satisfying Xcode 6.3&apos;s requirement that all your targets must share the same build number.</p>
<p>Big thanks to Conrad Kramer for getting me thinking in the right direction.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Getting data to your WatchKit app]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p><mark>Update Aug 27, 2015</mark>: I&apos;ve written an <a href="https://blog.curtisherbert.com/data-synchronization-with-watchos/">updated post</a> for WatchOS 2+. This article will still work with WatchKit based apps running on WatchOS 2, but if you&apos;re planning on writing a native WatchOS app check the new article out.</p>
<hr>
<p>With WatchKit beta 2 Apple provided</p>]]></description><link>https://blog.curtisherbert.com/data-synchronization-with-watchkit/</link><guid isPermaLink="false">597e0854b3f09c2420285397</guid><category><![CDATA[watchkit]]></category><dc:creator><![CDATA[Curtis herbert]]></dc:creator><pubDate>Wed, 21 Jan 2015 19:53:00 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p><mark>Update Aug 27, 2015</mark>: I&apos;ve written an <a href="https://blog.curtisherbert.com/data-synchronization-with-watchos/">updated post</a> for WatchOS 2+. This article will still work with WatchKit based apps running on WatchOS 2, but if you&apos;re planning on writing a native WatchOS app check the new article out.</p>
<hr>
<p>With WatchKit beta 2 Apple provided an easy new way for developers to ask their iPhone app (what Apple is calling the &quot;parent&quot; app) for data: <code>openParentApplication:reply:</code> on <code>WKInterfaceController</code>. That works great for many situations, but Apple has also <a href="https://devforums.apple.com/message/1082630#1082630">recommended</a> the use of the C-level <a href="https://developer.apple.com/library/ios/documentation/CoreFoundation/Reference/CFNotificationCenterRef/index.html">Dawrin notification center</a> for communication.</p>
<p>Add these on top of the existing use of <code>NSUserDefaults initWithSuite:</code> that was introduced for extension communication and we&apos;re got a lot of ways to communicate.</p>
<h2 id="method1marcopoloopenparentapp">Method 1: Marco, Polo: OpenParentApp</h2>
<p><code>openParentApplication:reply:</code> is a two-way communication system, but it can <em>only</em> be triggered from the WatchKit extension. That&apos;s a pretty big gotcha, but most of the time it works out OK.</p>
<p>In your WatchKit extension you can call <code>openParentApplication:reply:</code> on <code>WKInterfaceController</code> and pass along a dictionary to your main app. You&apos;ll probably want to include details on why your parent app is being called, and any other data the iPhone app would need to respond. You&apos;ll get back a dictionary of data as a reply:</p>
<pre><code>[WKInterfaceController openParentApplication:@{@&quot;action&quot; : @&quot;getUserCount&quot;} reply:^(NSDictionary *replyInfo, NSError *error) {
  if (error) {
  	NSLog(@&quot;Error from parent: %@&quot;, error);
  } else {
  	//do something with the reply info....
  }
}];
</code></pre>
<p>When you call the above your iPhone app is launched and <code>application:handleWatchKitExtensionRequest:reply:</code> on your appdelegate is passed that dictionary you set up. You&apos;re expected to call the reply block when you&apos;re done to let the system know you&apos;ve completed any task you needed to execute to complete the response:</p>
<pre><code>- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply {
	//look at the userInfo dictionary to figure out why we&apos;re being called
	//do some stuff
    //send back a dictionary of data to the watchkit extension
    reply(@{@&quot;numberOfUsers&quot;: @(5)})
}
</code></pre>
<p><strong>Pros and cons</strong></p>
<p>There are some very nice advantages to this method. You might have noticed that I said your iPhone app will be <em>launched</em>? The biggest advantage here is that your app will be called and given the opportunity to respond, <mark>even if it wasn&apos;t currently running</mark>.</p>
<p>Another great use for this method of communication is asking your app to do something. For example you might have a music app that&apos;s running in the background and you want to pause it. <code>openParentApplication:reply:</code> is perfect for that -- you don&apos;t care about the reply, you&apos;ll just use the dictionary to pass a pause command.</p>
<p>One important gotcha is that while this will launch your app if needed, it won&apos;t bring your app to the foreground; the app is launched in the background state (this means it&apos;s impossible to force your UI to appear if it isn&apos;t already on-screen). If your app wasn&apos;t already running it will be terminated after you reply to the request, unless you trigger a background mode to start (location updates, audio, etc).</p>
<p>This can be a little tricky because all the normal app lifecycle stuff will still happen -- <code>application:willFinishLaunchingWithOptions:</code> is still called, your storyboard is loaded, etc (more on that in a second). So just be aware that while your UI code will be loaded it can&apos;t be seen unless the user already had your app open.</p>
<p>It can be a little annoying to have all your viewcontroller code start up when the view is loaded but not shown (maybe you start a network request when your main view is displayed)...</p>
<p><strong>Protip to stop UI from loading in background launches</strong></p>
<p>At the time of writing all normal app lifecycle stuff is triggered, storyboard loading / etc, causing all my <code>viewwillappear:</code> etc to be called. I worked around this by only loading my storyboard after first checking to make sure we aren&apos;t being launched in a background state in <code>application:didFinishLaunchingWithOptions:</code>:</p>
<pre><code>if (application.applicationState != UIApplicationStateBackground) {
	self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
	UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@&quot;myStoryboardName&quot; bundle:nil];
    self.window.rootViewController = [storyboard instantiateViewControllerWithIdentifier:@&quot;myRootController&quot;];
}
</code></pre>
<p>This does require you to remove the main storyboard entry from Info.plist and maunally load it yourself old-school style, but it should look familiar to most developers.</p>
<h3 id="method2ashareddatacachewithnsuserdefaultsandappgroups">Method 2: A Shared Data Cache with NSUserDefaults and App Groups</h3>
<p>Sometimes waking up your parent app for data each time you need it is excessive.</p>
<p>Since the release of extensions on iOS8 developers have had a concept called App Groups. Diving into how to set that up would be a whole other post, and a trip to the developer portal, but it&apos;s basically a way to share data between multiple executables on the phone in a separate sandbox. You permission your apps and extensions to all be a part of the same group and they can share data through a common sandbox.</p>
<p>Most of the time you&apos;re dealing with data, like <code>[NSUserDefaults standardDefaults]</code>, that&apos;s in the app&apos;s sandbox. <code>NSUserDefaults</code> has an alternate constructor, <code>initWithSuite:</code> that lets you save and read data using the NSUserDefaults API between multiple executables.</p>
<p>(you can also throw CoreData or plain old files into these App Groups, but we&apos;ll keep things simple for now)</p>
<p>Beyond changing the init method nothing else really changes from the <code>NSUserDefaults</code> you&apos;re used to. In your iPhone app:</p>
<pre><code>NSUserDefaults *defaults = [NSUserDefaults initWithSuite:@&quot;myGroupName&quot;];
[defaults setInteger:4 forKey@&quot;myKey&quot;];
[defaults synchronize];
</code></pre>
<p>And then in your WatchKit extension you can read that data at any time:</p>
<pre><code>NSUserDefaults *defaults = [NSUserDefaults initWithSuite:@&quot;myGroupName&quot;];
NSInteger myInt = [defaults integerForKey@&quot;myKey&quot;];
</code></pre>
<p><strong>Pros and Cons</strong></p>
<p>You can use this for any data type you can serialize into <code>NSUserDefaults</code>. It&apos;s a great way for passing less dynamic data back and forth, and you don&apos;t have to wake your parent app up to view the data.</p>
<p>This method is also great for sharing data to a Today Widget, which doesn&apos;t have the option to open up the parent app and ask for data.</p>
<p>One downside I ran into this method of passing data around, though, is that there is no way to be notified when a value changes. Key-value-observing isn&apos;t working. So what happens if you have data that can change as the user is using your WatchKit app? Polling based on an <code>NSTimer</code> is less than ideal. This is where Apple recommended Dawrin notification center.</p>
<h3 id="method3sharingdatawithupdatecallbacksusingdarwinnotificationcenter">Method 3: Sharing Data with Update Callbacks Using Darwin Notification Center</h3>
<p>I say Darwin notification center, but really I mean the great CocoaPod <a href="https://github.com/mutualmobile/MMWormhole">MMWormhole</a> that&apos;s a lightweight wrapper (you didn&apos;t <em>really</em> want to worry about bridging down to a C API, did you?).</p>
<p>Using MMWormhole is very much like using <code>NSUserDefaults</code>, it even uses the same App Group config, but you get the opportunity to subscribe for updates. The notification aspect of CFNotificationCenter is in-memory, so this is all pretty lightweight.</p>
<p>Lets say we have a running app that shows current speed and we want the UI on the watch to always show the right data. In the iPhone app we&apos;d set up a wormhole and post the current speed whenever it changes:</p>
<pre><code>self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:@&quot;myAppGroup&quot; optionalDirectory:@&quot;wormhole&quot;];

//whenever the speed changes...
[self.wormhole passMessageObject:currentSpeed identifier:@&quot;currentSpeed&quot;];
</code></pre>
<p>Then on the watch side we need to init the UI with the current speed. Unlike UIKit for iOS the views are loaded and valid when <code>init</code> is called in <code>WKInterfaceController</code>, so that&apos;s were we&apos;d set the initial value:</p>
<pre><code>- (instancetype)init {
	if (self = [super init]) {
      self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:@&quot;myAppGroup&quot; optionalDirectory:@&quot;wormhole&quot;];
      NSNumber *currentSpeed = [self.wormhole messageWithIdentifier:@&quot;currentSpeed&quot;];
      [self.topSpeedLabel setText:[NSString stringWithFormat:@&quot;%@ MPH&quot;, currentSpeed];
    }
    return self;
}
</code></pre>
<p>This is pretty similar to how you&apos;d do things with <code>NSUserDefaults</code>. The data can change on us behind the scenes after <code>init</code>, so how do we keep it in sync?</p>
<p><strong>KVO Through the Wormhole</strong></p>
<p>We&apos;ll use <code>willActivate</code> and <code>didDeactivate</code> on <code>WKInterfaceController</code> to know when we should update the data. These are conceptually a combination of <code>viewWillAppear</code> and <code>viewDidDisappear</code> on iOS, plus notification for when the screen turns on and off (remember Apple Watch will shut off the screen to save power if the user isn&apos;t interacting with it).</p>
<p>With these we can subscribe to changes to the current speed, but only while our view is on-screen and the watch screen is on:</p>
<pre><code>- (void)willActivate {
	// This method is called when watch view controller is about to be visible to user
	[super willActivate];
    
    [self.wormhole listenForMessageWithIdentifier:@&quot;currentSpeed&quot; listener:^(id messageObject) {
        [self.topSpeedLabel setText:[NSString stringWithFormat:@&quot;%@ MPH&quot;, (NSNumber *)messageObject];
    }];
}

- (void)didDeactivate {
	// This method is called when watch view controller is no longer visible
	[super didDeactivate];
    
    [self.wormhole stopListeningForMessageWithIdentifier:@&quot;currentSpeed&quot;];
}
</code></pre>
<p>This means that while the screen is off (our app <em>may</em> still be alive, just a blank screen) we don&apos;t waste time communicating with the phone and updating a UI the user can&apos;t see. With WatchKit we&apos;ll want to be smart with our UI updates so we don&apos;t waste power, so make sure you take advantage of <code>willActivate</code> and <code>didDeactivate</code> appropriately.</p>
<p>You can also use MMWormhole with Today Widgets, same as <code>NSUserDefaults</code>, so you can have a centralized data store for all your widgets with change notification support.</p>
<hr>
<p>I think many developers are going to be OK just using <code>openParentApplication:reply:</code> to grab data from their iPhone app as neeed, but the other two methods are pretty powerful and may actually be required in situations where the data can change frequently.</p>
<p>Personally I&apos;ve refactored <a href="http://getslopes.com">Slopes</a> to mostly use MMWormhole, but I use <code>openParentApplication:reply:</code> to trigger actions like start/pause/resume.</p>
<hr>
<p><mark>Update 2/12/2015:</mark> I expanded on this post as a full talk for Philly Cocoaheads. You can <a href="https://vimeo.com/119606531">check out the talk on Vimeo</a>.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item></channel></rss>