Perils of Embedded Software

August 7th, 2014

Hardware manufacturers shouldn’t be responsible for software. The problem is crystal clear.

Your new TV has a computer in it, running some embedded OS and some software that came with it. If it’s a smart TV, it probably knows how to connect to the Internet and stream Netflix.

And that’s great, until it stops working. Maybe it’s a Netflix API change. Or some security problem surfaces. Or customers find bugs.

Almost every hardware company treats the software in the devices the way they treat the hardware:  They build it, ship it, and then forget about it. If there’s a problem, your recourse is a refund, or repair. But they’re not going to make your older product better.

This is on my mind because I’m buying a car. The car I’m buying is a 2010 Ford Flex, and it comes with Ford’s SYNC system.

SYNC is a small custom computer running an embedded version of Windows, which provides communications and entertainment features. 

The version of SYNC in my 2010 vehicle is no longer being updated.

In practical terms, this means that the next iPhone may not work with my car, and that will never get fixed. Ford’s solution is that I should buy a new car.

Dumb devices is the answer. The car shouldn’t have a built-in computer (beyond for basic car functionality), it should have a display that connects to an external, replaceable computer, like an iPhone. CarPlay, or Google’s equivalent. And a TV shouldn’t have an embedded OS; smart TV features should come from something outboard like an Apple TV.

Why Apple?  Because they’re honestly the only tech company that seems like they’re committed to keeping older hardware up to date. Apple’s not perfect, but I can’t think of anyone doing a better job.

I would have included Microsoft in that list, because of their history with Windows, but they’re the ones responsible for the Ford system, and their history with obsoleting mobile phones has been pretty bad.

What other companies are there that care about keeping old hardware up to date?

Making Enough to Afford Marketing

July 29th, 2014

There’s been some chatter going around (spawned by Brent Simmons) about whether it’s possible to run a business solely on the profits of selling an iOS app in the App Store. Here’s my two cents on why the low price of apps is hurting our ability to promote them.

Fall Day Software’s two main apps, Resume Designer and MealPlan, are both doing well, and they’re only available on iOS. 

There’s a good chance you’ve never heard of them, unless you’ve gone looking for an app in one of these categories and run across my products. I don’t advertise them, because I can’t afford to.

In most product businesses, scaling is based on marketing. You figure out a price point and a marketing plan where spending $1 on marketing results in >$1 in sales, and then you turn up the marketing tap until your market is saturated.

Think about the products you buy. How many of them are from companies that you’ve never seen any advertising for? 

In the iOS app world, a focal point of app marketing is the initial press push, because it’s free. We all know what the sales curve for an app promoted this way works:  An initial spike in sales, and then a drop off to trickle.

There’s social, which works great for apps that are naturally social or involve sharing. This doesn’t work well for productivity apps. I want to make it easier for MealPlan users to share meal plans, but my app’s marketing shouldn’t depend on it.

There’s App Store search, which honestly is how I get a lot of my sales. But that’s fickle, and almost completely out of our control.

There’s word of mouth (and I appreciate every one of you). But that’s also hard to scale.

The proven way to scale a product business is through advertising, and we don’t make enough money off apps to afford it. The cost per customer acquired is too high. 

What can we do?

Honestly, I don’t think there’s much that we aren’t already doing. Unless everyone in a given category agrees to raise their price by enough to cover marketing costs, you’ll find it difficult for your $9.99 app to compete with the $0.99 or freemium alternatives. That’s just too much of a price premium, and customers have been acclimated to the lower prices.

Adding a Mac counterpart and pricing it at a premium seems like the best way to go. Mac users are used to the higher prices, and Apple is making it very easy for your Mac app to be a great adjunct to your iOS app. 

Hybrid Handoff

June 24th, 2014

Will Apple ever make a hybrid tablet / desktop computer?

Traditional mouse and keyboard user interfaces don’t work well on touchscreen devices, and apps designed for touch don’t work well with a traditional user interface. They are fundamentally different, so much so that Apple decided, back when first designing the iPhone, to build a completely new UI framework and paradigm for touch-based apps.

