<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Phil's Blog]]></title><description><![CDATA[Hi, I'm Phil Zakharchenko. This is my personal blog with thoughts and ideas around application development on Apple platforms, macOS user interface, Swift, AppKit, SwiftUI, and technology in general.]]></description><link>https://philz.blog/</link><image><url>https://philz.blog/favicon.png</url><title>Phil&apos;s Blog</title><link>https://philz.blog/</link></image><generator>Ghost 5.87</generator><lastBuildDate>Thu, 12 Mar 2026 07:20:46 GMT</lastBuildDate><atom:link href="https://philz.blog/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[NSAttributedString and AttributedString Bridging Performance on macOS]]></title><description><![CDATA[<p>With the introduction of the Swift-native <code>AttributedString</code> in iOS 15 and macOS 12, it&#x2019;s become pretty common to convert between the existing <code>NSAttributedString</code> and the new type. Certain first-party frameworks, like SwiftUI and App Intents, require developers to use the new <code>AttributedString</code>. In existing apps, simply moving off</p>]]></description><link>https://philz.blog/nsattributedstring-attributedstring-bridging-performance-on-macos-swift/</link><guid isPermaLink="false">696ef45807baf7ebec44f98e</guid><category><![CDATA[Apps]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Swift]]></category><category><![CDATA[SwiftUI]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Tue, 20 Jan 2026 03:24:32 GMT</pubDate><media:content url="https://philz.blog/content/images/2026/01/Frame.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://philz.blog/content/images/2026/01/Frame.jpg" alt="NSAttributedString and AttributedString Bridging Performance on macOS"><p>With the introduction of the Swift-native <code>AttributedString</code> in iOS 15 and macOS 12, it&#x2019;s become pretty common to convert between the existing <code>NSAttributedString</code> and the new type. Certain first-party frameworks, like SwiftUI and App Intents, require developers to use the new <code>AttributedString</code>. In existing apps, simply moving off of <code>NSAttributedString</code> can be too heavy of a lift, and if you&#x2019;re using Objective-C at any point in the stack, such a migration simply wouldn&#x2019;t be feasible. I also would be the last person to shame you for enjoying well-written Objective-C code.</p><p>Luckily, we can convert between <code>AttributedString</code> and <code>NSAttributedString</code> with the help of built-in methods. While it is fairly widely known that the bridging is not toll-free, as the types use distinct underlying representations, I haven&#x2019;t found any comprehensive explorations or official first-party insight into <strong>exactly</strong> how fast or how slow such conversions are.</p><p>If you&#x2019;ve worked in an established SwiftUI codebase, or if you maintain one of your own, you&#x2019;ve likely seen code like this:</p><pre><code class="language-swift">Text(AttributedString(viewModel.nsAttributedString))
</code></pre><p>As any experienced SwiftUI developer is likely to know, our views&#x2019; <code>body</code> computations should be as lightweight and cheap to execute as possible. AttributeGraph will ultimately decide when and how often to reevaluate the <code>body</code>, and putting accidentally expensive computations that should&#x2019;ve been precomputed and cached inside a view model &#x2013; or elsewhere outside the view &#x2013; is likely one of the more common and easier to fix performance problems in SwiftUI view hierarchies.</p><p>Depending on just how expensive these conversions are, <strong>performance of our entire app could be at stake!</strong> Given the weight of the situation, exactly how concerned should we be when we see the bridging between the two attributed string representations? Are the performance characteristics the same for both directions of the conversion? How does the performance scale with the size of the strings?</p><p>Apple&#x2019;s documentation does not provide a definitive answer. So, I decided to set up some performance tests on macOS 26.2 and try to find out. I&#x2019;m posting the findings here hoping they could be of use to someone asking themselves similar questions.</p><h2 id="string-length">String length</h2><p>In this test, I held the number of attributes per run <em>(4)</em> and run count <em>(32)</em> constant while increasing the length of the string.</p>
<!--kg-card-begin: html-->
<table>
<thead>
<tr>
<th>String Length</th>
<th><code>NSAttributedString</code> to <code>AttributedString</code></th>
<th><code>AttributedString</code> to <code>NSAttributedString</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>256</td>
<td>148.254 &#x3BC;s</td>
<td>103.584 &#x3BC;s</td>
</tr>
<tr>
<td>2048</td>
<td>185.604 &#x3BC;s</td>
<td>102.726 &#x3BC;s</td>
</tr>
<tr>
<td>16384</td>
<td>483.992 &#x3BC;s</td>
<td>110.828 &#x3BC;s</td>
</tr>
<tr>
<td>131072</td>
<td>2814.156 &#x3BC;s</td>
<td>143.640 &#x3BC;s</td>
</tr>
</tbody>
</table>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2026/01/Screenshot-2026-01-19-at-6.06.20-PM.png" class="kg-image" alt="NSAttributedString and AttributedString Bridging Performance on macOS" loading="lazy" width="1208" height="874" srcset="https://philz.blog/content/images/size/w600/2026/01/Screenshot-2026-01-19-at-6.06.20-PM.png 600w, https://philz.blog/content/images/size/w1000/2026/01/Screenshot-2026-01-19-at-6.06.20-PM.png 1000w, https://philz.blog/content/images/2026/01/Screenshot-2026-01-19-at-6.06.20-PM.png 1208w" sizes="(min-width: 720px) 720px"></figure><p>This shows that the cost of conversion from <code>NSAttributedString</code> to <code>AttributedString</code> scales strongly with length: 148.3 &#x3BC;s at 256 characters to 2814.2 &#x3BC;s at 131072 characters (about 19.0x). This raises concerns for code like the above <code>Text(AttributedString(viewModel.nsAttributedString))</code>, where the conversion happens on a hot path, especially if you can&#x2019;t guarantee the string to be sufficiently small.</p><p>The other direction stays much flatter with length, rising from 103.6 &#x3BC;s to 143.6 &#x3BC;s over the same range. It seems to generally be a lot cheaper to convert an <code>AttributedString</code> to an <code>NSAttributedString</code> than the other way around.</p><h2 id="number-of-attributes">Number of attributes</h2><p>The length of the string is not the only axis we may want to test when dealing with attributed strings. I wanted to see what happens if I held both the string length (8,192) and run count (32) constant while increasing attributes per run (1 &#x2192; 4). This would show the sensitivity to &#x201C;attribute richness&#x201D; of the strings.</p>
<!--kg-card-begin: html-->
<table>
<thead>
<tr>
<th>Attributes per Run</th>
<th><code>NSAttributedString</code> to <code>AttributedString</code></th>
<th><code>AttributedString</code> to <code>NSAttributedString</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>229.298 &#x3BC;s</td>
<td>52.362 &#x3BC;s</td>
</tr>
<tr>
<td>2</td>
<td>260.248 &#x3BC;s</td>
<td>76.036 &#x3BC;s</td>
</tr>
<tr>
<td>3</td>
<td>282.270 &#x3BC;s</td>
<td>91.080 &#x3BC;s</td>
</tr>
<tr>
<td>4</td>
<td>315.272 &#x3BC;s</td>
<td>108.800 &#x3BC;s</td>
</tr>
</tbody>
</table>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2026/01/Screenshot-2026-01-19-at-6.06.11-PM.png" class="kg-image" alt="NSAttributedString and AttributedString Bridging Performance on macOS" loading="lazy" width="1152" height="874" srcset="https://philz.blog/content/images/size/w600/2026/01/Screenshot-2026-01-19-at-6.06.11-PM.png 600w, https://philz.blog/content/images/size/w1000/2026/01/Screenshot-2026-01-19-at-6.06.11-PM.png 1000w, https://philz.blog/content/images/2026/01/Screenshot-2026-01-19-at-6.06.11-PM.png 1152w" sizes="(min-width: 720px) 720px"></figure><p>This shows a close to linear increase for both directions.</p><h2 id="a-combination">A combination</h2><p>I also wanted to grow the length and attribute richness together to simulate a very frequent real-world scenario. Based on the above results, the hypothesis was that the growth would be dominated by the growth of the length of the string.</p>
<!--kg-card-begin: html-->
<table>
<thead>
<tr>
<th>String Length</th>
<th>Attr. / Run</th>
<th><code>NSAttributedString</code> to <code>AttributedString</code></th>
<th><code>AttributedString</code> to <code>NSAttributedString</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>256</td>
<td>1</td>
<td>61.008 &#x3BC;s</td>
<td>41.972 &#x3BC;s</td>
</tr>
<tr>
<td>2048</td>
<td>2</td>
<td>127.972 &#x3BC;s</td>
<td>61.394 &#x3BC;s</td>
</tr>
<tr>
<td>16384</td>
<td>3</td>
<td>454.824 &#x3BC;s</td>
<td>74.008 &#x3BC;s</td>
</tr>
<tr>
<td>131072</td>
<td>4</td>
<td>2886.374 &#x3BC;s</td>
<td>139.574 &#x3BC;s</td>
</tr>
</tbody>
</table>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2026/01/Screenshot-2026-01-19-at-6.11.21-PM.png" class="kg-image" alt="NSAttributedString and AttributedString Bridging Performance on macOS" loading="lazy" width="1224" height="856" srcset="https://philz.blog/content/images/size/w600/2026/01/Screenshot-2026-01-19-at-6.11.21-PM.png 600w, https://philz.blog/content/images/size/w1000/2026/01/Screenshot-2026-01-19-at-6.11.21-PM.png 1000w, https://philz.blog/content/images/2026/01/Screenshot-2026-01-19-at-6.11.21-PM.png 1224w" sizes="(min-width: 720px) 720px"></figure><p>The shape of the results is pretty much the same as what we saw when growing the length of the string, leading to very similar conclusions.</p><h2 id="what-this-shows">What this shows</h2><p>The initializers of both <code>AttributedString</code> and <code>NSAttributedString</code> accepting the other representation are designed to be syntactically lightweight, but as we can see from this test, are not guaranteed to be. What this tells me is that I shouldn&#x2019;t treat the work done by these conversion operations as trivial, and should therefore avoid cases where I perform a lot of these conversions on a hot code path like inside a SwiftUI view&#x2019;s <code>body</code>, especially if I don&#x2019;t know the length of the input string.</p><h3 id="in-swiftui">In SwiftUI</h3><p>My practical recommendation here, solely based on the data above, would be as follows.</p><ul><li>If you&#x2019;re dealing with very small strings, or strings of a deterministic but short length, it&#x2019;s probably &#x201C;fine&#x201D; to keep converting them inline.<ul><li>It likely doesn&#x2019;t matter how saturated with attributes the string is.</li></ul></li><li>If the string we&#x2019;re working with is based on a user input or is otherwise of a nondeterministic length, convert it once.<ul><li>It is best to store the string as the representation you&#x2019;re going to present at the View Model (or equivalent) layer. If you&#x2019;re presenting via SwiftUI <code>Text</code>, store the string as an <code>AttributedString</code>.</li><li>When the input value affecting the string changes, rebuild it.</li><li>This way, you&#x2019;re the one in control of how often the string conversion occurs.</li></ul></li></ul><h2 id="what-this-doesn%E2%80%99t-show">What this doesn&#x2019;t show</h2><p>My goal with this little performance test was to see how converting between the two built-in attributed string representations performed on the current version of macOS (26.2). It was not meant to be a comprehensive test, since other platforms, devices, and OS versions may behave differently. Optimizations may be introduced at the framework level in the future. Similarly, the performance could regress, and surely both instances have happened in the past.</p>]]></content:encoded></item><item><title><![CDATA[DocumentGroup Breaks focusedSceneValue Propagation]]></title><description><![CDATA[<p>I have already written about another gnarly bug around document-based apps in SwiftUI <a href="https://philz.blog/document-based-swiftui-apps-on-visionos-2-4-unusable/">in a previous post</a>. In that post, I found that on visionOS, any <code>DocumentGroup</code> scene would duplicate your view hierarchy, causing pretty funny looking visual overlaying glitches. It was most visible on visionOS due to the amount</p>]]></description><link>https://philz.blog/documentgroup-breaks-focusedscenevalue-propagation/</link><guid isPermaLink="false">68bd1a1407baf7ebec44f970</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Menus]]></category><category><![CDATA[UI]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Sun, 07 Sep 2025 05:38:36 GMT</pubDate><media:content url="https://philz.blog/content/images/2025/09/menu-bar-groups-macos@2x.png" medium="image"/><content:encoded><![CDATA[<img src="https://philz.blog/content/images/2025/09/menu-bar-groups-macos@2x.png" alt="DocumentGroup Breaks focusedSceneValue Propagation"><p>I have already written about another gnarly bug around document-based apps in SwiftUI <a href="https://philz.blog/document-based-swiftui-apps-on-visionos-2-4-unusable/">in a previous post</a>. In that post, I found that on visionOS, any <code>DocumentGroup</code> scene would duplicate your view hierarchy, causing pretty funny looking visual overlaying glitches. It was most visible on visionOS due to the amount of UI translucency, but I later realized the bug was also reproducible on iOS.</p><p>It is fairly evident that document-based apps have not been a major focus area in the recent SwiftUI evolution and development lifecycle, because today we&#x2019;re looking at another bug that makes <code>DocumentGroup</code> quite unusable. Turns out, if you have a <code>DocumentGroup</code> scene and you try using the <code>focusedSceneValue</code> API to define self-validating <code>Commands</code> for that scene, it <em>just won&#x2019;t work</em> on iOS or visionOS, as your focused value will always be <code>nil</code>.</p><h2 id="implementing-self-validating-menu-commands">Implementing self-validating menu commands</h2><p>The SwiftUI focus APIs can be quite helpful for implementing automatically validated key equivalents if your application uses SwiftUI App Lifecycle. I&#x2019;ll aim to provide a quick introduction and demonstrate the behavior here, so please feel free to skip to the next section if you&#x2019;re already familiar with these techniques.</p><p>Adding some key commands to your iOS app, or menu items to the macOS (and now iPadOS) menu bar, and wanting to automatically enable or disable them based on where the user is focused within your <code>Scene</code>, is a pretty canonical use case for <code>focusedSceneValue</code>. Perhaps you&#x2019;d also like to know which instance of your <code>Scene</code> any invoked command should go to if your application supports multiple windows.</p><blockquote>If you&#x2019;re not yet familiar with using SwiftUI for building good menu bar experiences on the Mac (and iPad starting with iOS 26), Apple&#x2019;s own <a href="https://developer.apple.com/documentation/swiftui/building-and-customizing-the-menu-bar-with-swiftui">Building and customizing the menu bar with SwiftUI</a> is a good starting point.</blockquote><blockquote>If you&#x2019;re coming from a macOS and particularly AppKit background, <code>FocusedValue</code> and <code>focusedSceneValue</code> will sound very conceptually similar to the Responder Chain.</blockquote><p>The simplest example would be implementing key equivalents (or <code>Commands</code>, if you&#x2019;re willing to forgive my Mac-ism) to operate on a text view that is currently being edited somewhere in your app. Suppose we have a very simple <code>Observable</code> object that stores the contents of our document and the high-level state of the app&#x2019;s UI, like so:</p><pre><code class="language-swift">@Observable
final class DocumentState {
    var text: String = &quot;&quot;
    var selection: TextSelection?
    
    func formatBold() { &#x2026; }
}

struct ContentView: View {
    @State private var state = DocumentState()

    var body: some View {
        TextEditor(text: $state.text, selection: $state.selection)
            .focusedSceneValue(state)
    }
}
</code></pre><p>Now, since I have added my <code>Observable</code> object as a <code>focusedSceneValue</code>, I can implement my formatting commands and add them to the scene:</p><pre><code class="language-swift">struct EditingCommands: Commands {
    @FocusedValue(DocumentState.self) private var state
    
    var body: some Commands {
        CommandGroup(after: .textEditing) {
            Button(&quot;Bold&quot;) { state?.formatBold() }
                .disabled(state == nil)
        }
    }
}

@main
struct FocusedSceneValueDocumentApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands { EditingCommands() }
    }
}
</code></pre><p>To see the effects of what we&#x2019;ve just accomplished, it is probably the easiest to run the app on macOS. You will see that when no instances of our <code>WindowGroup</code> are present (for example, if we close all of the application&#x2019;s windows), our custom Bold menu item will be disabled in the Edit menu. If we open at least one window, and if at least one of these windows is key, it will become enabled. Moreover, we get the desired effect of SwiftUI knowing which instance of the <code>Scene</code> the command should go to when multiple windows are open. Invoking the Bold menu item in this sample app will always operate on the key window, which is what the user of your app expects.</p><p><code>focusedSceneValue</code> is very powerful for implementing automatic action validation in your app, and its use is likely required if you want to build anything close to a Mac-assed Mac app with SwiftUI App Lifecycle. The above example can be useful for establishing the baseline of how this is supposed to work. And now&#x2026;</p><h2 id="completely-broken-if-your-scene-is-a-documentgroup">Completely broken if your Scene is a DocumentGroup</h2><p>The above simply does not work if the following 2 conditions are true:</p><ul><li>Your scene is a <code>DocumentGroup</code>, and</li><li>You&#x2019;re running on any platform other than macOS.</li></ul><p>As far as I can tell, the bug reproduces on all latest versions of iOS (including iPadOS) and visionOS, as well as the Seed versions of iOS 26 and visionOS 26.</p><p>To reproduce this issue, it is enough to create a document-based SwiftUI app from the Xcode template and try using the <code>focusedSceneValue</code> API to implement a simple autovalidating command. You&#x2019;ll notice that in your <code>Commands</code> implementation, the <code>FocusedValue</code> is always simply <code>nil</code> and your <code>body</code> is never invoked again with an updated value, even as you present more instances of the <code>DocumentGroup</code> or close the existing ones.</p><h2 id="workaround">Workaround</h2><p>After reporting the bug, I wanted to determine if a conceptually simple and lightweight workaround was possible. Since <code>focusedSceneValue</code> works fine on macOS, I only needed a targeted workaround for UIKit based platforms.</p><ul><li>First, we need some kind of shared state that our <code>Commands</code> implementation can read, since it&#x2019;s by far the most lightweight way to pass the state across the <code>Scene</code> boundary. It&#x2019;s not the &#x201C;Swiftiest,&#x201D; but felt like an appropriate balance of conciseness and complexity.</li><li>We need a way to track what the active <code>Scene</code> is. We can use the <code>scenePhase</code> Environment key with some added handling for <code>onAppear</code> and <code>onDisappear</code>.</li><li>Finally, we can combine these into a <code>ViewModifier</code> that can be reused across various <code>Scene</code>s, and adopt it in our <code>Commands</code> implementation.</li></ul><p>Putting this all together, here is where I landed:</p><pre><code class="language-swift">#if canImport(UIKit)

import SwiftUI

extension View {
    func documentGroupSceneFocusBinding&lt;T: AnyObject&gt;(
        _ document: T,
        keyPath: ReferenceWritableKeyPath&lt;ActiveScene, T?&gt;
    ) -&gt; some View {
        ModifiedContent(content: self, modifier: DocumentGroupFocusWorkaround(document: document, keyPath: keyPath))
    }
}

struct DocumentGroupFocusWorkaround&lt;T: AnyObject&gt;: ViewModifier {
    let document: T
    let keyPath: ReferenceWritableKeyPath&lt;ActiveScene, T?&gt;
    
    @Environment(\.scenePhase) private var scenePhase
    
    func body(content: Content) -&gt; some View {
        content
            .onChange(of: scenePhase) { _, newValue in scenePhaseDidChange(to: newValue) }
            .onAppear { scenePhaseDidChange(to: .active) }
            .onDisappear { scenePhaseDidChange(to: .background) }
    }
    
    private func scenePhaseDidChange(to scenePhase: ScenePhase) {
        let isActive = scenePhase == .active
        if isActive {
            ActiveScene.shared[keyPath: keyPath] = document
        } else if ActiveScene.shared[keyPath: keyPath] === document {
            ActiveScene.shared[keyPath: keyPath] = nil
        }
    }
}

@Observable @MainActor
final class ActiveScene {
    static let shared = ActiveScene()
}

