iOS Frameworks

I always believed you couldn’t use your own frameworks in iOS development like you could on the Mac.

There are a bunch of benefits of OS X-style frameworks. Here’s a partial list:

1. Dynamic linking. Your app could link against the current version of a framework, such as a third-party framework installed in a standard location, and when that framework was upgraded and your app was launched again, you got any bugfixes or improvements of the new version for free. This is unlike static linking, where you’re stuck with whatever version you build with.

2. Project and compilation convenience. A single reference to that project is all Xcode needs to be able to link against its binary and find its headers. This is unlike free-standing libraries and their headers, which must have separate, individual Xcode references.

3. Resource convenience. Any other free-floating files associated with the framework can also tag along inside the framework’s folder. Pictures, text files, even xibs or storyboards. This is unlike etc. etc.

In iOS apps, for security reasons, you aren’t allowed to link against anything except system frameworks dynamically, so 1. has always been impossible.

I thought 2. was also impossible, but it turns out, I was wrong.

The Dropbox SDK has a sample project in it which just has a single reference to the Dropbox.framework, both in the Project Navigator and in the “Link Binaries with Libraries” Xcode build phase. And while there is a custom framework search path in the project’s build settings, there’s no custom library search path or header search path. And the headers are available via the usual #import <Framework/Header.h> format.

How does it do that?

Turns out, Xcode and iOS have had informal support for iOS frameworks, possibly going back to 2010, but definitely available as of April 2013.

What does “informal” mean?

It means it’s not documented, and you can’t make an “iOS framework” target. Indeed, the steps to actually build one of these frameworks are not entirely trivial. (The extra script in the linked-to web page doesn’t seem to be necessary, though.)

But it does work. And since it does, you can get all the benefits of 2., even in an iOS app.

That’s pretty cool, and something I’m going to be taking advantage of.

And note that 2. is all you get. The resulting framework binary is still statically-linked in to your app, and does not appear separately in the app binary. Nor do the headers. Nor do any other resources—meaning, you don’t get 3., either.

Everything but the Kitchen Sync

Previously: part 1, provisioning and entitlements and part 2, iCloud syncing documentation.

Part 3: Some Bad, Some Good

When last we left our hero, he had read Apple’s iCloud documentation (PDF) and found it both helpful for the details it included, and frustrating for the difficult edge cases it left as “an exercise for the reader”. (Thus mirroring the opinions of this blog’s author!)

Listing and Syncing

One of those edge cases, surprisingly enough, appears to be just trying to maintain on ongoing list of documents.

My Documentary sample apps, for the sake of simplicity (and, once I started running into problems, a certain sense of orneriness) allow for the creation, deletion, and renaming of documents directly from their list, without requiring individual per-document UI. This in particular allows me to put all this logic in a platform-agnostic shared class, in my case called iCloudManager, instead of duplicating it in UIDocument for iOS and NSDocument for Mac.

Creating a new document involves calling the NSString method writeToURL:atomically:encoding:error: on a blank string instance, and this works, even in a ubiquity container, but it only works if you wrap it in a NSFileCoordinator call:

    NSError *coordinateError = nil;
            
    __block BOOL writeResult = NO;
    __block NSError *writeError = nil;
            
    [fileCoordinator coordinateWritingItemAtURL:newDocumentURL 
options:0 error:&coordinateError byAccessor:^(NSURL *newURL) {
        writeResult = [@"" writeToURL:newURL atomically:YES encoding:NSUTF8StringEncoding error:&writeError];
        NSLog(@"Write %@ success: %ld, error: %@", newDocumentNamePlusExtension, (long)writeResult, writeError);
    }];            

    if (coordinateError == nil && writeResult == YES) {
        succeeded = YES;
    }

The NSFileCoordinator is synchronous, but doesn’t return a boolean, unlike most such Apple APIs—if the NSError is nil, it succeeded…or did it? In fact, it seems to only return coordination errors. If there was an actual file writing error, you can see that there is no way for the custom block to return any value. My code gets around this by saving the results from within the block, and checking that as well. I have not had a chance to check what the UI/NSDocument APIs do; one would hope they would have similar workarounds. The deletion code is similar.

The bad news is, if you’re relying on NSMetadataQuery to maintain your list of files, it’s not going to update your list instantaneously. The documentation for NSMetadataQuery says, “By default, notification of updated results occurs at 1.0 seconds.” I’ve found that even if I set notificationBatchingInterval to a very low value, it still takes about a second. And the update, when it occurs, won’t say anything as helpful as, “Yes, we deleted a file.” Instead, it will just tell you there’s been some sort of change.