A lot of the “can you do real work on the iPad” debate boils down to input. Touch is better for some things, but for anything that involves creating or manipulating a lot of text, a physical keyboard and mouse is better. There are other issues, like screen real estate and the extra widget density afforded by not having to have such large targets for each button, but in my opinion, mouse and keyboard vs a touchscreen is the main differentiator.

It’s not as simple as using a keyboard to type text into an iPad app, because text editing involves a lot more than just typing. Selection, cursor movement, document navigation, these are all things that you expect to be able to do with the keyboard. Many users learn keyboard shortcuts for common operations, and can work almost entirely using the keyboard.

Apple has both traditional and touch-oriented versions of all their major applications. They also have a way for these applications to seamlessly share documents, through iCloud.

And with Handoff, in Yosemite and iOS 8, they now have a way for a user to switch between a touch-based app and a traditional app without losing their place.

The iOS simulator is a complete execution environment for iOS apps, that works on top of Mac OS X. Developers use it to develop their apps, so every iOS app can run on a Mac, in the iOS simulator environment. Convert this simulator into a real runtime for iOS apps on the Mac, and you’ve got the start of a workable hybrid.

Imagine a Mac that had two displays, with one of them being a touchscreen. Run the iOS environment on the touchscreen, and run the traditional Mac OS interface on the other screen. Put one of these touchscreens as the top lid on a MacBook Air style device, and the other as the inside display.

You could use iOS on one side, and Mac OS on the other side. But the really interesting thing is how you could switch between these two environments just by opening or closing the lid of the computer. Start working on a document in Pages for iOS, open the lid, and the document appears in the desktop version of Pages.

One of the missing pieces is security. iOS devices are locked down, to an extent that they could never lock down a Mac. But what if…

There have been rumours of an A7 or A8 based Mac. So let’s say they build that, but for this new Mac, only sandboxed applications are allowed. No user-installable device drivers, no acquiring apps outside the app store. It works just like iOS. Now you’ve got a Mac that’s as locked-down as iOS. It doesn’t have to replace the Mac line; it would be a new category of device.

I’m not sure Apple would ever go there, but I find it interesting how close they are.

Second Tier Upstream

June 23rd, 2014

I get the “Age of Context”. I get the desire to share the stuff you’re doing, the stuff you’re creating, even if it’s fairly mundane.

I have a PS4, and I like that Sony thought sharing was so important that they put a Share button on every controller. It’s easy to take the last few minutes of gameplay and turn it into a video, and then post it online for anyone to watch. This is cool.

You can even live-stream your game.

I use online backup for my Mac, via BackBlaze. It’s great.

My iOS devices automatically upload photos I take to iCloud.  They can even automatically make backups whenever I plug the devices in.

I had to turn this off, as much as I love this feature, because my upstream bandwidth is terrible.

I have DSL at home. It’s the best option available to me, and my upstream bandwidth is 800kbps, which works out to about 80k/second when uploading.

So if I take a photo on my iPhone, it takes about a minute and a half for it to upload. If I take a bunch of pictures, there goes my network connection for the next hour or so, because while my network connection is saturated with uploads, the latency on downloads goes way up and web browsing becomes nearly impossible.

I have BackBlaze set to only do backups when I’m sleeping.

And we just don’t use the sharing or streaming features of the PS4, as much my son would like to.

It sucks. And, because I already have “high-speed internet”, it’s not going to get better for me any time soon.

This is nothing new, of course, but I was reminded of it when I looked at DropCam, a connected camera service that uploads video to the cloud so you can watch it from anywhere.  I love the idea of DropCam, a camera that you can just place somewhere and then connect to from your phone or a web browser to see what’s going on or receive motion alerts, but I can’t have a device in my house constantly uploading video. It just wouldn’t work.

And DropCam doesn’t have any option for a local server. It’s the cloud or nothing.

Companies shouldn’t forget that a lot of people don’t have a fast internet connection, and for those that do, sometimes it’s only fast in one direction.  I looked at Manything, as another great-looking camera option, but I can’t use it either for the same reason.

Google clearly wants the “hub” for your home automation to be their servers and for me, at least, that’s not going to work.

WWDC 2014 Wish List

May 27th, 2014

