Frameworks in Xcode 6

 

EDITED TO ADD 2014-11-10: The edit below was written while I was setting up Squarespace 6 and published to the Squarespace 5 version. I noticed that over the weekend so I went back and copied the change here.

EDITED TO ADD 2014-10-17: I found a pair of interrelated gotchas about frameworks on iOS. so there's some new information below. I did not originally have step 10 about copying the framework into the app bundle. That worked OK on the simulator but I got a dynamic link error on hardware without step 10. I also didn't have step 11 which addresses if your app target is pure Objective-C but your framework contains a Swift file. My first version of HJSKit.framework was all Objective-C code from Combat Imp but I added a Swift file while working on the new project and it broke the framework version of Combat Imp until I set the proper build flag.


Combat Imp is in the store and I have a new version winding through App Review, so I've started looking at a new project. I immediately wanted some of my custom code from what I call HJSKit and I decided it was time to convert from just using source files in multiple projects to having a shiny new framework. However that's more complicated than it ought to be. Frameworks are new to iOS 8 but there have been some weird ways to hack them for years, so it's difficult to search the web for information about the modern ways. There was a WWDC talk about frameworks this year but what they showed was creating a framework inside a single project for multiple targets to use. The Apple development documents are more for OS X and say things like "You don't want to make an umbrella framework" which is wrong: see the Swift/Obj-C interaction document which specifically tells you to use an umbrella framework. I wanted to have a completely separate project, complete with version control, and then I could add that framework to multiple projects. Oh and the new project is in Swift so I wanted Swift/Obj-C interoperability.

Here's what I found. It's actually pretty straightforward but right at then end Xcode threw me a curveball. I also had some trouble figuring out how I wanted to set up git around all of this but that's probably another post (I'm currently using git submodules, but I'm not a git expert. Let me know if you want to know what I did in Git. It took a few tries to get it all right.) This post is focused on Xcode. (All of this is correct for Xcode 6.0.1.) TL;DR: Xcode behaves badly if you try to add a project to a workspace while the project is open in another window. See my OpenRadar for more details.

The steps to a fancy framework!

  1. Pull all the code you want in your framework into a folder. I'd already done this: ~/code/HJSKit already existed.

  2. Create a new project, pick IOS->Cocoa Touch Framework.

  3. Add the files you want to the project. So far this is all completely normal.

  4. Here's where thing get interesting. The header files are actually listed as part of the target. In your Project Navigator select a header file and open the File Inspector. You should see it listed as a member of the framework but there's a new drop-down to the right that can change between "Project", "Public", and "Private". If you want the contents of the header exposed to app clients you have to change the header to "Public". For example, in HJSKit I have both HJSDebugCenter.h and HJSDebugMailComposeDelegate.h. While the apps need to see HJSDebugCenter the MailComposeDelegate is an implementation detail I'd like to hide inside the framework. So I set HJSDebugCenter.h to "Public" and left the other headers at "Project" level.

  5. You also have a new framework "umbrella" header that Xcode created. For me it's simply HJSKit.h. It has a comment that reads "In this header, you should import all of the public headers of your framework using statements like #import <HSJKit/PublicHeader.h>" This didn't work for me because that's not where I wanted the headers. I just went with vanilla #import "HJSDebugCenter.h" and it worked fine.

  6. Build this, make sure it all works.

  7. SUPER IMPORTANT: Now close the framework project window. If you don't do this Xcode will do something visually weird in the next step and you'll think you broke something.

  8. Open the project/workspace for the app client. Drag the framework's xcodeproj bundle into the Project Navigator. Make sure it drops as a top-level line, not indented under anything. (NOTE: this is where it gets weird for git. For git reasons I ended up with a copy of the HJSKit folder in each place. That's fine, just make sure you're referencing the HJSKit you really wanted.)

  9. Select the app target, go to Build Phases, and in the "Link Binary With Libraries" section click the + button. In addition to the usual iOS8.0 frameworks you'll now see a "Workspace" folder with your framework listed. Click it and you're good to go!

  10. NEW 2014-10-17: While you're in Build Phases add a Copy Files phase. Set the Destination to Frameworks, then click the + button. Select the framework from the Products folder of your framework project. See Using an Embedded Framework to Share Code for more on this step. We're not really sharing code in the sense of that article but we still need to do this step and I don't see it documented anywhere else. (The WWDC talk about Modern Frameworks doesn't show it either.) I didn't have this in the first version of this checklist and things ran on the simulator but not on hardware.

  11. NEW 2014-10-17: If your framework contains Swift code but your application does not there is a build Setting you need to configure on the app target. Under Build Options look for "Embedded Content Contains Swift Code" and set it to "Yes". This makes Xcode copy the Swift runtime into your app. Note you have to do this even if you don't call the Swift parts of your framework. The simple presence of a Swift file requires this or you'll get an error immediately in dyld_fatal_error saying that libswitCore.dylib can't be loaded. My framework didn't have Swift code originally so I didn't have this step in the first version of the checklist. If your app target itself contains Swift code then you don't need to set this Yes, as the runtime is already present.

  12. Now you can just put #import "HJSKit/HJSKit.h" in any source file and anything imported in the umbrella folder is available.

  13. You can repeat steps 8-10 for other projects, and each will get a copy. I currently have two workspaces for two different apps and each has a HJSKit framework included.