So, for example, if you wanted to highlight the row of the newly-created file, you need to wait for the next update, then try to find that URL in the latest results list, and select it then. That should work, but it’s awfully fiddly. What if a network update comes through right then? Should you keep your conditional code running for the next several updates, till you find the URL you want? Deletion and renaming updates are similarly delayed and contextless.

For Documentary, I just punted and only updated the UI once the NSMetadataQuery results came in. But for a shipping app, I’d have to do a lot more polish work.

Putting the ”Document” in ”Documentary”

But finally, there was no more putting it off, I had to create per-platform Document subclasses if I wanted to implement in-app text editors. Actually, for Mac, I made it so that Cmd-O opened your document via Launch Services, by default with TextEdit. But I also included an “Open Document Internally” option, for reasons I’ll get to below.

And the editors, really just simple text views, worked pretty well! I had to hook up my text views to the undo manager to get iCloud to work properly, but that was described in the documentation and pretty easy. Here’s the change method on iOS:

    - (void)textDidChange:(NSNotification *)notification {
        // In a real app, we would only register a change after a certain amount of typing, or after a certain time. But not for this sample app.
    
        [[self.document.undoManager prepareWithInvocationTarget:self.document] setText:self.document.text];
        [self.document.undoManager setActionName:NSLocalizedString(@"Typing", @"Undo/redo label")];
    
        self.document.text = self.textView.text;
    }

I really should have coalesced the text changes, but didn’t have time, sorry.

And while I punted on conflict resolution on the iOS side due to time issues (again, sorry), I knew that on the Mac, it should all be taken care of if I used NSDocument.

So…was it?

I’m happy to report that it was. When I opened a document that had been modified locally and on two other devices, the document window presented a sheet with the three options, clearly labeling where they came from, and showing you a preview of their contents, all without my having to do any extra work. You could even choose to keep several of the conflicting versions around. Neat! Note: it won’t do any of that work for you until you open a document, but it’s very nice to have it then.

Conclusions

I spent about a week of free time putting all this together, and I have a much better sense of the contours of iCloud document syncing than I used to. The good: the syncing itself, Xcode 5, the documentation, and NSDocument. The bad: edge cases and lots of developer-required logic.

To play around with it, feel free to clone and build my Documentary applications, but keep in mind I did this in a week, and that you’d have to add a lot of extra error-handling and edge case-handling code before you’d want to ship anything based on it.

Still, I had fun, and I hope you had fun reading about it, too.

Sync or Swim

Previously: An introduction to my series of blog posts on iCloud document syncing, and issues with provisioning and entitlements.

Part 2: The Documentation for Documentary

When last we left our heroine, she had configured her ADC accounts properly to allow her to run the Documentary iOS and Mac sample apps. (Not a very exciting afternoon matinée, I’ll admit.)

Great, so they run, but what do they do?

The short answer is: in order to share documents between iOS and Mac, they do what the documentation says to do.

Shared Ubiquity Containers

Apple’s produced some good iCloud documentation (PDF), so go read that first if you’re interested (remember, this is not a full tutorial).

The first thing from the documentation that’s important to our specific project is that we want to share a ubiquity container for both of our apps, Documentary for iOS and Documentary for Mac. Normally, you’d use the unique, separate bundle IDs of your apps as their primary ubiquity container identifiers, but here we’re going to use the iOS bundle ID for both, so all their contents are shared. This is not a hack: Apple recommends it for precisely this case.

Just like for your app ID, this ubiquity container ID is actually prepended with your team ID. So make sure you use the same team for your iOS and Mac projects (this should be the default). Normally, therefore, you’d need to hard-code your team ID into your code to retrieve the URL for this ubiquity container. But Apple makes it easy by returning the first container in your app if you pass in nil:

    [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];

In our case, we only have one ubiquity container, so nil it is!

I was a little surprised to read that you can put your own folders inside the ubiquity container, really whatever you want to do there, and it will be replicated for you. It’s not just one level deep. The Documents folder is special because its contents are user-visible on the Mac side (for a certain value of “user visible”, keep reading), but other than that, anything goes, as long as you keep in mind that your free space is limited by your iCloud account’s storage quota.

Turning iCloud On and Off

The second thing that struck me in the documentation is all the work you need to do to handle turning on and off iCloud support. You’re supposed to call this method in your application:didLaunch... code:

    [[NSFileManager defaultManager] ubiquityIdentityToken]