Why not, here’s my WWDC wish list.

  • Some way of sharing files between apps. Not necessarily a file system, but a way of making a file or folder visible to another app.
  • Add file upload support to WebKit. Sometimes users need to create a file in an app, and then upload that file to a website. There’s no way to do this today.
  • Data binding.  Wiring up controls to data models, and doing data validation, is not creative work.
  • Officially bless CocoaPods, or supply something similar.
  • Provide a web API that developers can use to build a web presentation for the data their apps have stored in iCloud.
  • More APIs that use blocks. Why isn’t there a block-based way of using UIAlertView yet?
  • Auto layout is still too hard to use, and especially to debug. I’m not sure what you can do about that, but don’t stop trying.
  • Better LLDB.  Why does “p self.view.frame” print 5 lines of errors instead of just printing the frame rect?

This is boring stuff, in that it doesn’t really open up any new innovative areas the way something like iBeacons or the nearby networking framework does, but it makes the job of building apps easier.  That’s all I want this year.

The Economics of Backend as a Service

April 13th, 2014

Brent Simmons wrote a bit on how common back-end services should be a solved problem by now.  I agree 100%. I proposed something like this when I was at Adobe, when Adobe was trying to build Flash as a platform (which would also have the same sort of requirements), and couldn’t get them to bite on it.  It’s an idea whose had come years ago.

What I envision is a service that provides a core service that takes care of account management, password resets, manages OAuth tokens and all the other core guts that any service needs, and then provides additional higher-level services on top of that.

Image storage, including transcoding and thumbnail generation and other operations.  Blob storage.  Discussion thread management. Video transcoding. Think of a service that’s needed by more than 10 apps, find a way to generalize it, and implement it.

Problem is building and supporting these services costs money. Apple provides iCloud for free to users and developers since it’s subsidized by the hardware, but as Brent points out, Apple has no incentive to make these services available to developers for use with non-Apple clients, so developers need to look elsewhere.

There are some services attempting to solve these problems, but the problem is cost. An app that sells for $3.99 (say, for a “premium” app) has $2.73 profit left, to support the web service back end for the lifetime of the app. It sucks, but that’s reality.

The key to making this work is that your web services need to be incredibly inexpensive to run. 

The pure-hosting services, like linode, make it possible to have a server with a database, bandwidth and storage, that can run PHP or some other low-resource server side technology, very inexpensively.  $20/month and you can have a pretty nice VPS.  You could support thousands of users, maybe even tens of thousands of users, on one of these servers.

But then when you move up to the companies that are selling APIs, like Parse, the price goes way up. The pricing for these services is designed to make cheap or free to get started, but then ramps up as you get more users.  Parse is $199/month when you have over “1 million requests”, which sounds like a lot, but that’s 33,333 requests/day.  If you have 1000 users, that’s only 33 requests per user per day, which isn’t a lot.

And if you’ve only got 1000 users, $199/month is a lot of money.

The only solution that seems feasible to me is an open-source project that takes on the goal of building these services in a manner that’s designed to be easy to deploy on commodity servers.  I wish I had the time.

Didn’t get a WWDC Ticket? Buy a Treadmill.

April 8th, 2014

I’m sorry to hear you didn’t get a WWDC ticket. (I didn’t get one either).

But look at it this way:  even if you’re still planning to go to San Francisco for the week, that’s $1,599 that you won’t be spending on the conference ticket.

It’s hard to watch all the WWDC videos.  It’s a ton of content, and it’s all going to be available for you to watch starting with the live-streamed keynote, and then, if last year’s pattern is any indication, within about 24 hours the sessions will start to appear. There will be probably around 100 hours worth of video to watch.

It’s hard to find time to watch all these videos.

You know what else it’s hard to find time to do?  Exercise.

I have a hard time with exercise, because it feels like wasted time. I’d rather be working on code than cardio, and I think there are a lot of us who feel the same way.

But watching WWDC videos and working out go together excellently. So here’s the plan:

Buy yourself a treadmill. I don’t want to get into recommendations because I’m no expert, but I bought a LiveStrong 13.0T treadmill from Canadian Tire for about $1200 last year, and I’ve been pretty happy with it.

Three or four times a week, watch a WWDC session video while you’re on the treadmill.

I find the treadmill / WWDC combo is motivating. It gets me working out, and it helps me watch all the WWDC videos that I wouldn’t otherwise make time for.

