How Do You Know Whether or Not SwiftUI Previews and Preview Content Are Excluded From App Store Builds?

I’ve found what I believe to be a bug, or at least deeply disappointing behavior, in Xcode’s treatment of SwiftUI previews. I’ll put an explanation together in the paragraphs that follow, but the TL;DR is: I think you’ll probably want to start wrapping all your SwiftUI Previews and Preview Content Swift source code in #if DEBUG active compilation condition checks.


Screenshot of an Archive action build failure.

SwiftUI previews are, if you consider it from a broader perspective, comprised of two kinds of source code:

The SwiftUI preview dream is supposed to be this:

The reality is less than that ideal:

There are several workarounds, none of which are spectacular:

That’s the long-winded explanation.

Until and unless Apple makes ergonomic improvements to align SwiftUI Preview and Development Asset conditional compilation techniques, I recommend wrapping both kinds of source code in #if DEBUG to prevent accidental slippage of test data and source into production code, as well as to prevent unexpected build failures on multitenant hardware (for teams that build for App Store in CI, these build failures often don’t appear except on unattended machines around the time a release is being cut, making them perniciously difficult to spot during day-to-day code review).

|  20 May 2024




Be Careful When You Initialize a State Object

I’m going to share some best practices when using @StateObject property wrappers, things learned the hard way, via some bugs that were difficult to diagnose and nearly impossible to notice during code review—unless one knows what to look for.

The short version is this: if you have to explicitly initialize a @StateObject, pay close attention to the fact that the property wrapper’s initialization parameter is an escaping closure called thunk, not an object called wrappedValue. Do all the wrapped object initialization and prep inside the closure, or else you’ll undermine the performance benefits that likely motivated you to use @StateObject in the first place.

Several years ago, before the @StateObject property wrapper was introduced, if your SwiftUI view needed to create and own an object to perform view-specific duties that can only be performed by a reference type (say, to coordinate some Combine publishers), the only option was an @ObservedObject:

struct MyView: View {
    @ObservedObject private var someObject = SomeObject()
}

A chief problem with this API is that the wrapped object’s initializer (in this example the = SomeObject()) would be run every time MyView, the struct, was initialized. Since this view is just a child of some other ancestor view, any time the ancestor’s body property gets accessed, MyView will be initialized anew, causing SomeObject() to be initialized again and again:

struct SomeAncestor: View {
    var body: some View {
        MyView() <-- gets invoked anytime SomeAncestor.body is read
    }
}

Remember that a SwiftUI View is not the view object you see on screen, but rather just a template describing the view object that will be created for you at a later time. Since the body property of a view returns merely a template, the guts of the SwiftUI framework operate under the assumption that a body can be accessed as many times as needed to recompute these templates.

To prevent unwanted successive initialization of wrapped objects, the @StateObject property wrapper was introduced. It is often a simple drop-in replacement for an @ObservedObject:

struct MyView: View {
    @StateObject private var someObject = SomeObject()
}

With this change, anytime SwiftUI traverses the view hierarchy, recursively calling into body property after body property, if possible, the storage mechanism within the @StateObject property wrapper will be carried forward to the new view struct without causing the wrapped object to be initialized again. It’s a bit magical, but honestly a bit too magical, since what I’ve just described contains two hidden details that need close attention.

First, when I wrote “…if possible…” in the previous paragraph, I was referring to the fact that SwiftUI needs to be able to make the determination that two instances of a given view struct should be interpreted as being templates for the exact same on-screen view object. The term for this concept is “identity”. Two View structs are understood to have the same identity if they share the same identifier. This identifier can be either explicit, or implied.

Explicit identification looks like this:

struct SomeAncestor: View {
    var body: some View {
        MyView()
            .id("the-one-true-view")
    }
}

Implicit identification is harder to grok. Sometimes it can be inferred from the combination of an Identifiable model in conjunction with a ForEach:

struct Thing: Identifiable {
    let id: String <--- required
    let name: String
}
struct SomeAncestor: View {
    let stuff: [Thing]
    var body: some View {
        ForEach(stuff) { thing in
            MyView(thing)
        }
    }
}

In the above example, the particular init method of the ForEach accepts a collection of Identifiable model values, which allows the guts of the ForEach body to assign identifiers to each MyView, automatically on your behalf, using the id properties of the model values. Here’s that initializer from SwiftUI’s public interface:

extension ForEach 
where ID == Data.Element.ID, 
Content : AccessibilityRotorContent,
 Data.Element : Identifiable 
 {
    init(
        _ data: Data, 
        @AccessibilityRotorContentBuilder content: @escaping (Data.Element) -> Content
    )
}

SwiftUI has other mechanisms to try to infer identity, but if you’re not explicitly providing identity for a view that owns a @StateObject, it’s possible that your wrapped object is getting intialized more often than you desire. Setting breakpoints at smart places (like the init() method of your wrapped object) is a helpful place to look.

I wrote that there were two hidden details hiding in the magic of @StateObject. I just described the first one, it’s hidden reliance on view identity, but there is another issue that’s particularly subtle, and that’s the mechanism by which it’s possible for a @StateObject to avoid duplicate initializations of its wrapped object. The best way to see it is by looking at the public interface:

@propertyWrapper 
struct StateObject<ObjectType> : DynamicProperty 
where ObjectType : ObservableObject 
{
    init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
}

Look closely: the initialization parameter is an escaping closure called thunk, marked with the @autoclosure label. The parameter is not the object type itself. You might have assumed, like I did at first, that the initalizer looked like this:

@propertyWrapper 
struct StateObject<ObjectType> : DynamicProperty 
where ObjectType : ObservableObject 
{
    init(wrappedValue: ObjectType)
}

This might seem like an academic distinction, until you run into a situation where your view needs to explicitly initialize the @StateObject property wrapper. If you aren’t careful, it’s possible to completely undermine the benefits of StateObject.

Consider this example, which is similar to something that I actually had to do in some production code. Let’s say that I have a podcast app with a view that displays an episode’s download progress:

struct DownloadProgressView: View {
    ...
}

My app uses CoreData, so I have a class Episode that’s a managed object. It has some properties, among others, that track download progress for that episode:

class Episode: NSManagedObject {
    @NSManaged var bytesDownloaded: Int
    @NSManaged var bytesExpected: Int
    ...
}

I need my view to update in response to changes in those properties (and let’s say, for reasons outside the scope of this post, it isn’t possible to migrate this app to SwiftData, because it’s not ready for the limelight), which means I need to use KVO to observe the bytesDownloaded and bytesExpected properties. Since I can’t do that observation from my DownloadProgressView directly, I’ll need an intermediate object that sits between the managed object and the view:

class DownloadProgressObserver: NSObject, ObservableObject {
    @Published private(set) var progress = 0
    init(episode: Episode) {
        super.init()
        startObserving(episode)
    }
}

The only thing left is to update my view to use this new class. Since nothing else in my app needs this intermediate object, it’s sensible for my view itself to be what creates and owns it, just for the lifetime of my view being on screen. Sounds like a @StateObject is a good fit:

struct DownloadProgressView: View {
    @StateObject private var observer = DownloadProgressObserver(episode: WHAT_GOES_HERE)
    ...
}

OK, so I cannot use a default value to populate the observer, because my observer has a required initialization argument that cannot be obtained until runtime. So I need to provide an explicit initializer for my view:

struct DownloadProgressView: View {
    @StateObject private var observer: DownloadProgressObserver
    
    init(_ episode: Episode) {
        let observer = DownloadProgressObserver(episode: episode)
        _observer = StateObject(wrappedValue: observer)
    }
}

Looks great, right? Actually, it’s really bad. Because I initialize the observer object as a separate statement, and pass a local variable as the wrappedValue, it is (sort-of, in a pseudocode-y way) equivalent to the following code:

let observer = DownloadProgressObserver(episode: episode)
_observer = StateObject(thunk: { observer })

Remember that the initialization parameter is an escaping closure called thunk. This closure is only ever run once for the lifetime that my DownloadProgressView’s associated UI object is displayed on screen. The instance returned from the one-time execution of thunk() is what gets supplied to the SwiftUI views. But my DownloadProgressView’s initializer will be run many, many times, as often as SwiftUI needs. Each time, except for the very first initialization, all those DownloadProgressObserver objects that my init body is creating end up getting discarded as soon as they’re created. If anything expensive happens inside of DownloadProgressObserver.init(episode:), that’s a ton of needless work that could degrade performance at best, or at worst, could introduce unwanted side effects (like mutating some state somewhere else).