If you've read about how to hack this up in the past you'll notice what I didn't do: I didn't run lipo at any time, I didn't make an aggregate target, I didn't have to go fussing about and figure out where the Build folder is for the framework. Each app is building its own copy of the framework but that's the behavior I wanted. I have code completion from the framework in each app and the other git magic means I can push changes from one app to the other if I so desire, but I also have a version history where I can see that App A and App B are using different revisions of the framework.

Step 7 was the bugaboo for me. If you have the framework project open then the project file just sort of drops into the workspace without targets listed and isn't available when you go to "Link Binary With Libraries" in Step 9. If you close all the open windows and reopen the workspace it will work fine but I don't know why you'd know that without a lot of experimentation. After you have these all set up any other sequence I've tried to open both the workspace and the project simultaneously generates an error somewhere. But there's this one way that silently just does something broken. And it's the one that I was naturally trying yesterday because I kept thinking I had made the framework wrong, so I had the framework project open so I could tinker with it. It's basically a Heisenbug at that point.

Archiving an iPad only App

(I've submitted Combat Imp to Apple and while I wait for approval I'm doing some clean-up which includes verifying problems and writing up radars. There may be a handful of these posts coming up.)

Combat Imp is an iPad only project. Way back when I had just plunked some icons in manually (into the .plist I believe? It's been a while.) As I got ready to ship the damn thing I switched over to using an Asset Catalog for the icons and launch images. I only set up the iPad icons, because why include other sizes that wouldn't be used? And this was good.

At some point later in time I went to make an archive (inside Xcode - I was getting ready to make a beta build for testers.) The archive didn't have the nice icon, instead it had the generic grid/circles placeholder icon. Turns out that even in an iPad only app, the archive uses the iPhone iOS 7 icon and doesn't look for other icons if that slot is blank.

Not a huge deal but I was having some other icon problems at the time and I conflated two problems and went down a rabbit hole. So if your Xcode archive is lacking an icon, try adding an iPhone icon.

I wrote it up as radar 18252465 if somebody wants to dupe it. It happens on both Xcode 5.1.1 and Xcode 6 beta 7.

Road Trip 1.1 is Available

Did you get a shiny new iPhone 5? Well, an elongated version of Road Trip awaits you in the App Store! Support your favorite independent iOS developer by picking up a copy. If you've purchased it already then thank you and be sure to get the update.

If you do buy (or bought previously! Even if you rated 1.0 you can rate each version separately. Annoying but true.) a copy please take a moment to at least rate the app. I know, I know, I hate app developers begging for ratings as much (and probably more) than most people but it really does make a huge difference to sales. It amounts to the only marketing that can be done inside the App Store proper and Apple uses that data to drive how visible it is in search results and category lists. Version 1.0 never got enough ratings to display an aggregate and now that 1.1 is in the wild the review count is reset back to zero. I won't put a begging dialog in the app but I'm not above asking for ratings on the blog …

(If you're new around these parts you can read my story about developing Road Trip.)

App Store Link

UITableViews do not properly animate on an Airplay secondary screen

A few weeks back I ran into an interesting UITableView bug and I finally got around to writing up a test case and submitted a Radar to Apple. The test application source is on my GitHub and I've copied the radar to Open Radar.

I'm working on an app right now that has the option of driving a second screen. There are two methods of connecting such a screen: using a special dock cable to physically connect a monitor; and using Airplay to connect to an Apple TV device and using that screen.

Note that I'm not talking about mirroring the iPad display to the second display, I'm talking about having a completely different UIView on the second display. (This is confusing because iOS calls turning that on "Airplay Mirroring" although it's entirely up to the application whether it actually mirrors the main view or not.)

This second display is primarily a UITableView and it can potentially have quite a few rows to display. Also the intent is to have a screen that can be read at a distance so I use quite a large font (the current "large" size font is 36 point.) Since there's no touch UI there's no way for the user to directly scroll the secondary display but the app scrolls the second display to match a similar table on the iPad display. As the user manipulates the main display the secondary screen scrolls in unison. Well, at least it should and that right there is the bug. In my experience any UITableView method call that takes an animated: parameter does not animate on the Airplay secondary screen if animated: is set to YES. This works fine on the physical secondary screen, but not the Airplay. For example scrollToRowAtIndexPath:atScrollPosition:animated: will silently fail if you try to animate it.

There's also a workaround I have, and you can see it in the code on GitHub. Short form is this: animating the contentOffset value of the table in a GCD block via [UIView animateWithDuration:delay:options:animations:completion] works. So instead of simply scrolling to a row, I calculate a contentOffset for that row and scroll the underlying UIScrollView.

I was not able to find any references to this bug anywhere via Google so I'm not sure it's been reported before. If you want to animate a UITableView on an Airplay secondary screen you probably want to take a look at the GitHub repository.

MKMapView is a Naughty Minx

OK, that was nasty to find. I've been plagued on and off by a crash bug that I only seem to see on the iPad and it's never been very reproducible. Today I was putting textures in the background of the iPad screens and found a reproducible crash. Hooray! Except as soon as I put a breakpoint in the crash would go away so there's some sort of race condition/multithreading going on. Mumble grumble.

The breakpoint was everyone's friend EXC_BAD_ACCESS. Of course this means the bad code wasn't anywhere near the actual crash. (And as an aside, surprising to me because I use ARC and therefore I don't write the retain and release code. So there's probably an ARC bug in here as well. Later I'll try to make a test app and submit a Radar.)

How did I find it? That in and of itself is a convoluted story and maybe worth relaying. I turned on NSZombieEnabled (in Xcode 4 you go to the Edit Scheme window and there's a checkbox in the Run/Diagnostics panel for Enable Zombie Objects) and discovered it was my TripMapViewController class receiving a respondsToSelector: message after it had been deallocated (and after the view wasn't onscreen anymore). This is a bit of a red herring because I use respondsToSelector: a fair amount so I scrubbed through all of my code and verified that I wasn't the culprit.

OK, I have to admit that stumped me for a moment. So it would appear that some other system thread is calling respondsToSelector with a pointer that it snuck past ARC. I did something hacky just to confirm the situation: I stashed a copy of the TripMapViewController in a static pointer, which should mean ARC no longer released the object. That worked and the crash stopped. Now what? After that I realized my object was still around and that I could overload respondsToSelector: and at least see the damn callstack. Turns out this controller gets a lot of respondsToSelector: messages; enough that you can't just stick a breakpoint in there. I did some more clunky things so that I could turn on a breakpoint after viewWillDisappear had been called. Sure enough, this big pile o' hacks worked for me: I could see the callstack of a bogus call. Problem is that it was all system stuff, with several calls just labeled "MKLongHash". If you Google MKLongHash you only get one hit which turns out to be a Stack Overflow thread about ARC and messages to zombie MKMapViews. Oh really?


Sure enough that's it. If you set the delegate of a MKMapView to a controller it is possible that ARC will incorrectly release the controller while some MKMap thing is still messing about. The solution provided in the article is to explicitly set and clear the delegate on viewWillAppear/viewWillDisappear. This worked for me. Maybe the next person who Googles for MKLongHash will find this helpful :-)