MealPlan Sync with Ensembles

March 19th, 2014

MealPlan is a meal and grocery planning app that, up until recently, was only available on the iPad. I wanted to bring it to the iPhone, but I had to implement sync first.

I started with iCloud Core Data Sync from Apple, back before iOS 7, and ran into nothing but troubles. It was seriously broken, and that fact has been documented on dozens of sites so I won’t address it here. I lost about two months worth of development time to trying to get iCloud to sync using Core Data before abandoning that.

I assumed I’d have to write my own sync back end. I didn’t really want to do that for a number of reasons. One of the main ones is that, given the amount of revenue the app generates, it doesn’t make sense for me to be “on call” 24/7 in case there’s a problem on the server side. I wanted something that would just work, with no ongoing involvement from myself.

Drew McCormack’s Ensembles project provided the answer.

Ensembles provides the solution that iCloud Core Data sync should have been. It uses a similar mechanism: Sync changes from your application’s database into iCloud and then merging changes from other devices back into your local database. But Ensembles is simply a better design.

I want to talk a bit about implementing Ensembles in MealPlan.

Since MealPlan started with the intention of using iCloud to sync, the application’s main data is stored in a class derived from UIManagedDocument. UIManagedDocument is a UIDocument subclass that is conceptually a document, but is actually stored as a Core Data database.

In retrospect, this is a leaky abstraction that shows through in the mismatch between a database API and a document API. But UIManagedDocument does two things that I found useful.

First, it takes care of setting up the Core Data stack. This isn’t a big deal, but it does create a parent / child context where the child context is on the main thread and the parent context runs on a private queue. This is an efficient arrangement because it means database operations on the main thread often happen very quickly since they’re just querying or pushing data to the parent context, which does the actual database operations in the background.

And second, UIManagedDocument changes the way you manage saving data, so that instead of explicitly calling save whenever you make a change, you instead call [document updateChangeCount:UIDocumentChangeDone] to tell UIManagedDocument that you’ve made a change.

UIManagedDocument doesn’t immediately translate this into a save. Instead, it sets a timer for about 5 seconds, and if no other changes happen in that time, then the save occurs. This means that as the user is working with the app, saves happen less frequently. Whether this is a good thing or not, in retrospect, I can’t really say. Seems like a wash to me. But because MealPlan was built around this save mechanism, it would have been a lot of work to switch away from it.

One useful tip when working with UIManagedDocument is to override contentsForType:error: and print out that the document is being saved. This is called when UIManagedDocument needs to “save” the document.

- (id)contentsForType:(NSString *)typeName 
                error:(NSError *__autoreleasing *)outError
{
    DDLogInfo(@"Probably Auto-Saving Document");
	return [super contentsForType:typeName error:outError];
}

The API here looks like you’d expect the document content to be returned from this method, which would make no sense for a database, and in fact if you look at the data returned from the superclass it’s simply a dictionary with some keys that identify the database.

Ensembles, on the other hand, needs data to be on disk when a sync happens.

One of the nice things about Ensembles, compared to Core Data sync, is that it’s essentially deterministic. You drive the sync process, in that sync happens only when you request it. From there it’s an asynchronous operation, but there are enough callbacks that you can have a pretty good idea what’s going on.

In MealPlan, I use the contentsForType:error: as a save notification from UIManagedDocument, and queue up a sync operation there:

- (id)contentsForType:(NSString *)typeName 
                error:(NSError *__autoreleasing *)outError
{
    DDLogInfo(@"Probably Auto-Saving Document");

    // Only queue up a sync if there were changes to save
    BOOL shouldSync = (self.managedObjectContext.hasChanges);

    id dataToSave = [super contentsForType:typeName error:outError];
    
    if (shouldSync) {
        [self performSelector:@selector(triggerSync) 
                   withObject:self 
                   afterDelay:1.0];
    }
    
    return dataToSave;
}

The one second delay before sync gives UIManagedDocument a chance to finish the save before syncing. Although this looks like a race condition, if the save isn’t complete, that’s okay, we’ll pick up on that later.

I’m getting a bit ahead of myself .. let’s start back at the beginning. The first thing we need to do is initialize Ensembles, but we need to do this after UIManagedDocument has had a chance to set up the Core Data stack. The place to do this is in the open completion handler.