The only safe and correct way to explicitly initialize a @StateObject is to place your wrapped object’s initialization inside the autoclosure:

struct DownloadProgressView: View {
    @StateObject private var observer: DownloadProgressObserver
    
    init(_ episode: Episode) {
        _observer = StateObject(wrappedValue:
            DownloadProgressObserver(episode: episode)
        )
    }
}

That ensures that the object is initialized exactly once. This is particularly important to remember if preparing your wrapped object requires several statements:

struct MyView: View {
    @StateObject private var tricky: SomethingTricky
    
    init(_ model: Model, _ baz: Baz) {
        _tricky = StateObject(wrappedValue: {
            let foo = Foo(model: model)
            let bar = Bar(model: model)
            let tricky = SomethingTricky(
                foo: foo,
                bar: bar,
                baz: baz
            )
            return tricky
        }())
    }
}

It would be natural to want to write all those statements in the main init body, and then pass the tricky instance as the wrappedValue: tricky parameter, but that would be wrong.

Hopefully I’ve just saved you an hour (or more) of fretful debugging. Apple, to their credit, did include warnings and gotchas in their documentation, which I could have read before hand, but didn’t think to read it:

If the initial state of a state object depends on external data, you can call this initializer directly. However, use caution when doing this, because SwiftUI only initializes the object once during the lifetime of the view — even if you call the state object initializer more than once — which might result in unexpected behavior. For more information and an example, see StateObject.

Side note: this whole debacle I created for myself points out the risks of using @autoclosure parameters. Unless one pauses on a code completion and jumps into the definition, it’s very, very easy to mistake an autoclosure for a plain-old parameter. An end user like me is not entirely to blame for my mistake, given how most (all?) other property wrappers in SwiftUI do not use autoclosures.

|  14 Mar 2024




Scaled Metric Surprises on iOS & iPadOS

UIKit’s UIFontMetrics.scaledValue(for:) and SwiftUI’s @ScaledMetric property wrapper offer third-party developers a public means to scale arbitrary design reference values up and down relative to dynamic type size changes. Please be aware, however, that scaled values are not scaled proportionally with the default point sizes of the related text style. They scale according to some other scaling function that differs considerably from the related text style. An example will help illustrate this. Consider the following code:

let metrics = UIFontMetrics(forTextStyle: .body)
let size = metrics.scaledValue(for: 17.0)

If the device is set to the .large dynamic type setting, the identity value is returned (size == 17.0). If you then downscale the device’s dynamic type size setting to .medium, one might expect the returned value to be 16.0. After all, the default system .body font size at .large is exactly 17.0, and the default .body font size at .medium is exactly 16.0. But instead, this is what happens:

// current dynamic type size setting is .medium...
let metrics = UIFontMetrics(forTextStyle: .body)
let size = metrics.scaledValue(for: 17.0)
// size == 16.333...

The divergence is even more pronounced the further up/down the dynamic type size range one goes:

The red text in each pairing the above is the system default .body font, and the black text is a system body font obtained using a ScaledMetric:

@ScaledMetric(relativeTo: .body) var scaled = 17.0
var body: some View {
    Text("Quick, brown fox.")
        .foregroundStyle(.red)
        .font(.body)
        .overlay {
            Text("Quick, brown fox.")
                .foregroundStyle(.black)
                .font(.system(size: scaled))
        }
}

So if you need to scale a bit of UI in exact proportion to the .body font size, avoid using ScaledMetric/UIFontMetrics scaling APIs and instead directly obtain the pointSize from a system body font and use that value instead:

UIFont.preferredFont(
    forTextStyle: .body,
    compatibleWith: traitCollection
).pointSize

(EDITED: An earlier version of this post included an erroneous assumption that has been living rent-free in my brain for years. Thanks to Matthias for the clarification.)

|  2 Mar 2024




Dagmar Chili Pitas & Doxowox: Now With Slightly-Less Unofficial Mirrors

It is with great pleasure that I share that the following two Internet classics have been brought back to life at their own blessed domain names, with TLS and everything, honest to God 21st century websites:

Reader please note: these are not my writings, but I am honored to steward their continued presence on the Internet. They are works of art and deserve grace and aesthetic elbow room.

{}oe|e|ep[]

|  29 Oct 2023




Deep Fake