#endif
</code></pre><p>What&#x2019;s left is to add the fields we care about, like the <code>DocumentState</code> from the example app we&#x2019;ve defined, to the <code>ActiveScene</code> definition:</p><pre><code class="language-swift">final class ActiveScene {
    // &#x2026;
    var documentState: DocumentState?
}
</code></pre><p>Then, our <code>ContentView</code> can trivially apply the workaround using a single line of code:</p><pre><code class="language-swift">var body: some View {
    platformView
#if canImport(UIKit)
        .documentGroupSceneFocusBinding(state, keyPath: \.documentState)
#endif
}
</code></pre><p>The adoption inside our <code>Commands</code> definition would be trivial, too, not requiring any changes to the <code>body</code> of the implementation:</p><pre><code class="language-swift">struct EditingCommands: Commands {
#if canImport(UIKit)
    private var state: DocumentState? {
        ActiveScene.shared.documentState
    }
#else
    @FocusedValue(DocumentState.self) private var state
#endif

    var body: some Commands {
        // &#x2026;
    }
}
</code></pre><h2 id="conclusion">Conclusion</h2><p>I&#x2019;ve reported the bug to Apple (tracked as FB20116555), and I&#x2019;m including both the repro case and the above workaround <a href="https://github.com/philptr/DocumentGroupFocusedSceneValueBug">as sample code available on GitHub</a>. I truly hope both <code>DocumentGroup</code> and Document-Based App lifecycle can get the attention they deserve in the near future, as these issues are quite prohibitive when it comes to attempting to use these APIs in production applications.</p>]]></content:encoded></item><item><title><![CDATA[Conditional SwiftUI View Modifiers are Evil]]></title><description><![CDATA[Custom modifiers in SwiftUI are a great way to reuse code. However, that should never lead to obfuscating semantics. And, unfortunately, most use cases for conditional modifiers do both of those things. Here's why.]]></description><link>https://philz.blog/conditional-swiftui-view-modifiers-are-evil/</link><guid isPermaLink="false">68abfa2707baf7ebec44f930</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[UI]]></category><category><![CDATA[Apps]]></category><category><![CDATA[Performance]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Mon, 25 Aug 2025 05:57:42 GMT</pubDate><content:encoded><![CDATA[<p>Okay, forgive me for the attention grabbing title; evil is a pretty harsh term and doesn&#x2019;t quite convey the nuance. But there are plenty of misconceptions around conditional view modifiers, as well as widespread misuse and bad advice that is quietly killing your apps&#x2019; performance. Let&#x2019;s unpack it.</p><p>Swift is an extremely expressive language by its design, allowing its users to design APIs that are syntactically lightweight but powerful at the same time. SwiftUI fully takes advantage of this exciting property of the language to abstract away a whole layer of concepts into a declarative easy-to-use API surface.</p><p>There are many things to love about SwiftUI&#x2019;s approach to writing UI code. Even when the built-in functionality is not enough, the framework lets you go deeper by bridging your existing AppKit and UIKit controls, and, yes, writing new view modifiers.</p><h2 id="conditional-modifiers">Conditional Modifiers</h2><p>Custom modifiers in SwiftUI are a great way to hide complexity and reuse UI concepts specific to your application. They can help you extract common concepts, making your code more expressive, easier to write and read. However, <strong>hiding complexity</strong> should never mean <strong>making code harder to reason about</strong> or lead to <strong>obfuscating semantics</strong>. And, unfortunately, most use cases for conditional modifiers do both of those things.</p><p>So, what are conditional modifiers? These are custom modifiers with a lightweight <code>if</code> syntax, allowing you to branch between 2 unrelated modifiers, conditionally applying one or the other, without having to write an <code>if</code> statement in the <code>body</code> of the declaring view. It could be declared like so:</p><pre><code class="language-swift">extension View {
    func `if`&lt;Content: View&gt;(_ condition: Bool, modifier: (Self) -&gt; Content) -&gt; some View {
        if condition {
            modifier(self)
        } else {
            self
        }
    }
}
</code></pre><p>Such extensions are commonly used for dynamically hiding the <code>View</code> by conditionally applying the <code>hidden</code> modifier, or for applying a background to a <code>View</code> only when some condition is true.</p><h2 id="conditionalcontent-in-swiftui">_ConditionalContent in SwiftUI</h2><p>What&#x2019;s the issue with conditional modifiers then? The key to understanding this lies in result builders and what an <code>if</code> statement actually means to SwiftUI when you use it inside your <code>View</code>&#x2019;s <code>body</code> or any other <code>@ViewBuilder</code> property or closure.</p><p>Under the hood, the <code>if</code> statement is represented by a <code>View</code> called <code>_ConditionalContent</code>. When you switch between 2 branches of an <code>if</code> statement, it has 2 generic parameters: one for the <code>true</code> branch of the expression, and one for the <code>false</code> branch. The role of this view is to switch between the 2 views it is wrapping when the result of the expression you&#x2019;ve provided changes. The types of both views are known at compile time and used by SwiftUI internals.</p><blockquote>The <code>if</code> statement ultimately allows <strong>different types</strong> of views to be displayed.</blockquote><p>Let&#x2019;s zoom in on &#x201C;different types&#x201D; here. Internally, SwiftUI will allocate and manage storage (including what can be referred to as a <em>subgraph</em>) for each of the branches of the conditional, but only one of them will ultimately be created and updated at a given time. It seems like it&#x2019;s what we want, right?</p><p>Let&#x2019;s consider one of the most common use cases for a conditional modifier wrapper: applying a background conditionally.</p><pre><code class="language-swift">var body: some View {
    contentView
        .if(User.wantsGlassBackground) {
            $0.background(CustomGlassBackgroundView())
        }
}
</code></pre><p>This seems like a convenient way to modify content at first glance. But let&#x2019;s remove the syntactic sugar and see what the view is actually evaluated to. It is essentially equivalent to the following:</p><pre><code class="language-swift">var body: some View {
    if User.wantsGlassBackground {
        contentView
            .background(CustomGlassBackgroundView())
    } else {
        contentView
    }
}
</code></pre><p>If you print the type of each of the above views (via something like <code>print(&quot;\(type(of: view))&#x201D;)</code>), you will find that both of them evaluate to something like the following, demonstrating equivalence.</p><pre><code class="language-swift">_ConditionalContent&lt;ModifiedContent&lt;SampleContentView, _BackgroundModifier&lt;CustomGlassBackgroundView&gt;&gt;, SampleContentView&gt;
</code></pre><p>This is not optimal for various reasons. Here we actually create 2 top-level branches containing not quite the same, but actually very much different, <code>View</code> types. To put this into more canonical terms, the <strong>structural identity</strong> of each of the branches is distinct.</p><blockquote>If you&#x2019;d like to go deeper and understand where these additional <code>View</code> types come from, or even why knowing the types at compile time might be useful to SwiftUI, you have to meet AttributeGraph. I highly recommend <a href="https://rensbr.eu/blog/swiftui-attribute-graph/">this article by Rens Breur</a>.</blockquote><blockquote>To better understand the concept of identity in SwiftUI, as well as the concept of a view&#x2019;s lifetime, please refer to <a href="https://developer.apple.com/videos/play/wwdc2021/10022/">this WWDC21 talk</a>.</blockquote><p>So, to SwiftUI, your seemingly shorthand and convenient code actually means something really important and probably unexpected: instead of declaring a view with a conditionally applied background, you&#x2019;re switching between 2 completely different views.</p><p>If you&#x2019;re not careful, this has the potential to really hinder your application&#x2019;s performance, especially if your <code>contentView</code> happens to contain a complex view hierarchy, bridged (<code>NSViewRepresentable</code> / <code>UIViewRepresentable</code>) types, or cause SwiftUI to set up and tear down a lot of platform views like <code>List</code>s.</p><p>But, that&#x2019;s not all. The accidentally different semantics will also impact the behavior of your view in very meaningful ways.</p><ul><li>When you toggle the property and switch between branches, the branch about to go away will receive an <code>onDisappear</code> and the freshly <code>true</code> branch will receive an <code>onAppear</code>.</li><li>Because these are different views, SwiftUI might try to trigger a transition between them, causing you to see unnecessary and most likely weird animations and transient gaps in your layouts.</li></ul><p>Both of the above behaviors would not be surprising to anyone reading the code if our declaration had <strong>intentionally</strong> used a standard <code>ViewBuilder</code> if statement at the root level. But our conditional modifier actually obfuscated our real intent, and would likely eventually force you or someone else on your team to debug a subtly wrong behavior or spend time investigating poor performance. So, in an effort to make your code a bit nicer to read and more convenient to write, you&#x2019;ve made it harder to reason about and maintain.</p><h2 id="best-practices">Best Practices</h2><p>I&#x2019;ll try to enumerate some of the use cases for conditional modifiers I commonly see in SwiftUI code and provide concrete alternatives. Let&#x2019;s start with the example of a conditional background we&apos;ve explored above.</p><h3 id="conditional-background">Conditional Background</h3><p>There is a version of the <code>background</code> modifier (as well as other related modifiers like <code>overlay</code>) that accepts a <code>@ViewBuilder</code> closure and lets you express your intent more precisely.</p><pre><code class="language-swift">var body: some View {
    contentView
        .background {
            if User.wantsGlassBackground {
                CustomGlassBackgroundView()
            }
        }
}
</code></pre><p>This doesn&#x2019;t read overly verbose or weird at all. In fact, it&#x2019;s easier to read because you&#x2019;re not using some bespoke modifier that the reader of your code might have to go and look up elsewhere in your codebase.</p><p>So, what&#x2019;s the type of this view?</p><pre><code class="language-swift">ModifiedContent&lt;SampleContentView, _BackgroundModifier&lt;Optional&lt;CustomGlassBackgroundView&gt;&gt;&gt;
</code></pre><p>The benefit of this approach is clear. You&#x2019;re describing a <code>View</code> that constructs a content view (however complex or deeply nested it may be), then applies a background to it. Within that background, you&#x2019;re switching between a simpler <code>CustomGlassBackgroundView</code> and no view. Causing a background to be re-created is nowhere close to being as expensive as causing the whole view (including the background) and the associated storage and subgraph to be thrown away and recreated with a slightly different property.</p><h3 id="hiding-a-view">Hiding a View</h3><p>When people look at the <code>hidden</code> modifier, one of the first things they notice is that it is not conditional: it does not accept a Boolean flag allowing the user to conditionally enable or disable it. Some other modifiers, like <code>allowsHitTesting(_:)</code> or <code>scrollClipDisabled(_:)</code>, do provide such a hook.</p><p>Since hiding a view conditionally is a very common use case, folks often reach for a conditional modifier like so:</p><pre><code class="language-swift">var body: some View {
    contentView
        .if(shouldHide) { $0.hidden() }
}
</code></pre><p>Let&#x2019;s develop some intuition by finding the equivalent declaration using the built-in <code>ViewBuilder</code> if syntax. The above would be equivalent to the following declaration:</p><pre><code class="language-swift">var body: some View {
    if shouldHide {
        contentView
            .hidden()
    } else {
        contentView
    }
}
</code></pre><p>The type of these views would be something like <code>_ConditionalContent&lt;ModifiedContent&lt;SampleContentView, _HiddenModifier&gt;, SampleContentView&gt;</code></p><p>The parallel to the previous background example is very likely obvious at this point. And, because this condition can be even more sneaky and compact, you&#x2019;re probably even more likely to run into trouble and have to wonder why your hierarchy is all slow and messed up!</p><p>So, what do you do to conditionally hide a view then? Developers have different reasons for using <code>hidden</code>, of course, but assuming the canonical use case of hiding a view based on some condition, you&#x2019;re much better off with a ternary expression inside the <code>opacity</code> modifier.</p><pre><code class="language-swift">var body: some View {
    contentView
        .opacity(shouldHide ? 0 : 1)
}
</code></pre><p>The type of the view here is actually the simplest one yet, evaluating to <code>ModifiedContent&lt;SampleContentView, _OpacityEffect&gt;</code>.</p><p>You might be tempted to overoptimize this and also include <code>allowsHitTesting</code>, but that&#x2019;s actually redundant. SwiftUI is smart enough to turn off hit testing by default for views that are fully transparent.</p><p>You might also be concerned about the unnecessary overhead related to applying the opacity of 1 when the view is visible, but in this case, again, SwiftUI is smart enough to recognize that the opacity of exactly 1 is effectively a no-op. The modifier of <code>opacity(1.0)</code> will be recognized as an <strong>inert modifier</strong>, and associated work will be skipped.</p><p>If you come from an AppKit or UIKit background, you might ask an even more insightful question. Even though the modifier is inert, isn&#x2019;t it still causing unnecessary wrapping of our <code>contentView</code> into a modifier (which effectively creates another view) every time the <code>body</code> of our <code>View</code> is reevaluated, which could happen very frequently at SwiftUI&#x2019;s discretion? Unlike AppKit or UIKit, SwiftUI <code>View</code>s have negligible overhead, especially very lightweight ones that don&#x2019;t have heavy computations in their initializers or bodies. After all, remember that <code>View</code>s are structs.</p><p>On the other hand, throwing away large subgraphs and other pieces of internal storage, potentially even AppKit or UIKit views backing the contents of your views behind the scenes, <strong>is</strong> expensive and <strong>will</strong> affect your app&#x2019;s performance if you&#x2019;re not very intentional about when and where it happens.</p><h2 id="takeaways">Takeaways</h2><p>Adding useful abstractions that make your view implementations easier to write and maintain is, of course, a completely worthwhile pursuit. In fact, sometimes a conditional modifier can even be a good idea &#x2013; for instance, SwiftUI doesn&#x2019;t provide an easy way to branch your <code>View</code>s based on the OS version that your customer is running. I am also by no means trying to advocate against the use of custom view modifiers. They are extremely useful, and the world of Apple platforms development would be a bleak place without <code>ViewModifier</code> in it.</p><p>Nonetheless, it is crucial to be very intentional about where you introduce abstractions, and that your abstractions don&#x2019;t actually obfuscate the true semantics of your <code>View</code>s and relationships between them.</p><p>Fighting against the framework is rarely a good idea. The guardrails are always there for a reason &#x2013; after all, we&#x2019;ve always spent a lot of time and effort on API design at Apple, emphasizing concepts like progressive disclosure and making it easier to follow best practices. Instead, embrace the SwiftUI conventions: use the conveniences provided by <code>@ViewBuilder</code>, and carefully consider the implications of any changes you&#x2019;re making to accommodate a particular use case.</p>]]></content:encoded></item><item><title><![CDATA[Overuse and Misuse of Spacers in SwiftUI Code]]></title><description><![CDATA[<p>When it comes to writing good user interface code, and code in general, it&#x2019;s not exactly groundbreaking to claim that in any given situation, we should strive to use the <strong>right</strong> tool for the <strong>right</strong> job. When working with tools and frameworks that are designed to seem simple</p>]]></description><link>https://philz.blog/swiftui-spacers-overused-and-misunderstood/</link><guid isPermaLink="false">68abbe2607baf7ebec44f8f9</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[Apps]]></category><category><![CDATA[UI]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Mon, 25 Aug 2025 01:50:20 GMT</pubDate><content:encoded><![CDATA[<p>When it comes to writing good user interface code, and code in general, it&#x2019;s not exactly groundbreaking to claim that in any given situation, we should strive to use the <strong>right</strong> tool for the <strong>right</strong> job. When working with tools and frameworks that are designed to seem simple on the surface and easy to get started with, it can be more challenging to discern what is <strong>easy</strong> to reach for from what is <strong>right</strong>. One of the greatest complexities of good API design, in my opinion, is getting <em>progressive disclosure</em> right, while also not obfuscating best practices.</p><h2 id="progressive-disclosure">Progressive Disclosure</h2><p>Apple&#x2019;s modern API design is heavily influenced by the concept of progressive disclosure. While some frameworks that have to preserve their existing conventions and philosophy, like AppKit and UIKit, may not make it evident to the fullest extent, many Swift standard library and SwiftUI APIs have been guided by it from the beginning.</p><p>Progressive disclosure as a concept was explicitly introduced and explained by Apple in <a href="https://developer.apple.com/videos/play/wwdc2022/10059/">this WWDC22 talk</a>.</p><blockquote>Progressive disclosure &lt;...&gt; is designing APIs so that the complexity of the call site grows with the complexity of the use case. An ideal API is both simple and approachable but also be able to accommodate powerful use cases.</blockquote><p>SwiftUI takes this definition to heart. While you can see progressive disclosure as the guiding principle of many of its APIs, the approach to describing layout, and particularly flexible layouts, is what I&#x2019;d like to zoom in on in this post.</p><h2 id="flexible-layouts">Flexible Layouts</h2><p>Let&#x2019;s start with a simple example of a layout where flexibility plays a key role. Here&#x2019;s the list of messages in our imaginary messaging app.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/08/Frame.png" class="kg-image" alt loading="lazy" width="2000" height="954" srcset="https://philz.blog/content/images/size/w600/2025/08/Frame.png 600w, https://philz.blog/content/images/size/w1000/2025/08/Frame.png 1000w, https://philz.blog/content/images/size/w1600/2025/08/Frame.png 1600w, https://philz.blog/content/images/2025/08/Frame.png 2000w" sizes="(min-width: 720px) 720px"></figure><p>If you start with iOS as your default platform, it can be a nice simplifying assumption that this layout is quite static &#x2013; after all, there is no resizing, moving pieces, and the size is (kind of) statically known. We can achieve a very nice result by just taking the simplest path and describing the layout in a very literal way. SwiftUI makes it easy and doesn&#x2019;t force you to think about the complexity until you&#x2019;re ready. There is nothing inherently wrong with that, especially if you&#x2019;re learning or prototyping something simple! So, our code could look like this:</p><pre><code class="language-swift">HStack(spacing: 12) {
    AvatarView()
    
    VStack(alignment: .leading, spacing: 4) {
        Text(conversation.name)
            .font(.headline)
        
        Text(conversation.lastMessage)
            .font(.subheadline)
    }
    
    Spacer()
    
    if conversation.isUnread {
        UnreadIndicator()
    }
}
</code></pre><p>The complexity does come, eventually. You quickly realize that not only do iPhone screens come in many different sizes, our sidebar will actually become a flexible resizable container on iPad and Mac. This will lead us to the first problem.</p><h2 id="problem-1-limited-space">Problem 1: Limited Space</h2><p>It&#x2019;s not that hard to see that using a <code>Spacer</code> as a stand-in for a flexible layout is sloppy at best. As your view shrinks horizontally and real estate becomes a premium, you&apos;ll notice the first problem: the text will start shrinking and truncating way too early.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/08/Screenshot-2025-08-24-at-6.14.04-PM.png" class="kg-image" alt loading="lazy" width="510" height="178"></figure><p>By visualizing the view hierarchy, we can see that the <code>Spacer</code> will actually double the minimum spacing between your text and the trailing badge. </p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/08/Screenshot-2025-08-24-at-6.17.04-PM.png" class="kg-image" alt loading="lazy" width="510" height="184"></figure><p>It should come as no surprise: a <code>Spacer</code> is a view, after all, so it participates in layout like any other view. Since our <code>HStack</code> has some built-in spacing between views, and since the <code>Spacer</code> is one of those views, the minimum visible space between the title and the unread indicator is actually double the value we declared. At this point, I find that a great question to ask myself is the following:</p><blockquote>Does my layout really need some added flexible space, or am I misusing a <code>Spacer</code> where I should&#x2019;ve actually made another view flexible?</blockquote><p>As a view, a <code>Spacer</code> is flexible along its dominant axis. This is not true for other views, like <code>Text</code> or <code>Image</code>, by default.</p><p>What I actually want in cases like this is to be precise about which views you want to grow and shrink as the container grows larger or shrinks in size. We&#x2019;d want to omit the spacer, and make the <code>VStack</code>&#x2019;s width flexible using the <code>frame</code> modifier with the desired alignment.</p><pre><code class="language-swift">HStack(spacing: 12) {
    AvatarView()
    
    VStack(alignment: .leading, spacing: 4) {
        Text(conversation.name)
            .font(.headline)
        
        Text(conversation.lastMessage)
            .font(.subheadline)
    }
    .frame(maxWidth: .infinity, alignment: .leading) // A simple modifier instead of a separate view.
    
    if conversation.isUnread {
        UnreadIndicator()
    }
}
</code></pre><p>Here&#x2019;s what that would look like:</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/08/Screenshot-2025-08-24-at-6.19.08-PM.png" class="kg-image" alt loading="lazy" width="510" height="178"></figure><h2 id="problem-2-text-selection-editing">Problem 2: Text Selection &amp; Editing</h2><p>Let&#x2019;s consider the following view representing the detail of the conversation in our imaginary messaging app. We&#x2019;ll show the latest message as a very simple text view with a set of actions on the right.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/08/Screenshot-2025-08-24-at-6.21.59-PM.png" class="kg-image" alt loading="lazy" width="1778" height="1124" srcset="https://philz.blog/content/images/size/w600/2025/08/Screenshot-2025-08-24-at-6.21.59-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/08/Screenshot-2025-08-24-at-6.21.59-PM.png 1000w, https://philz.blog/content/images/size/w1600/2025/08/Screenshot-2025-08-24-at-6.21.59-PM.png 1600w, https://philz.blog/content/images/2025/08/Screenshot-2025-08-24-at-6.21.59-PM.png 1778w" sizes="(min-width: 720px) 720px"></figure><p>The caveat here is that I&#x2019;d like to create a great text selection (and editing) experience for devices with a mouse pointer, such as a Mac. Something like this would work at first glance:</p><pre><code class="language-swift">VStack(alignment: .leading, spacing: 16) {
    HStack(alignment: .top) {
        Text(conversation.lastMessage)
            .textSelection(.enabled)
        
        Spacer()
        
        ConversationActions()
    }
    .padding()
    
    Spacer()
}
</code></pre><p>But let&#x2019;s consider the case where the conversation contains a message with a lot of explicit newline characters. Perhaps someone sent us a code snippet?</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/08/Screenshot-2025-08-24-at-6.22.15-PM.png" class="kg-image" alt loading="lazy" width="1778" height="1124" srcset="https://philz.blog/content/images/size/w600/2025/08/Screenshot-2025-08-24-at-6.22.15-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/08/Screenshot-2025-08-24-at-6.22.15-PM.png 1000w, https://philz.blog/content/images/size/w1600/2025/08/Screenshot-2025-08-24-at-6.22.15-PM.png 1600w, https://philz.blog/content/images/2025/08/Screenshot-2025-08-24-at-6.22.15-PM.png 1778w" sizes="(min-width: 720px) 720px"></figure><p>If we try selecting this, we will have to be very precise about it. You may also notice that the selection of multiple lines doesn&#x2019;t extend as far to the right as it could (and maybe should), which would be more in line with the experience you&#x2019;d see in an app like TextEdit.</p><pre><code class="language-swift">VStack(alignment: .leading, spacing: 16) {
    HStack(alignment: .top) {
        TextField(&#x2026;)
            .textSelection(.enabled)
            .frame(maxWidth: .infinity, alignment: .leading)

        ConversationActions()
    }
    .padding()
    .frame(maxHeight: .infinity, alignment: .topLeading)
}
</code></pre><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/08/Screenshot-2025-08-24-at-6.32.22-PM.png" class="kg-image" alt loading="lazy" width="1700" height="1176" srcset="https://philz.blog/content/images/size/w600/2025/08/Screenshot-2025-08-24-at-6.32.22-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/08/Screenshot-2025-08-24-at-6.32.22-PM.png 1000w, https://philz.blog/content/images/size/w1600/2025/08/Screenshot-2025-08-24-at-6.32.22-PM.png 1600w, https://philz.blog/content/images/2025/08/Screenshot-2025-08-24-at-6.32.22-PM.png 1700w" sizes="(min-width: 720px) 720px"></figure><p>Of course, this is a somewhat contrived example, but the general point is a fairly important one.</p><blockquote>While imprecisions in how your layouts are expressed may seem like implementation details at first, in some cases they may become exposed to the user by the framework, leading to an inconsistent and subpar user experience.</blockquote><p>Text selection is a great example of this: multiline selections will always expose the actual bounds of your text view to the user. Other examples include various backgrounds and effects (including a glass effect), context menu shapes on iOS and visionOS, and contextual menus on macOS.</p><h2 id="problem-3-layout-performance">Problem 3: Layout Performance</h2><p>The next point would be very superficial in the simple example like our tiny messaging app. However, as your application scales, and you become more aware of the limitations of your control over performance, or start dealing with very dynamic layouts and large conversations, considerations like performance will become exceedingly important.</p><p>Consider the following scenario. Your messaging app has evolved to be an enterprise solution for business communication, and users often exchange very large multiline messages. Your conversations are also highly dynamic and users can edit existing messages, making text bold and italic, at any point.</p><p>At this point, we should remember that multiline text measurement is expensive, especially when compared to the measurement of simpler shapes or controls like buttons. And, by default, SwiftUI will size our text to fit. This means that every time a user performs an edit, our text will be re-measured. This measurement, expensive enough by itself, will cause the view to resize (because we&#x2019;ve told SwiftUI we wanted it to size it to fit). After the text view is resized, the <code>Spacer</code> will need to be laid out as well, alongside with other views in the layout.</p><p>Of course, you don&#x2019;t have to worry about this when building a very simple application. But, if we had expressed the intention for our text view to fill available space along the horizontal axis, the framework could have had the opportunity to avoid a bulk of the redundant layout work. As the text is updated, as long as the new width does not exceed the current width it occupies, we could elide the work to measure or re-layout any other views if they have not changed.</p><h2 id="conclusion">Conclusion</h2><p><code>Spacer</code>s are not some great evil or a mistake of SwiftUI API design. They are a great tool in your toolkit, especially when constructing more complex masks or controlling explicit alignment during animations and transitions. But, since<code>Spacer</code>s are extremely easy to reach for, they are often misunderstood and overused.</p><p>As you progress on your SwiftUI journey, try to consider the toolkit available to you and always strive to describe the layout in the most accurate way. By doing so, you&#x2019;ll notice you&#x2019;re working together with the framework instead of against it, and will be able to add more advanced functionality to your UI with minimal code changes.</p>]]></content:encoded></item><item><title><![CDATA[Closures in SwiftUI Environment are Killing Your App’s Performance]]></title><description><![CDATA[<p>SwiftUI undoubtedly has a lot of impressive capabilities and features, but unfortunately <strong>transparency</strong> is not one of them. Many components are designed and built as black boxes, with developers left attempting to piece together the full picture using the sparse information gathered from the documentation, WWDC videos, and reverse engineering.</p>]]></description><link>https://philz.blog/closures-in-swiftui-environment-are-killing-your-apps-performance/</link><guid isPermaLink="false">6860965207baf7ebec44f8cc</guid><category><![CDATA[Apps]]></category><category><![CDATA[SwiftUI]]></category><category><![CDATA[Swift]]></category><category><![CDATA[Performance]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Sun, 29 Jun 2025 01:31:37 GMT</pubDate><media:content url="https://philz.blog/content/images/2025/06/Frame-1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://philz.blog/content/images/2025/06/Frame-1.jpg" alt="Closures in SwiftUI Environment are Killing Your App&#x2019;s Performance"><p>SwiftUI undoubtedly has a lot of impressive capabilities and features, but unfortunately <strong>transparency</strong> is not one of them. Many components are designed and built as black boxes, with developers left attempting to piece together the full picture using the sparse information gathered from the documentation, WWDC videos, and reverse engineering. One of the most unfortunate consequences of that, in my opinion, is the lack of general intuition around possible causes for poor performance. In other words, if something I built in SwiftUI performs horribly, am I hitting one of the framework&#x2019;s limitations, or am I holding it wrong?</p><p>In most cases, poor SwiftUI performance can boil down to accidentally causing unnecessary invalidations. This can be hard to debug, and even harder to develop good intuition about. One of the most easy to reach for tools is perhaps <code>_printChanges()</code>, which can be used to match your expectation of what <em>should</em> be happening in your hierarchy with what is <em>actually</em> happening at runtime.</p><p>Unnecessary invalidations can be caused by a variety of different things &#x2013; and I may explore more of them in a separate post in the near future. For now, I&#x2019;d like to zoom in on one particular cause that has a lot of misinformation and bad advice on it online: passing closures using the <code>Environment</code>.</p><h2 id="the-problem">The Problem</h2><p>If you&#x2019;ve ever looked for SwiftUI advice online or explored some open-source code out there, you&#x2019;ve probably seen people passing actions through the <code>Environment</code> like so:</p><pre><code class="language-swift">import SwiftUI