Here’s the code I use to open the document:

- (void)setupDocument
{
    self.document = [MPDocument documentWithName:self.docFileName completionHandler:^(BOOL success) {
        if (!success) {
            DDLogError(@"Document failed to open.");
            return;
        }

		// Set the merge policy
        self.document.managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
        NSManagedObjectContext *parentContext = self.document.managedObjectContext.parentContext;
        [parentContext performBlockAndWait:^{
            parentContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
        }];

        [[NSNotificationCenter defaultCenter] addObserver:self 
  selector:@selector(handleManagedObjectContextDidSaveNotification:)
  name:NSManagedObjectContextDidSaveNotification 
  object:self.document.managedObjectContext];

        [self setupEnsembles];
        
        CDECodeBlock documentReadyBlock = ^{
            [self watchEnsemblesNotifications];
            [self syncNow];
        };
        
        if (!self.ensemble.isLeeched) {
            if ([MPSettings syncEnabled]) {
                DDLogInfo(@"Leeching Store");
                [self.ensemble leechPersistentStoreWithCompletion:^(NSError *error) {
                    if (error != nil) {
                        DDLogError(@"Error leeching: %@", error);
                    } else {
                        DDLogInfo(@"Leech complete");
                    }
                    documentReadyBlock();
                }];
            } else {
                documentReadyBlock();
            }
        } else {
            documentReadyBlock();
        }
        
    }];
}

There are a few items of note here.

Leeching is the Ensembles term for when your database is being synced by Ensembles. You must leech your database once, and it will stay leeched until you deleech it, or a deleech happens due to an error.

In MealPlan, whether or not sync is enabled is a user setting. If the user has not enabled sync, then although MealPlan still initializes Ensembles, the database is not leeched and no syncing happens. If sync is enabled, this means the user would like the database to be leeched, so attempt to leech it. If the leech fails, that’s okay (this can happen for various reasons, that I’ll mention later), but there will be no sync happening.

The merge policy is being set for both the parent and child contexts. This is required.

Let’s take a look at setupEnsembles.

- (void)setupEnsembles
{
    DDLogVerbose(@"Setting up Ensembles");
    
    NSPersistentStore *store = [self.document.managedObjectContext
                                  .persistentStoreCoordinator.persistentStores lastObject];
    NSString *storePath = store.URL.path;
    
    // Setup Ensemble
    self.cloudFileSystem = [[CDEICloudFileSystem alloc]
                            initWithUbiquityContainerIdentifier:@"TEAMID.com.mycompany.myappid"];
    
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"];
    self.ensemble = [[CDEPersistentStoreEnsemble alloc] initWithEnsembleIdentifier:@"MainStore"
                                                               persistentStorePath:storePath
                                                             managedObjectModelURL:modelURL
                                                                   cloudFileSystem:self.cloudFileSystem];
    
    self.ensemble.delegate = self;
}

The only thing that’s unique here for working with UIManagedDocument is where to find the NSPersistentStore.

Ensembles imports data from iCloud, and is watching the iCloud container, but won’t act unless you tell it to. But Ensembles will tell you when new files have appeared in the iCloud container, which is a good time to sync. The CDEICloudFileSystemDidDownloadFilesNotification is your cue that you should sync.

Syncing looks like this:

- (void)syncNow
{
    if (![MPSettings syncEnabled]) {
        return;
    }
    
    if (self.ensemble == nil) {
        // Not ready yet - this can happen if, for example, you trigger a sync when restoring from background, but
        // the initial document open isn't completed yet.
        return;
    }

    if (self.ensemble.isMerging) {
        DDLogVerbose(@"Skipping sync because Ensembles is already syncing");
        return;
    }

    if (!self.ensemble.isLeeched) {
        // Not leeched
        return;
    }
    
    DDLogInfo(@"Initiating sync: Saving Document");

    [self.document.managedObjectContext performBlockAndWait:^{
        NSError *error = nil;
        NSSet *objects = self.document.managedObjectContext.registeredObjects.allObjects;
        if (![self.document.managedObjectContext obtainPermanentIDsForObjects:objects
                                                                        error:&error]) {
            DDLogError(@"Error obtaining permanent IDs");
        }
        
    }];
    
    [self.document saveContextsWithCompletion:^(BOOL success) {
        if (!success) {
            DDLogError(@"Saving document failed");
            return;
        }
        DDLogInfo(@"Save complete, initiating sync.");
        [self syncNowWithoutSaving];
    }];
}

