Typed Payloads in SwiftUI using NSUserActivity
This post is brought to you by Emerge Tools, the best way to build on mobile.
SwiftUI and NSUserActivity
tend to go together like peanut butter and…not jelly1. Around since iOS 8, good ol’ user activity does a lot in iOS. It ranges from handoff, to spotlight search, universal links, quick note, Siri Shortcuts…and that’s not even all of what it can be used for.
And so it is, to restore state in SwiftUI, you’re left vending a user activity and it can feel a little brutal to use:
struct BooksView: View {
@State private var selectedBook: Book? = nil
var body: some View {
BooksList(selected: $selectedBook)
.onContinueUserActivity(BookActivityType) { userActivity in
if let userInfo: [AnyHashable:Any] = userActivity.userInfo,
let author = userActivity["author"] as? String,
let publishDate = userActivity["publishDate"] as? Date,
let title = userActivity["title"] as? String,
let rating = userActivity["rating"] as? Int {
// Either make a book, or set a bunch of @State vars...
}
}
}
The process of diving endlessly through a grabbag dictionary has always felt a bit risqué at best, and a runtime exception at worst. Further, the process of getting there wasn’t much better:
.userActivity(BookActivityType) { userActivity in
userActivity.addUserInfoEntries(from: ["author":book.author])
userActivity.addUserInfoEntries(from: ["publishedDate":book.publishedAt])
userActivity.addUserInfoEntries(from: ["title":book.title])
userActivity.addUserInfoEntries(from: ["rating":book.rating])
}
Of course, if the world was pure and we lived in simpler times, ideally we’d just forgo the whole dictionary bookkeeping ceremony and stick a model in there. After all, that’s all the data we’re after anyways, yeah? Something like this:
.userActivity(BookActivityType) { userActivity in
userActivity.addUserInfoEntries(from: ["model":book])
}
Alas, we know how this story ends. We can’t have nice things, and trying to do this would result in a crash, Thread 1: "userInfo contained an invalid object type"
. And indeed, the exception is warranted - we’re restricted to using only primitive types and a trip to the documentation over user activity will tell us as much:
Discussion Each key and value must be of the following types: NSArray, NSData, NSDate, NSDictionary, NSNull, NSNumber, NSSet, NSString, or NSURL.
To that end, I’ve often said “You don’t want to stick models into user info when restoring activities, because it won’t work” and I’ve been right along.
record scratch
Until I wasn’t! Or, to be clear - I’ve been half right, and half wrong after discovering some API added in iOS 14 that I missed. Exclusively in SwiftUI, you can absolutely use models if they adopt Codable
right inside your user activities.
Here it is:
struct BooksView: View {
@State private var selectedBook: Book? = nil
var body: some View {
BooksList(selected: $selectedBook)
.onContinueUserActivity(BookActivityType) { userActivity in
if let book = try? userActivity.typedPayload(Book.self) {
self.book = book
}
}
}
My goodness, so much cleaner.
Plus, saving it affords us the same conscise approach:
struct BookEditorView: View {
@StateObject private var book: Book = .default
var body: some View {
AnotherView()
.userActivity(BookActivityType) { userActivity in
try? userActivity.setTypedPayload(book)
}
}
}
Amazing! So, how does it work? Did Apple somehow do some black magic to shove a model, made of our own volition, into user info?
Well, kinda.
What it’s actually doing is leveraging the code contract that Codable
provides. Using it, the system matches the user info keys according to the coding keys from the Codable
type. So basically, it takes each property and keys them in the dictionary for us. And, in the other direction, it pulls them out and initializes your model.
I love finding stuff like this. If you’re using state restoration with SwiftUI, this is a lovely way to do it.
Until next time ✌️
-
I recently read that peanut butter and jelly is a very American sandwich - is this true?! ↩