Mike Polan

Home

Thoughts on SwiftUI

Published Apr 13, 2021

Over the last few months, I’ve been building some simple apps using the SwiftUI framework. Just for context, until recently I haven’t done any serious development in the Apple ecosystem in a very long time, so this turned out to be an exercise in both learning something new, and relearning things I’ve long since forgotten. In fact, the last time I touched anything related to macOS development specifically was probably around 2012, and that involved writing Objective-C and Cocoa from what I recall.

What brought this on was a project I was involved in at work, which required me to get my hands dirty with UIKIt and Swift for an iOS app. After a good few months of that, I started reading up on SwiftUI and where Apple had taken their developer experience in the last couple of years.

This quick post is intended to share my initial thoughts about SwiftUI, along with some cherrypicked situations where I found things to be less than intuitive. Given the nature of how much importance Apple seems to place on this framework, I wouldn’t be surprised if some or all of my points are addressed by the next release.

The Good Stuff

Overall, I’m very impressed with SwiftUI in general and where Apple has evolved the development environment. Although there are still some rough spots, the idea that you can eventually build a cross-Apple-platform app using a single set of APIs and design principles is certainly appealing. Couple that with the fact that Swift is a much cleaner, more streamlined language compared to Objective-C and you have a pretty solid foundation in place.

Previews

In my opinion, this is a killer feature just by itself. Gone are the days of messing with complex and almost unparsable storyboards and xib files. No more fumbling around with Auto Layout and trying to get that magical set of constraints to do what you want it to do on the simulator. Now we can simply write some code, and watch it come to life right in Xcode in seconds.

The main benefit here is really obvious: a faster feedback loop. This reminds me of the typical experience when working on frontend code, especially frameworks like React where you can make a few changes, and see them in action almost immediately afterwards. This is especially useful when I’m trying to build a piece of UI that appears further down on the view hierarchy, which would normally require several clicks to get to if the app were running live. On top of that, being able to feed various combinations of data into the view under test as part of the preview setup will, in many cases, help identify and call out potential layout or visual issues right off the bat.

View Modifiers

Hands down one of my favorite features in the framework. At first when I heard of this functionality, I wasn’t really sure how I would leverage it in my projects. It wasn’t until my apps started getting a bit more complex that I realized the value of simply attaching a custom view modifier and bringing in a particular behavior for specific views on demand.

The real beauty of this is the fact that I can easily create very specific, supplemental view behaviors without needing to create my own View implementations outright. I found this most practical for applying visual styles to my existing views.

struct ImageHover: ViewModifier {
    @State private var hovered: Bool = false
    let systemName: String
    
    func body(content: Content) -> some View {
        HStack {
            content
            Spacer()
            Image(systemName: hovered ? "\(systemName).fill" : systemName)
        }
        .onHover { hovering in
            hovered = hovering
        }
    }
}

extension View {
    func imageHover(_ systemName: String) -> some View {
        return self.modifier(ImageHover(systemName: systemName))
    }
}

The above may not seem all that useful, but the key thing that struck me most is that I can apply this modifier to any view in my hierarchy, without caring about the details of the view in question. It’s a neat concept that I think was very well thought out by Apple.

AppKit/UIKit Integration

NSViewRepresentable and UIViewRepresentable are nice escape hatches when there is something that can only be done by dropping back to AppKit and UIKit functionality, respectively. At least from the onset, this is to be expected as the framework evolves. More importantly, these protocols cover the occasional need to use existing third-party libraries which haven’t managed to (or maybe never will) migrate to SwiftUI. I personally have tried to do as much as I can in plain SwiftUI, but the few times I needed to reach for an AppKit component, I found the integration to be fairly decent apart from the the random NSView-derivative that really refused to cooperate.

The Quirky Stuff

It’s not all sunshine and rainbows, but that’s to be expected. Since SwiftUI is still a fairly new framework, it’s not surprising that I’ve hit a few bumps in the road when trying to leverage it for more complex scenarios. Here are just a few that come to mind as potential pitfalls that I’ve encountered as someone who’s just getting into SwiftUI.