(The following is an excerpt from a short story. Read the whole thing here.) I have reposted this November 2022 since our dear dear Elon Musk has nudged this work of fiction one more notch closer to reality.

On weekday mornings the Safespace product leadership gathers in an airy conference room for triage. Usually it’s forgettable stuff, scaling issues with a server cluster or the homepage stumbling over a program error, but today’s different. Ralgo, the founder and CEO of Safespace, mounts the dais and says there’s good news and bad news. The bad news is Vencent, our senior Distressing Content Moderator, jumped off a balcony in the rotunda and burst open on the foosball table. The good news is Vencent’s replacement is already lined up so none of our deadlines will slip.

The news of Vencent’s suicide lands on the leadership team in uneven ways. Glarry is sobbing with Aimie in the Quality Assurance pit, grieving the loss of their Borg LARPing comrade. Stavros in Image Analysis says nothing, dons headphones and resumes tweaking the scrotal recognition model. As for me, well, I suspect I’m in for a reaming when Ralgo tells me to meet him in the Privacy Cylinder.

Vencent’s death marks the third time I’ve lost a Distressing Content Moderator in the last eighteen months. The previous one flung himself in front of the Palo Alto BART, and the one before that self-immolated at the annual company acid trip, taking an investor’s dog with her. I thought I’d never stop being the butt of the What’s your burn rate? wisecracks. Content moderation is a rough job, don’t get me wrong. A moderator has to sit in front of a grid of displays all day, double-checking the media that our A.I. flags as too disgusting, incendiary, or violent for human consumption. There’s gonna be some turnover. But the spate of suicides is a big problem for me personally because I’m the corporate Vice President of Spiritual Health and, at least on paper, I’m supposed to be the load-bearing wall that keeps morale from caving so badly.

Ralgo seals the red anodized walls of the Privacy Cylinder around the two of us and tears into me. He says it would be cheaper to install jumper nets in the rotunda than it would be to keep me on staff another six months. He wants to see Vencent’s spiritual performance review notes. I tell him I don’t have any. I’ve stopped taking notes because the distraction interferes with intersoul harmonics. Oh boy, does he ever not like hearing that.

“W. T. Fuck?” he says. “I’ve got senators crawling up my ass over these suicides, and the investors are spooked another public incident will tank the valuation. If they hear about Vencent and come snooping and we don’t have a liability paper trail, we’re one-hundred percent fucked. Do you hear me? Eternally. By thorny cocks.”

I vow that I will recommit myself to diligent notekeeping and he says he’ll believe it when he sees it. I ask him what he meant during standup RE: the good news about Vencent’s replacement.

“About that,” he says. “I’ve hired a Deep Phakes consultant to replace Vencent. And don’t get pissy that I didn’t loop you into the candidate selection process, there wasn’t one. Their salespeople won’t allow it.”

The instant I hear Ralgo mention Deep Phakes I realize why we’re having this conversation in the Privacy Cylinder and not in the breezeway he uses for public humiliation. Deep Phakes is one of the darkest secrets in the Valley. You’ll never hear anyone admit publicly they’re working with Deep Phakes. What they do isn’t entirely legal, and for my money it’s morally fuzzy, but their product is legendary. A Deep Phake shows up at your office a nameless nonentity of programmable meatware, unencumbered by personality or desire. They’ll become anyone you want, utterly and wholly. I’ve heard they’ll even swap genders if you spring for the surcharge.

Ralgo says we should count our blessings that Vencent offed himself on company property, out of the reach of journalists, and that all the witnesses are on the payroll.

“It needs to be like Vencent never jumped off that balcony,” Ralgo says.

“I’m not sure I get it,” I say.

“I’m saying the Deep Phake is New Vencent. We dump him in the same cubicle, have him pick up wherever Old Vencent left off. He’s got to become Vencent, sans, obviously, the suicidal tendencies.”

“Obviously,” I echo, and he gives me a look that says don’t be cute, fuckwad.

“Against my better judgement,” he says, “I’m putting you in charge of New Vencent’s onboarding. Don’t make me regret it.”

We emerge from the Privacy Cylinder and Ralgo grabs a compostable dry erase marker and scribbles NEW VENCENT on the Kanban board over the same column of post-its that Old Vencent had been assigned.

Read the whole thing here.

|  11 Nov 2022