Bookmarking Xcode Project Search Results

Some friends at work have shown me a hard-to-find feature in Xcode that offers a speedy, convenient way to run project-wide searches that you find yourself repeating frequently: bookmarking search results. Rather than having to retype a search query again and again, you can bookmark them for later use.

Here’s how to do it:

  1. Go to the “Find” tab of the Navigator pane (this is the “Navigator > Find” menu option, which is keymapped to CMD+4 by default).

  2. Enter your search query.

  3. While keeping your text input cursor in the query box (that part is important), go to the Edit menu and you’ll see an option to bookmark your search query:


This menu item only appears under the Edit menu.

This action creates a bookmark of all currently-matching search results:


The bookmarked search results.

The bookmark can be refreshed with the click of a button at any time. Just mouse-over the bookmark and click the refresh control that appears (see the image above).

To give you a better real-world example of how I use this feature, one of the searches I perform most frequently is for <<<<<<< git merge conflict indicators:


Bookmarking merge conflicts.

Anything to reduce friction in an already friction-prone part of my work day is welcome.

One last thing: you can edit the description of the bookmark to make it easier to read. I like to edit the description of my merge conflict search from <<<<<<< to read Merge Conflicts instead. Just right click on the bookmark description and select “Edit description” from the contextual menu:


How to edit the bookmark description.

Enjoy!

|  12 Jul 2024




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