What’s happening here is the app determines if it really wants to sync now by checking some conditions, asks Core Data to assign permanent IDs to all the objects it’s managing (since Ensembles needs these), forces both the child and parent context to save, and then calls syncNowWithoutSaving.

Saving is straightforward, but asynchronous, so it signifies completion using a callback.

- (void)saveContextsWithCompletion:(void (^)(BOOL success))completionHandler
{
    NSError *error = nil;
    if (![self.managedObjectContext save:&error]) {
        DDLogError(@"Error saving document context: %@", error);
        completionHandler(NO);
    }
    
    [self.managedObjectContext.parentContext performBlock:^{
        NSError *error = nil;
        if (![self.managedObjectContext.parentContext save:&error]) {
            DDLogError(@"Error saving document parent context: %@", error);
            completionHandler(NO);
        } else {
            completionHandler(YES);
        }
    }];
}

And this brings us to the syncNowWithoutSaving method, which actually makes the sync happen.

- (void)syncNowWithoutSaving
{
    [MPSettings setSyncStatusLine:@"Sync in progress"];

    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        if (!self.ensemble.isMerging) {
            [self.ensemble mergeWithCompletion:^(NSError *error) {
                if (error) {
                    if ([error.domain isEqualToString:CDEErrorDomain]) {
                        if (error.code == CDEErrorCodeSaveOccurredDuringMerge) {
                            DDLogError(@"Save occurred during merge; retry in 5 seconds");
                        } else if (error.code != CDEErrorCodeCancelled) {
                            [self updateSyncStatusLineFromError:error];
                            DDLogError(@"Sync failed: %@", error);
                        }
                    } else {
                        [self updateSyncStatusLineFromError:error];
                        DDLogError(@"Sync failed: %@", error);
                    }
                    
                    // Try again in 5 secs
                    [self performSelector:@selector(syncNowWithoutSaving) withObject:nil afterDelay:5.0];
                } else {
                    [MPSettings setSyncStatusLine:nil];
                    [MPSettings setLastSyncDate:[NSDate date]];
                    
                    if (self.bgTaskIdentifier != UIBackgroundTaskInvalid) {
                        [[UIApplication sharedApplication] endBackgroundTask:self.bgTaskIdentifier];
                        self.bgTaskIdentifier = UIBackgroundTaskInvalid;
                    }
                    
                    DDLogInfo(@"Sync complete");
                }
            }];
        }
    }];
}

Part of what I wanted to achieve with my integration with Ensembles is keeping the user informed, without being overwhelming. In the app’s Settings, there is a status line below the iCloud switch that shows a short message. This message will be either “Last sync: ” and the date, or one of the messages you see above. Typically, it will be “Sync in progress” for a short while, “Importing”, and then back to the idle message.

Because the rest of the app is alive while the sync is happening, the user may well have done something, and because of how UIManagedDocument works, the change the user has made may not yet be saved. Ensembles gives the application a chance to handle this:

- (BOOL)persistentStoreEnsemble:(CDEPersistentStoreEnsemble *)ensemble
  shouldSaveMergedChangesInManagedObjectContext:(NSManagedObjectContext *)savingContext
  reparationManagedObjectContext:(NSManagedObjectContext *)reparationContext
{
    if (self.document.managedObjectContext.hasChanges) {
        DDLogVerbose(@"Should merge?  No, there are unsaved changes.");
        return NO;
    }

    if (self.document.managedObjectContext.parentContext.hasChanges) {
        DDLogVerbose(@"Should merge?  No, the parent context has unsaved changes.");
        return NO;
    }
    
    return YES;
}

And then once the sync has completed, Ensembles gives you the notification that you must use to merge the changes into your own context:

-      (void)persistentStoreEnsemble:(CDEPersistentStoreEnsemble *)ensemble 
 didSaveMergeChangesWithNotification:(NSNotification *)notification
{
    NSAssert(self.document != nil, @"Expected a document");
    NSAssert(self.document.managedObjectContext != nil, @"Expected a context");
    
    [self.document.managedObjectContext.parentContext performBlock:^{
        [self.document.managedObjectContext.parentContext mergeChangesFromContextDidSaveNotification:notification];

        [self.document.managedObjectContext performBlock:^{
            [self.document.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
            [self.document updateChangeCount:UIDocumentChangeDone];

            [[NSNotificationCenter defaultCenter] postNotificationName:MPSyncCompletedNotification object:nil];
        }];
    }];
}

Notice the changes being merged into both the parent context (first) and then the child context.

So how does the user turn on Sync? I have a switch in Settings, and when the user toggles it, that’s when the database is leeched or deleeched.

- (IBAction)enableiCloudSwitchChanged:(id)sender
{
    if (self.enableiCloudSwitch.on) {
        [self updateFromSettings];
        [MPSettings setSyncEnabled:YES];
        if (self.ensemble.isLeeched) {
            DDLogVerbose(@"Ensemble is already leeched, syncing now");
            [[MPAppDelegate instance] syncNow];
        } else {
            DDLogVerbose(@"Saving document in preparation for leech");
	        [MPSettings setSyncStatusLine:@"Preparing to sync"];
            [self.document saveContextsWithCompletion:^(BOOL success) {
                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    [self attemptLeech];
                }];
            }];
        }
    } else {
        [MPSettings setSyncEnabled:NO];
        [self.ensemble deleechPersistentStoreWithCompletion:^(NSError *error) {
            [self updateFromSettings];
        }];
    }
}

The attemptLeech function actually has a fair bit of work to do, since this is where the user is trying to turn on sync for the first time, and many things can go wrong.

- (void)attemptLeech
{
	[self.ensemble leechPersistentStoreWithCompletion:^(NSError *error) {
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            if (error != nil) {
                if ([error.domain isEqualToString:CDEErrorDomain] && 
                     error.code == CDEErrorCodeSaveOccurredDuringLeeching) {
                    // Silently try leech again
                    [self performSelector:@selector(attemptLeech) withObject:nil afterDelay:1.0];
                } else if ([error.domain isEqualToString:CDEErrorDomain] && error.code == CDEErrorCodeCancelled) {
                    // Canceled also means a save happened during the leech, and we should just try again
                    [self performSelector:@selector(attemptLeech) withObject:nil afterDelay:1.0];
                } else if ([error.domain isEqualToString:CDEErrorDomain] && 
                           ((error.code == CDEErrorCodeFileCoordinatorTimedOut) || 
                            (error.code == CDEErrorCodeMissingDependencies))) {
                    // These are errors we recognize as transient, so just update the progress messages
                    if (error.code == CDEErrorCodeMissingDependencies) {
                        [MPSettings setSyncStatusLine:@"Waiting for data"];
                    }
                    
                    if (error.code == CDEErrorCodeFileCoordinatorTimedOut) {
                        [MPSettings setSyncStatusLine:@"Waiting for iCloud sync"];
                    }
                    
                    [self performSelector:@selector(attemptLeech) withObject:nil afterDelay:1.0];
                } else {
                	// This is a failure
                    [MPSettings setSyncEnabled:NO];
                    [MPSettings setSyncStatusLine:[NSString stringWithFormat:@"Error initiating sync."]];
                    NSString *format = @"Check your connectivity and iCloud status and try again.\nError %@:%d";
                    NSString *message = [NSString stringWithFormat:format, error.domain, error.code];
                    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error initiating sync" message:message
                                                                   delegate:nil
                                                          cancelButtonTitle:@"OK"
                                                          otherButtonTitles:nil];
                    
                    [alert show];
                }
            } else {
                [[MPAppDelegate instance] syncNow];
            }
        }];
    }];
}

One nuance here is that again because of the asynchronous save nature of UIManagedDocument, there may be changes that you need to save before you can leech. Leeching a document with unsaved changes will lead to problems.

And that, in a rather large nutshell, is the basics of syncing a UIManagedDocument-based application using Ensembles. Some of this seems obvious in retrospect, but it took me some time to tune sync so that it felt right, including getting the status reporting right. I hope you found this useful.