extension EnvironmentValues {
    @Entry var openSomething: () -&gt; Void = { }
}

struct ContentView: View {
    @State private var isHovered: Bool = false
    
    var body: some View {
        CustomButton()
            .padding()
            .environment(\.openSomething, openSomething)
            .background {
                Color.primary
                    .opacity(isHovered ? 0.2 : 0.05)
            }
            .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
            .onHover {
                isHovered = $0
            }
    }
    
    private func openSomething() {
        print(&quot;Open clicked...&quot;)
    }
}

struct CustomButton: View {
    @Environment(\.openSomething) var openSomething
    
    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;Click to Open&quot;)
            .onTapGesture {
                openSomething()
            }
    }
}
</code></pre><p>Seems like a much nicer alternative to passing closures directly or passing some ViewModel-esque object containing all the actions around, right? Sure, especially if you have a view nested deep inside the hierarchy responding to an action defined somewhere closer to the root. But let&#x2019;s use the <code>_printChanges</code> I&#x2019;ve added to <code>CustomButton</code> to <strong>match our expectation to the runtime behavior</strong>.</p><p>Here&#x2019;s what I&#x2019;d <em>expect</em> to happen:</p><ul><li>Our print is triggered once during the initial evaluation of the view hierarchy, printing out something like <code>CustomButton: @self changed.</code>.</li><li>Toggling <code>isHovered</code>, or any other hypothetical state of the parent view, does not invalidate <code>CustomButton</code>, and therefore does not trigger a print.</li><li>Tapping the button and triggering the action similarly does not cause an invalidation.</li></ul><p>Essentially, I&#x2019;m expecting that the custom button will be only evaluated <em>once</em>, or some <em>very low</em> constant number of times, where the number is not dependent on the number of state changes. This expectation can be important for performance, especially if you imagine a more complex or deeply nested view in the place of <code>CustomButton</code>. Let&#x2019;s run the app and check what happens in reality:</p><ul><li>&#x2705; There is an initial evaluation of the <code>body</code>.</li><li>&#x274C; However, during the initial presentation, the button&#x2019;s body is churned through multiple times (6 in my case!), claiming my <code>openSomething</code> closure has changed:</li></ul><pre><code>CustomButton: @self, @identity, _openSomething changed.
CustomButton: _openSomething changed.
CustomButton: _openSomething changed.
CustomButton: _openSomething changed.
CustomButton: _openSomething changed.
CustomButton: _openSomething changed.
</code></pre><ul><li>&#x274C; Once I hover the button, it invalidates its body and causes a print to happen again, claiming <code>@self</code>, <code>_openSomething</code> changed yet again. If I keep changing state, the button&#x2019;s <code>body</code> is re-evaluated <strong>each time</strong>.</li></ul><p>This just caused a lot more calls to <code>body</code> for our <code>CustomButton</code> than we expected! And, of course, these changes would&#x2019;ve propagated further down the view hierarchy. That is, if our button component composed more views, those would be re-evaluated too, increasing the impact on performance. Similarly, if the view that contains the button component had more state that it managed, any changes to that state would also invalidate <code>CustomButton</code>. If we had more instances of <code>CustomButton</code>, those would be invalidated in a similar fashion.</p><p>It&#x2019;s probably obvious why this is a huge performance issue waiting to happen and cause us much headache. If we decide to look into the issue a bit, we can use the hint from our <code>_printChanges</code> exploration to hypothesize that something must be causing SwiftUI to consider our <code>Environment</code> value to have changed, which is probably in turn causing all those redundant invalidations.</p><h2 id="closures-do-not-support-equality">Closures do not support equality</h2><p>In Swift, closures are reference types, but are neither equatable nor reference equatable. This is not some bug in Swift, but is actually very much by design. In the words of Chris Lattner on the Old Developer Forums (preserved thanks to <a href="https://stackoverflow.com/questions/24111984/how-do-you-test-functions-and-closures-for-equality">this direct quote on StackOverflow</a>):</p><blockquote>This is a feature we intentionally do not want to support. There are a variety of things that will cause pointer equality of functions (in the swift type system sense, which includes several kinds of closures) to fail or change depending on optimization. If &quot;===&quot; were defined on functions, the compiler would not be allowed to merge identical method bodies, share thunks, and perform certain capture optimizations in closures. Further, equality of this sort would be extremely surprising in some generics contexts, where you can get reabstraction thunks that adjust the actual signature of a function to the one the function type expects.</blockquote><p>The easiest way to confirm this behavior is declaring a closure and then attempting to reference-compare it <em>to itself</em>, like so:</p><pre><code>let closureWithoutArguments: () -&gt; Void = { }
print(closureWithoutArguments == closureWithoutArguments)
print(closureWithoutArguments === closureWithoutArguments)
</code></pre><p>Neither of the print statements will compile:</p><blockquote>Cannot check reference equality of functions; operands here have types &apos;() -&gt; Void&apos; and &apos;() -&gt; Void&#x2019;.</blockquote><p>Note that in previous versions of Swift, reference comparison (<code>===</code>) did compile, but the statement above would&#x2019;ve returned <code>false</code> at runtime.</p><h2 id="actions-in-environment-exploring-alternatives">Actions in Environment: Exploring Alternatives</h2><p>So, how do we pass an action down the <code>Environment</code>? Should we simply give up on the flexibility it offers and stick to other alternatives?</p><h3 id="bad-closures-in-environment">Bad: Closures in Environment</h3><p>At this point, we can see why putting our closure in the <code>Environment</code> directly was such a bad idea. Since the internals of SwiftUI&#x2019;s <code>Environment</code> implicitly track changes to values and invalidate the views as appropriate, we can hopefully see the problem now. Every time it checks our value, it&#x2019;s <em>technically</em> different, so the framework must invalidate any views that depend on this value.</p><h3 id="better-passing-closures-directly">Better: Passing Closures Directly</h3><p>We could revert to a simple action model that you would typically see on views like buttons.</p><pre><code class="language-swift">struct ContentView: View {
    @State private var isHovered: Bool = false
    
    var body: some View {
        CustomButton(openSomething: openSomething)
            .padding()
            .background {
                Color.primary
                    .opacity(isHovered ? 0.2 : 0.05)
            }
            .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
            .onHover {
                isHovered = $0
            }
    }
    
    private func openSomething() {
        print(&quot;Open clicked...&quot;)
    }
}

struct CustomButton: View {
    let openSomething: () -&gt; Void
    
    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;Click to Open&quot;)
            .onTapGesture {
                openSomething()
            }
    }
}
</code></pre><p>The behavior here is much more predictable. We get:</p><ul><li>A single <code>CustomButton: @self changed.</code> print on initial evaluation.</li><li>One <code>CustomButton: @self changed.</code> print every time <code>isHovered</code> changes.</li></ul><p>The latter is still not ideal, of course. After all, my custom button does not depend on the hover state of its parent view. But in practice, if you&#x2019;re dealing with a leaf view like a simple button, this can be a good tradeoff to make. The direct passing of the action is much more scoped and much more explicit than passing it through the environment, and the invalidation behavior is certainly more predictable. But, most importantly, <em>it&#x2019;s so simple</em>!</p><p>Of course, one other alternative would be to encapsulate such actions inside some <code>@Observable</code> object that you can hold near the root pass through the <code>Environment</code>, then have your button call some function on it. But, of course, you are sacrificing some flexibility, and it doesn&#x2019;t work for every use case.</p><h3 id="best-use-a-stable-object">Best: Use a Stable Object</h3><p>If you&#x2019;ve seen how SwiftUI&#x2019;s built-in <code>DismissAction</code> works, you probably know what I&#x2019;m about to suggest. If not, here&#x2019;s a brief summary.</p><h4 id="case-study-dismissaction">Case Study: DismissAction</h4><p>SwiftUI includes a <code>dismiss</code> action in the <code>Environment</code> that you can use to <em>dismiss</em> things like sheets and popovers.</p><pre><code class="language-swift">private struct SheetContents: View {
    @Environment(\.dismiss) private var dismiss


    var body: some View {
        Button(&quot;Done&quot;) {
            dismiss()
        }
    }
}
</code></pre><p>You use it like a closure, but it is not <em>actually</em> a closure. If you look at its definition, you will see that it&#x2019;s a struct that implements a &#x201C;magic&#x201D; <code>callAsFunction</code> method.</p><pre><code class="language-swift">@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
@MainActor public struct DismissAction {
    public func callAsFunction()
}
</code></pre><p><code>callAsFunction</code> is what allows the environment value to be used as if it were a closure or a function. You can read more about methods with special names in Swift <a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/declarations/#Methods-with-Special-Names">on the documentation website</a>.</p><h4 id="implementing-callable-handlers">Implementing Callable Handlers</h4><p>Let&#x2019;s do the same for our <code>openSomething</code> closure. The first step is to wrap our action in a callable type and implement <code>callAsFunction</code>:</p><pre><code class="language-swift">struct CallableHandler&lt;Input&gt; {
    typealias Handler = (Input) -&gt; Void
    private let handler: Handler
    
    init(_ handler: @escaping Handler) {
        self.handler = handler
    }
    
    func callAsFunction(_ input: Input) {
        handler(input)
    }
}

extension EnvironmentValues {
    @Entry var openSomething: CallableHandler&lt;Void&gt;?
}
</code></pre><p>Now, let&#x2019;s use it for our button. I may be tempted to do this in a fairly naive way, initializing the handler &#x201C;on the fly.&#x201D;</p><pre><code class="language-swift">struct ContentView: View {
    @State private var isHovered: Bool = false
    
    var body: some View {
        CustomButton()
            .environment(\.openSomething, CallableHandler {
                print(&quot;Open clicked...&quot;)
            })
            .onHover { isHovered = $0 }
    }
}

struct CustomButton: View {
    @Environment(\.openSomething) var openSomething
    
    var body: some View {
        let _ = Self._printChanges()
        Text(&quot;Click to Open&quot;)
            .onTapGesture {
                openSomething?(())
            }
    }
}
</code></pre><p>&#x274C; However, this would still cause hovering the parent view to invalidate the button &#x2013; the same behavior as when passing a closure to it directly. The reason for that is because every time the parent view&#x2019;s body is evaluated, we will put a new handler in the <code>Environment</code>, which will cause SwiftUI to <em>attempt</em> to diff it with the old one. In this case, SwiftUI will consider the new value to be different.</p><p>&#x26A0;&#xFE0F; We could, of course, work around this by implementing an explicit <code>Equatable</code> conformance for our <code>CallableHandler</code>. However, a more important piece of intuition about diffing that we can extract from this is:</p><ul><li>Writing newly initialized structs and newly allocated objects to the environment will incur diffing costs, and</li><li>In some cases, the outcome of the diffing may be unexpected.</li></ul><p>&#x2705; Given that, a more general and flexible solution would involve making the handler stable and passing it down, like so:</p><pre><code class="language-swift">private struct HandlerModifier: ViewModifier {
    @State var handler: CallableHandler&lt;Void&gt;
    
    init(handler: @escaping CallableHandler&lt;Void&gt;.Handler) {
        _handler = .init(wrappedValue: CallableHandler(handler))
    }
    
    func body(content: Content) -&gt; some View {
        content
            .environment(\.openSomething, handler)
    }
}

extension View {
    func openSomethingHandler(_ handler: @escaping () -&gt; Void) -&gt; some View {
        self
            .modifier(HandlerModifier(handler: handler))
    }
}
</code></pre><p>We can then use the new modifier in our content view:</p><pre><code class="language-swift">CustomButton()
    .openSomethingHandler {
        print(&quot;Open clicked...&quot;)
    }
</code></pre><p>If we check the behavior now, we will notice that changing the state of the parent view does not cause a <code>body</code> reevaluation of our custom button, which is precisely the effect we desired.</p><h3 id="generalizing">Generalizing</h3><p>The solution can, of course, be generalized to allow you to pass actions with a variable number of parameters, return types, and environment keys.</p><p>For instance, I can make my <code>CallableHandler</code> use parameter packs for its inputs. Further, I can generalize the modifier to be useful for any action.</p><pre><code class="language-swift">struct CallableHandler&lt;each Input, Output&gt; {
    typealias Action = ((repeat each Input)) -&gt; Output
    private let action: Action
    
    init(_ action: @escaping Action) {
        self.action = action
    }
    
    func callAsFunction(_ update: repeat each Input) -&gt; Output {
        action((repeat each update))
    }
}

private struct HandlerModifier&lt;each Input, Output&gt;: ViewModifier {
    typealias Key = WritableKeyPath&lt;EnvironmentValues, Handler?&gt;
    typealias Handler = CallableHandler&lt;repeat each Input, Output&gt;
    
    let key: Key
    @State private var handler: Handler
    
    init(key: Key, action: @escaping Handler.Action) {
        self.key = key
        self._handler = .init(wrappedValue: CallableHandler(action))
    }
    
    func body(content: Content) -&gt; some View {
        content
            .environment(key, handler)
    }
}

extension View {
    func environmentHandler&lt;each Input, Output&gt;(
        _ key: WritableKeyPath&lt;EnvironmentValues, CallableHandler&lt;repeat each Input, Output&gt;?&gt;,
        action: @escaping CallableHandler&lt;repeat each Input, Output&gt;.Action
    ) -&gt; some View {
        self
            .modifier(HandlerModifier(key: key, action: action))
    }
}
</code></pre><p>We could then easily declare new actions as entries, and reuse the same modifier to pass actions down.</p><pre><code class="language-swift">extension EnvironmentValues {
    @Entry var openSomething: CallableHandler&lt;Void&gt;?
}

struct ContentView: View {
    @State private var isHovered: Bool = false
    