and if it returns a non-nil value, then the user is signed in to iCloud and your app can use it…well, maybe you can use it. You should also call the above-mentioned URLForUbiquityContainerIdentifier:—on a background thread, mind—before doing anything. In theory, a non-nil result from the former should be enough to assume iCloud availability. But what if the first returns non-nil but the second returns nil?

In any case, if iCloud is available but your app isn’t using it, you should ask the user whether they want to use it, and cache that choice so you don’t ask it again the next time. If you were using it, but now it’s off, you should…tell the user they just lost all their data? “Sorry, fuck off”? The documentation isn’t forthcoming. Wenderlich’s tutorial makes it sound like you can still access the local cache of the iCloud files, despite iCloud being off. Even if that’s true, presumably anything that was “in the cloud” and not downloaded can’t now be downloaded after you’ve logged out. Same if you’ve switched accounts.

But if the user agrees to start using iCloud, you’re supposed to copy all your local documents to the ubiquity container, where they will then be synced with iCloud. This is entirely on you, the developer. If there are naming conflicts, you need to resolve them in your own custom code. This is the first of many places where I wish Apple had done more to help us out. Making each app do this individually means that all apps will do it inconsistently, and many will miss the edge cases that don’t show up in casual testing.

(The Documentary project punt on this entirely by assuming iCloud is always on, sorry.)

Mac Versus iOS File Philosophies

I show the documents on both Mac and iOS in my own list UI, which is…frowned upon in OS X. The documentation makes clear, both in explicit and implied ways, that on the Mac they really want you to rely on the user employing the standard Open dialog to find files, and they want all file interaction to go through NSDocument, which handles downloading and uploading file content, file version conflicts, and files being moved or modified elsewhere.

On iOS, there is no such functionality, so you must find the files yourself using NSMetadataQuery. The iCloud document makes mention of NSMetadataQuery but doesn’t explain how to use it in any way. It just refers to a second document, File Metadata Search Programming Guide (PDF). This is the danger of using what my Edge Cases cohost refers to as “collaborating objects”, where all sorts of framework objects tie together to make a more capable whole; learning to do even the simplest things from scratch means hopscotching from one long document to another, never quite learning the exact steps you need. Frustration with that approach led to my finding Ray Wenderlich’s tutorial, where he had a very helpful example of NSMetadataQuery usage that I copied wholesale.

The long and short of it is, if you keep the query going, you’ll be notified when there are changes, and you can update your list to match. I do this on iOS, but I also do this in the Mac app, because even though it’s not recommended, the functionality is in Foundation and shared between both platforms.

But to manipulate files directly, instead of through a UI/NSDocument class, requires using a new class, NSFileCoordinator. It’s interesting how little change was made to low-level systems to get this to work; instead, new functionality and restrictions were piled on top, just like NSFileCoordinator was piled on top of preexisting NSFileManager methods. One doesn’t replace the other, both are still needed.

Also keep in mind that unlike on iOS, all your documents are fully reachable by the user on Mac, if they know how to look. The way to look is to do a Spotlight search for a specifically-named document, which will find the iCloud Documents folder for your app. It won’t be labeled anything specific, just iCloud. But once you’re there, you can delete files, move files, rename files, and even drag them into different apps to edit them there, all without your app being able to do anything about it. Is this a feature? A bug Apple just didn’t get around to fixing? Dunno.

Further Mac Versus iOS Difficulties

The documentation mentions, but has no overarching solution for, the fact that many of what you would consider to be foundation-level classes are actually quite different on Mac vs. iOS. For one, think about colors: NSColor (Mac) and UIColor (iOS) are not transferable.

For two, the coordinate systems is different.

For three, the file system is different, or probably so. On iOS, the file system is case-sensitive, and you can’t change that. On the Mac, by default it’s case-insensitive, and while a diehard Unix neckbeard might change that, most users will not, so it’s a scenario you have to be ready to deal with.

The docs blithely say, “To make your document file format cross-platform compatible, you must read and write files in a case-insensitive manner.”

Sure, sure, is that all? Remember, I spent a whole episode of Edge Cases talking about how complicated and crazy Unicode is. What does case even mean for non-ASCII, non-Western characters? I couldn’t tell you.

And even if you set that aside, let’s say you want to create a new file with a certain name, “Document 1.txt”. How do you tell, on iOS, that there’s no file in that location already with any case-only variant of that name, such as “document 1.txt” or “Document 1.TXT”?

