iOS 13, Unit Tests, and Mocking a SceneDelegate

I've been working through the exercises in the excellent iOS Unit Testing by Example book by Jon Reid, which I highly recommend. However, the book is in beta at the moment and there are some curveballs thrown by iOS 13 that aren't handled in the text yet. Specifically, when I hit the section about using a testing AppDelegate class I thought "This is very good. But what about the SceneDelegate?"

That kicked off quite the odyssey to be honest. If this is interesting to you I wrote up a Gist documenting what I found, and a partial solution to the issue.

Simple keyboard/text field interaction handling on iOS

EDITED TO ADD 5:15 PM PST I updated the gist because I realized I wanted to have a block run whenever I finished adjusting the view and therefore I added a completionBlock property. I'm using this to scroll a UITableView that contains text fields: whenever the keyboard finishes adjusting then I scroll the UITableView so the cell being edited is at the bottom. It works pretty well!


RoadTrip has a couple of places where if you hold the phone in landscape the keyboard can cover up a text field you're trying to edit. It's always struck me as weird that iOS can't handle this for you automatically but it doesn't, so there's a little weird piece of code you have to write to rearrange views somehow. In both places in RoadTrip I wanted to do the same thing: simply slide the main view upwards so that the field being edited is just over the top of the keyboard. I even wrote a comment saying that I should make this code some sort of generic solution, but I never went back to do so.

While working on 1.6 I noticed that the new predictive text bar on iOS 8 caused a problem with that code, so I patched it up. While doing that I noticed that the API for keyboard position changes does something other than the documentation does: if you ask for the notification curve you're supposed to get a UIViewAnimationCurve enum but sometimes you get 7, which is really a bitfield you can assign to the proper parts of the UIViewAnimationOptions. General consensus on the web seems to be to just take the "UIViewAnimationCurve", bitshift it up 16 places, and slam it into the UIViewAnimationOptions but that seems awful fragile to me.

Anyway, I fixed it up for RoadTrip, made another mental note to extract that code and submitted it.

Yesterday was the day I really did. I rewrote it into Swift and made a simple little widget that you can just create, feed it a view, and it watches for the keyboard notifications. It's a pretty simple class overall and I threw up a gist with the source. Take a look if that sounds like something you'd like to have. It uses my logger, but you can easily remove those calls.

I'm not sure what the deal is with the damn HeaderDoc comments. Sometimes they work for me in Xcode's Quick Help, sometimes they don't and I can't find anything that makes a difference. If anybody knows how to use HeaderDoc to make comments in Swift framework code that works reliably in both Objective-C and Swift let me know. I can't find anything about it.

Using Adaptive Transitions in iOS 8

Hey Party Peoples! I've been writing a new app, all iOS 8-only. Gonna use Swift! Gonna use adaptive segues! It's gonna be modern, it's gonna be cutting edge! No more separate iPhone/iPad storyboards for this guy. I've watched my WWDC talks, I'm good to go! Well hold on there space cowboy!

Let's talk a bit about popovers. I'll assume you've watched 214 about View Controller Advancements, you've watched 216 about Building Adaptive Apps, and you've watched 228 about Presentation Controllers. There are several talks that all say something like "and you can use an adaptive popover segue and it will work on an iPhone. You need to add a back button which we can't go into here, but go watch 228 for more on that." 228 seems to be the endpoint for popover knowledge and 288 does something very subtle that … I don't think the intent was to mislead but it led me astray. I watched the key bit several times before I caught the omission.

The main glitch, to my mind, is that the popover example (the "Important People" app) isn't using storyboards and that they show code snippets on the slides but they don't provide the entire source to the Important People app. There is a point around 9 minutes in where they show flipping between a compact and regular horizontal size class and how the view is a popover in regular and fullscreen in compact. Of course, they couldn't talk at WWDC about why you cared but we know now: this is what happens when you rotate a iPhone 6+ into landscape.

Here's the transcript from asciiwwdc.com:

So to do adaptivity with popovers is fairly simple.

Going back to Peter's code that we left off with is that this code doesn't actually change.

All through that, this was the code that presented that popover.

But to influence how the popover actually works, we need to set a delegate on the popover Presentation Controller.

And we need to implement just two methods.

And in the slides they show us the two methods and it seems great! Except if you do that in your app it won't work. Plus the sample code is weird. They create a UINavigationController up from scratch and make the root controller the presented view. How do you ever get out back out of that? Where did that weird Dismiss button come from in the first place?

Look at that transition when they demonstrate the "Dismiss" button very carefully. It isn't a push transition, it is a Cover Vertical modal presentation. That's why the button isn't a "Back" button and it's not on the left. That's the rub: I'm pretty sure they manually built a button and stuffed it in the navigation item but they never show that code. If you blindly "just implement two methods" and drop in the code they show in the slide you will be sad. You get stuck when you run in compact horizontal (ie: any phone in portrait) where the view is full screen and there's no way to dismiss it. The missing code is simple but I think the presentation inadvertently implies the runtime provides it and it does not. Here's my Swift version:

// Clipped from viewDidLoad
    navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Dismiss",
        style: .Plain,
        target: self,
        action:"dismiss")

func dismiss() {
    dismissViewControllerAnimated(true, nil)
}

I was naively assuming we had a UINavigationController at root and the popover segue adaptively became a push. It does not. I don't think you can do that. Popovers adaptively become modal presentations and thus you need to dismiss the view controller when you're done. (It doesn't have to be a Cover Vertical transition. It will use whatever modal transition you set on the destination view. You can use a flip or whatever if you'd prefer.)

I had a version of my where the storyboard had the root UINavigationController and I had both the push and the popover transition in the storyboard and checked the size class at run time to select the proper segue. It worked, but you couldn't rotate the device after presentation and have it change adaptively. The code example above, plus the code from the slides in 228 does exactly that. On the 6+ the screen will flip between a full screen with a "Dismiss" button and a popover fluidly as you rotate.

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.