    var body: some View {
        CustomButton()
            .environmentHandler(\.openSomething) {
                print(&quot;Open clicked...&quot;)
            }
    }
}
</code></pre><h2 id="takeaways">Takeaways</h2><p>It feels important to reiterate that the above solution is not a silver bullet or a hack that will somehow solve all your performance problems. This solves an extremely targeted problem, and before implementing a similar system in your codebase, it&#x2019;s important to understand the problem space alongside the available alternatives. And, if a simpler solution works for your particular use case, it&#x2019;s almost always better to start there!</p><ul><li>If you&#x2019;re dealing with extremely simple leaf views like <code>Button</code>s, you&#x2019;re likely better off simply passing the closure to it directly. It will make your code much clearer, simpler to reason about, and easier to iterate on. Of course, be aware of the tradeoffs you&#x2019;ve made and reevaluate the decision if the button ever becomes more complex, if you start needing a lot of them, and / or if you start seeing a performance impact.</li><li>If you already keep most of your business logic on some object (e.g., an instance of an <code>Observable</code> type that is the <code>State</code> of the root view of your <code>Scene</code>), and you don&#x2019;t need any view-specific state to perform the action, adding the action as an instance method on the type and calling it directly can be the lowest overhead solution.</li></ul><p>Even if the flexibility of passing the actions through the <code>Environment</code> is indeed what your use case is calling for, it can be incredibly useful to align your expectations of the behavior with what is actually happening at runtime. This can help make SwiftUI internals seem a bit less opaque and develop a more robust intuition that will undoubtedly be of use in the future.</p>]]></content:encoded></item><item><title><![CDATA[Built-in Support for Swift Observable in AppKit and UIKit on macOS 26 and iOS 26]]></title><description><![CDATA[<p>macOS 14 and iOS 17 saw the introduction of the <code>Observation</code> framework and the <code>Observable</code> macro, allowing your SwiftUI views to automatically update in response to underlying data changing. However, that initial implementation contained really limited capabilities for consuming <code>Observable</code> types from <strong>outside</strong> SwiftUI.</p><h2 id="before-macos-26-and-ios-26">Before macOS 26 and iOS 26</h2>]]></description><link>https://philz.blog/built-in-support-for-swift-observable-in-appkit-and-uikit-on-macos-26-and-ios-26-3/</link><guid isPermaLink="false">684b742707baf7ebec44f8ab</guid><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Fri, 13 Jun 2025 00:48:38 GMT</pubDate><media:content url="https://philz.blog/content/images/2025/06/Frame.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://philz.blog/content/images/2025/06/Frame.jpg" alt="Built-in Support for Swift Observable in AppKit and UIKit on macOS 26 and iOS 26"><p>macOS 14 and iOS 17 saw the introduction of the <code>Observation</code> framework and the <code>Observable</code> macro, allowing your SwiftUI views to automatically update in response to underlying data changing. However, that initial implementation contained really limited capabilities for consuming <code>Observable</code> types from <strong>outside</strong> SwiftUI.</p><h2 id="before-macos-26-and-ios-26">Before macOS 26 and iOS 26</h2><p>Previously, if I wanted to <em>continuously</em> drive updates to an AppKit or UIKit view in response to <code>Observable</code> changing, I&#x2019;d have to use <code>withObservationTracking</code>, whose closure would be called on <code>willSet</code> of the underlying value and wasn&#x2019;t continuous. So, you could end up with hacky methods that kind of worked like:</p><pre><code>extension Observation.Observable {
    public func onChange&lt;T&gt;(
        of value: @escaping @autoclosure () -&gt; T,
        execute: @escaping (T) -&gt; Void
    ) {
        withObservationTracking {
            execute(value())
        } onChange: {
            self.onChange(of: value(), execute: execute)
        }
    }
}
</code></pre><p>You&#x2019;d then do work on the next turn of the <code>RunLoop</code> or spin up an unstructured <code>Task</code> to get the <strong>new</strong> value from the updated property. Overall, it&#x2019;s clear the original <code>Observable</code> wasn&#x2019;t meant for these use cases!</p><h2 id="new-in-appkit-and-uikit">New in AppKit and UIKit</h2><p>On macOS 26 and iOS 26, both AppKit and UIKit have built-in support for <code>Observable</code> types, covering a lot of the use cases I&#x2019;ve personally faced in a mixed UI framework codebase. The UIKit changes are covered in <a href="https://developer.apple.com/videos/play/wwdc2025/243">this WWDC video</a>, while the corresponding AppKit changes weren&#x2019;t explicitly mentioned. I&#x2019;ll summarize both here and aim to provide some depth for the underlying implementation.</p><h3 id="how-it-works">How it works</h3><p>In update methods on <code>NSViewController</code> and <code>NSView</code> in AppKit, alongside <code>UIViewController</code> and <code>UIView</code> in UIKit, the framework now automatically tracks any <code>Observable</code> you reference, wires up dependencies, and invalidates the right views. This works via a mechanism similar to <code>withObservationTracking</code>, where any property you reference within the <code>apply</code> closure would inform the caller of value changes made to participating properties by way of the <code>onChange</code> closure, except, instead of an explicit <code>apply</code> closure, you are tracking any <code>Observable</code> property you&#x2019;re referencing within your update methods. This eliminates the recursive call pattern from the hacky <code>onChange</code> handler.</p><p>For an example of how this works, consider the following snippet from the UIKit WWDC talk, which will be similar in the AppKit world as well:</p><pre><code class="language-swift">
@Observable final class UnreadMessagesModel {
    var showStatus: Bool
    var statusText: String
}

final class MessageListViewController: UIViewController {
    var unreadMessagesModel: UnreadMessagesModel

    var statusLabel: UILabel
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        statusLabel.alpha = unreadMessagesModel.showStatus ? 1.0 : 0.0
        statusLabel.text = unreadMessagesModel.statusText
    }
}
</code></pre><p>Because I reference the <code>Observable</code> properties in my <code>viewWillLayoutSubviews</code> implementation, they are automatically tracked. Any further change to those referenced properties invalidates the view and reruns <code>viewWillLayoutSubviews</code>, keeping the label in sync with the model without extra code or manual <code>didSet</code> hooks.</p><p>It&apos;s worth noting that this tracking is property-specific &#x2013; if your <code>UnreadMessagesModel</code> had additional properties like <code>messageCount</code> or <code>lastUpdated</code>, but you didn&apos;t reference them in <code>viewWillLayoutSubviews</code>, changes to those properties wouldn&apos;t trigger layout updates.</p><p>So, which update methods participate in this new tracking behavior? While this Is not explicitly documented at the time of this writing, I&#x2019;ve compiled a summary here thanks to the magic of reverse engineering.</p><h4 id="appkit">AppKit</h4><p>For <code>NSViewController</code>:</p><ul><li><a href="https://developer.apple.com/documentation/appkit/nsviewcontroller/updateviewconstraints()" rel="noreferrer"><code>updateViewConstraints</code></a></li><li><a href="https://developer.apple.com/documentation/appkit/nsviewcontroller/viewwilllayout()" rel="noreferrer"><code>viewWillLayout</code></a></li><li><a href="https://developer.apple.com/documentation/appkit/nsviewcontroller/viewdidlayout()" rel="noreferrer"><code>viewDidLayout</code></a></li></ul><p>For <code>NSView</code>:</p><ul><li><a href="https://developer.apple.com/documentation/appkit/nsview/updateconstraints()" rel="noreferrer"><code>updateConstraints</code></a></li><li><a href="https://developer.apple.com/documentation/appkit/nsview/updatelayer()" rel="noreferrer"><code>updateLayer</code></a></li><li><a href="https://developer.apple.com/documentation/appkit/nsview/layout()" rel="noreferrer"><code>layout</code></a></li><li><a href="https://developer.apple.com/documentation/appkit/nsview/draw(_:)" rel="noreferrer"><code>draw(_:)</code></a></li></ul><h4 id="uikit">UIKit</h4><p>For <code>UIViewController</code>:</p><ul><li><a href="https://developer.apple.com/documentation/uikit/uiviewcontroller/updateviewconstraints()" rel="noreferrer"><code>updateViewConstraints</code></a></li><li><a href="https://developer.apple.com/documentation/uikit/uiviewcontroller/viewwilllayoutsubviews()" rel="noreferrer"><code>viewWillLayoutSubviews</code></a></li><li><a href="https://developer.apple.com/documentation/uikit/uiviewcontroller/viewdidlayoutsubviews()" rel="noreferrer"><code>viewDidLayoutSubviews</code></a></li><li><a href="https://developer.apple.com/documentation/uikit/uiviewcontroller/updatecontentunavailableconfiguration(using:)" rel="noreferrer"><code>updateContentUnavailableConfiguration(using:)</code></a></li></ul><p>For <code>UIView</code>:</p><ul><li><a href="https://developer.apple.com/documentation/uikit/uiview/updateconstraints()" rel="noreferrer"><code>updateConstraints</code></a></li><li><a href="https://developer.apple.com/documentation/uikit/uiview/layoutsubviews()" rel="noreferrer"><code>layoutSubviews</code></a></li><li><a href="https://developer.apple.com/documentation/uikit/uiview/draw(_:)" rel="noreferrer"><code>draw(_:)</code></a></li></ul><p>For <code>UITableViewHeaderFooterView</code>, <code>UIButton</code>, <code>UICollectionViewCell</code>, <code>UITableViewCell</code>:</p><ul><li><a href="https://developer.apple.com/documentation/uikit/uitableviewheaderfooterview/configurationupdatehandler-49slo" rel="noreferrer"><code>configurationUpdateHandler</code></a></li></ul><h2 id="backwards-compatibility">Backwards compatibility</h2><p>The new <code>Observation</code> tracking behavior in both AppKit and UIKit is enabled by default in macOS 26 and iOS 26.</p><p>However, the related changes have actually been present in the source since prior year&#x2019;s macOS 15 and iOS 18 releases &#x2013; but were off by default. You can turn them on manually and get the same behavior on both OSes by adding the following keys to your <code>Info.plist</code>:</p><ul><li>For AppKit, add the <code>NSObservationTrackingEnabled</code> key and set its value to <code>YES</code> for compatibility with macOS 15.</li><li>For UIKit, add the <code>UIObservationTrackingEnabled</code> key and set its value to <code>YES</code> for compatibility with iOS 18.</li></ul><h2 id="new-in-swift-62">New in Swift 6.2</h2><p>Swift 6.2 saw an implementation of <a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0475-observed.md">SE-0475: Transactional Observation of Values</a>, adding a general-purpose way to observe changes to <code>Observable</code> models using an <code>AsyncSequence</code>. This change closes the remaining gap in the ability to consume continuous changes to <code>Observable</code> types in arbitrary contexts not limited by the UI framework use cases.</p><p>You can create an <code>Observations</code> instance with a closure that accesses properties it wants to observe &#x2013; a pattern familiar to <code>Observation</code> consumers. Subsequently, you can access the <code>AsyncSequence</code> directly:</p><pre><code>let model = UnreadMessagesModel()

let statusChanges = Observations {
    model.statusText
}

for await statusText in statusChanges {
    print(&#x201C;Status text is \(statusText).&#x201D;)
}
</code></pre><p>The new additions across AppKit, UIKit, and Swift not only close the gaps for general-purpose <code>Observable</code> consumption use cases, but also make interoperability between UI frameworks vastly easier, providing more flexibility for the ways you work with <code>Observable</code> types. This is just one of the few changes to SDKs announced during WWDC25 with the common theme of simplifying interoperability and enhancing flexibility &#x2013; and I&#x2019;m here for it.</p>]]></content:encoded></item><item><title><![CDATA[In-process animations and transitions with CADisplayLink, done right]]></title><description><![CDATA[<p>Admittedly, animation and transition techniques on macOS are not as widely applied or evolved as they may be on iOS-based platforms, and information and advice around them can be scattered and oftentimes outdated. So, let&#x2019;s fix that and try to outline some best practices, as well as establish</p>]]></description><link>https://philz.blog/in-process-animations-and-transitions-with-cadisplaylink-done-right/</link><guid isPermaLink="false">683cc8a207baf7ebec44f893</guid><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Sun, 01 Jun 2025 21:41:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDI1fHxhbmltYXRpb258ZW58MHx8fHwxNzQ4ODE0MDQzfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDI1fHxhbmltYXRpb258ZW58MHx8fHwxNzQ4ODE0MDQzfDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="In-process animations and transitions with CADisplayLink, done right"><p>Admittedly, animation and transition techniques on macOS are not as widely applied or evolved as they may be on iOS-based platforms, and information and advice around them can be scattered and oftentimes outdated. So, let&#x2019;s fix that and try to outline some best practices, as well as establish some intuition about some of the core tools available to us when working with animations.</p><p><code>CADisplayLink</code> is an unavoidable component in the vast majority of <strong>in-process</strong> animation techniques. However, so much of the existing usage, both open-source and proprietary, does not consistently and correctly manage <code>CADisplayLink</code>s &#x2013; at least on macOS.</p><h2 id="in-process-vs-out-of-process">In-process vs. Out-of-process</h2><p>Before moving on to the discussion of using <code>CADisplayLink</code> for animations on macOS (and other platforms), it feels important to provide some background on why one might choose to use it in the first place.</p><p><strong>Out-of-process</strong> animation techniques on both macOS and iOS involve interfacing with the <em>render server</em>. If you&#x2019;re familiar with Core Animation (or LayerKit, if you prefer), you&#x2019;ve used <code>CABasicAnimation</code>, <code>CAKeyframeAnimation</code>, or <code>CASpringAnimation</code>. These are all examples of out-of-process animations as they are run outside of your app&#x2019;s process by committing your changes to your layers&#x2019; models and any explicit transactions you commit to the render server. While the animation is running, it does not block your app&#x2019;s main thread. And, similarly, if your app&#x2019;s main thread happens to get clogged during one of these animations, you will not experience frame drops within the animation itself. (You may still experience glitches in other areas of the app or when trying to modify or remove the animation.)</p><p>Sounds pretty neat, especially for simple use cases like fading a view or moving a button. And it most certainly is. However, one of the biggest limitations of the out-of-process animation techniques, and Core Animation in particular, is that the <em>set of properties</em> that can be animated, as well as <em>the ways in which they can</em> be animated, are both <strong>pre-defined</strong> and <strong>limited</strong>. You cannot extend the server&#x2019;s behavior by e.g. telling it how to animate a custom color matrix on your custom Metal layer. Similarly, you cannot implement a fluid spring that is both interruptible and can preserve its velocity when interrupted using <code>CASpringAnimation</code> alone. These behaviors are implemented outside of your app&#x2019;s process and should be viewed as immutable.</p><p>These are specifically the cases where you may want to consider an <strong>in-process</strong> animation technique. Perhaps you want a fluid spring, or a customizable decay function &#x2013; or you want to interpolate between 2 custom data structures. To accomplish this, you&#x2019;d implement some of the implicit render server behavior inside your app&#x2019;s process. You would synchronize on <em>the</em> display&#x2019;s (or <em>a</em> display&#x2019;s &#x2013; more on that later) refresh rate, interpolate between the source and the target value using an interpolation strategy you prefer, and at each update, or tick, of the animation, you would update the value of the animated property.</p><p>In-process animation techniques typically rely on being run on the main thread, since they need to modify main thread only properties, like anything on <code>NSView</code> or <code>UIView</code>, or risk runtime warnings and undefined behavior. There are notable exceptions, of course &#x2013; for instance, if you&#x2019;re okay with using SPI, you can update <code>CALayer</code> properties from a background thread &#x2013; but I&#x2019;ll leave that discussion outside the scope of this post. As anything that is run on the main thread, these animations will be subject to resource contention with UI framework code and your own work that is being done on the main thread. If anything is blocking the main thread and you happen to miss the commit deadline, you will start seeing frame drops. And, now that is on you to handle, since you&#x2019;re owning the animation stack. How fun!</p><h2 id="cadisplaylink">CADisplayLink</h2><p>Until macOS 14, <a href="https://developer.apple.com/documentation/quartzcore/cadisplaylink" rel="noreferrer"><code>CADisplayLink</code></a> was unavailable on macOS, and the majority of clients implementing in-process animations and transitions used the <a href="https://developer.apple.com/documentation/CoreVideo/cvdisplaylink-k0k" rel="noreferrer"><code>CVDisplayLink</code></a> family of APIs. These were in principle the same, having the same goal, which was to allow running callbacks in sync with the display. Some typical code to get started with receiving callbacks you&#x2019;d see would be something like:</p><pre><code class="language-swift">CVDisplayLinkCreateWithActiveCGDisplays(&amp;displayLink)
CVDisplayLinkSetOutputHandler(displayLink!) { (_, _, _, _, _) -&gt; CVReturn in
    animationStep()
    return kCVReturnSuccess
}
CVDisplayLinkStart(displayLink!)
</code></pre><p>Similarly, for a <code>CADisplayLink</code> with an Objective-C API surface you&#x2019;d see something like:</p><pre><code class="language-swift">let displayLink = CADisplayLink(target: target, selector: selector)
displayLink?.preferredFramesPerSecond = Config.preferredFramesPerSecond
displayLink?.add(to: .main, forMode: .common)
displayLink?.isPaused = false
</code></pre><p>In the vast majority of use cases I&#x2019;ve seen, like the ones above, the handling of both <code>CVDisplayLink</code> and <code>CADisplayLink</code> is incomplete and therefore the animation strategies can be <em>fundamentally flawed</em> in certain scenarios. <strong>That FPS is hard coded!</strong></p><p>In a better, but still very much flawed, scenario, you might see something more sane like using <a href="https://developer.apple.com/documentation/uikit/uiscreen/maximumframespersecond" rel="noreferrer"><code>UIScreen.main.maximumFramesPerSecond</code></a> (with <a href="https://developer.apple.com/forums/thread/79895" rel="noreferrer"><code>CADisableMinimumFrameDuration</code></a> Info.plist key) on iOS.</p><p>But now, for instance, imagine I&#x2019;m driving a 60 Hz external display with an iPad Pro or a MacBook Pro. You can imagine much more complicated external display use cases, especially on macOS, and especially when you consider other cases like Screen Mirroring or Sidecar, which demand these updates to happen dynamically even for the same display identities. Neither of the techniques above would correctly handle even the simplest case of switching between displays, let alone some of the more conceptually complicated use cases.</p><p>Ultimately, when you want to run an animation that smoothly runs on whatever display may be connected with the maximum possible refresh rate, you have to have the following considerations in mind:</p><ul><li>Multiple displays with varying refresh rates may be connected at any given time.</li><li>The refresh rate of any display can change at any point while a display link is running.</li><li>A window in which something is being animated may be moved to another display, which may have a different refresh rate compared to the original.</li><li>A view in which something is being animated may be reparented and moved to a different window, potentially on a different display, which may, again, have a different refresh rate.</li><li>A view (or a window) you&#x2019;re animating may leave a display while the animation is running, in which case you should suspend your display link, and resume it when the animated object appears on some display again.</li></ul><h2 id="appkit-to-the-rescue">AppKit to the rescue</h2><p>macOS 14.0 saw the deprecation of <code>CVDisplayLink</code> family of APIs in favor of the new AppKit methods: <a href="https://developer.apple.com/documentation/macos-release-notes/appkit-release-notes-for-macos-14" rel="noreferrer"><code>displayLink(target:selector:)</code></a> which exist on <code>NSView</code>, <code>NSWindow</code>, and <code>NSScreen</code> instances. These methods vend an opaque <code>CADisplayLink</code>, which you <em>can</em> customize if you wish but don&#x2019;t have to. You can simply call</p><pre><code class="language-swift">displayLink.add(to: .current, forMode: .common)
</code></pre><p>And start receiving callbacks on the target you&#x2019;ve specified.</p><p>These AppKit-managed display links (backed by an internal <code>_NSDisplayLink</code> class) will handle two very important cases:</p><ul><li>It will track which display your view, window, or screen belongs to, and adjust the refresh rate of the display link accordingly.</li><li>It will automatically suspend itself if the view or window isn&#x2019;t on a display.</li></ul><p>This gives you the complete set of tools you may need in order to handle all of the cases I&#x2019;ve outlined above, and therefore to achieve animations that run as smoothly as possible on whichever display they may be running on at any given time. Of course, you may still choose to manage your display link directly (though I certainly wouldn&#x2019;t recommend it if you&#x2019;re targeting macOS), in which case the conceptual understanding of the variables at play will be incredibly helpful in making sure your implementation handles more than just the single most obvious use case out there.</p><h2 id="more-to-consider">More to consider</h2><p>Once you&#x2019;ve figured out your display link creation and management, it&#x2019;s time to actually run your animation or transition. While interpolation techniques and ways to handle retargeting or spring physics are intentionally out of scope of this post (or any single post, for that matter) to preserve its focus, there are a few more things one must consider that are direct consequences of the variable refresh rates.</p><p>First and foremost, the duration of your frame is not <code>TimeInterval(1 / 60)</code> &#x2013; and never has been. Once we&#x2019;ve taken the time to understand display links and moved away from hard coding FPS, we must make similar adjustments to our code elsewhere. Luckily, the <code>CADisplayLink</code> object we&#x2019;re working with provides 3 helpful properties: <code>timestamp</code>, <code>duration</code>, and <code>targetTimestamp</code>. Understanding the difference between these is crucial to make sure your interpolations are correct and consistent across displays.</p><ul><li><a href="https://developer.apple.com/documentation/quartzcore/cadisplaylink/timestamp" rel="noreferrer"><code>timestamp</code></a> represents the <code>TimeInterval</code> when the <strong>last</strong> frame was displayed.</li><li><a href="https://developer.apple.com/documentation/quartzcore/cadisplaylink/targettimestamp" rel="noreferrer"><code>targetTimestamp</code></a> is the time when the <strong>next</strong> frame is scheduled the display. When you get a callback from the display link and need to calculate the interpolation step of your animation, this is what you need to use &#x2013; not <code>timestamp</code>, since your job within that callback is calculating what to display <strong>next</strong>.</li><li><a href="https://developer.apple.com/documentation/quartzcore/cadisplaylink/duration" rel="noreferrer"><code>duration</code></a> is the interval between display updates. If you&#x2019;d like to query it, note that it will only be guaranteed to be valid within the display link callbacks.</li></ul><p>In combination with <a href="https://developer.apple.com/documentation/quartzcore/cacurrentmediatime()" rel="noreferrer"><code>CACurrentMediaTime()</code></a>, these properties provide the complete information about display updates that may be needed in order to calculate the interpolating values for your animations and transitions.</p>]]></content:encoded></item><item><title><![CDATA[Document-based SwiftUI Apps on visionOS 2.4 are Unusable due to a Regression]]></title><description><![CDATA[<blockquote>TL;DR: As a result of a gnarly regression, every document-based SwiftUI app on visionOS 2.4 is duplicating views, rendering document-based app completely unusable on the platform.</blockquote><p>I was working on porting one of my side project apps from macOS to visionOS, and at some point wanted to test</p>]]></description><link>https://philz.blog/document-based-swiftui-apps-on-visionos-2-4-unusable/</link><guid isPermaLink="false">67f3496907baf7ebec44f866</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[Apps]]></category><category><![CDATA[Xcode]]></category><category><![CDATA[Swift]]></category><category><![CDATA[UI]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Mon, 07 Apr 2025 03:46:39 GMT</pubDate><media:content url="https://philz.blog/content/images/2025/04/Screenshot-2025-04-06-at-8.41.07-PM.png" medium="image"/><content:encoded><![CDATA[<blockquote>TL;DR: As a result of a gnarly regression, every document-based SwiftUI app on visionOS 2.4 is duplicating views, rendering document-based app completely unusable on the platform.</blockquote><img src="https://philz.blog/content/images/2025/04/Screenshot-2025-04-06-at-8.41.07-PM.png" alt="Document-based SwiftUI Apps on visionOS 2.4 are Unusable due to a Regression"><p>I was working on porting one of my side project apps from macOS to visionOS, and at some point wanted to test it against the newly released visionOS 2.4. What I found was my app, which I hadn&#x2019;t touched in any substantial way since the previous release, was very close to unusable. I observed the following symptoms. Images and text seemed overexposed, <strong>as if multiple copies had been layered on top of one another</strong>. Once I tried scrolling in the main scroll view, that proved to be the case. There was an actual copy of the view hierarchy in its initial state <strong>behind the interactive portion of the view</strong>.</p><figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://philz.blog/content/media/2025/04/Untitled_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://philz.blog/content/media/2025/04/Untitled.webm" poster="https://img.spacergif.org/v1/1920x1080/0a/spacer.png" width="1920" height="1080" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://philz.blog/content/media/2025/04/Untitled_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container kg-video-hide">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:04</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            
        </figure><p>The result was comical but concerning. Had I done something wrong? Had I accidentally regressed the functionality? <em>(I had never ever done that before, of course!)</em></p><p>It turns out, the same symptoms were reproducible in <strong>any</strong> document-based app, even the one created from the Xcode template. It reproduced on both the physical device and the simulator running visionOS 2.4. The regression wasn&#x2019;t mine, it&#x2019;s Apple&#x2019;s!</p><p>After contemplating whether anyone had bothered to test document-based apps before the public visionOS 2.4 release, and making a few initial hypotheses about what could&#x2019;ve happened based on my first-hand familiarity with Apple&#x2019;s engineering processes, I realized that things were not looking good for being able to test my app on my shiny Vision Pro.</p><h2 id="debugging">Debugging</h2><p>I made a sample document-based app from the Xcode template, built it, and ran it on a device running visionOS 2.4. So, this wasn&#x2019;t anything about the app itself; in fact, it seems like any document-based app is suffering the same fate. Multiple copies of my content stacked on top of each other, creating that distinctive &#x201C;overexposed&#x201D; look, and scrolling the content produced the same duplication, as if multiple copies of my view hierarchy were stacked on top of one another. I tried removing scrollable content from the equation. The issue still reproduced. I then tried a non-document-based app, and the issue was gone.</p><figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://philz.blog/content/media/2025/04/Screen-Recording-2025-04-06-at-8.40.15-PM_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://philz.blog/content/media/2025/04/Screen-Recording-2025-04-06-at-8.40.15-PM.webm" poster="https://img.spacergif.org/v1/2230x1304/0a/spacer.png" width="2230" height="1304" playsinline preload="metadata" style="background: transparent url(&apos;https://philz.blog/content/media/2025/04/Screen-Recording-2025-04-06-at-8.40.15-PM_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:09</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            
        </figure><p>With that preliminary investigation out of the way, I needed to understand what was happening under the hood. My initial hypothesis was that root SwiftUI views were being added to the hierarchy multiple times, and were never removed, which produced both the overexposed look (due to <code>plusLighter</code> blending liberally used throughout visionOS) and the duplication effect during scrolling.</p><p>To get a better intuition and visibility into what was going on, the next step was to set a breakpoint on <code>init</code> of the root view.</p><pre><code class="language-swift">init(document: Binding&lt;MaterialBugDocument&gt;) {
    self._document = document
}
</code></pre><p>The first 2 times, we were called with the following UIKit symbols in the stack trace:</p><pre><code class="language-objective-c">#49 0x0000000185227bc8 in -[UIPresentationController _sendWillDismiss] ()
#50 0x0000000185227a2c in -[UIPresentationController _sendDismissalsAsNeeded] ()
#51 0x0000000185df8f60 in -[UIDocumentViewControllerLaunchOptions _dismissBrowserViewController] ()
#52 0x0000000185df7e60 in -[UIDocumentViewControllerLaunchOptions _documentCloseStateDidChange] ()
#53 0x00000001855ad6e8 in -[UIDocumentViewController _documentStateChanged:] ()
</code></pre><p>The third (and final) time, it was something more intuitively expected:</p><pre><code class="language-objective-c">#44 0x00000001860145cc in -[UIView _postMovedFromSuperview:] ()
#45 0x00000001860210c8 in -[UIView(Internal) _addSubview:positioned:relativeTo:] ()
#46 0x00000001d41e0434 in __C.UIDocumentViewController.embedDocumentHostingController(Swift.Optional&lt;__C.UIViewController&gt;) -&gt; () ()
#47 0x00000001d41ea340 in merged SwiftUI.DocumentViewController.documentDidOpen() -&gt; () ()
#48 0x00000001d41df80c in @objc SwiftUI.DocumentViewController.documentDidOpen() -&gt; () ()
#49 0x00000001855aca24 in __62-[UIDocumentViewController openDocumentWithCompletionHandler:]_block_invoke_4 ()
</code></pre><p>The SwiftUI view being <strong>created</strong>, of course, doesn&#x2019;t mean much by itself &#x2013; a <code>View</code> is a transient object, after all, and is instantiated by SwiftUI machinery many times throughout the lifecycle, driven by graph updates. However, given the symptoms and the above, I suspected that the hosting view the above calls produced was actually added to the view hierarchy each time, and was never removed.</p><p>To sanity check myself, I added some <em>basic</em> instrumentation to track view lifecycle events:</p><pre><code class="language-swift">.onAppear {
    print(&quot;View appeared.&quot;)
}
.onDisappear {
    print(&quot;View disappeared.&quot;)
}
</code></pre><p>The output was revealing: <code>onAppear</code> was called <strong>three times</strong> per document, while <code>onDisappear</code> wasn&apos;t called at all. So, in addition to providing strong evidence to the initial intuition, this seemed to indicate the SwiftUI view hierarchy wasn&apos;t just duplicated &#x2013; it was <em>triplicated</em>. For every one view I intended to show, the visionOS 2.4 SDK was creating three! How very generous.</p><h2 id="diving-deeper">Diving Deeper</h2><p>I wanted to understand what exactly was going on and hopefully fix it &#x2013; on my own timeline. As the next step, I wrote a quick utility method to print the entire view hierarchy:</p><pre><code class="language-swift">func printViewHierarchy(level: Int = 0) {
    let indent = String(repeating: &quot;  &quot;, count: level)
    let className = &quot;\(type(of: self))&quot;
    
    print(&quot;\(indent)&#x2022; \(className): frame=\(frame)&quot;)
    
    // Print specific properties for common control types
    if let label = self as? UILabel {
        print(&quot;\(indent)  text: \&quot;\(label.text ?? &quot;nil&quot;)\&quot;&quot;)
    }
    // ... other control-specific info
    
    // Recursively print subviews
    for subview in self.subviews {
        subview.printViewHierarchy(level: level + 1)
    }
}