Well, you could iterate through every file in the directory, and compare its name in a case-insensitive fashion with your new name string. (Hard if you’ve got thousands of files.) Or you could keep your own dictionary of actual names mapped to, say, uppercase versions of those same strings, and so look up an uppercase version of your new name in a dictionary of them. Either way, it’s extra, fiddly, work, that’s still not guaranteed to be completely full proof.

Remember, Apple has said more than once that you should just go ahead with file operations, instead of trying to preflight them, because the situation could change out from under you between your test and your actual file system work. That’s still true here, and there’s no way to fully compensate for it.

Next up: Some bad (NSMetadataQuery issues), some good (NSDocument conflict resolution), and conclusions.

That Syncing Feeling

I’ve been interested in Mac/iOS app communication for some time now. While there are other options, most recently I’ve been exploring iCloud document syncing.

Apple has comparatively verbose documentation for iCloud (I prefer downloading the PDF), and Ray Wenderlich has not one but two tutorials, an introductory one (parts 1 and 2) and a more in-depth one (parts 1, 2, 3, and 4).

So while I’m going to talk about it at some length (and several blog posts), I don’t need to duplicate all that.

Instead, I’m going to fill in the gaps I found, and describe my own experiences.

I’ll mostly be talking about a sample app I wrote, called Documentary, that lets you make simple unformatted text documents and share them through iCloud, either to another device of the same type, or between Mac and iOS.

Part 1: Feeling Entitled

To run the iOS app on an iOS device, or the Mac app on your Mac, you’ll need to be able to support iCloud entitlements in your apps. To do that, you’ll need a paid $99/year iOS ADC account or a $99/year Mac ADC account, respectively

But what’s interesting is that, as far as I know, once you have your account(s) set up, you can download my sample code and run it, and with no other effort or changes on your part, the iCloud syncing portion of it will work right away.

Go ahead, build the iOS version (for example), and run it on two different devices, say your iPhone and an iPod touch. You can create a document on one, and it will show up a few seconds later (if you’re lucky) on the other.

For one, hey, that’s cool! Apple allows you to use its server storage even when you’re just playing around with an app. It doesn’t have to be an app you’ve registered to ship.

For two, notice that you’re using the same bundle identifier that, say, I am when I run my app, and yet there’s no worries about conflicting IDs on the iCloud servers.

This may seem strange to old Mac developers. But in the brave no-longer-so-new world of provisioning, the app ID is prefixed with your team ID (which Xcode 5 seems curiously reticent to ever show you, except in obscure error messages). Xcode handles the prefixing part of it without needing to include it in your project, and so you can use the same project that I use, while still getting your own app ID unique to you.

For three, if you build and run iOS, then Mac (or vice versa), you may get a complaint from Xcode about needing to add the iCloud entitlement again each time you build.

I’m not quite sure what’s going on under the hood, but I believe it might be because there’s no dedicated provisioning profile for each app yet. Remember that you didn’t register the app’s ID with Apple’s Dev Center website? That was a plus when you were just playing around. But it might come bite you here, if there’s some sort of temporary, fit-all-sizes profile that’s being used for both apps, and that keeps getting changed each time you build.

I was able to make the build error go away by registering those app IDs in, respectively, the iOS and Mac Dev Centers. That gives each of them an individual provisioning profile.

What’s curious is that even after I deleted the app entries again (and thus their profiles) and updated Xcode (and quit and restarted Xcode, for good measure), the builds continued to work repeatedly with no errors. So I’m not sure what’s going on under the hood. Makes it more likely it’s just a straight-up bug, eh? But keep an eye out for these problems if you try this for yourself.

Next up: What Apple’s documentation does and doesn’t say.

My Lists from 2010

If you have blogging software, you’ll often have a bunch of old draft posts, which just keep getting more and more outdated, greet you every time you open the app.

My version of this is two posts, “10 Things I Was Proud of as an Apple Engineer” and “10 Things I Wasn’t Proud of as an Apple Engineer”, started in 2010 after I left Apple.

I finished the first one, but the references in it are too old to post now. And I didn’t even get to 10 on the second; I was only able to think of 9. So I’m just going to publish the lists as-is, for a bit of nostalgia:

Proud:

99-cent songs
No-DRM music
WebKit
Environmentally-friendly progress on hardware
Infrastructure-improvements-only Snow Leopard
OS X-based mobile strategy
iOS itself
iOS’s delayed, done-right multitasking introduction
iPad
Clang

Not Proud:

“100 features” nonsense
Increasingly cheap WWDC amenities
Killing WWDC feedback sessions
Breaking own HI guidelines
Ongoing BugReporter crappiness
Suing bloggers
Suing competitors
App Store review arbitrariness
Corporate war with Google