SwiftUI: Handling optionals

A friend recently asked me if I’d write a few words about SwiftUI and optionals. Like nearly everything in SwiftUI, you have to rewire your brain a little bit when thinking about this because SwiftUI is perfectly happy working with optional views, such as Image? and Text?.

The tl;dr of this post is going to be “use map” but before I get there, let me dive in a little deeper. And, of course, whatever I got wrong, please let me know so I can learn more and correct this.

You can feed SwiftUI an optional view, such as Text? with the understanding that the system will only render non-nil values. Here are some screenshots that show the output in both cases:

But what happens when you want to work with optional data that’s driving your view layout? You don’t want to use nil-coalescing (unless you have some compelling backup view case). Instead, if you want to render without a backup value, you have to dig a little deeper. Don’t automatically reach for the familiarity of conditional binding. You can’t if-let in SwiftUI the way you expect to:

My “clever” workarounds really weren’t very clever:

Although, SwiftUI supports the if-statement, prefer map as your first line of attack:

You can see how much more elegant the map version is in comparison. Force-unwraps make unicorns cry and contribute to overall levels of human misery. That’s not to say that if isn’t useful, rather it’s just not my preferred approach for optionals in SwiftUI:

VStack {
  Text("Top")
  if name != nil {
    Text(name!)
  }
  Text("Bottom")
}

(Note: I’m exploring @ViewBuilder closures right now and there’s some really cool stuff including buildEither and buildIf content that I haven’t dived deep into yet.)

Be especially careful and read the documentation when you think you’re going to be working with failable initializers because sometimes you won’t be. For example, SwiftUI’s Image does not use a failable initializer.

I can’t tell given the current stability of the system whether Image(systemName:"notarealname") returns an empty image, which I guess wouldn’t be too bad, or always crashes (I’ve had a bunch of bad crashes) but my most common outcome is a frozen playground with a severe emotional breakdown cowering in the corner and hugging itself.

I emphasize this gotcha because you might not catch the potential meltdown if you only pass it well behaved strings during testing (as in the following case). It’s important because it can bite:

In contrast, UIImage uses a failable initializer and returns an optional, which you can map through an Image with consistent good outcomes at each point:

If you want to get really OCD about all this stuff, you could add an extension on optional that allows you to include a visual error instead of omitting the view, but I’m not entirely sure that’s tremendously useful:

I’m out of time and have to head back to work. Thanks for having lunch with me.

One Comment

  • Hey I liked your post. I would recommend you also describe how to use map as first class function.
    I mean your example would looks much better when you write
    name.map(Image.init(systemName:))

    instead of
    name.map ({ Image(systemName: $0) })

    Thanks for your article.