let scenes = UIApplication.shared.connectedScenes.first as? UIWindowScene
let window = scenes?.windows.first
window?.rootViewController?.view.printViewHierarchy()
</code></pre><p>Running this on the main window&#x2019;s root view controller&#x2019;s view after it had appeared yielded the following output (I took the liberty to truncate it to make it easier to parse):</p><pre><code>&#x2022; UILayoutContainerView: frame=(0.0, 0.0, 1280.0, 720.0), tag=0, accessibilityID=nil
  &#x2022; UINavigationTransitionView: frame=(0.0, 0.0, 1280.0, 720.0), tag=0, accessibilityID=nil
    &#x2022; UIViewControllerWrapperView: frame=(0.0, 0.0, 1280.0, 720.0), tag=0, accessibilityID=nil
      &#x2022; UIView: frame=(0.0, 0.0, 1280.0, 720.0), tag=0, accessibilityID=nil
        &#x2022; _UIHostingView&lt;ModifiedContent&lt;ModifiedContent&lt;AnyView, DocumentSceneRootBoxModifier&gt;, DocumentBaseModifier&gt;&gt;: frame=(0.0, 0.0, 0.0, 0.0), tag=0, accessibilityID=nil
          &lt;...&gt;
        &#x2022; _UIHostingView&lt;ModifiedContent&lt;ModifiedContent&lt;AnyView, DocumentSceneRootBoxModifier&gt;, DocumentBaseModifier&gt;&gt;: frame=(0.0, 0.0, 0.0, 0.0), tag=0, accessibilityID=nil
          &lt;...&gt;
        &#x2022; _UIHostingView&lt;ModifiedContent&lt;ModifiedContent&lt;AnyView, DocumentSceneRootBoxModifier&gt;, DocumentBaseModifier&gt;&gt;: frame=(0.0, 0.0, 0.0, 0.0), tag=0, accessibilityID=nil
          &lt;...&gt;
  &#x2022; UIKitNavigationBar: frame=(0.0, 0.0, 1280.0, 92.0), tag=0, accessibilityID=nil
    &lt;...&gt;
</code></pre><p>And here is the smoking gun: three identical sibling instances of <code>_UIHostingView</code> were being created for each document window.</p><p>Looking at the hierarchy output, I noticed that these extra views were identical siblings &#x2013; same class, same parent view, same everything. This gave me an idea for a surgical workaround: scan the view hierarchy for groups of sibling views with the same class name that contains &quot;_UIHostingView&quot;, and remove all but <strong>the last one</strong> of them. If this works, I&#x2019;ll at least be able to test the application on the new visionOS without it being completely broken.</p><p>I crafted a utility extension that does exactly this:</p><pre><code class="language-swift">extension UIView {
    /// Removes redundant sibling view instances of the same class
    /// containing the provided substring in their class name,
    /// keeping only one instance per group of sibling views.
    func cleanupRedundantViews(withClassNameContaining classNameSubstring: String) {
        func processViewGroup(_ viewGroup: [UIView]) {
            // Group sibling views by their class name
            var viewsByClassName: [String: [UIView]] = [:]
            
            for view in viewGroup {
                let className = NSStringFromClass(type(of: view))
                if className.contains(classNameSubstring) {
                    viewsByClassName[className, default: []].append(view)
                }
                
                // Recursively process this view&apos;s children
                processViewGroup(view.subviews)
            }
            
            // For each group of same-class views, keep only one
            for (_, views) in viewsByClassName where views.count &gt; 1 {
                // Keep the last one, remove the rest
                for view in views.dropLast() {
                    view.removeFromSuperview()
                    print(&quot;Removing \(view)...&quot;)
                }
            }
        }
        
        // Start with the root view&apos;s immediate subviews
        processViewGroup(self.subviews)
    }
}
</code></pre><p>With this utility in hand, I could now apply it when the view appears:</p><pre><code>.onAppear {
    let scenes = UIApplication.shared.connectedScenes.first as? UIWindowScene
    let window = scenes?.windows.first
    
    window?.rootViewController?.view.cleanupRedundantViews(withClassNameContaining: &quot;_UIHostingView&quot;)
}
</code></pre><p>I ran the app again, and... success! The redundant views were removed, leaving only one instance of each view. The SwiftUI views were also cleaned up correctly, and the disappear handler ran as expected. But, most importantly, the app was usable again, with proper rendering and scrolling behavior.</p><h2 id="the-aftermath">The Aftermath</h2><p>I&apos;ve posted the sample project, which includes the bug as well as the aforementioned hacky workaround, here: <a href="https://github.com/philptr/SwiftUIDocumentBasedBug">https://github.com/philptr/SwiftUIDocumentBasedBug</a>. This workaround isn&#x2019;t perfect. It&apos;s a hack that shouldn&#x2019;t be necessary, and it comes with some limitations. The real fix, of course, needs to be originated by Apple (I have reported it as FB17146267).</p><p>Whatever the cause, this is a clear reminder that it&#x2019;s much easier to regress something you aren&#x2019;t using yourself. The SwiftUI document-based app architecture is notoriously pretty limited and oftentimes feels underpowered next to their Kit counterparts, <code>NSDocument</code> and <code>UIDocument</code>. Given how forcibly lightweight and thus underpowered this initial architecture is, it is completely unsurprising that none of Apple&#x2019;s first-party apps on visionOS adopt the SwiftUI document-based architecture. It is therefore even less surprising (though, of course, pretty upsetting) that a bug like this one was not caught in testing passes, during the dogfooding process, or throughout the Seed cycle.</p><p>Whatever the present issues may be, I remain excited and hopeful for SwiftUI&#x2019;s future, which includes the document-based architecture, among many other things. After all, it is perhaps the only light at the end of the tunnel that can save native development on Apple platforms at this point, and the best way for third-party developers like myself to do our part is by providing feedback.</p>]]></content:encoded></item><item><title><![CDATA[The Curious Case of NSPanel's Nonactivating Style Mask Flag]]></title><description><![CDATA[<p>While working on a behavior that required dynamically switching between the nonactivating panel behavior and one that is more similar to the regular window behavior, I discovered a subtle but significant issue in AppKit: changing the <code>NSWindowStyleMaskNonactivatingPanel</code> flag after an <code>NSPanel</code> has been initialized doesn&apos;t fully update the</p>]]></description><link>https://philz.blog/nspanel-nonactivating-style-mask-flag/</link><guid isPermaLink="false">67e9cd4507baf7ebec44f84c</guid><category><![CDATA[AppKit]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Swift]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Sun, 30 Mar 2025 23:06:59 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1620121692029-d088224ddc74?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fG1hY29zfGVufDB8fHx8MTc0MzM3NTcwN3ww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1620121692029-d088224ddc74?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDN8fG1hY29zfGVufDB8fHx8MTc0MzM3NTcwN3ww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="The Curious Case of NSPanel&apos;s Nonactivating Style Mask Flag"><p>While working on a behavior that required dynamically switching between the nonactivating panel behavior and one that is more similar to the regular window behavior, I discovered a subtle but significant issue in AppKit: changing the <code>NSWindowStyleMaskNonactivatingPanel</code> flag after an <code>NSPanel</code> has been initialized doesn&apos;t fully update the window&apos;s activation behavior the way the API contract seems to suggest.</p><p>This creates a perplexing situation where changing a window&apos;s style mask &#x2013; something the API suggests should be fully supported &#x2013; results in inconsistent window behavior that can be difficult to diagnose, and is certainly hard to reason about without fully understanding the window behavior on macOS, including WindowServer window tags.</p><h2 id="understanding-nonactivating-panels">Understanding Nonactivating Panels</h2><p>The nonactivating panel behavior, which is canonically only valid for subclasses of <code>NSPanel</code>, not <code>NSWindow</code>, allows the window that declares itself as a nonactivating panel to be sent and handle key events without activating the owning application. In other words, you will be able to type into a nonactivating panel and do everything you&#x2019;d be able to do with a key window in an active application, but the application itself will not become &#x201C;active.&#x201D; Among other things, this will cause it to not acquire the menu bar.</p><p>The nonactivating panel behavior is useful for utility panels, inspector windows, and other elements that shouldn&apos;t disrupt the user&apos;s current context. This can be achieved by either overriding <code>_isNonactivatingPanel</code> instance method and returning <code>YES</code>, or setting the <code>NSWindowStyleMaskNonactivatingPanel</code> style mask flag.</p><h2 id="a-deeper-dive">A Deeper Dive</h2><p>Before a key event on macOS is routed to the client process and ultimately lands in <code>NSApplication</code>, it is dequeued from the hardware and is routed by WindowServer. The server ultimately maintains and owns key aspects of the truth about the window positioning, order, which process currently shows the menu bar, and the active application state. Each window, in addition to things like geometry, has a set of tags, which determine its behavior.</p><p>When you click on a window, what happens in most cases &#x2013; unless you perform a nonactivating click by holding Command, for example &#x2013; is that the application will be made active. That means that it will start receiving key events by default. However, one of the aforementioned window tags, <code>kCGSPreventsActivationTagBit</code>, dictates that performing a canonically activating action on a window that has this tag will <strong>not</strong> activate the application that owns it.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/03/Screenshot-2025-03-30-at-3.14.41-PM.png" class="kg-image" alt="The Curious Case of NSPanel&apos;s Nonactivating Style Mask Flag" loading="lazy" width="1332" height="1030" srcset="https://philz.blog/content/images/size/w600/2025/03/Screenshot-2025-03-30-at-3.14.41-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/03/Screenshot-2025-03-30-at-3.14.41-PM.png 1000w, https://philz.blog/content/images/2025/03/Screenshot-2025-03-30-at-3.14.41-PM.png 1332w" sizes="(min-width: 720px) 720px"></figure><p>How does the application get the key events in this state then? When a window is nonactivating,</p><ol><li>The window will be drawn as key by the application (the process managed by AppKit), but</li><li>The application will not be considered active and will not own the menu bar. In other words, the previously active application will <em>still</em> be active.</li></ol><p>How is it not the case that the application can receive the key events then? While a nonactivating panel is key, the application will &#x201C;steal key focus&#x201D; from the <em>true</em> active application, through a process referred to as key focus theft, managed by the CoreProcesses subsystem of the WindowServer (abbreviated as CPS). The client process will make a call to <code>-[NSApplication _stealKeyFocusWithOptions:]</code>, which will ultimately funnel down to <code>CPSStealKeyFocusReturningID</code> and IPC over to the server-side code that will alter the focus stack.</p><p>Likewise, when it&#x2019;s time to restore the normal behavior, <code>-[NSApplication _releaseKeyFocus]</code> will be called and will funnel down to <code>CPSReleaseKeyFocusWithID</code>. Note the use of ID in both routines, which helps maintain the correctness of the <em>focus stack</em>.</p><h2 id="reproducing-the-bug">Reproducing the Bug</h2><p>I created a minimal reproduction case to demonstrate the issue. The full code is available at <a href="https://github.com/philptr/NonactivatingPanelBug">https://github.com/philptr/NonactivatingPanelBug</a>.</p><p>The sample app has:</p><ul><li>A panel that starts as nonactivating</li><li>A button to toggle the nonactivating state</li><li>A text field for testing keyboard input</li></ul><p>When you run this application and:</p><ol><li>Launch the app (which creates a nonactivating panel),</li><li>Switch to another application,</li><li>Click the &quot;Toggle Nonactivating Panel Bit&quot; button to make it a regular window,</li><li>Click on the panel;</li></ol><p>You&apos;ll observe the following unexpected behavior:</p><ul><li>The panel&apos;s appearance changes correctly (gaining a title bar and close button);</li><li>The panel shows visual indication that it has keyboard focus, i.e. will draw as key;</li><li><strong>BUT</strong> typing into the text field won&#x2019;t work!</li></ul><h2 id="the-root-cause">The Root Cause</h2><p>After investigation, I determined that the issue stems from how <code>NSPanel</code> manages window server tags:</p><ol><li>During initialization, <code>NSPanel</code> calls an internal method <code>-_setPreventsActivation:</code> to set the <code>kCGSPreventsActivationTagBit</code> window tag when the nonactivating style mask bit is present.</li><li>When you change the style mask later during the lifetime of the panel instance via <code>-setStyleMask:</code>, it doesn&apos;t call <code>-_setPreventsActivation:</code> again to update this window tag.</li></ol><p>Based on the way things work under the hood that we&#x2019;ve established above, we can gain pretty good insight into what is actually going on. The bug occurs due to a mismatch:</p><ul><li>The framework thinks the panel is a regular window (because the style mask no longer includes <code>NSWindowStyleMaskNonactivatingPanel</code>).</li><li>The WindowServer still treats it as nonactivating (because the window tag remains set).</li></ul><p>Since the panel doesn&#x2019;t have the <code>NSWindowStyleMaskNonactivatingPanel</code> bit in its style mask anymore, its <code>-_isNonactivatingPanel</code> returns <code>NO</code>, and the application will not steal key focus for it when it is supposed to become key. However, mousing down on the window will <strong>also</strong> not make the application frontmost, because we&#x2019;ve never removed the <code>kCGSAvoidsActivationTagBit</code>. As a result, the window will appear key, since that is the behavior managed by AppKit, but will not be receiving key events.</p><h2 id="reverse-engineering-the-behavior">Reverse Engineering the Behavior</h2><p>Discovering the root cause required investigating how AppKit interacts with the WindowServer. Here&apos;s the outline of a process one could use to reverse engineer this or similar behavior:</p><h3 id="symbolic-breakpoints-are-your-friends">Symbolic Breakpoints are Your Friends</h3><p>You could use LLDB to trace method calls when setting the style mask. With the application running:</p><pre><code>(lldb) breakpoint set -n &quot;-[NSWindow setStyleMask:]&quot;
(lldb) breakpoint set -n &quot;-[NSWindow _setPreventsActivation:]&quot;
</code></pre><p>This revealed that <code>-_setPreventsActivation:</code> is called during panel initialization but not when the style mask is changed. In our sample application, this is tested by clicking the button that toggles the nonactivating panel style mask bit.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/03/Screenshot-2025-03-30-at-3.41.00-PM.png" class="kg-image" alt="The Curious Case of NSPanel&apos;s Nonactivating Style Mask Flag" loading="lazy" width="1092" height="402" srcset="https://philz.blog/content/images/size/w600/2025/03/Screenshot-2025-03-30-at-3.41.00-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/03/Screenshot-2025-03-30-at-3.41.00-PM.png 1000w, https://philz.blog/content/images/2025/03/Screenshot-2025-03-30-at-3.41.00-PM.png 1092w" sizes="(min-width: 720px) 720px"></figure><h3 id="inspecting-window-server-tags">Inspecting Window Server Tags</h3><p>To verify my hypothesis about window server tags, you can forward declare the (private) <code>CGSGetWindowTags</code> routine:</p><pre><code class="language-c">CG_EXTERN CGError CGSGetWindowTags(CGSConnectionID cid, CGWindowID wid, const CGSWindowTagBit tags[2], size_t maxTagSize);
</code></pre><p>By calling this function before and after changing the style mask, one may be able to confirm that the <code>kCGSPreventsActivationTagBit</code> tag (value <code>1 &lt;&lt; 16</code>) indeed remains set even after removing the nonactivating style mask.</p><h3 id="looking-at-disassembly">Looking at Disassembly</h3><p>Finally, to confirm the hypothesis that the bug indeed stems from AppKit simply not calling the relevant method that sets the window tag on the path of changing the style mask during the lifetime of the window, you can use a tool like Hopper to find its callers, which will reveal that the only time the method is called during the lifetime of an <code>NSPanel</code> instance is indeed during the <code>_panelInitCommonCode</code> C helper routine called during the initialization.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/03/Screenshot-2025-03-30-at-3.46.24-PM.png" class="kg-image" alt="The Curious Case of NSPanel&apos;s Nonactivating Style Mask Flag" loading="lazy" width="1768" height="1020" srcset="https://philz.blog/content/images/size/w600/2025/03/Screenshot-2025-03-30-at-3.46.24-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/03/Screenshot-2025-03-30-at-3.46.24-PM.png 1000w, https://philz.blog/content/images/size/w1600/2025/03/Screenshot-2025-03-30-at-3.46.24-PM.png 1600w, https://philz.blog/content/images/2025/03/Screenshot-2025-03-30-at-3.46.24-PM.png 1768w" sizes="(min-width: 720px) 720px"></figure><h2 id="workaround">Workaround</h2><p>To work around the issue, we can implement a workaround by manually setting the prevents-activation state after changing the style mask. Please note that this requires calling into <code>NSWindow</code> SPI. The least invasive version of the workaround involves something like the following:</p><pre><code class="language-swift">extension NSWindow {
    func updateActivationBehavior() {
        // Access private method via performSelector to update activation state.
        perform(Selector((&quot;_setPreventsActivation:&quot;)), 
                with: NSNumber(value: styleMask.contains(.nonactivatingPanel)))
    }
}
</code></pre><p>Then you may call this method after changing the style mask:</p><pre><code class="language-swift">panel.styleMask = newStyleMask
panel.updateActivationBehavior()
</code></pre><h2 id="some-implications">Some Implications</h2><p>The proper fix would be for <code>-setStyleMask:</code> to call <code>-_setPreventsActivation:</code> whenever the nonactivating panel bit changes, ensuring the window tags stay in sync with the style mask.</p><p>When working with windows and panels whose behavior needs to change at runtime, be aware that some style mask changes may require additional synchronization with window server state.</p><p>The bug has been reported as FB16484811. The full code of the sample application is available at <a href="https://github.com/philptr/NonactivatingPanelBug">https://github.com/philptr/NonactivatingPanelBug</a>.</p>]]></content:encoded></item><item><title><![CDATA[Adding Subtitles macOS Menu Items using SwiftUI]]></title><description><![CDATA[<p>At this point, AppKit developers with their finger on the pulse may be familiar with setting subtitles on <code>NSMenuItem</code> using the <code>subtitle</code> property, which has been available since macOS 14.4. In fact, I was the one who added it back when I worked at Apple.</p><p>Achieving the same in</p>]]></description><link>https://philz.blog/adding-subtitles-macos-menu-items-using-swiftui/</link><guid isPermaLink="false">67a02fd607baf7ebec44f836</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Menus]]></category><category><![CDATA[Swift]]></category><category><![CDATA[Apps]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Mon, 03 Feb 2025 02:55:44 GMT</pubDate><content:encoded><![CDATA[<p>At this point, AppKit developers with their finger on the pulse may be familiar with setting subtitles on <code>NSMenuItem</code> using the <code>subtitle</code> property, which has been available since macOS 14.4. In fact, I was the one who added it back when I worked at Apple.</p><p>Achieving the same in SwiftUI requires a different approach and is definitely less obvious and opaque &#x2013; but is also possible. Here I&apos;ll aim to briefly show how to add subtitles to menu items in SwiftUI, and how this technique could work across both <code>Menu</code> and <code>Picker</code> components.</p><h2 id="the-trick">The Trick</h2><p>The key insight is that we can use multiple <code>Text</code> views within a <code>Button</code>&apos;s label to force SwiftUI to interpret the first <code>Text</code> view as a title, and the second one as a subtitle. When bridging to <code>NSMenuItem</code>, the contents of the <code>Text</code> views will map to <code>-[NSMenuItem title]</code>, and <code>-[NSMenuItem subtitle]</code> respectively.</p><p>Let&apos;s look at a simple example first:</p><pre><code class="language-swift">struct ContentView: View {
    var body: some View {
        Menu(&quot;Options&quot;) {
            Button {
                print(&quot;Cappuccino selected&quot;)
            } label: {
                Text(&quot;Cappuccino&quot;)
                Text(&quot;Rich espresso with steamed milk&quot;)
            }
            
            Button {
                print(&quot;Flat White selected&quot;)
            } label: {
                Text(&quot;Flat White&quot;)
                Text(&quot;Espresso with velvety microfoam&quot;)
            }
        }
    }
}
</code></pre><p>This results in the following appearance.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/02/Screenshot-2025-02-02-at-6.42.51-PM.png" class="kg-image" alt loading="lazy" width="550" height="352"></figure><h2 id="using-a-picker">Using a Picker</h2><p>Note that since this is a <code>Menu</code>, it is not the appropriate choice if you&apos;d like to automatically bind its value to some data. You would use a <code>Picker</code> in that case instead.</p><p>The trick here remains the same, but now the <code>action</code> closure on your <code>Button</code> effectively becomes a no-op, used only as a hint for the SwiftUI bridging logic, since the action execution to update the <code>Binding</code>&apos;s wrapped value will be handled by the <code>Picker</code> directly.</p><p>Consider the following, more complete example for ordering coffee, combining both techniques.</p><pre><code class="language-swift">struct CoffeeOrder: Identifiable, Hashable {
    let id: UUID
    let name: String
    let description: String
    let price: Double
    
    static let sampleOrders = [
        CoffeeOrder(id: UUID(), 
                   name: &quot;Cappuccino&quot;, 
                   description: &quot;Rich espresso with steamed milk&quot;,
                   price: 4.50),
        CoffeeOrder(id: UUID(), 
                   name: &quot;Flat White&quot;, 
                   description: &quot;Espresso with velvety microfoam&quot;,
                   price: 4.75),
        CoffeeOrder(id: UUID(), 
                   name: &quot;Cold Brew&quot;, 
                   description: &quot;Smooth, cold-steeped coffee&quot;,
                   price: 3.95)
    ]
}

