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.

Ensembles and CocoaLumberjack

February 25th, 2014

I’ve switched one of my apps over to using CocoaLumberjack for logging, so that I can redirect logging to a file on disk, and then when I need to upload a crash log to HockeyApp, I can include some recent log data.  Often this is invaluable in figuring out what really went wrong.

I’m working with Drew McCormack’s Core Data Ensembles project for sync, and wanted to include the Ensembles logging output in my log files. Ensembles has its own CDELog method, which now supports calling through a callback method.

Setting this up is easy enough. Somewhere early in the launch of your app, probably where you set up CocoaLumberjack, set up the Ensembles logging callback:

CDESetLogCallback(FDSLogBridge);
 
And implement this function:

void FDSLogBridge(NSString *format, ...)
{
    va_list arglist;
    va_start(arglist, format);
    DDLogvCError(format, arglist);
    va_end(arglist);
}

The DDLogv variants of the DDLog methods, like NSLogv for NSLog, take a variadic argument list. This is what lets you pass on the arguments without actually knowing what they are.

 

Pro Apps and User Input

January 31st, 2014

I spent some time over the last couple of weeks playing with Logic Pro X and Final Cut Pro X.  These are Apple’s Pro apps.  They’re a good place to look if you’re trying to figure out what a “Pro” iPad might be, because to me, something worthy of the Pro name would be a device that one might consider as equivalent, or better, to run apps like these.

Productivity with these apps isn’t so much about how easy they are to learn, as it is about how efficient you can be once you’ve learned them. 

I was watching some tutorials on Final Cut Pro X on Lynda.com, and it struck me how rich the user interface was.  Rich not just in the amount of widgets, but in the richness of the interface as a whole.  Almost everything you see is interactive, and even more becomes interactive as you hover the mouse over, or near, various elements. 

Many of the interactions with the timeline can be affected by both modifiers, and by tool selection, which you can make with the keyboard.  There are probably 50 different ways a click on a spot in a video clip can be interpreted, depending on various things, and yes, this is confusing for new users.  But once you know your way around, watching someone proficient with these tools is an impressive display of input efficiency.

I said “about 50” but there are actually 4 modifiers on the Mac keyboard.  Command, Control, Shift, and Option.  Four bits, as it were, allowing for 16 combinations of keys that you can hold down.  It just feels like 50 because it’s hard to remember all the combinations.  But if there’s one that you use regularly, it’s worth learning and remembering.  Cmd-Option-Shift-Paste, to paste text into an email without pasting the style, is one that I remember because it’s such a useful feature.

When I try to work on an iPad, I feel hobbled. The debate about whether an iPad is a creation or a consumption machine is silly.  Of course you can create on it. But if you’re creating words, or code, or an edit of an video, chances are you can do the same creation more efficiently on device with a keyboard. It’s a bandwidth issue; the keyboard and mouse offer greater text input bandwidth than a touchscreen keyboard, and the addition of modifiers really helps streamline the user interface since not every option needs to be visible.

This doesn’t mean a keyboard is the best solution.  When working with images in Photoshop, you hold down the shift key to constrain when dragging.  When editing a mask, holding Option means subtract from the mask, and Shift means add to the mask. These don’t make a lot of inherent sense.

The biggest advancement for a “Pro” iPad would be bridging this input bandwidth gap between the desktop and the touchscreen.

Connected Device Privacy

January 19th, 2014

So Google bought Nest. This seems like a big deal, and maybe it is. It’s too early to tell.

I was listening to John Siracusa in a recent ATP episode talking about how the privacy concern with Google adding the Nest data to the collection of data they collect on all their users isn’t that Google is going to do something evil with this information. John’s take is that Google really does have the best intentions for this data, and that the real danger is that with all this information in one place, it becomes a valuable target.

I was thinking something similar recently regarding the nest. The data from your thermostat, be it Nest or any other connected thermostat, is probably the best signal to a criminal that your home is a good target for robbery.

I live in Canada, and in a cold climate, one of the biggest uses for the Nest would be to turn the temperature down when nobody is at home, or at night when people are sleeping. The Nest uses motion sensing and learning algorithms to figure out when it’s a good time to turn down the temperature, and the user can also feed this data in to the system using various APIs. In a nutshell, the Nest knows when you’re not at home.

Your phone may also know when you’re not at home, but what your phone doesn’t know is if your house is empty. There may be other people sharing your house; you may have a house-sitter if you’re away. There are plenty of scenarios where all the GPS-enabled phones in the house may be out of the home, but the house is still occupied.

But if the temperature is lowered, and stays lowered, you have a much stronger signal that nobody is home. And if the temperature is at 15 degrees celsius and the thermostat is set to keep that temperature for a few days, you have a very strong signal that nobody’s going to be home for a few days. The perfect target for a robbery.

Not only that, but you know the home has a Nest, which means they spend money on expensive toys.

The risk of this data becoming available for criminals is probably actually lessened by Google buying Nest. Google has a very strong track record regarding leaks of user data. I can’t think of a time when a security hole has led to Google leaking user data, and they have a lot of it to leak, and a lot of API through which this sort of hole could be found. They’re doing a good job keeping your information secure.