Ordering of View Modifiers and Methods

I had a basic understanding that the order in which you place modifiers on a view can have some implicit impact on the way the view ultimately is rendered and how it behaves.

struct MyView: View {
    var body: some View {
        Image(systemName: "star.fill")
            .resizable()
            .background(Color.blue)
            .foregroundColor(.yellow)
    }
}

In the code above, you can swap the ordering of background and foregroundColor, and you’ll end up with the same result. This made sense to me, since both of those modifiers operate on and return a View. If you tried to move the call to resizable after either of the former, then you’d wind up with a compiler error. Again, this made sense since resizable is a method specifically on the Image struct only.

One situation that caught me by surprise, however, involved a view where I needed to support both onTapGesture and contextMenu.

struct MyView: View {
    var body: some View {
        List(data, id: \.self) { item in
            Text(item)
                .contextMenu {
                    Button("Menu Item") {
                        print("Menu!")
                    }
                }
                .onTapGesture {
                    print("Click!")
                }
        }
    }
    
    private var data: [String] {
        ["January", "February", "March"]
    }
}

The intent was to have each list row respond to either a single click or show a context menu. In my initial attempt, I placed contextMenu before onTapGesture, like in the code above. The interesting result is that list items only responded to single clicks! No amount of control-clicking brought up the context menu I was expecting. It wasn’t until I changed the ordering of the two that I got the desired effect.

I’m still not exactly clear on why this is. My guess is that onTapGesture, and maybe other gestures as well, takes precedence over contextMenu when it’s applied to a view.

Building Complex Text

This is one area where I’d really like to see Apple improve SwiftUI. The standard Text view is fairly spartan; you can assign it a string, apply some basic styles, and even concatenate multiple Text views. The latter of these is neat. You can, for example, programmatically build a complex Text view just by concatenating many smaller Text views together.

struct MyView: View {
    var body: some View {
        makeText()
    }
    
    private func makeText() -> Text {
        let messages = [
            Message(string: "Blue!", color: .blue),
            Message(string: "Red!", color: .red),
        ]
        
        return messages.map { message in
            return Text(message.string)
                .foregroundColor(message.color)
        }.reduce(Text(""), +)
    }
    
    private struct Message {
        let string: String
        let color: Color
    }
}

I think this is a pretty powerful construct. The code above is a bit contrived, but given the range of other text styles you can apply, you can imagine the possibilities here.

Things get a bit more complicated when you attempt to apply a style that has no existing Text modifier available. For example, if I wanted to construct a Text view with a background color, I would instinctively reach for the background modifier.

struct MyView: View {
    var body: some View {
        Text("Old school!")
            .foregroundColor(.green)
            .background(Color.black) +
        Text("New school!")
            .foregroundColor(.blue)
    }
}

But wait, there’s a gotcha here! Recall that the background modifier returns View, not Text. The end result is that concatenation won’t work since you’re effectively trying to combine a Text with some View, and that’s not going to fly.

Semantically, I can understand why: the background modifier places a view behind the target view, according to Apple’s documentation on the matter. Meaning we’re not actually changing the background of the Text, but instead composing another view made up of our Text and another one with our assigned background color.

This is just one example, but there are certainly many more. Trying to apply more involved styles, such as successive background colors, hover effects and clickable text with popovers, composed in a single piece of text, is not very straightforward with simple Text views alone. I had some success placing successive Texts into an HStack, although that would eventually lead to some weird wrapping behavior when all of the content could not fit on a single line. In these cases, I usually resorted to using an NSAttributedString inside of a NSTextView, which has its own pains to deal with, but at least it fills the void in the meantime.

Conclusion

Long story short, I’m looking forward to seeing where Apple decides to take SwiftUI next. I’m hopeful that some of the well-known gaps in functionality will be addressed, but more importantly I see this as an opportunity for Apple to consider what APIs and components to introduce to attain parity to begin with. Time will tell!