struct CoffeeOrderView: View {
    @State private var selectedOrder: UUID?
    let orders = CoffeeOrder.sampleOrders
    
    var body: some View {
        VStack {
            // Subtitles in Menu:
            Menu(&quot;Place Order&quot;) {
                ForEach(orders) { order in
                    Button {
                        print(&quot;Selected: \(order.name)&quot;)
                    } label: {
                        Text(order.name)
                        Text(&quot;\(order.description) - $\(String(format: &quot;%.2f&quot;, order.price))&quot;)
                    }
                }
            }
            
            // Subtitles in Picker:
            Picker(&quot;Select Coffee&quot;, selection: $selectedOrder) {
                Text(&quot;Choose a coffee&quot;)
                    .tag(Optional&lt;UUID&gt;(nil))
                
                ForEach(orders) { order in
                    Button { } label: {
                        Text(order.name)
                        Text(&quot;\(order.description) - $\(String(format: &quot;%.2f&quot;, order.price))&quot;)
                    }
                    .tag(Optional(order.id))
                }
            }
        }
        .padding()
    }
}
</code></pre><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/02/Screenshot-2025-02-02-at-6.46.53-PM.png" class="kg-image" alt loading="lazy" width="782" height="442" srcset="https://philz.blog/content/images/size/w600/2025/02/Screenshot-2025-02-02-at-6.46.53-PM.png 600w, https://philz.blog/content/images/2025/02/Screenshot-2025-02-02-at-6.46.53-PM.png 782w" sizes="(min-width: 720px) 720px"></figure><h2 id="adding-images">Adding Images</h2><p>As you may have guessed by now, we can use the same technique to provide an image, a title, and a subtitle at the same time, by simply adding an <code>Image</code> as part of the label.</p><pre><code class="language-swift">Button {
    print(&quot;Selected: \(order.name)&quot;)
} label: {
    Image(systemName: &quot;cup.and.saucer&quot;)
    Text(order.name)
    Text(&quot;\(order.description) - $\(String(format: &quot;%.2f&quot;, order.price))&quot;)
}
</code></pre><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/02/Screenshot-2025-02-02-at-6.53.22-PM.png" class="kg-image" alt loading="lazy" width="636" height="390" srcset="https://philz.blog/content/images/size/w600/2025/02/Screenshot-2025-02-02-at-6.53.22-PM.png 600w, https://philz.blog/content/images/2025/02/Screenshot-2025-02-02-at-6.53.22-PM.png 636w"></figure><h2 id="binary-compatibility">Binary Compatibility</h2><p>Due to some implementation details of <code>NSMenuItem</code>, you may see different results depending on whether your title maps to a plain or an attributed string.</p><p>If your title is effectively an attributed string, it will be mapped to <code>-[NSMenuItem attributedTitle]</code> instead of <code>-[NSMenuItem title]</code>. Before macOS 15, if your title <code>Text</code> results in a mapping to <code>NSAttributedString</code>, the <code>subtitle</code> will be ignored. While this is not the case on macOS 15 and later, it is generally useful to keep the distinction in mind, as menus generally tend to handle attributed titles differently, and since SwiftUI <code>Text</code> doesn&apos;t make that clear of a distinction.</p>]]></content:encoded></item><item><title><![CDATA[Building a Universal Mini vMac Application]]></title><description><![CDATA[This project demonstrates a collection of techniques for consolidating various Mini vMac variations into one application, enabling better code reuse and simplifying maintenance.]]></description><link>https://philz.blog/building-universal-mini-vmac-macos-application/</link><guid isPermaLink="false">679ec57707baf7ebec44f809</guid><category><![CDATA[AppKit]]></category><category><![CDATA[Apps]]></category><category><![CDATA[Cocoa]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Vintage Computing]]></category><category><![CDATA[Xcode]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Sun, 02 Feb 2025 01:14:17 GMT</pubDate><media:content url="https://philz.blog/content/images/2025/02/IMG_1019.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://philz.blog/content/images/2025/02/IMG_1019.jpeg" alt="Building a Universal Mini vMac Application"><p>Early Macintosh computers have played a huge role not just in personal computing in general, but in core areas like UI design and systems and framework engineering. Apart from these machines just being really, <em>really</em> cool, I have a firm belief that interacting with old technology can serve as inspiration for future innovation in the areas of HCI, frameworks, and systems engineering.</p><p>The most accessible way to interact with older Macintosh models is using emulators. Mini vMac, Basilisk II, and SheepShaver all serve a similar purpose: emulating Macintosh computers from different eras.</p><p>However, I&apos;ve always found the process of setting up one of these emulators to be quite cumbersome and not user-friendly. It takes a lot of research and familiarity with software development to get started. In other words, you kind of have to be a bit of a nerd to set one up. If we want more people to discover the joy of vintage computing &#x2013; and I really do &#x2013; the process has to become a whole lot more refined and user-friendly. That&apos;s why, on and off for a few years, I&apos;ve been working on a modern macOS emulation environment to make these machines more accessible to an average user.</p><p>In this (admittedly more technical) post, I&apos;ll walk through one specific aspect of that effort: the techniques for combining multiple Mini vMac emulator variations into a single universal macOS binary with a well-defined public interface.</p><h2 id="background">Background</h2><p>Mini vMac&apos;s architecture relies heavily on C preprocessor directives for conditional compilation. Each Macintosh model variation requires its own set of configuration headers (typically generated by the setup tool) that define hardware capabilities, screen dimensions, ROM specifications, and other model-specific features. This design choice makes sense from an emulation perspective &#x2013; it allows the maintainer to optimize specifically for each model&apos;s characteristics and ensures only the necessary code paths are included.</p><p>However, this traditionally means maintaining separate application builds for each model. And, while building a more universal environment for emulating many kinds of Macintosh computers, I&apos;d effectively have to maintain a binary at least for each of the supported models. Let&apos;s explore how we can maintain this compilation model while packaging everything into a single, more maintainable application.</p><p>The general approach will be the following:</p><ul><li>Automate the generation of the required header files for each of the 7 computers we want to support: 128K, 512Ke, Plus, SE, Classic, SE FDHD, and II.</li><li>Implement the principal class that conforms to the common protocol exposed to both the universal application and each of the frameworks.</li><li>Package each model into a framework bundle, with reused source code.</li><li>Configure the application to dynamically load the correct framework bundle and interface with its principal class to emulate the specific machine known at runtime.</li></ul><h2 id="generating-configuration-for-each-model">Generating Configuration for Each Model</h2><p>The first challenge is generating appropriate configuration headers for each model. The general setup process using <code>minivmac</code> for a specific model involves building the setup tool:</p><pre><code class="language-bash">gcc -o setup_t ./setup/tool.c
</code></pre><p>After that, we can use the setup tool to generate the configuration headers for the model. On macOS, that also generates the Xcode project that wraps it into an application.</p><pre><code class="language-bash">./setup_t -e xcd -t mcar -sound 1 -drives 20 -sony-sum 1 -sony-tag 1 -sony-dc42 1 -speed z &gt; generate_xcodeproj.sh
bash ./generate_xcodeproj.sh
</code></pre><p>For the purposes of generating a <em>single</em> application that encapsulates multiple conditionally compiled models, we don&apos;t really care about the generated Xcode project, and only want the headers.</p><p>Ideally, this process can be automated for each of the models we want to support. So, I have written the <code>setup.sh</code> script that defines the 6 Macintosh models and goes through the process of building the configuration for each one. It then takes only the generated headers and puts them into <code>Configuration</code> folder, removing the other artifacts. What we&apos;re left with is 6 directories for each of the Macintosh models, and the unmodified <code>minivmac</code> source.</p><p>The script will also include our custom additions for dynamic loading. We&apos;ll define a unique principal class name and the display name for each of the frameworks, like so:</p><pre><code class="language-c">// Custom configuration for Macintosh 128K:
#define MMPrincipalClassName Macintosh128KEmulatorVariation
#define MMDisplayName Macintosh 128K
</code></pre><h2 id="framework-architecture">Framework Architecture</h2><p>Each model variation is compiled as a separate framework. The key is that all frameworks:</p><ol><li>Share the same Mini vMac source code;</li><li>Use model-specific configuration headers;</li><li>Conform to a common protocol.</li></ol><p>Here&apos;s our base protocol:</p><pre><code class="language-objc">@protocol EmulatorVariation &lt;NSObject&gt;

- (instancetype)initWithROMAtPath:(NSString *)path;
- (void)start;
- (BOOL)insertDiskAtPath:(NSString *)path;

@end
</code></pre><p>We will then define a common implementation file that will be reused across each of the frameworks.</p><h2 id="setting-up-the-m-file">Setting up the .m File</h2><p>Now let&apos;s build the single implementation that we&apos;ll reuse across our targets. This is the single case in the entire architecture where we won&apos;t use the <code>minivmac</code> source verbatim, instead customizing it and adding a wrapper class.</p><ol><li>Locate the <code>OSGLUCCO.m</code> implementation file and copy its contents into our <code>MacintoshVariation.m</code> file (the name is arbitrary; feel free to come up with something better).</li><li>Define the principal class. Note how we&apos;ll use the <code>#define</code> key.</li></ol><pre><code class="language-objc">#import &quot;EmulatorVariation.h&quot;

NSString *_ROMFilePath;

NS_ASSUME_NONNULL_BEGIN

@interface MMPrincipalClassName : NSObject &lt;EmulatorVariation&gt;

@end

NS_ASSUME_NONNULL_END
</code></pre><p>Note how we&apos;re using <code>MMPrincipalClassName</code> to define the principal class. This will be replaced with the value we defined earlier, resulting in a unique principal class name for each of the generated frameworks. This also allows us to share the same source code, including the .m file, across all of the implementations, without the need to write <em>any</em> target specific code.</p><ol><li>Implement the wrapper.</li></ol><pre><code class="language-objc">@implementation MMPrincipalClassName

- (instancetype)initWithROMAtPath:(NSString *)path {
    if (self = [super init]) {
        _ROMFilePath = path;
    }
    return self;
}

- (void)start {
    ZapOSGLUVars();
    
    if (InitOSGLU()) {
        ProgramMain();
    }
    UnInitOSGLU();
}

- (BOOL)insertDiskAtPath:(NSString *)path {
    return Sony_Insert1(path.stringByStandardizingPath, false);
}

@end
</code></pre><ol start="2"><li>Remove the application initialization code.</li></ol><p>Since we&apos;ll initialize the emulator from an existing <code>NSApplication</code>, we don&apos;t need to do that setup again, so simply remove it.</p><p>From <code>InitCocoaStuff</code>, we&apos;ll remove the following line:</p><pre><code class="language-objc">[MyNSApp run];
</code></pre><h2 id="xcode-build-settings-setup">Xcode Build Settings Setup</h2><p>Let&apos;s outline how to create an individual framework target.</p><p>We have already created the <code>EmulatorVariation</code> protocol definition. This will be exposed to both the top-level application and each of the framework targets.</p><p>Next, let&apos;s create the template <code>Info.plist</code> that will be preprocessed by each of the frameworks. I&apos;ve called it <code>Variation-Info.plist</code>. This is where we&apos;ll make use of our <code>MM</code>-prefixed <code>#define</code>s.</p><pre><code class="language-plist">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;
    &lt;key&gt;CFBundleDevelopmentRegion&lt;/key&gt;
    &lt;string&gt;en&lt;/string&gt;
    &lt;key&gt;CFBundleDisplayName&lt;/key&gt;
    &lt;string&gt;MMDisplayName&lt;/string&gt;
    &lt;key&gt;CFBundleExecutable&lt;/key&gt;
    &lt;string&gt;$(EXECUTABLE_NAME)&lt;/string&gt;
    &lt;key&gt;CFBundleGetInfoString&lt;/key&gt;
    &lt;string&gt;MMDisplayName&lt;/string&gt;
    &lt;key&gt;CFBundleIdentifier&lt;/key&gt;
    &lt;string&gt;$(PRODUCT_BUNDLE_IDENTIFIER)&lt;/string&gt;
    &lt;key&gt;CFBundleInfoDictionaryVersion&lt;/key&gt;
    &lt;string&gt;6.0&lt;/string&gt;
    &lt;key&gt;CFBundleName&lt;/key&gt;
    &lt;string&gt;$(PRODUCT_NAME)&lt;/string&gt;
    &lt;key&gt;CFBundleShortVersionString&lt;/key&gt;
    &lt;string&gt;1.0&lt;/string&gt;
    &lt;key&gt;CFBundleSignature&lt;/key&gt;
    &lt;string&gt;????&lt;/string&gt;
    &lt;key&gt;CFBundleVersion&lt;/key&gt;
    &lt;string&gt;$(CURRENT_PROJECT_VERSION)&lt;/string&gt;
    &lt;key&gt;NSPrincipalClass&lt;/key&gt;
    &lt;string&gt;MMPrincipalClassName&lt;/string&gt;