But I think the risk of someone inside Google “going rogue” and acting as a gateway for this information, either selling it, or acting as a mole for some organization, is much higher. There are a lot of people who would want the data that Google has.

Realistically, though, what can we do about this?

There’s a lot of benefit to these devices operating in a connected manner, and interoperating with the other devices we own.  I don’t think it’s practical to suggest that the data stays in the home; the logistics just make that unworkable for most people.

I don’t trust that Sony’s “Smart TV” data will remain secure.  As far as I can tell the firmware in my Sony TV, which connects to the Internet and does who knows what online, has never received a security update.  A connected device is a computer on the network, and needs the same level of security awareness as any other.  While I’m not sure about Nest, I know that I don’t trust Sony to do this job well (or most of the other TV manufacturers – I’m not just picking on Sony).  But I do trust Google to keep my Nest secure.

I would understand, however, if one decided that the benefits of having this information leave the home weren’t worth the risk.

Carrier Concessions

December 4th, 2013

Apple has this odd relationship with carriers. They make the best selling phone, but carriers do everything they can to steer customers away from it.

I’m a Bell Canada customer, and so I receive a lot of promotional material from them. And it almost always treats the iPhone as a second class citizen.

Their “2013 Holiday Wishbook”, for example, has four devices on the cover, none of which are an iPhone.  It’s not until page 8 before we see a full-page spread for the iPad Air and the new iPhones, but even there, something is off.  The previous pages show all the “superphones” (I hate that term) that you can get and how little they cost – $0 for many, $149 for the most expensive).  The Apple page says “To learn more about our Apple products, visit bell.ca/devices” and doesn’t mention price.  

Visit bell.ca/devices and you see a list of 19 phones in their “LTE Devices” category, but no iPhone.  You have to click on Apple to finally see the iPhone prices.

I don’t completely understand why carriers do this, but it must have something to do with profit. I assume they make more off a $0 Nexus 5 phone than they do off a $199 iPhone.

This has a lot to do with why Android is doing so well these days. Carriers spend a lot on advertising and they own the customer relationship. And they work hard to push users away from Apple.

Apple needs to fix this.

That’s not going to be easy, of course. All that crapware that the carriers load onto non-Apple phones is there because it makes the carrier money.  But maybe there’s a middle ground here.

Apple has been becoming more and more permissive with what they allow apps to do in the App Store.  If their odd special treatment of Clumsy Ninja is any indication, they’re endorsing free-with-IAP apps that bug the crap out of the user to do things like buy an spend coins in-game, or spam their Facebook and Twitter friends for in-game credit.  In my book, this is worse than carriers preloading software onto phones, because it’s not something that you can just ignore once.  It’s an ongoing problem.

How bad would it be to grant carriers a single folder on the phone that has their “stuff” in it.  Bell’s apps have been getting better in recent years, to the point where there are Bell apps on my phone that I use regularly.  Having that automatically installed for any customer who activates a phone on their network wouldn’t be such a bad thing.

Would that tip the scales to the point where the carriers would feel like it’s worth their time to promote the iPhone?

Shared Photo Stream and Movies

November 9th, 2013

There was some traffic on Twitter yesterday around a new Apple knowledge base article Photo Stream Limits.  Some people were under the impression that this article is saying that Apple will now host an unlimited number of your photos and videos.

This article doesn’t actually say that, although it doesn’t say they won’t do that.  But other pages on Apple’s site still mention the 30 day limit for iCloud photos, so it seems Photo Stream is still limited.

But this article does introduce some new capabilities that are rather interesting.

Did you know you can upload videos to shared Photo Streams?  Videos up to 5 minutes, exactly the kinds of videos you take on your phone, can be shared with up to 100 people, including yourself.  And the Apple TV can subscribe to those photo streams and play those videos.

This is the solution for managing “home movies” that I’ve been looking for.  I created a “Home Movies 2013” shared photo stream on my phone and added all the videos I’ve taken this year to it.  After all this uploaded, I can now play all these videos on my Apple TV.  This takes up none of my iCloud storage quota, and those files will stay online indefinitely.

To set this up on the iPhone:

  1. Launch the Photos app.
  2. Tap the Shared tab at the bottom.
  3. Tap the + button to create a new Shared Photo Stream, and give it a name, like “Home Movies 2013″.
  4. Enter the names of anyone you’d like to share it with, and tap Create.
  5. Tap on your new Photo Stream.
  6. Tap the + button.
  7. Tap on all the videos you want to share, and then tap Done.

Your phone should start uploading videos, and some time later, you’ll be able to go to the iCloud Photos app on the Apple TV and see your “Home Movies 2013” shared photo stream with the videos in it.

So what are the relevant limits?

  • Maximum shared streams an owner can share: 100
  • Maximum number of photos per shared stream: 5000
  • Videos can be up to 5 minutes in length.

A video seems to count as a photo, so it seems the limit is 5,000 5-minute videos per stream, and up to 100 streams.  It’s not unlimited, and may be restrictive if you try to create topical photo streams (“Our trip to the park”) but used sparingly, this is perfect as a way of storing and sharing a collection of short home movies.