Questions? Comments? I’m @stevex on Twitter.

Calling Handlers on the Main Thread

March 14th, 2014

Brent’s post on API Design and the Main Thread kind of rankles a bit because I see this differently.

The main thread is special, yes, but to me it’s special in that you should stay off it unless you have a good reason to be there.

The main thread is where responsiveness happens.

When your finger is scrolling a table view, the main thread is busy making that fluid. A long enough interruption could cause the frame rate to drop, and you don’t know when this responsiveness is required. There’s no “I’m busy animating right now so hold off on the notifications” flag. If you dispatch to the main thread, then the next turn of the run loop (the same run loop that’s busy animating), it’s going to do whatever you asked it to do.

In Brent’s example:

- (void)notesWithUniqueIDs:(NSArray *)uniqueIDs 
fetchResultsBlock:(QSFetchResultsBlock)fetchResultsBlock;

He’s dispatching back to the main thread to call the completion handler, even though the method may be called from a background thread.  He’s doing this so that the chance that somebody will mess up and use the result on a thread that isn’t the main thread is reduced. This can help a programmer who isn’t careful about what thread he’s on, but you’re playing russian roulette with your framerate.

The problem isn’t just the one dispatch back to the main thread, it’s that you don’t know how many are happening or what else is going on. Let’s say Brent designs his library with this pattern in mind, and now someone else is using the library. They want to perform a background operation that requires updating a number of notes, so they fire some work onto a background queue that fetches a number of notes and does work on them. This operation could be completed entirely in the background without involving the main thread at all, but instead will now require bouncing off the main thread at least once per object. If every library adopted this pattern, then maybe many times per object. It would add up to a drop in performance that would be impossible to resolve without changes to the API of one or more libraries.

In my opinion, a better pattern is this:

When you receive a notification or a callback, dispatch onto the thread required to handle it.

Unless the API specifically documents its behaviour here, don’t depend on the caller having bounced you to the main thread.

You often see the thread bounce happening on both sides of the call:  Someone in Brent’s camp dispatches to the main thread, and then the handler also dispatches to the main thread. The second dispatch (the one that’s dispatching to the same thread you’re already on) seems to be handled specially by Cocoa – you can see on the call stack that it just calls through to the handler instead of queueing it, which is a nice optimization and makes the second context switch essentially free.

Uploading a Resume from iOS

March 2nd, 2014

Marco Arment linked to an article by Lukas Mathis which largely talks about creation on mobile devices. Lukas uses producing a resume and cover letter for a job application as an example of where one might find the job frustrating given iOS’s limitations.

I build and sell the app Resume Designer, an easy way to build a resume on an iPhone or iPad, so I have a few thoughts on this.

Lukas’s point about not being able to refer to more than one document at a time is absolutely right. This comes up in so many contexts that it’s an obvious limitation of iOS.  So obvious that I expect Apple will address it eventually. 

Adding a photo to the document is only really awkward in his example because the photo source is a CD-ROM. Is that really how photographers deliver images these days? Every photo shoot I’ve been involved (even school photos) has delivered the images electronically, and iOS is pretty well set up for that. Add the photo to your camera roll, and then in Resume Designer you can pick the photo and include it in your resume. This doesn’t seem like much of a limitation.

Judging from the support email I get, most of my customers are people who use their phone or tablet as their primary device. They want to apply for a job, so they choose an app to help them build a resume, and then when they’re happy with the result, they look for a way to upload it.

And that’s where iOS really falls down.  Resume Designer produces a PDF, and most job sites allow uploading a PDF. But Safari doesn’t support uploading anything other than photos.

I’ve tried to work around this limitation by supplementing UIWebView to support uploading to sites that use simple HTML upload methods, but many job sites do fancy JavaScript based file upload forms and there’s just no way to make something that works for every site.  

I have to answer many of my support emails with “Do you have access to a desktop or laptop computer you can use to upload the file?  Unfortunately your best option would be to email the document to yourself, and then use the computer to upload the file.”  And that just sucks.

Maybe Apple’s vision here is that every website would have an app and you’d just “Open In..” the file in the job site’s app to transfer the file over.  But in the real world, that’s not how it works.  There are a thousand places you might want to upload this file, and you just can’t.