What I didn’t talk about was what a pain in the ass working with the Keychain is.
Over the years, I’ve seen a lot of codebases that included a lot of utility classes to make dealing with those ancient, ancient C-based Keychain APIs a little easier.
What I haven’t been able to find is a modern Swift library that does so. For Secrets, I wound up just copying in the files from Sam Soffes’s SAMKeychain library, which from my cursory googling seemed like one of the most recently-updated Keychain helper libraries. But it’s in Objective-C.
Any Swift-y Keychain libraries out there?
Turns out, my entire previous post was trying to solve a problem that doesn’t exist.
I assumed, because I’d heard rumors about it and found this authoritative-sounding forum post, that Apple had indeed removed the persistence of Keychain entries for an app if a user deleted the app from their iOS device.
But while Apple did this in a beta release, they didn’t ship it in the final version (thanks to Nick Lockwood for pointing this out to me).
I verified this for myself with the new sample project Secrets, and you can too by downloading and running it for yourself, on both iOS 10 and iOS 11.
The app just shows a simple view with one text field, where you can type in anything you’d like. It is then saved in the app’s Keychain.
If you kill the app and come back to it, that value is again displayed.
If you delete the app and reinstall it, the value is still displayed. This is true both in the iOS 10.3.1 Simulator and the iOS 11 Simulator in Xcode 9 beta 4.
Now, in the iOS 11 Simulator, due to a bug, you can’t delete an app through the regular Simulator user interface.
So instead, you must delete it by hand from the file system, by going to
~/Developer/Library/CoreSimulator/Devices/, finding the UUID that matches that of the Simulator you’re using, then within that finding the UUID of your app, inside
But if you go through all that trouble in iOS 11 Simulator, then restart the Simulator and verify that the app is gone, then reinstall it…the Keychain entry is still there.
So you don’t need to use Shared Web Credentials the way I described in my last blog post. You can continue to rely on local app Keychain entries to keep your users logged in, even if the user deletes your app and reinstalls it.
Update: See my next post for why the problem described here isn’t actually a problem.
In Apple’s WWDC 2017 session “Introducing Password Autofill for Apps”, the presenter describes this scenario: a user downloads an app, but is put off from using it when they’re required to log in with a username and password first.
I’ve worked at a number of companies built around a consumer service, and they all worry about this.
If a user who doesn’t use something like 1Password sets up a strong password, it’s likely they’ll forget it by the next time they need to log in. You can have them reset it, but…will they bother?
Apple’s solution assumes the user has logged into the service in Safari, either on that device or, if they have iCloud Keychain turned on, on another Apple device.
When they go to the username or password text fields in your app on iOS 11, in the QuickType bar above the on-screen keyboard, Password Autofill shows a little key icon to the right, and some text telling them that it now contains the username and password for that service. If they tap it, it will fill in the text fields with the saved credentials, and they’re on their way.
Password Autofill will attempt, via heuristics, to provide this information even if you’ve done nothing to support it in your app (besides connecting your app to your website, see below), though there are ways you can tweak your app to make the user interface connection more explicit.
If the user hasn’t logged into the service in Safari previously, or they did it on another device and they don’t have iCloud Keychain turned on, they’re screwed. Apple doesn’t have that information, and can’t offer it to them.
What if they’ve logged into the service before in that app, then deleted the app, then reinstalled it?
In the past, you could rely on the local app-specific Keychain entries surviving under such circumstances. So your app could still access the username and password from there, and automatically log the user in. But Apple took this away in iOS 10.3.
Both technologies require an Apple-recognized association between your website and your application; the WWDC session for Password Autofill and the documentation for Shared Web Credentials both spend a lot of time describing how to set up that relationship, which is also needed for Universal Links.
By using the Shared Web Credentials APIs, you can tap into that same reservoir of usernames and passwords that Password Autofill uses, but completely bypass the username and password fields of your application: you get the data yourself and can use it directly.
Why does anyone need Password Autofill, then? My hunch is that Shared Web Credentials didn’t take off the way Apple wanted — it does require more work, after all. So they made something even easier, something app writers might not need to do anything at all to support.
Now let’s think about it in the other direction: if Password Autofill is so easy, why worry about Shared Web Credentials at all?
Because Shared Web Credential let you go in the other direction.
If a user does log in via your app, you can use Shared Web Credential to push that information into Safari (and iCloud Keychain, if it’s on). Then, if they delete the app and restore it, you can still log them in automatically by getting that information back out of Safari. Note: doing so isn’t invisible: the user will be prompted with an Apple-provided confirmation alert in both cases.
So if you really want to make things as easy as possible for your users, it seems to me you’ll want to support Shared Web Credentials in both directions, before worrying about Password Autofill at all. It’s not as foolproof as the old local Keychain solution, since the user can reject sharing their credentials with Safari, but it’s better than nothing.
Am I missing anything about this?
For the tl;dr crowd, my take is that interviews are extremely difficult, and so you (and I) should have some empathy.
A while ago, I did a bunch of interviews for a junior iOS developer. I gave them what is apparently a quite common exercise: in a simple iOS app, get some JSON data from a server and use it to populate a table, including, for each entry, a link to an image file to download separately.
I just made such a project, called Numbers, available here.
When you first run it, it shows a table filled with entries 1-20, where the brightly-colored number icons are each loaded separately.
A button to the right of the navigation bar is labeled “Wrong”, meaning you’re currently using the image loading implementation that’s incorrect.
If you scroll to the bottom of the table quickly, you’ll see that initially, the rows you uncover will be temporarily populated with incorrect icons:
That’s because, in the incorrect implementation, the async network calls insert the loaded images into the cells that originally requested them.
But, as Everyone Knows, cells in UITableView are reused when you scroll, which means by the time the network calls finish, the original cell might be in use for at a different row, and it shouldn’t display the original row’s contents.
Instead, the network call for an image should update the cell that currently represents the row.
If you tap the “Wrong” button, it will change to the text “Right”, and that’s how the application will behave when it reloads the table. Scrolling quickly to the bottom of the table won’t result in erroneously populated images anymore.
When I was interviewing, if the interviewee said they knew table views, I would give them this exercise, and would consider them not worth hiring if they made that rookie mistake.
Nowadays, it’s clear to me that this is a Gotcha! question like any other.
Instead of it being a question about how they think, how they solve problems, it requires a very specific piece of information you either have it your head at that moment, or you don’t.
In a recent interview where I was debugging a problematic table view implementation, I failed to recognize a similar incorrect image loading mechanism — until nudged to do so by the interviewer. I just didn’t see it. If that interviewer had been as quick to judge as I had been in the past, I wouldn’t have gotten the job.
As a final note of curiosity, you might notice that, in the Numbers project, I reset the NSURLSession each time before reloading the table view’s contents. That’s because NSURLSession has its own cache of network call results, and if I didn’t reset it, you would only be able to reproduce the “wrong” behavior the very first time you tried it. Every subsequent time (including across app relaunches), the images would “load” instantaneously from the cache.
While I would never recommend shipping the wrong implementation, even if you did, these days, Apple’s frameworks would mitigate its impact.
Here are the favorites of mine that are still around and active.
Erica Sadun (Twitter)
I’ve known about Erica’s work since the 2013 Edge Cases episode “Rectangles on a String”. (I even know how to pronounce her last name, even if she does pronounce tuple wrong.) Her blog updates several times a week, often about some detail of Swift development. She’s written a bunch of books, mostly focused on developers, and one book on Swift.
Becky Hansmeyer (Twitter)
From her own description, her blog is “my own little place to comment on Apple & general technology news, as well as what it’s like to be a novice developer with no prior programming experience.” Her tag line is “100% grass-fed Swift”.
And finally, a great blogger who’s been very active recently who’s not on the list:
I found an interesting (to me) aspect of Swift/Objective-C interactions this week.
Take this Objective-C method:
+ (nullable NSData *)dataWithString:(nullable NSString *)string error:(NSError **)error
It uses the standard Apple pattern of having both a return value and an error. (I left out the error’s nullability annotations for brevity, as Apple always assumes them.)
In theory — and, if I’m remembering correctly, according to Apple guidelines — first, you’re supposed to check if the return value is invalid. Only once you’ve verified that it’s invalid should you check to see if there’s an error.
And as far as I’m been aware, there’s never been any assumption that you’ll get an error. That’s why, throughout your Objective-C code, you always have to check the return value and treat that as gospel.
If you use this method in Swift, the auto-generated Swift signature is:
func data(with string: String?) throws -> Data
I mean, besides the fact that Apple’s compiler/runtime magic smoothly converts between the Objective-C’s last-parameter-is-an-error-pointer pattern and Swift’s “throws” pattern.
The return type doesn’t allow for nil anymore.
You can’t check for an invalid value, if “invalid” means nil.
Instead, you can only assume that the original Objective-C implementation will “throw” an error if there is a problem.
Now, go back to your original Objective-C method. What if you return nil but don’t set the error? What does Swift do?
It does something clever.
In my testing, even when you haven’t set an error, the Swift translation layer throws an error anyway.
If you log it, it’s called
It’s got a
Foundation._GenericObjCError and a
Feels a bit like a hack, doesn’t it?
But it does prevent the problem of old Objective-C code not indicating the desired result under Swift.
I’m putting together a post comparing Mac drag and drop APIs and iOS drag and drop APIs.
Since I haven’t internalized the pattern between Objective-C and Swift method conversions, I was often frustrated by how to translate Objective-C method calls to Swift method calls.
While I was working on the project, it seemed that 4 times out of 5, when I tried to go to a class or protocol’s declaration in Apple’s headers and see its Swift-ified methods, Xcode would take me to the Objective-C header instead, even though I was starting off in a Swift file.
Of course, now that I’m trying to reproduce it to file a Radar, it doesn’t happen. I wonder if that’s because the final project has no Objective-C files in it at all.
It doesn’t help that the translations changed between Swift 3 and Swift 4.
NSPasteboardTypeTIFF in Swift 3 is now
NSPasteboard.PasteboardType.tiff in Swift 4, with a similar pattern for all its friends.
register(forDraggedTypes newTypes: [String]) is now
registerForDraggedTypes(_ newTypes: [NSPasteboard.PasteboardType]).
It’ll be nice to be working exclusively in Swift for the rest of this effort.