&lt;/dict&gt;
&lt;/plist&gt;
</code></pre><p>Now, let&apos;s actually create a framework target, making sure to name it in the same way we named the corresponding <code>Configuration</code> subdirectory, for example &quot;Macintosh 128K.&quot; We&apos;ll now have to do a few things in the new target&apos;s Build Settings.</p><ol><li>Set up <code>Info.plist</code> preprocessing to replace those template values. Use the newly created header and the target-specific header containing the corresponding <code>#define</code>s for preprocessing. Finally, enable preprocessing.</li></ol><pre><code class="language-plist">INFOPLIST_FILE = $(SRCROOT)/Mini vMac/Emulators/Variation-Info.plist
INFOPLIST_PREFIX_HEADER = $(SRCROOT)/Configuration/$(PRODUCT_NAME)/CNFUDALL.h
INFOPLIST_PREPROCESS = YES
</code></pre><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.35.26-PM.png" class="kg-image" alt="Building a Universal Mini vMac Application" loading="lazy" width="1098" height="206" srcset="https://philz.blog/content/images/size/w600/2025/02/Screenshot-2025-02-01-at-4.35.26-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/02/Screenshot-2025-02-01-at-4.35.26-PM.png 1000w, https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.35.26-PM.png 1098w" sizes="(min-width: 720px) 720px"></figure><ol start="2"><li>Disable ARC.</li></ol><pre><code class="language-plist">CLANG_ENABLE_OBJC_ARC = NO
</code></pre><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.36.08-PM.png" class="kg-image" alt="Building a Universal Mini vMac Application" loading="lazy" width="1088" height="134" srcset="https://philz.blog/content/images/size/w600/2025/02/Screenshot-2025-02-01-at-4.36.08-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/02/Screenshot-2025-02-01-at-4.36.08-PM.png 1000w, https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.36.08-PM.png 1088w" sizes="(min-width: 720px) 720px"></figure><ol start="3"><li>Limit header search paths to those required for the target.</li></ol><p>This is a subtle step: the target will compile without it, but with default search paths, you won&apos;t end up using the target-specific headers you want; instead, we&apos;ll always pick up whichever ones we find first &#x2013; in my case, ones in <code>Configuration/Macintosh 128K</code>. So, without this step, we&apos;ll accidentally end up with 7 emulators for Macintosh 128K.</p><p>The first step here is simple: disable header maps.</p><p>Then, we must customize User Header Search Paths.</p><ul><li><code>&quot;$(SRCROOT)/Configuration/$(PRODUCT_NAME)&quot;</code> will pick up the configuration specific headers we have generated.</li><li><code>&quot;&quot;$(SRCROOT)/Submodules/minivmac/src&quot;</code> will find the headers from the <code>minivmac</code> source.</li><li><code>&quot;$(SRCROOT)/Mini vMac&quot;</code> will pick up our protocol definition.</li></ul><p>In total, after step 3, we&apos;ll have:</p><pre><code class="language-plist">USER_HEADER_SEARCH_PATHS = &quot;$(SRCROOT)/Configuration/$(PRODUCT_NAME)&quot; &quot;$(SRCROOT)/Submodules/minivmac/src&quot; &quot;$(SRCROOT)/Mini vMac&quot;
USE_HEADERMAP = NO
</code></pre><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.40.40-PM.png" class="kg-image" alt="Building a Universal Mini vMac Application" loading="lazy" width="1102" height="170" srcset="https://philz.blog/content/images/size/w600/2025/02/Screenshot-2025-02-01-at-4.40.40-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/02/Screenshot-2025-02-01-at-4.40.40-PM.png 1000w, https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.40.40-PM.png 1102w" sizes="(min-width: 720px) 720px"></figure><h2 id="build-phases">Build Phases</h2><p>Let&apos;s setup the build phases for our framework.</p><ol><li>Make sure to include the 3 <code>CNFUD</code>-prefixed headers from the corresponding configuration subfolder.</li></ol><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.42.37-PM.png" class="kg-image" alt="Building a Universal Mini vMac Application" loading="lazy" width="1182" height="642" srcset="https://philz.blog/content/images/size/w600/2025/02/Screenshot-2025-02-01-at-4.42.37-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/02/Screenshot-2025-02-01-at-4.42.37-PM.png 1000w, https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.42.37-PM.png 1182w" sizes="(min-width: 720px) 720px"></figure><ol start="2"><li>In Compile Sources, add the required <code>minivmac</code> sources and our <code>.m</code> file that contains the principal class we had defined earlier.</li></ol><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.43.23-PM.png" class="kg-image" alt="Building a Universal Mini vMac Application" loading="lazy" width="1188" height="938" srcset="https://philz.blog/content/images/size/w600/2025/02/Screenshot-2025-02-01-at-4.43.23-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/02/Screenshot-2025-02-01-at-4.43.23-PM.png 1000w, https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.43.23-PM.png 1188w" sizes="(min-width: 720px) 720px"></figure><h3 id="generating-the-rest">Generating the Rest</h3><p>To generate the rest of the frameworks, you can simply duplicate the target. You&apos;ll need to customize the name, bundle identifier, and redo the Build Phases step, but all of the Build Settings will carry over automatically in the process.</p><h2 id="universal-application-design">Universal Application Design</h2><p>The application will need to include all of the individual frameworks, ideally without loading them. Ideally, we can go to the Build Phases tab and:</p><ol><li>Add a new Copy Files phase.</li><li>Change the Destination to Frameworks.</li><li>Add all of the frameworks.</li></ol><p>This will copy the frameworks into the Private Frameworks folder in our application&apos;s bundle.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.53.22-PM.png" class="kg-image" alt="Building a Universal Mini vMac Application" loading="lazy" width="1172" height="612" srcset="https://philz.blog/content/images/size/w600/2025/02/Screenshot-2025-02-01-at-4.53.22-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/02/Screenshot-2025-02-01-at-4.53.22-PM.png 1000w, https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.53.22-PM.png 1172w" sizes="(min-width: 720px) 720px"></figure><p>Next, we will dynamically load the appropriate framework based on launch arguments. We use Swift&apos;s <code>ArgumentParser</code> for clean argument handling. In a more sophisticated implementation, this may be determined automatically by verifying the checksum of the supplied ROM file, and / or allow for a GUI based configuration by the user at runtime.</p><pre><code class="language-swift">struct AppArguments: ParsableCommand {
    @Option(name: .customLong(&quot;emulator&quot;, withSingleDash: true))
    var emulatorName: String
    
    @Option(name: .customLong(&quot;rom&quot;, withSingleDash: true))
    var romPath: String
    
    @Option(name: .customLong(&quot;disks&quot;, withSingleDash: true))
    var disks: [String]
}
</code></pre><p>The application delegate handles framework loading and emulator initialization:</p><pre><code class="language-swift">@NSApplicationMain
final class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let arguments = AppArguments.parseOrExit()
        
        // Dynamic framework loading code...
    }
}
</code></pre><h2 id="framework-loading-and-initialization">Framework Loading and Initialization</h2><p>The framework loading process is particularly interesting:</p><ol><li>We locate the framework in the application bundle.</li><li>Load it dynamically.</li><li>Access the principal class (our emulator variation).</li><li>Initialize the emulator with the provided ROM.</li></ol><pre><code class="language-swift">guard let frameworksURL = Bundle.main.privateFrameworksURL,
      let urls = Bundle.urls(forResourcesWithExtension: nil, subdirectory: nil, in: frameworksURL) else {
    fatalError(&quot;Could not find the private frameworks URL for the main bundle.&quot;)
}

let bundles = urls
    .filter { $0.deletingPathExtension().lastPathComponent == arguments.emulatorName }
    .compactMap { Bundle(url: $0) }

guard let bundle = bundles.first else {
    fatalError(&quot;Could not load an emulator bundle for name &apos;\(arguments.emulatorName)&apos;.&quot;)
}

try! bundle.loadAndReturnError()
</code></pre><p>Of course, for any production code, we should handle the above errors more gracefully!</p><h2 id="building-and-running">Building and Running</h2><p>To test our implementation, I&apos;ll use Xcode&apos;s Scheme editor to pass the app arguments and emulate the Macintosh 128K with System 1.0 on board.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.56.15-PM.png" class="kg-image" alt="Building a Universal Mini vMac Application" loading="lazy" width="1454" height="538" srcset="https://philz.blog/content/images/size/w600/2025/02/Screenshot-2025-02-01-at-4.56.15-PM.png 600w, https://philz.blog/content/images/size/w1000/2025/02/Screenshot-2025-02-01-at-4.56.15-PM.png 1000w, https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-4.56.15-PM.png 1454w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-3.30.40-PM-1.png" class="kg-image" alt="Building a Universal Mini vMac Application" loading="lazy" width="1248" height="964" srcset="https://philz.blog/content/images/size/w600/2025/02/Screenshot-2025-02-01-at-3.30.40-PM-1.png 600w, https://philz.blog/content/images/size/w1000/2025/02/Screenshot-2025-02-01-at-3.30.40-PM-1.png 1000w, https://philz.blog/content/images/2025/02/Screenshot-2025-02-01-at-3.30.40-PM-1.png 1248w" sizes="(min-width: 720px) 720px"></figure><p>I&apos;ve called the project <code>minivmac-universal</code>, and made all code available on <a href="https://github.com/philptr/minivmac-universal">GitHub</a>. I intend to use this technique to continue building my universal user-friendly vintage Macintosh environment, and will hopefully share more progress and details soon.</p><p>In its current version, the environment maintains various emulators (like Mini vMac, Basilisk II, and SheepShaver) as embedded executables, which are managed using <code>NSTask</code>. Upon launch, each uses injected code to host its windows in the parent process using some SPI (private API) tricks that I will not share. This technique will allow me to run all of the emulators in-process instead, which will simplify event handling and layer hosting, and allow for better code reuse and faster iteration on user-facing features.</p><h2 id="further-reading">Further Reading</h2><p>If you&apos;re not very familiar with vintage Mac emulation, I highly recommend reading <a href="https://patchbay.tech/a-guide-to-legacy-mac-emulators/">A Guide to Legacy Mac Emulators</a>.</p><p>In addition, when working with Mini vMac, it can be useful to familiarize yourself with the <a href="https://www.gryphel.com/c/minivmac/control.html#control_mode">Control Mode</a> and <a href="https://www.gryphel.com/c/minivmac/develop.html#option_n">Options for Developers</a>.</p><p>Finally, the <a href="https://github.com/minivmac/minivmac">minivmac source</a> is always a fascinating read.</p>]]></content:encoded></item><item><title><![CDATA[Mysterious Runtime Crashes in Swift 6 Language Mode]]></title><description><![CDATA[<p>Swift 6 language mode promises compile-time stricter concurrency checking, aimed at preventing and eliminating data races, runtime memory corruption, as well as other common but hard to debug issues. While adopting the new language mode can be a bit of a challenge for larger or less modular projects, Swift 6</p>]]></description><link>https://philz.blog/mysterious-runtime-crashes-in-swift-6-language-mode/</link><guid isPermaLink="false">678c56a107baf7ebec44f7f7</guid><category><![CDATA[AppKit]]></category><category><![CDATA[Apps]]></category><category><![CDATA[Swift]]></category><category><![CDATA[SwiftUI]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Sun, 19 Jan 2025 01:36:05 GMT</pubDate><media:content url="https://philz.blog/content/images/2025/01/Artboard.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://philz.blog/content/images/2025/01/Artboard.jpg" alt="Mysterious Runtime Crashes in Swift 6 Language Mode"><p>Swift 6 language mode promises compile-time stricter concurrency checking, aimed at preventing and eliminating data races, runtime memory corruption, as well as other common but hard to debug issues. While adopting the new language mode can be a bit of a challenge for larger or less modular projects, Swift 6 is an obvious choice for new targets.</p><p>However, it is worth noting that Swift 6 language mode also includes new runtime checks and can make some of the existing checks more aggressive. Many of them become preemptive assertions rather than silent failures. Surprisingly, this can lead to hard to reproduce occasional runtime crashes in production when interacting with common first-party modules like SwiftUI and AppKit.</p><h2 id="swiftui-runtime-crashes">SwiftUI Runtime Crashes</h2><p>Incorrect threading that violates the concurrency declarations of these frameworks may therefore cause runtime crashes in your applications. One such example is the new SwiftUI <a href="https://developer.apple.com/documentation/swiftui/view/ongeometrychange(for:of:action:)"><code>onGeometryChange</code></a> modifier, which <a href="https://philz.blog/swiftui-geometry-modifiers-ongeometrychange/">I wrote about in some detail as part of an earlier post</a>.</p><p>Here is an excerpt of a stack trace of one of these crashes captured by an analytics platform:</p><pre><code>Thread 3 Crashed::  Dispatch queue: com.apple.SwiftUI.DisplayLink
0   libdispatch.dylib                      0x190b379c0 _dispatch_assert_queue_fail + 120
1   libdispatch.dylib                      0x190b37948 dispatch_assert_queue + 196
2   libswift_Concurrency.dylib             0x2712f2e08 swift_task_isCurrentExecutorImpl(swift::SerialExecutorRef) + 284
3   SomeApp                            0x102262edc 0x102260000 + 11996
4   SwiftUI                                0x1bffd8748 _GeometryActionModifier.value(geometry:) + 28
5   SwiftUI                                0x1bffdc268 partial apply for closure #1 in GeometryActionBinder.updateValue() + 52
6   SwiftUICore                            0x2303feae0 partial apply for closure #1 in _withObservation&lt;A&gt;(do:) + 48
7   SwiftUICore                            0x2303fd214 closure #1 in _withObservation&lt;A&gt;(do:)partial apply + 16
8   SwiftUICore                            0x23046d5ec withUnsafeMutablePointer&lt;A, B, C&gt;(to:_:) + 160
9   SwiftUICore                            0x2303fc664 StatefulRule.withObservation&lt;A&gt;(do:) + 872
10  SwiftUICore                            0x2303fc264 StatefulRule.withObservation&lt;A&gt;(do:) + 72
</code></pre><p>The <code>onGeometryChange</code> modifier contains a closure that is implicitly non-<a href="https://developer.apple.com/documentation/swift/sendable"><code>Sendable</code></a>. This means the closure is expected to execute on the main thread. However, in some cases SwiftUI ends up calling it on a non-main thread, or, in this case, <code>com.apple.SwiftUI.DisplayLink</code>.</p><p>The tricky part in diagnosing an issue like this is that it may not be reproducible on demand and may depend on the device, platform, and the larger context, such as the load of the system. It&apos;s entirely possible to go through all stages of testing and end up shipping this runtime crash to production.</p><p>You might also think you can fix this issue by immediately dispatching to the main thread, using either <code>DispatchQueue</code> or a <code>MainActor</code> annotated <code>Task</code>:</p><pre><code class="language-swift">contentView
    .onGeometryChange(for: CGSize.self, of: \.size) { value in
        DispatchQueue.main.async {
            // Do some work here.
        }
    } 
</code></pre><p>However, this will still cause the assertion to trigger, as the check happens on the closure executed by SwiftUI, not the one inside the <code>async</code> block you provided.</p><p>This situation happens when the imported module has not adopted strict concurrency yet and / or has not annotated its API with correct sendability expectations. In this case, the closure is likely expected to be <code>Sendable</code> but is not correctly marked as such, which leads the runtime to assume it is implicitly not <code>Sendable</code>.</p><p>The exact case is outlined in the official <a href="https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/incrementaladoption/#Unmarked-Sendable-Closures">Swift 6 Migration Guide</a>.</p><blockquote>The sendability of a closure affects how the compiler infers isolation for its body. A callback closure that actually does cross isolation boundaries but is missing a <code>Sendable</code> annotation violates a critical invariant of the concurrency system.</blockquote><p>The fix, until the imported module annotates the API it vends, is to manually annotated the closure you provide with <code>@Sendable</code>, like so:</p><pre><code class="language-swift">contentView
    .onGeometryChange(for: CGSize.self, of: \.size) { @Sendable value in // This is now sendable!
        // Do some work or dispatch to main queue.
    } 
</code></pre><p>Adding this annotation will have the effect of leading the compiler to assume the closure&apos;s body is <code>nonisolated</code>, which may lead to some new compile-time errors. So, depending on the use case, you might have to <em>also</em> dispatch to the main thread after all. But, this is much better than randomly crashing at runtime in production, isn&apos;t it?</p><h2 id="actor-isolation-and-appkit">Actor Isolation and AppKit</h2><p>But, SwiftUI users aren&apos;t the only victims of this nasty behavior. Even if you are using a battle tested framework like AppKit, you are not exempt. Let&apos;s look at a crash of the same nature, but this time without SwiftUI involvement:</p><pre><code>* thread #5, queue = &apos;com.apple.root.default-qos&apos;, stop reason = EXC_BREAKPOINT (code=1, subcode=0x1004f8a5c)
  * frame #0: 0x00000001004f8a5c libdispatch.dylib`_dispatch_assert_queue_fail + 120
    frame #1: 0x00000001004f89e4 libdispatch.dylib`dispatch_assert_queue + 196
    frame #2: 0x0000000275bd293c libswift_Concurrency.dylib`swift_task_isCurrentExecutorImpl(swift::SerialExecutorRef) + 280
    frame #3: 0x0000000275b78588 libswift_Concurrency.dylib`Swift._checkExpectedExecutor(_filenameStart: Builtin.RawPointer, _filenameLength: Builtin.Word, _filenameIsASCII: Builtin.Int1, _line: Builtin.Word, _executor: Builtin.Executor) -&gt; () + 60
    frame #4: 0x0000000108922788 MyApp.debug.dylib`@objc static FileDocument.autosavesInPlace.getter at &lt;compiler-generated&gt;:0
    frame #5: 0x00000001a2f7d780 AppKit`-[NSDocument(NSDocument_Versioning) _preserveContentsIfNecessaryAfterWriting:toURL:forSaveOperation:version:error:] + 92
    frame #6: 0x00000001a2fe6d1c AppKit`__85-[NSDocument(NSDocumentSaving) _saveToURL:ofType:forSaveOperation:completionHandler:]_block_invoke_2.398 + 116
</code></pre><p>In this case, as the <a href="https://forums.swift.org/t/crash-when-running-in-swift-6-language-mode/72431">Swift Forums</a> user reports, there is no closure involved. Instead, there is an <code>NSDocument</code> subclass annotated with <code>MainActor</code>. When a type is annotated <code>MainActor</code>, its members are expected to execute on the main actor, unless annotated as <code>nonisolated</code>, which voids the inherited isolation. AppKit, as a largely Objective-C framework, is not aware of these tricks. In many cases, it assumes your subclass is a normal Objective-C class, all assertions in which are either triggered by you, the client, or AppKit&apos;s internals. Therefore, it reserves the right to manage concurrency as it sees fit, invoking the members of your subclass from whatever thread necessary.</p><p>In this case, the issue may be much easier to debug, however, since the thread on which AppKit invokes the members of your subclass is much more likely to be deterministic than in the case of the AttributeGraph driven SwiftUI. Once you hit the issue, the answer would then be to annotate the problematic member with <code>nonisolated</code>, optionally filing a Radar with Apple requesting them to fix the Swift overlay, and moving on.</p><h2 id="conclusion">Conclusion</h2><p>When you hit a runtime crash in the Swift 6 language mode, you may get lucky and hit it during the debugging process, or receive it as an angry crash report from your users. In either case, you will be much better prepared to diagnose and fix the problem if you are familiar with the Swift concurrency system and, perhaps more importantly in this context, <a href="https://www.swift.org/migration/documentation/migrationguide">the migration guide</a>.</p>]]></content:encoded></item><item><title><![CDATA[Implementing a Modern SwiftUI Circular Progress Bar & Control]]></title><description><![CDATA[<p>When I first wrote <a href="https://github.com/philptr/PZCircularControl">PZCircularControl</a> in 2019, SwiftUI was still in its early days. The framework has matured significantly since then, introducing new features that enable interactions that were previously only achievable by dropping down to the Kits. Recently, I revisited the codebase and decided that a complete refactor of</p>]]></description><link>https://philz.blog/implementing-modern-swiftui-circular-progress-bar-control/</link><guid isPermaLink="false">676c93c607baf7ebec44f7cc</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[UI]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Wed, 25 Dec 2024 23:31:23 GMT</pubDate><media:content url="https://philz.blog/content/images/2024/12/CircularControl.png" medium="image"/><content:encoded><![CDATA[<img src="https://philz.blog/content/images/2024/12/CircularControl.png" alt="Implementing a Modern SwiftUI Circular Progress Bar &amp; Control"><p>When I first wrote <a href="https://github.com/philptr/PZCircularControl">PZCircularControl</a> in 2019, SwiftUI was still in its early days. The framework has matured significantly since then, introducing new features that enable interactions that were previously only achievable by dropping down to the Kits. Recently, I revisited the codebase and decided that a complete refactor of both the API surface and the implementation was overdue.</p><h2 id="original-api">Original API</h2><p>The original implementation had several issues that made it less than suitable for modern SwiftUI.</p><p>For example, the original API contract required three generic type parameters for styling (<code>InnerBackgroundType</code>, <code>OuterBackgroundType</code>, <code>TintType</code>), which limited composition &#x2013; the cornerstone of all SwiftUI API design. We also used a <code>PZCircularControlParams</code> class to configure the control. While the configuration object pattern is common in AppKit and UIKit, it doesn&apos;t take advantage of SwiftUI&apos;s declarative nature.</p><p>Despite the name, the control was also initially designed to serve display purposes only, and was not directly interactive or editable by the user, unless the developer chose to extend it. With the refactor, there was an opportunity to change this and offer a built-in capability for the user to interactively edit the value.</p><h2 id="redesigning-the-api">Redesigning the API</h2><p>The new API prioritizes ergonomics and SwiftUI idioms while maintaining and oftentimes improving flexibility.</p><p>You can declare a simple read-only version of the control in a single trivial line of code.</p><pre><code class="language-swift">// A simple example of the display-only version of the control.
CircularControl(progress: 0.4)</code></pre><figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://philz.blog/content/media/2024/12/Screen-Recording-2024-12-24-at-1.17.12-PM_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://philz.blog/content/media/2024/12/Screen-Recording-2024-12-24-at-1.17.12-PM.webm" poster="https://img.spacergif.org/v1/1390x610/0a/spacer.png" width="1390" height="610" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://philz.blog/content/media/2024/12/Screen-Recording-2024-12-24-at-1.17.12-PM_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container kg-video-hide">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:02</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            
        </figure><p>But you can now also declare the control to be editable, and allow the users to drag the knob on the control to change its value live. This works on iOS, macOS &#x2013; and now also visionOS.</p><pre><code class="language-swift">// A more advanced example of the editable circular control. 
CircularControl(
    progress: $value,
    strokeWidth: 15,
    style: .init(
        track: Color.secondary.opacity(0.2),
        progress: LinearGradient(colors: [.blue, .purple])
    )
) {
    CustomLabel()
}
</code></pre><figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://philz.blog/content/media/2024/12/Screen-Recording-2024-12-24-at-1.16.36-PM_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://philz.blog/content/media/2024/12/Screen-Recording-2024-12-24-at-1.16.36-PM.webm" poster="https://img.spacergif.org/v1/1390x610/0a/spacer.png" width="1390" height="610" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://philz.blog/content/media/2024/12/Screen-Recording-2024-12-24-at-1.16.36-PM_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container kg-video-hide">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:09</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            
        </figure><p><code>CircularControl</code> is now generic over the shape styles its contents, as well as the label, which lets us shed the wrapping types and rely more heavily on Swift&apos;s type system and type inference.</p><pre><code class="language-swift">public struct CircularControlStyle&lt;Track: ShapeStyle, Progress: ShapeStyle, Knob: ShapeStyle&gt; {
    let track: Track
    let progress: Progress
    let knob: Knob
}
</code></pre><p>The label can be customized just like the content of any other view via <code>ViewBuilder</code>. Editing capabilities are built-in, along with the native support for <code>Binding</code>-based configuration.</p><h2 id="interactive-control">Interactive Control</h2><p>The most interesting technical challenge was likely implementing the interactive control, the behavior of dragging along the circular path, and handling the various edge cases of how the user might interact with the knob across the various supported platforms.</p><h3 id="interaction-model">Interaction Model</h3><p>The interactive version of the new control has a knob that travels between the start and the end points. The user can drag it along the circular track. The developer may also customize whether the knob will wrap around the track when dragged beyond either the start or the end point.</p><p>The circular control&apos;s interactive behavior relies on two key mathematical concepts:</p><h4 id="converting-touches-or-clicks-to-angles">Converting touches or clicks to angles</h4><p>When the user touches the control, we need to convert their touch point into an angle. This is done by:</p><ul><li>Finding the relative position from the center point using <code>atan2</code></li><li>Converting the angle into a 0-1 progress value, where 1 corresponds to <code>2&#x3C0;</code>, the maximum value.</li><li>Adjusting for SwiftUI&apos;s coordinate system where 0&#xB0; is at the top of the control and increases clockwise.</li></ul><p>Converting back from progress to knob position is the reverse process:</p><pre><code class="language-swift">let angle = Double.pi * 2 * progress - Double.pi / 2
let position = CGPoint(
    x: cos(angle) * radius,
    y: sin(angle) * radius
)
</code></pre><h4 id="handling-the-wraparound-behavior">Handling the wraparound behavior</h4><p>Since a circle wraps around at <code>2&#x3C0;</code>, we need special handling when users drag across the start / end point. We detect this by comparing the change in angle &#x2013; if it&apos;s too large in magnitude, we know the user has crossed the boundary. For example, when the user drags the control&apos;s knob in the increasing direction and crosses the end point, the computed angle delta will be negative and close to 1 in magnitude, despite the positive direction of travel.</p><h2 id="modern-swiftui">Modern SwiftUI</h2><p>The new <code>@Entry</code> macro, introduced at WWDC 2024, has greatly reduced the boilerplate of declaring new <code>EnvironmentValues</code>.</p><pre><code class="language-swift">extension EnvironmentValues {
    @Entry public var circularControlAllowsWrapping: Bool = false
    @Entry public var circularControlKnobScale: CGFloat = 1.4
}
</code></pre><p>The complete implementation is available on <a href="https://github.com/philptr/PZCircularControl">GitHub</a>.</p>]]></content:encoded></item><item><title><![CDATA[NSDrawer, Child Windows, and Modern macOS Applications]]></title><description><![CDATA[<p><code>NSDrawer</code> is undoubtedly one of the more creative and iconic use cases for so-called &quot;child windows&quot; on macOS. Despite that, drawers have been deprecated in macOS 10.13 and seemingly forgotten since then. You can hardly find any use of drawers in any macOS applications these days. Visiting</p>]]></description><link>https://philz.blog/nsdrawer-child-windows-and-modern-macos-applications/</link><guid isPermaLink="false">676b422607baf7ebec44f79e</guid><category><![CDATA[AppKit]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Cocoa]]></category><category><![CDATA[Vintage Computing]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Tue, 24 Dec 2024 23:30:27 GMT</pubDate><media:content url="https://philz.blog/content/images/2024/12/Screenshot-2024-12-24-at-3.23.02-PM.png" medium="image"/><content:encoded><![CDATA[<img src="https://philz.blog/content/images/2024/12/Screenshot-2024-12-24-at-3.23.02-PM.png" alt="NSDrawer, Child Windows, and Modern macOS Applications"><p><code>NSDrawer</code> is undoubtedly one of the more creative and iconic use cases for so-called &quot;child windows&quot; on macOS. Despite that, drawers have been deprecated in macOS 10.13 and seemingly forgotten since then. You can hardly find any use of drawers in any macOS applications these days. Visiting the <code>NSDrawer</code> page on the modern <a href="https://developer.apple.com/documentation/appkit/nsdrawer">Developer Documentation</a> website shows a big and scary deprecation warning:</p><blockquote>Drawers are deprecated and should not be used in modern macOS apps.</blockquote><p>Funny enough, the class is implicitly marked <code>@MainActor</code>.</p><p>Now, I&apos;m by no means advocating going against the modern HIG and encouraging widespread adoption of drawers in the year 2025, but I&apos;m a firm believer that the history of our tools, frameworks, and software is well worth studying and drawing inspiration from.</p><h2 id="what-are-drawers">What are Drawers?</h2><p><code>NSDrawer</code> is conceptually an auxiliary window that is attached to a visible window and moves with it. Drawers can be resized but can never get larger than their parent. A window may have multiple drawers associated with it and open at the same time.</p><p>Drawers can be open, closed, toggled, or in the process of opening or closing. When closed, they&#x2019;re invisible. Drawers open on a specific edge of their parent, or a preferred edge if not specified. By default, drawers have a delightful animation for opening and closing, in which they appear from underneath and collapse into their parent window.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://philz.blog/content/images/2024/12/drawer_sizing_left.gif" class="kg-image" alt="NSDrawer, Child Windows, and Modern macOS Applications" loading="lazy" width="541" height="386"><figcaption><span style="white-space: pre-wrap;">A diagram from Documentation Archive showing the anatomy of an NSDrawer positioned to the left of the parent window</span></figcaption></figure><p>You can still find examples of drawers in some older OS X applications. <a href="https://latenightsw.com/freeware/ui-browser/" rel="noreferrer">UI Browser</a> is the first example that comes to mind. It uses a drawer as an inspector for viewing individual attributes of a selected Accessibility element.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://philz.blog/content/images/2024/12/Screenshot-2024-01-05-at-2.51.02-PM.png" class="kg-image" alt="NSDrawer, Child Windows, and Modern macOS Applications" loading="lazy" width="1848" height="1826" srcset="https://philz.blog/content/images/size/w600/2024/12/Screenshot-2024-01-05-at-2.51.02-PM.png 600w, https://philz.blog/content/images/size/w1000/2024/12/Screenshot-2024-01-05-at-2.51.02-PM.png 1000w, https://philz.blog/content/images/size/w1600/2024/12/Screenshot-2024-01-05-at-2.51.02-PM.png 1600w, https://philz.blog/content/images/2024/12/Screenshot-2024-01-05-at-2.51.02-PM.png 1848w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">NSDrawer used as the Accessibility attribute inspector in UI Browser 3</span></figcaption></figure><p>To me, this use of drawers doesn&apos;t really feel out of place &#x2013; even on macOS Sequoia. In a modern application, you would&apos;ve probably used an inspector panel, which would imply a more compact amount of horizontal space, or a vertical Split View. The drawer here does feel somewhat unusual, simply because it&apos;s not something you see across the system anymore, but it&apos;s honestly quite welcome and makes me feel in a certain Mac nerd type of way when using UI Browser.</p><h2 id="drawers-on-macos-sequoia">Drawers on macOS Sequoia</h2><p>As a developer in the Golden Age of drawers, you would use an AppKit class called <code>NSDrawer</code> to create, present, and manipulate drawers. The API surface was fairly straightforward and very similar to that of an <code>NSWindow</code>, albeit simplified. Method names like <code>open</code>, <code>toggle</code>, and <code>close</code> are self explanatory and could get you started quickly.</p><p>If you are skeptical that <code>NSDrawer</code> is indeed simply a window under the hood, you may convince yourself by using any tool available to you to inspect the application&apos;s window list. You will find that there is a one-to-one mapping between the conceptual representation of the <code>NSDrawer</code> and its window, which has the <code>NSDrawerWindow</code> type. The latter class is, of course, a framework implementation detail.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2024/12/Screenshot-2024-12-24-at-2.58.49-PM.png" class="kg-image" alt="NSDrawer, Child Windows, and Modern macOS Applications" loading="lazy" width="690" height="486" srcset="https://philz.blog/content/images/size/w600/2024/12/Screenshot-2024-12-24-at-2.58.49-PM.png 600w, https://philz.blog/content/images/2024/12/Screenshot-2024-12-24-at-2.58.49-PM.png 690w"></figure><pre><code class="language-objective-c">@interface NSDrawerWindow : NSWindow {
    NSDrawer * _drawer;
    NSWindow * _drawerParentWindow;
}</code></pre><p>In the best traditions of AppKit&apos;s bincompat, the entirety of the API surface still works. You can still use drawers and compile code that uses them from all those years ago. Now, why wouldn&apos;t I want to play with them?</p><p>To have some fun with drawers, I went digging in the depths of the best resource known to an old school Apple developer, the <a href="https://developer.apple.com/library/archive/navigation/">Documentation Archive</a>. I found a sample project aptly named <a href="https://developer.apple.com/library/archive/samplecode/DrawerMadness/Introduction/Intro.html#//apple_ref/doc/uid/DTS40008832">DrawerMadness</a>, originally published in May 2009 (more than 15 years ago!) and last updated in June 2012. Excited for some drawer madness, I proceeded to download it, open it in my Xcode 16.2 on macOS 15.2 and press Command-R. It built and ran on the first try, reminding me of one of the many reasons I love Objective-C.</p><blockquote>Remember <code>NSDrawer</code>s? This is them now. Feel old yet?</blockquote><p>The sample app demonstrates the ability to open, close, and toggle multiple drawers along different edges of the window. Dragging the window while drawers are visible clearly still utilizes server-side dragging, and leads to a smooth dragging experience. Like any <a href="https://developer.apple.com/documentation/appkit/nswindow/childwindows">child windows</a>, the drawers become part of the parent window&apos;s movement group. The window&apos;s behavior doesn&apos;t regress in any way due to the use of drawers.</p><figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://philz.blog/content/media/2024/12/Screen-Recording-2024-12-24-at-3.27.30-PM_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://philz.blog/content/media/2024/12/Screen-Recording-2024-12-24-at-3.27.30-PM.webm" poster="https://img.spacergif.org/v1/2180x1580/0a/spacer.png" width="2180" height="1580" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://philz.blog/content/media/2024/12/Screen-Recording-2024-12-24-at-3.27.30-PM_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container kg-video-hide">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:11</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            
        </figure><h2 id="child-windows">Child Windows</h2><p>There are, of course, many reasons why one should avoid using deprecated APIs in production code, but I believe drawers can serve as inspiration for more creative uses of the movement group mechanics of <code>NSWindow</code>s. After all, you would be able to replicate the majority of the characteristic behaviors of <code>NSDrawerWindow</code> at home by using the following methods.</p><p>First, you&apos;d set up a drawer as a borderless auxiliary <code>NSWindow</code>, then call <code>addChildWindow</code> with the parent window as the receiver to present the drawer.</p><pre><code class="language-swift">parentWindow.addChildWindow(drawerWindow, ordered: .below)
</code></pre><p>Now, you&apos;ve gotten the behavior of the window moving with the parent window whenever it is dragged or moved programmatically without doing any additional work. What&apos;s even better is that you didn&apos;t have to opt out of server-side dragging to accomplish that. Your child window will also seamlessly participate in system behaviors like Window Tiling (try Globe-Control-Right Arrow).</p><p>We can even implement a simple tear-off behavior by recognizing a drag originating in the drawer, transforming its <code>styleMask</code> and <code>collectionBehavior</code> to be a more standard window, and call <code>removeChildWindow</code> to detach the drawer from the movement group. Of course, once you&apos;re done, don&apos;t forget to order the window out and make sure it&apos;s properly deallocated. It&apos;s that simple.</p><p>We could further experiment with implementing fun animations for our custom drawers, allowing them to slide from underneath the parent window and perhaps even pop on the tear-off gesture. Perhaps that could be a fun exploration for another post?</p>]]></content:encoded></item><item><title><![CDATA[A Time Capsule from NeXT Computer, Inc.]]></title><description><![CDATA[<p>A couple of months ago I took a trip to Fortuna, CA &#x2013; a roughly 5 hour drive from my home in San Jose &#x2013; to pick up a Macintosh Quadra 700 for my modest albeit growing collection of vintage hardware.</p><p>From what I gathered, the person selling the machine</p>]]></description><link>https://philz.blog/time-capsule-from-next-computer-inc/</link><guid isPermaLink="false">676a5ae307baf7ebec44f747</guid><category><![CDATA[AppKit]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Cocoa]]></category><category><![CDATA[Vintage Computing]]></category><category><![CDATA[NeXT]]></category><dc:creator><![CDATA[Phil Zakharchenko]]></dc:creator><pubDate>Tue, 24 Dec 2024 07:29:55 GMT</pubDate><media:content url="https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-10.55.55-PM.png" medium="image"/><content:encoded><![CDATA[<img src="https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-10.55.55-PM.png" alt="A Time Capsule from NeXT Computer, Inc."><p>A couple of months ago I took a trip to Fortuna, CA &#x2013; a roughly 5 hour drive from my home in San Jose &#x2013; to pick up a Macintosh Quadra 700 for my modest albeit growing collection of vintage hardware.</p><p>From what I gathered, the person selling the machine finds and repairs them for fun, mostly obtaining the devices, various accessories and software from estate sales. He seemed far more knowledgeable about the hardware than I was (I&#x2019;m a software guy, okay?), and we had a very interesting conversation.</p><p>As part of my general motivation and interest in old Apple stuff, I mentioned having worked on AppKit at Apple alongside some ex-NeXT folks. I ended up leaving with the Quadra and a somewhat worn down envelope addressed to a former Oracle employee with a NeXT Computer, Inc. logo and return address on it. I was admittedly more excited about the latter.</p><p>Inside the envelope, I found a memo from NeXT Computer, Inc. dated June 6, 1991.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-11.21.21-PM.png" class="kg-image" alt="A Time Capsule from NeXT Computer, Inc." loading="lazy" width="1328" height="1656" srcset="https://philz.blog/content/images/size/w600/2024/12/Screenshot-2024-12-23-at-11.21.21-PM.png 600w, https://philz.blog/content/images/size/w1000/2024/12/Screenshot-2024-12-23-at-11.21.21-PM.png 1000w, https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-11.21.21-PM.png 1328w" sizes="(min-width: 720px) 720px"></figure><p>Alongside it, there were mint condition booklets with the company&#x2019;s software and hardware offerings from the time. A time capsule from a unique moment in time for computing.</p><p>The booklets were all printed on surprisingly high quality dense paper. At the end of one of the booklets, it read:</p><blockquote>This booklet was produced using NeXT computers. Text was written with Write Now 2.0. Page composition was done with FrameMaker 2.0. Proofs were printed using a NeXT 400 dpi Laser Printer. All text and image files were transfered directly from a NeXT optical disk to film using NeXT computers and an electronic output device.</blockquote><p>The complete contents of the package included stuff I hadn&#x2019;t seen posted anywhere else online. In total, the envelope included the following materials:</p><ul><li>A memo from NeXT;</li><li>A table of all of the company&#x2019;s offerings and a price list with discounts;</li><li>NeXT Software and Peripherals Spring 1991</li><li>NeXTdimension, NeXTstation &amp; NeXTcube</li><li>&#x201C;Why does the world need a new computer?&#x201D;</li></ul><p>The Software and Peripherals booklet contained concise summaries of the company&#x2019;s offerings at the time, with about a paragraph written about each one. The iconography in there was great!</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-11.24.35-PM.png" class="kg-image" alt="A Time Capsule from NeXT Computer, Inc." loading="lazy" width="1728" height="504" srcset="https://philz.blog/content/images/size/w600/2024/12/Screenshot-2024-12-23-at-11.24.35-PM.png 600w, https://philz.blog/content/images/size/w1000/2024/12/Screenshot-2024-12-23-at-11.24.35-PM.png 1000w, https://philz.blog/content/images/size/w1600/2024/12/Screenshot-2024-12-23-at-11.24.35-PM.png 1600w, https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-11.24.35-PM.png 1728w" sizes="(min-width: 720px) 720px"></figure><p>It was fascinating to see the NeXTdimension, NeXTstation &amp; NeXTcube brochure, as it was by far the most technical. Multiple tables outlined the specifications and features of each, alongside a few notable peripherals. Notably, each of the devices got a hero page with a cool graphic, and a summary table containing not just a list of features, but a concise explanation of the benefits of each.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-11.25.34-PM.png" class="kg-image" alt="A Time Capsule from NeXT Computer, Inc." loading="lazy" width="2000" height="1710" srcset="https://philz.blog/content/images/size/w600/2024/12/Screenshot-2024-12-23-at-11.25.34-PM.png 600w, https://philz.blog/content/images/size/w1000/2024/12/Screenshot-2024-12-23-at-11.25.34-PM.png 1000w, https://philz.blog/content/images/size/w1600/2024/12/Screenshot-2024-12-23-at-11.25.34-PM.png 1600w, https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-11.25.34-PM.png 2000w" sizes="(min-width: 720px) 720px"></figure><p>&#x201C;Why does the world need a new computer?&#x201D; was undoubtedly the star of the show. A 15-page booklet, according to Steve himself, was aimed at changing &#x201C;the way we work in the 90s.&#x201D; This was somewhat of a pitch deck, focusing on the bigger picture of the what, the how, and the why. A few notable mentions of really cool software made it in, too.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-11.27.59-PM.png" class="kg-image" alt="A Time Capsule from NeXT Computer, Inc." loading="lazy" width="1574" height="2152" srcset="https://philz.blog/content/images/size/w600/2024/12/Screenshot-2024-12-23-at-11.27.59-PM.png 600w, https://philz.blog/content/images/size/w1000/2024/12/Screenshot-2024-12-23-at-11.27.59-PM.png 1000w, https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-11.27.59-PM.png 1574w" sizes="(min-width: 720px) 720px"></figure><p>The captures you see above depict the scans I performed using my iPhone 14 Pro camera. While the results were not bad, <em>I am committed to scanning these materials professionally</em>, and posting them here and in online archives. In the meantime, please feel free to email or contact me and I will be happy to send you any of the scans.</p><figure class="kg-card kg-image-card"><img src="https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-11.29.09-PM.png" class="kg-image" alt="A Time Capsule from NeXT Computer, Inc." loading="lazy" width="1618" height="2166" srcset="https://philz.blog/content/images/size/w600/2024/12/Screenshot-2024-12-23-at-11.29.09-PM.png 600w, https://philz.blog/content/images/size/w1000/2024/12/Screenshot-2024-12-23-at-11.29.09-PM.png 1000w, https://philz.blog/content/images/size/w1600/2024/12/Screenshot-2024-12-23-at-11.29.09-PM.png 1600w, https://philz.blog/content/images/2024/12/Screenshot-2024-12-23-at-11.29.09-PM.png 1618w" sizes="(min-width: 720px) 720px"></figure>]]></content:encoded></item></channel></rss>