{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-11-automatic-observation-tracking-in-uikit-and-appkit-the/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "**TL;DR**: iOS 18 and macOS 15 secretly ship with automatic observation tracking for UIKit/AppKit. Enable it with a plist key, and your views magically update when your `@Observable` models change. No more manual `setNeedsDisplay()` calls!\n\nRemember when SwiftUI came out and we all marveled at how views automatically updated when `@Published` properties changed? Well, Apple has been quietly working on bringing that same magic to UIKit and AppKit. The best part? It shipped in iOS 18/macOS 15, but hardly anyone knows about it. You don’t even need Xcode 26, it’s just one simple plist entry away.\n\n## The Problem We’ve All Faced\n\nLet’s be honest - keeping your UI in sync with your data model in UIKit has always been a chore. Here’s the dance we’ve all done:\n\n```swift\nclass ProfileViewController: UIViewController {\n    var user: User? {\n        didSet {\n            updateUI()\n        }\n    }\n    \n    func updateUI() {\n        nameLabel.text = user?.name\n        avatarImageView.image = user?.avatar\n        // ... 20 more lines of manual updates\n        setNeedsLayout()\n    }\n}\n```\n\nForgot to call `updateUI()`? Enjoy your stale UI. Called it too often? Hello, performance issues. It’s tedious and error-prone.\n\n## Enter Automatic Observation Tracking\n\nWith the new observation framework, this entire pattern becomes obsolete. Here’s the same code with automatic tracking:\n\n```swift\nimport Observation\n\n@Observable\nclass User {\n    var name: String = \"\"\n    var avatar: UIImage?\n    var unreadCount = 0\n    \n    var hasUnread: Bool {\n        unreadCount > 0\n    }\n}\n\nclass ProfileViewController: UIViewController {\n    let user = User()\n    \n    override func viewWillLayoutSubviews() {\n        super.viewWillLayoutSubviews()\n        // UIKit tracks these property accesses automatically!\n        nameLabel.text = user.name\n        avatarImageView.image = user.avatar\n        badgeView.isHidden = !user.hasUnread\n    }\n}\n```\n\nThat’s it. Change `user.name` anywhere in your app, and the label updates. No manual calls, no forgotten updates, no performance overhead from unnecessary refreshes. It just works.\n\n## Where Observation Tracking Works\n\nThe automatic observation tracking is supported in a variety of UIKit and AppKit methods. For most cases, `viewWillLayoutSubviews()` in UIKit view controllers, `layoutSubviews()` in UIKit views, and their AppKit equivalents (`viewWillLayout()` and `layout()`) are the go-to choices.\n\n**View the complete list of supported methods**\n\n### UIView\n\n- [`updateProperties()`](https://developer.apple.com/documentation/uikit/automatic-trait-tracking) (iOS 26+)\n- [`layoutSubviews()`](https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews)\n- [`updateConstraints()`](https://developer.apple.com/documentation/uikit/uiview/1622512-updateconstraints)\n- [`draw(_:)`](https://developer.apple.com/documentation/uikit/uiview/1622529-draw)\n\n### UIViewController\n\n- [`updateProperties()`](https://developer.apple.com/documentation/uikit/automatic-trait-tracking) (iOS 26+)\n- [`viewWillLayoutSubviews()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621437-viewwilllayoutsubviews)\n- [`viewDidLayoutSubviews()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621398-viewdidlayoutsubviews)\n- [`updateViewConstraints()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621379-updateviewconstraints)\n- [`updateContentUnavailableConfiguration(using:)`](https://developer.apple.com/documentation/uikit/uiviewcontroller/4195484-updatecontentunavailableconfigur)\n\n### UIPresentationController\n\n- [`containerViewWillLayoutSubviews()`](https://developer.apple.com/documentation/uikit/uipresentationcontroller/1618335-containerviewwilllayoutsubviews)\n- [`containerViewDidLayoutSubviews()`](https://developer.apple.com/documentation/uikit/uipresentationcontroller/1618329-containerviewdidlayoutsubviews)\n\n### UIButton\n\n- [`updateConfiguration()`](https://developer.apple.com/documentation/uikit/uibutton/3750773-updateconfiguration)\n- When executing the [`configurationUpdateHandler`](https://developer.apple.com/documentation/uikit/uibutton/3750772-configurationupdatehandler)\n\n- [`updateConfiguration(using:)`](https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600950-updateconfiguration) (UICollectionViewCell)\n- [`updateConfiguration(using:)`](https://developer.apple.com/documentation/uikit/uitableviewcell/3601058-updateconfiguration) (UITableViewCell)\n- [`updateConfiguration(using:)`](https://developer.apple.com/documentation/uikit/uitableviewheaderfooterview/3601061-updateconfiguration) (UITableViewHeaderFooterView)\n- When executing the `configurationUpdateHandler`\n\n### NSView (AppKit)\n\n- [`updateConstraints()`](https://developer.apple.com/documentation/appkit/nsview/1526891-updateconstraints)\n- [`layout()`](https://developer.apple.com/documentation/appkit/nsview/1526146-layout)\n- [`draw(_:)`](https://developer.apple.com/documentation/appkit/nsview/1483686-draw)\n- [`updateLayer()`](https://developer.apple.com/documentation/appkit/nsview/1526068-updatelayer)\n\n### NSViewController (AppKit)\n\n- [`updateViewConstraints()`](https://developer.apple.com/documentation/appkit/nsviewcontroller/1434401-updateviewconstraints)\n- [`viewWillLayout()`](https://developer.apple.com/documentation/appkit/nsviewcontroller/1434481-viewwilllayout)\n- [`viewDidLayout()`](https://developer.apple.com/documentation/appkit/nsviewcontroller/1434491-viewdidlayout)\n\n## Enabling the Magic\n\nHere’s where it gets interesting. This feature isn’t enabled by default (yet). You need to add a key to your Info.plist:\n\n### For UIKit (iOS 18+)\n\n```xml\n<key>UIObservationTrackingEnabled</key><true/>\n```\n\n### For AppKit (macOS 15+)\n\n```xml\n<key>NSObservationTrackingEnabled</key><true/>\n```\n\nThis plist key enables observation tracking in iOS 18 and macOS 15. Starting with their 26 releases, this is on by default and the key will simply be ignored.\n\n## iOS 26 and Beyond\n\niOS 26 (already in beta!) brings improvements. The new `updateProperties()` method on both `UIView` and `UIViewController` provides an even better place for observable property access. For a comprehensive overview of all iOS 26 UIKit additions, check out [Jordan Morgan’s excellent writeup](https://www.swiftjectivec.com/ios-26-notable-uikit-additions/).\n\n```swift\nclass MyView: UIView {\n    let model: MyModel\n    \n    override func updateProperties() {\n        super.updateProperties()\n        // This runs before layoutSubviews for even better performance\n        backgroundColor = model.backgroundColor\n        layer.cornerRadius = model.cornerRadius\n    }\n}\n```\n\nThis method is specifically designed for property updates and runs before `layoutSubviews`, allowing for more efficient updates and clearer separation of concerns.\n\nJust like the layout system has `setNeedsLayout()` and `layoutIfNeeded()`, the property update system provides [`setNeedsUpdateProperties()`](https://developer.apple.com/documentation/uikit/uiview/setNeedsUpdateProperties) and [`updatePropertiesIfNeeded()`](https://developer.apple.com/documentation/uikit/uiview/updatePropertiesIfNeeded). You can call `setNeedsUpdateProperties()` to schedule a property update on the next update cycle, or use `updatePropertiesIfNeeded()` to force an immediate update if one is pending. This gives you fine-grained control over when property updates occur, which is especially useful for optimizing performance in complex view hierarchies.\n\nApple’s [automatic trait tracking documentation](https://developer.apple.com/documentation/uikit/automatic-trait-tracking) provides detailed guidance on using these new APIs. Plus, automatic observation tracking is enabled by default in iOS 26, so you won’t even need the plist key anymore.\n\n## The Gotchas\n\nOf course, it’s not all roses. Here are a few things to watch out for:\n\n- **Observation happens in specific methods**: Only properties accessed in the supported methods (see list above) are tracked\n- **Timing matters**: If you’re doing expensive computations, consider caching results since these methods can be called frequently\n- **Memory considerations**: Observable objects are retained while being observed, so be mindful of retain cycles\n- **Thread safety**: While `@Observable` is thread-safe, mutations from different threads could lead to inconsistent UI representations. Keep all mutations on the main thread to avoid surprises\n\n## A Pattern to Avoid\n\nYou might be tempted to create a method that pre-accesses all observable properties:\n\n```swift\n// ❌ Don't do this\noverride func trackObservableProperties() {\n    // Accessing all properties upfront\n    _ = model.backgroundColor\n    _ = model.cornerRadius\n    _ = model.title\n    // ... etc\n}\n```\n\nThis is an anti-pattern for two reasons:\n\n1. **Inefficiency**: It establishes observation dependencies for ALL properties, even those not used in the current UI state. The beauty of automatic observation is that it only tracks properties actually accessed during updates.\n\n2. **Fragility**: You’re maintaining a duplicate list of properties that can easily fall out of sync with your actual UI code.\n\nInstead, access properties directly where they’re used:\n\n```swift\n// ✅ Do this\noverride func layoutSubviews() {\n    super.layoutSubviews()\n    // Only accessed properties create dependencies\n    if model.isOptionEnabled {\n        view.foo = model.bar  // Only bar is observed\n    } else {\n        view.foo = model.baz  // Only baz is observed\n    }\n}\n```\n\nThis way, only the properties actually affecting your UI get observation dependencies, making your code both more efficient and maintainable.\n\n## Performance Considerations\n\nYou might be wondering about performance. The beauty of this system is that it only tracks dependencies when views are actually laying out. If a view isn’t visible, it’s not tracking. The observation framework uses a sophisticated dependency graph that ensures minimal overhead.\n\n## Complete Example Project\n\nAutomatic observation tracking is one of those features that makes you wonder how you lived without it. It brings the best parts of SwiftUI’s reactive programming model to UIKit and AppKit, without requiring a complete rewrite of your app.\n\nAll the code snippets in this post come from a fully working example project. Check it out on GitHub: [ObservationTrackingExample](https://github.com/steipete/ObservationTrackingExample)\n\n## The Missing Piece: Custom Traits\n\nIf you’ve used SwiftUI, you know the joy of `@EnvironmentObject` - drop an object at the root, access it anywhere. UIKit developers have been jealous of this pattern for years. Well, jealous no more. (Mac devs miss out tho - there’s no equivalent on AppKit yet)\n\nSince iOS 17, UIKit has quietly introduced custom traits - a way to attach arbitrary values to the trait collection that flows through your view hierarchy. These aren’t just for dark mode and size classes anymore. Keith Harrison has an [excellent deep dive into custom traits](https://useyourloaf.com/blog/custom-traits-and-swiftui/) if you want the full story.\n\nThe magic happens when you combine custom traits with observable objects. You get automatic propagation AND automatic updates. It’s like having your cake and eating it too.\n\n**View the complete Example**\n\nLet’s build an app-wide state container that any view can access and observe:\n\n```swift\nimport UIKit\nimport Observation\n\n@Observable \nclass AppModel {\n    var currentUser: User?\n    var theme: Theme = .light\n    var isOnline = true\n    \n    // Add more app-wide state as needed\n}\n\n// Define a custom trait for your app model\nstruct AppModelTrait: UITraitDefinition {\n    static let defaultValue: AppModel? = nil\n}\n\n// Add convenient accessors\nextension UITraitCollection {\n    var appModel: AppModel? {\n        self[AppModelTrait.self]\n    }\n}\n\nextension UIMutableTraits {\n    var appModel: AppModel? {\n        get { self[AppModelTrait.self] }\n        set { self[AppModelTrait.self] = newValue }\n    }\n}\n```\n\n## Injecting the Model\n\nAt your app’s root (usually in your scene delegate or root view controller), inject the model:\n\n```swift\nclass SceneDelegate: UIResponder, UIWindowSceneDelegate {\n    var window: UIWindow?\n    let appModel = AppModel()\n    \n    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: UIScene.ConnectionOptions) {\n        guard let windowScene = (scene as? UIWindowScene) else { return }\n        \n        window = UIWindow(windowScene: windowScene)\n        let rootViewController = MainTabBarController()\n        \n        // Inject the app model into the trait system\n        rootViewController.traitOverrides.appModel = appModel\n        \n        window?.rootViewController = rootViewController\n        window?.makeKeyAndVisible()\n    }\n}\n```\n\n## Observing Changes Anywhere\n\nNow the magic part - any view controller in your hierarchy can access and observe the model:\n\n```swift\nclass ProfileViewController: UIViewController {\n    let nameLabel = UILabel()\n    let statusIndicator = UIView()\n    \n    override func viewWillLayoutSubviews() {\n        super.viewWillLayoutSubviews()\n        \n        guard let model = traitCollection.appModel else { return }\n        \n        // These automatically update when model properties change!\n        nameLabel.text = model.currentUser?.name ?? \"Guest\"\n        statusIndicator.backgroundColor = model.isOnline ? .systemGreen : .systemRed\n        \n        // Theme updates\n        view.backgroundColor = model.theme.backgroundColor\n        nameLabel.textColor = model.theme.textColor\n    }\n}\n```\n\nNo delegates. No notifications. No manual updates. Change `appModel.currentUser` anywhere in your app, and every view observing it updates automatically.\n\n## Resources\n\n- [Apple’s Observation Framework Documentation](https://developer.apple.com/documentation/observation)\n- [WWDC 2023: Discover Observation in Swift](https://developer.apple.com/videos/play/wwdc2023/10149/) (covers the foundation)\n- [Example Project on GitHub](https://github.com/steipete/ObservationTrackingExample)\n- [Swift Forums Discussion on Observation Tracking](https://forums.swift.org/t/observation-tracking-in-uikit)\n- [Custom Traits and SwiftUI](https://useyourloaf.com/blog/custom-traits-and-swiftui/) by Keith Harrison\n",
  "authors": [],
  "captured_at": "2026-09-27T19:29:15.293681927+00:00",
  "category": "programming",
  "content_html": "<p><strong>TL;DR</strong>: iOS 18 and macOS 15 secretly ship with automatic observation tracking for UIKit/AppKit. Enable it with a plist key, and your views magically update when your <code>@Observable</code> models change. No more manual <code>setNeedsDisplay()</code> calls!</p>\n<p>Remember when SwiftUI came out and we all marveled at how views automatically updated when <code>@Published</code> properties changed? Well, Apple has been quietly working on bringing that same magic to UIKit and AppKit. The best part? It shipped in iOS 18/macOS 15, but hardly anyone knows about it. You don’t even need Xcode 26, it’s just one simple plist entry away.</p>\n<h2>The Problem We’ve All Faced</h2>\n<p>Let’s be honest - keeping your UI in sync with your data model in UIKit has always been a chore. Here’s the dance we’ve all done:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">class ProfileViewController: UIViewController {\n    var user: User? {\n        didSet {\n            updateUI()\n        }\n    }\n    \n    func updateUI() {\n        nameLabel.text = user?.name\n        avatarImageView.image = user?.avatar\n        // ... 20 more lines of manual updates\n        setNeedsLayout()\n    }\n}\n</span></code></pre>\n<p>Forgot to call <code>updateUI()</code>? Enjoy your stale UI. Called it too often? Hello, performance issues. It’s tedious and error-prone.</p>\n<h2>Enter Automatic Observation Tracking</h2>\n<p>With the new observation framework, this entire pattern becomes obsolete. Here’s the same code with automatic tracking:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">import Observation\n\n@Observable\nclass User {\n    var name: String = &quot;&quot;\n    var avatar: UIImage?\n    var unreadCount = 0\n    \n    var hasUnread: Bool {\n        unreadCount &gt; 0\n    }\n}\n\nclass ProfileViewController: UIViewController {\n    let user = User()\n    \n    override func viewWillLayoutSubviews() {\n        super.viewWillLayoutSubviews()\n        // UIKit tracks these property accesses automatically!\n        nameLabel.text = user.name\n        avatarImageView.image = user.avatar\n        badgeView.isHidden = !user.hasUnread\n    }\n}\n</span></code></pre>\n<p>That’s it. Change <code>user.name</code> anywhere in your app, and the label updates. No manual calls, no forgotten updates, no performance overhead from unnecessary refreshes. It just works.</p>\n<h2>Where Observation Tracking Works</h2>\n<p>The automatic observation tracking is supported in a variety of UIKit and AppKit methods. For most cases, <code>viewWillLayoutSubviews()</code> in UIKit view controllers, <code>layoutSubviews()</code> in UIKit views, and their AppKit equivalents (<code>viewWillLayout()</code> and <code>layout()</code>) are the go-to choices.</p>\n<p><strong>View the complete list of supported methods</strong></p>\n<h3>UIView</h3>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/automatic-trait-tracking\"><code>updateProperties()</code></a> (iOS 26+)</li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews\"><code>layoutSubviews()</code></a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uiview/1622512-updateconstraints\"><code>updateConstraints()</code></a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uiview/1622529-draw\"><code>draw(_:)</code></a></li>\n</ul>\n<h3>UIViewController</h3>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/automatic-trait-tracking\"><code>updateProperties()</code></a> (iOS 26+)</li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uiviewcontroller/1621437-viewwilllayoutsubviews\"><code>viewWillLayoutSubviews()</code></a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uiviewcontroller/1621398-viewdidlayoutsubviews\"><code>viewDidLayoutSubviews()</code></a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uiviewcontroller/1621379-updateviewconstraints\"><code>updateViewConstraints()</code></a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uiviewcontroller/4195484-updatecontentunavailableconfigur\"><code>updateContentUnavailableConfiguration(using:)</code></a></li>\n</ul>\n<h3>UIPresentationController</h3>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uipresentationcontroller/1618335-containerviewwilllayoutsubviews\"><code>containerViewWillLayoutSubviews()</code></a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uipresentationcontroller/1618329-containerviewdidlayoutsubviews\"><code>containerViewDidLayoutSubviews()</code></a></li>\n</ul>\n<h3>UIButton</h3>\n<ul>\n<li>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uibutton/3750773-updateconfiguration\"><code>updateConfiguration()</code></a></p>\n</li>\n<li>\n<p>When executing the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uibutton/3750772-configurationupdatehandler\"><code>configurationUpdateHandler</code></a></p>\n</li>\n<li>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600950-updateconfiguration\"><code>updateConfiguration(using:)</code></a> (UICollectionViewCell)</p>\n</li>\n<li>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uitableviewcell/3601058-updateconfiguration\"><code>updateConfiguration(using:)</code></a> (UITableViewCell)</p>\n</li>\n<li>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uitableviewheaderfooterview/3601061-updateconfiguration\"><code>updateConfiguration(using:)</code></a> (UITableViewHeaderFooterView)</p>\n</li>\n<li>\n<p>When executing the <code>configurationUpdateHandler</code></p>\n</li>\n</ul>\n<h3>NSView (AppKit)</h3>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/appkit/nsview/1526891-updateconstraints\"><code>updateConstraints()</code></a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/appkit/nsview/1526146-layout\"><code>layout()</code></a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/appkit/nsview/1483686-draw\"><code>draw(_:)</code></a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/appkit/nsview/1526068-updatelayer\"><code>updateLayer()</code></a></li>\n</ul>\n<h3>NSViewController (AppKit)</h3>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/appkit/nsviewcontroller/1434401-updateviewconstraints\"><code>updateViewConstraints()</code></a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/appkit/nsviewcontroller/1434481-viewwilllayout\"><code>viewWillLayout()</code></a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/appkit/nsviewcontroller/1434491-viewdidlayout\"><code>viewDidLayout()</code></a></li>\n</ul>\n<h2>Enabling the Magic</h2>\n<p>Here’s where it gets interesting. This feature isn’t enabled by default (yet). You need to add a key to your Info.plist:</p>\n<h3>For UIKit (iOS 18+)</h3>\n<pre><code class=\"language-xml\"><span class=\"code-snippet\" data-language=\"XML\"><span class=\"syntax-text syntax-xml\"><span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>UIObservationTrackingEnabled<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span><span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">true</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">/&gt;</span></span>\n</span></span></code></pre>\n<h3>For AppKit (macOS 15+)</h3>\n<pre><code class=\"language-xml\"><span class=\"code-snippet\" data-language=\"XML\"><span class=\"syntax-text syntax-xml\"><span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>NSObservationTrackingEnabled<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span><span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">true</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">/&gt;</span></span>\n</span></span></code></pre>\n<p>This plist key enables observation tracking in iOS 18 and macOS 15. Starting with their 26 releases, this is on by default and the key will simply be ignored.</p>\n<h2>iOS 26 and Beyond</h2>\n<p>iOS 26 (already in beta!) brings improvements. The new <code>updateProperties()</code> method on both <code>UIView</code> and <code>UIViewController</code> provides an even better place for observable property access. For a comprehensive overview of all iOS 26 UIKit additions, check out <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.swiftjectivec.com/ios-26-notable-uikit-additions/\">Jordan Morgan’s excellent writeup</a>.</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">class MyView: UIView {\n    let model: MyModel\n    \n    override func updateProperties() {\n        super.updateProperties()\n        // This runs before layoutSubviews for even better performance\n        backgroundColor = model.backgroundColor\n        layer.cornerRadius = model.cornerRadius\n    }\n}\n</span></code></pre>\n<p>This method is specifically designed for property updates and runs before <code>layoutSubviews</code>, allowing for more efficient updates and clearer separation of concerns.</p>\n<p>Just like the layout system has <code>setNeedsLayout()</code> and <code>layoutIfNeeded()</code>, the property update system provides <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uiview/setNeedsUpdateProperties\"><code>setNeedsUpdateProperties()</code></a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/uiview/updatePropertiesIfNeeded\"><code>updatePropertiesIfNeeded()</code></a>. You can call <code>setNeedsUpdateProperties()</code> to schedule a property update on the next update cycle, or use <code>updatePropertiesIfNeeded()</code> to force an immediate update if one is pending. This gives you fine-grained control over when property updates occur, which is especially useful for optimizing performance in complex view hierarchies.</p>\n<p>Apple’s <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uikit/automatic-trait-tracking\">automatic trait tracking documentation</a> provides detailed guidance on using these new APIs. Plus, automatic observation tracking is enabled by default in iOS 26, so you won’t even need the plist key anymore.</p>\n<h2>The Gotchas</h2>\n<p>Of course, it’s not all roses. Here are a few things to watch out for:</p>\n<ul>\n<li><strong>Observation happens in specific methods</strong>: Only properties accessed in the supported methods (see list above) are tracked</li>\n<li><strong>Timing matters</strong>: If you’re doing expensive computations, consider caching results since these methods can be called frequently</li>\n<li><strong>Memory considerations</strong>: Observable objects are retained while being observed, so be mindful of retain cycles</li>\n<li><strong>Thread safety</strong>: While <code>@Observable</code> is thread-safe, mutations from different threads could lead to inconsistent UI representations. Keep all mutations on the main thread to avoid surprises</li>\n</ul>\n<h2>A Pattern to Avoid</h2>\n<p>You might be tempted to create a method that pre-accesses all observable properties:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">// ❌ Don&#39;t do this\noverride func trackObservableProperties() {\n    // Accessing all properties upfront\n    _ = model.backgroundColor\n    _ = model.cornerRadius\n    _ = model.title\n    // ... etc\n}\n</span></code></pre>\n<p>This is an anti-pattern for two reasons:</p>\n<ol>\n<li>\n<p><strong>Inefficiency</strong>: It establishes observation dependencies for ALL properties, even those not used in the current UI state. The beauty of automatic observation is that it only tracks properties actually accessed during updates.</p>\n</li>\n<li>\n<p><strong>Fragility</strong>: You’re maintaining a duplicate list of properties that can easily fall out of sync with your actual UI code.</p>\n</li>\n</ol>\n<p>Instead, access properties directly where they’re used:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">// ✅ Do this\noverride func layoutSubviews() {\n    super.layoutSubviews()\n    // Only accessed properties create dependencies\n    if model.isOptionEnabled {\n        view.foo = model.bar  // Only bar is observed\n    } else {\n        view.foo = model.baz  // Only baz is observed\n    }\n}\n</span></code></pre>\n<p>This way, only the properties actually affecting your UI get observation dependencies, making your code both more efficient and maintainable.</p>\n<h2>Performance Considerations</h2>\n<p>You might be wondering about performance. The beauty of this system is that it only tracks dependencies when views are actually laying out. If a view isn’t visible, it’s not tracking. The observation framework uses a sophisticated dependency graph that ensures minimal overhead.</p>\n<h2>Complete Example Project</h2>\n<p>Automatic observation tracking is one of those features that makes you wonder how you lived without it. It brings the best parts of SwiftUI’s reactive programming model to UIKit and AppKit, without requiring a complete rewrite of your app.</p>\n<p>All the code snippets in this post come from a fully working example project. Check it out on GitHub: <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/ObservationTrackingExample\">ObservationTrackingExample</a></p>\n<h2>The Missing Piece: Custom Traits</h2>\n<p>If you’ve used SwiftUI, you know the joy of <code>@EnvironmentObject</code> - drop an object at the root, access it anywhere. UIKit developers have been jealous of this pattern for years. Well, jealous no more. (Mac devs miss out tho - there’s no equivalent on AppKit yet)</p>\n<p>Since iOS 17, UIKit has quietly introduced custom traits - a way to attach arbitrary values to the trait collection that flows through your view hierarchy. These aren’t just for dark mode and size classes anymore. Keith Harrison has an <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://useyourloaf.com/blog/custom-traits-and-swiftui/\">excellent deep dive into custom traits</a> if you want the full story.</p>\n<p>The magic happens when you combine custom traits with observable objects. You get automatic propagation AND automatic updates. It’s like having your cake and eating it too.</p>\n<p><strong>View the complete Example</strong></p>\n<p>Let’s build an app-wide state container that any view can access and observe:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">import UIKit\nimport Observation\n\n@Observable \nclass AppModel {\n    var currentUser: User?\n    var theme: Theme = .light\n    var isOnline = true\n    \n    // Add more app-wide state as needed\n}\n\n// Define a custom trait for your app model\nstruct AppModelTrait: UITraitDefinition {\n    static let defaultValue: AppModel? = nil\n}\n\n// Add convenient accessors\nextension UITraitCollection {\n    var appModel: AppModel? {\n        self[AppModelTrait.self]\n    }\n}\n\nextension UIMutableTraits {\n    var appModel: AppModel? {\n        get { self[AppModelTrait.self] }\n        set { self[AppModelTrait.self] = newValue }\n    }\n}\n</span></code></pre>\n<h2>Injecting the Model</h2>\n<p>At your app’s root (usually in your scene delegate or root view controller), inject the model:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">class SceneDelegate: UIResponder, UIWindowSceneDelegate {\n    var window: UIWindow?\n    let appModel = AppModel()\n    \n    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: UIScene.ConnectionOptions) {\n        guard let windowScene = (scene as? UIWindowScene) else { return }\n        \n        window = UIWindow(windowScene: windowScene)\n        let rootViewController = MainTabBarController()\n        \n        // Inject the app model into the trait system\n        rootViewController.traitOverrides.appModel = appModel\n        \n        window?.rootViewController = rootViewController\n        window?.makeKeyAndVisible()\n    }\n}\n</span></code></pre>\n<h2>Observing Changes Anywhere</h2>\n<p>Now the magic part - any view controller in your hierarchy can access and observe the model:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">class ProfileViewController: UIViewController {\n    let nameLabel = UILabel()\n    let statusIndicator = UIView()\n    \n    override func viewWillLayoutSubviews() {\n        super.viewWillLayoutSubviews()\n        \n        guard let model = traitCollection.appModel else { return }\n        \n        // These automatically update when model properties change!\n        nameLabel.text = model.currentUser?.name ?? &quot;Guest&quot;\n        statusIndicator.backgroundColor = model.isOnline ? .systemGreen : .systemRed\n        \n        // Theme updates\n        view.backgroundColor = model.theme.backgroundColor\n        nameLabel.textColor = model.theme.textColor\n    }\n}\n</span></code></pre>\n<p>No delegates. No notifications. No manual updates. Change <code>appModel.currentUser</code> anywhere in your app, and every view observing it updates automatically.</p>\n<h2>Resources</h2>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/observation\">Apple’s Observation Framework Documentation</a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/videos/play/wwdc2023/10149/\">WWDC 2023: Discover Observation in Swift</a> (covers the foundation)</li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/ObservationTrackingExample\">Example Project on GitHub</a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://forums.swift.org/t/observation-tracking-in-uikit\">Swift Forums Discussion on Observation Tracking</a></li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://useyourloaf.com/blog/custom-traits-and-swiftui/\">Custom Traits and SwiftUI</a> by Keith Harrison</li>\n</ul>\n",
  "content_markdown": "**TL;DR**: iOS 18 and macOS 15 secretly ship with automatic observation tracking for UIKit/AppKit. Enable it with a plist key, and your views magically update when your `@Observable` models change. No more manual `setNeedsDisplay()` calls!\n\nRemember when SwiftUI came out and we all marveled at how views automatically updated when `@Published` properties changed? Well, Apple has been quietly working on bringing that same magic to UIKit and AppKit. The best part? It shipped in iOS 18/macOS 15, but hardly anyone knows about it. You don’t even need Xcode 26, it’s just one simple plist entry away.\n\n## The Problem We’ve All Faced\n\nLet’s be honest - keeping your UI in sync with your data model in UIKit has always been a chore. Here’s the dance we’ve all done:\n\n```swift\nclass ProfileViewController: UIViewController {\n    var user: User? {\n        didSet {\n            updateUI()\n        }\n    }\n    \n    func updateUI() {\n        nameLabel.text = user?.name\n        avatarImageView.image = user?.avatar\n        // ... 20 more lines of manual updates\n        setNeedsLayout()\n    }\n}\n```\n\nForgot to call `updateUI()`? Enjoy your stale UI. Called it too often? Hello, performance issues. It’s tedious and error-prone.\n\n## Enter Automatic Observation Tracking\n\nWith the new observation framework, this entire pattern becomes obsolete. Here’s the same code with automatic tracking:\n\n```swift\nimport Observation\n\n@Observable\nclass User {\n    var name: String = \"\"\n    var avatar: UIImage?\n    var unreadCount = 0\n    \n    var hasUnread: Bool {\n        unreadCount > 0\n    }\n}\n\nclass ProfileViewController: UIViewController {\n    let user = User()\n    \n    override func viewWillLayoutSubviews() {\n        super.viewWillLayoutSubviews()\n        // UIKit tracks these property accesses automatically!\n        nameLabel.text = user.name\n        avatarImageView.image = user.avatar\n        badgeView.isHidden = !user.hasUnread\n    }\n}\n```\n\nThat’s it. Change `user.name` anywhere in your app, and the label updates. No manual calls, no forgotten updates, no performance overhead from unnecessary refreshes. It just works.\n\n## Where Observation Tracking Works\n\nThe automatic observation tracking is supported in a variety of UIKit and AppKit methods. For most cases, `viewWillLayoutSubviews()` in UIKit view controllers, `layoutSubviews()` in UIKit views, and their AppKit equivalents (`viewWillLayout()` and `layout()`) are the go-to choices.\n\n**View the complete list of supported methods**\n\n### UIView\n\n- [`updateProperties()`](https://developer.apple.com/documentation/uikit/automatic-trait-tracking) (iOS 26+)\n- [`layoutSubviews()`](https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews)\n- [`updateConstraints()`](https://developer.apple.com/documentation/uikit/uiview/1622512-updateconstraints)\n- [`draw(_:)`](https://developer.apple.com/documentation/uikit/uiview/1622529-draw)\n\n### UIViewController\n\n- [`updateProperties()`](https://developer.apple.com/documentation/uikit/automatic-trait-tracking) (iOS 26+)\n- [`viewWillLayoutSubviews()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621437-viewwilllayoutsubviews)\n- [`viewDidLayoutSubviews()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621398-viewdidlayoutsubviews)\n- [`updateViewConstraints()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621379-updateviewconstraints)\n- [`updateContentUnavailableConfiguration(using:)`](https://developer.apple.com/documentation/uikit/uiviewcontroller/4195484-updatecontentunavailableconfigur)\n\n### UIPresentationController\n\n- [`containerViewWillLayoutSubviews()`](https://developer.apple.com/documentation/uikit/uipresentationcontroller/1618335-containerviewwilllayoutsubviews)\n- [`containerViewDidLayoutSubviews()`](https://developer.apple.com/documentation/uikit/uipresentationcontroller/1618329-containerviewdidlayoutsubviews)\n\n### UIButton\n\n- [`updateConfiguration()`](https://developer.apple.com/documentation/uikit/uibutton/3750773-updateconfiguration)\n- When executing the [`configurationUpdateHandler`](https://developer.apple.com/documentation/uikit/uibutton/3750772-configurationupdatehandler)\n\n- [`updateConfiguration(using:)`](https://developer.apple.com/documentation/uikit/uicollectionviewcell/3600950-updateconfiguration) (UICollectionViewCell)\n- [`updateConfiguration(using:)`](https://developer.apple.com/documentation/uikit/uitableviewcell/3601058-updateconfiguration) (UITableViewCell)\n- [`updateConfiguration(using:)`](https://developer.apple.com/documentation/uikit/uitableviewheaderfooterview/3601061-updateconfiguration) (UITableViewHeaderFooterView)\n- When executing the `configurationUpdateHandler`\n\n### NSView (AppKit)\n\n- [`updateConstraints()`](https://developer.apple.com/documentation/appkit/nsview/1526891-updateconstraints)\n- [`layout()`](https://developer.apple.com/documentation/appkit/nsview/1526146-layout)\n- [`draw(_:)`](https://developer.apple.com/documentation/appkit/nsview/1483686-draw)\n- [`updateLayer()`](https://developer.apple.com/documentation/appkit/nsview/1526068-updatelayer)\n\n### NSViewController (AppKit)\n\n- [`updateViewConstraints()`](https://developer.apple.com/documentation/appkit/nsviewcontroller/1434401-updateviewconstraints)\n- [`viewWillLayout()`](https://developer.apple.com/documentation/appkit/nsviewcontroller/1434481-viewwilllayout)\n- [`viewDidLayout()`](https://developer.apple.com/documentation/appkit/nsviewcontroller/1434491-viewdidlayout)\n\n## Enabling the Magic\n\nHere’s where it gets interesting. This feature isn’t enabled by default (yet). You need to add a key to your Info.plist:\n\n### For UIKit (iOS 18+)\n\n```xml\n<key>UIObservationTrackingEnabled</key><true/>\n```\n\n### For AppKit (macOS 15+)\n\n```xml\n<key>NSObservationTrackingEnabled</key><true/>\n```\n\nThis plist key enables observation tracking in iOS 18 and macOS 15. Starting with their 26 releases, this is on by default and the key will simply be ignored.\n\n## iOS 26 and Beyond\n\niOS 26 (already in beta!) brings improvements. The new `updateProperties()` method on both `UIView` and `UIViewController` provides an even better place for observable property access. For a comprehensive overview of all iOS 26 UIKit additions, check out [Jordan Morgan’s excellent writeup](https://www.swiftjectivec.com/ios-26-notable-uikit-additions/).\n\n```swift\nclass MyView: UIView {\n    let model: MyModel\n    \n    override func updateProperties() {\n        super.updateProperties()\n        // This runs before layoutSubviews for even better performance\n        backgroundColor = model.backgroundColor\n        layer.cornerRadius = model.cornerRadius\n    }\n}\n```\n\nThis method is specifically designed for property updates and runs before `layoutSubviews`, allowing for more efficient updates and clearer separation of concerns.\n\nJust like the layout system has `setNeedsLayout()` and `layoutIfNeeded()`, the property update system provides [`setNeedsUpdateProperties()`](https://developer.apple.com/documentation/uikit/uiview/setNeedsUpdateProperties) and [`updatePropertiesIfNeeded()`](https://developer.apple.com/documentation/uikit/uiview/updatePropertiesIfNeeded). You can call `setNeedsUpdateProperties()` to schedule a property update on the next update cycle, or use `updatePropertiesIfNeeded()` to force an immediate update if one is pending. This gives you fine-grained control over when property updates occur, which is especially useful for optimizing performance in complex view hierarchies.\n\nApple’s [automatic trait tracking documentation](https://developer.apple.com/documentation/uikit/automatic-trait-tracking) provides detailed guidance on using these new APIs. Plus, automatic observation tracking is enabled by default in iOS 26, so you won’t even need the plist key anymore.\n\n## The Gotchas\n\nOf course, it’s not all roses. Here are a few things to watch out for:\n\n- **Observation happens in specific methods**: Only properties accessed in the supported methods (see list above) are tracked\n- **Timing matters**: If you’re doing expensive computations, consider caching results since these methods can be called frequently\n- **Memory considerations**: Observable objects are retained while being observed, so be mindful of retain cycles\n- **Thread safety**: While `@Observable` is thread-safe, mutations from different threads could lead to inconsistent UI representations. Keep all mutations on the main thread to avoid surprises\n\n## A Pattern to Avoid\n\nYou might be tempted to create a method that pre-accesses all observable properties:\n\n```swift\n// ❌ Don't do this\noverride func trackObservableProperties() {\n    // Accessing all properties upfront\n    _ = model.backgroundColor\n    _ = model.cornerRadius\n    _ = model.title\n    // ... etc\n}\n```\n\nThis is an anti-pattern for two reasons:\n\n1. **Inefficiency**: It establishes observation dependencies for ALL properties, even those not used in the current UI state. The beauty of automatic observation is that it only tracks properties actually accessed during updates.\n\n2. **Fragility**: You’re maintaining a duplicate list of properties that can easily fall out of sync with your actual UI code.\n\nInstead, access properties directly where they’re used:\n\n```swift\n// ✅ Do this\noverride func layoutSubviews() {\n    super.layoutSubviews()\n    // Only accessed properties create dependencies\n    if model.isOptionEnabled {\n        view.foo = model.bar  // Only bar is observed\n    } else {\n        view.foo = model.baz  // Only baz is observed\n    }\n}\n```\n\nThis way, only the properties actually affecting your UI get observation dependencies, making your code both more efficient and maintainable.\n\n## Performance Considerations\n\nYou might be wondering about performance. The beauty of this system is that it only tracks dependencies when views are actually laying out. If a view isn’t visible, it’s not tracking. The observation framework uses a sophisticated dependency graph that ensures minimal overhead.\n\n## Complete Example Project\n\nAutomatic observation tracking is one of those features that makes you wonder how you lived without it. It brings the best parts of SwiftUI’s reactive programming model to UIKit and AppKit, without requiring a complete rewrite of your app.\n\nAll the code snippets in this post come from a fully working example project. Check it out on GitHub: [ObservationTrackingExample](https://github.com/steipete/ObservationTrackingExample)\n\n## The Missing Piece: Custom Traits\n\nIf you’ve used SwiftUI, you know the joy of `@EnvironmentObject` - drop an object at the root, access it anywhere. UIKit developers have been jealous of this pattern for years. Well, jealous no more. (Mac devs miss out tho - there’s no equivalent on AppKit yet)\n\nSince iOS 17, UIKit has quietly introduced custom traits - a way to attach arbitrary values to the trait collection that flows through your view hierarchy. These aren’t just for dark mode and size classes anymore. Keith Harrison has an [excellent deep dive into custom traits](https://useyourloaf.com/blog/custom-traits-and-swiftui/) if you want the full story.\n\nThe magic happens when you combine custom traits with observable objects. You get automatic propagation AND automatic updates. It’s like having your cake and eating it too.\n\n**View the complete Example**\n\nLet’s build an app-wide state container that any view can access and observe:\n\n```swift\nimport UIKit\nimport Observation\n\n@Observable \nclass AppModel {\n    var currentUser: User?\n    var theme: Theme = .light\n    var isOnline = true\n    \n    // Add more app-wide state as needed\n}\n\n// Define a custom trait for your app model\nstruct AppModelTrait: UITraitDefinition {\n    static let defaultValue: AppModel? = nil\n}\n\n// Add convenient accessors\nextension UITraitCollection {\n    var appModel: AppModel? {\n        self[AppModelTrait.self]\n    }\n}\n\nextension UIMutableTraits {\n    var appModel: AppModel? {\n        get { self[AppModelTrait.self] }\n        set { self[AppModelTrait.self] = newValue }\n    }\n}\n```\n\n## Injecting the Model\n\nAt your app’s root (usually in your scene delegate or root view controller), inject the model:\n\n```swift\nclass SceneDelegate: UIResponder, UIWindowSceneDelegate {\n    var window: UIWindow?\n    let appModel = AppModel()\n    \n    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: UIScene.ConnectionOptions) {\n        guard let windowScene = (scene as? UIWindowScene) else { return }\n        \n        window = UIWindow(windowScene: windowScene)\n        let rootViewController = MainTabBarController()\n        \n        // Inject the app model into the trait system\n        rootViewController.traitOverrides.appModel = appModel\n        \n        window?.rootViewController = rootViewController\n        window?.makeKeyAndVisible()\n    }\n}\n```\n\n## Observing Changes Anywhere\n\nNow the magic part - any view controller in your hierarchy can access and observe the model:\n\n```swift\nclass ProfileViewController: UIViewController {\n    let nameLabel = UILabel()\n    let statusIndicator = UIView()\n    \n    override func viewWillLayoutSubviews() {\n        super.viewWillLayoutSubviews()\n        \n        guard let model = traitCollection.appModel else { return }\n        \n        // These automatically update when model properties change!\n        nameLabel.text = model.currentUser?.name ?? \"Guest\"\n        statusIndicator.backgroundColor = model.isOnline ? .systemGreen : .systemRed\n        \n        // Theme updates\n        view.backgroundColor = model.theme.backgroundColor\n        nameLabel.textColor = model.theme.textColor\n    }\n}\n```\n\nNo delegates. No notifications. No manual updates. Change `appModel.currentUser` anywhere in your app, and every view observing it updates automatically.\n\n## Resources\n\n- [Apple’s Observation Framework Documentation](https://developer.apple.com/documentation/observation)\n- [WWDC 2023: Discover Observation in Swift](https://developer.apple.com/videos/play/wwdc2023/10149/) (covers the foundation)\n- [Example Project on GitHub](https://github.com/steipete/ObservationTrackingExample)\n- [Swift Forums Discussion on Observation Tracking](https://forums.swift.org/t/observation-tracking-in-uikit)\n- [Custom Traits and SwiftUI](https://useyourloaf.com/blog/custom-traits-and-swiftui/) by Keith Harrison\n",
  "dateCreated": "2026-09-27T19:29:15.293681927+00:00",
  "date_created": "2025-06-11T09:00:00+00:00",
  "date_modified": "2025-06-11T09:00:00+00:00",
  "date_published": "2025-06-11T09:00:00+00:00",
  "description": "Discover how iOS 18's hidden automatic observation tracking brings SwiftUI-like reactive programming to UIKit and AppKit, making your UI code cleaner and more maintainable.",
  "external_url": "https://steipete.me/posts/2025/automatic-observation-tracking-uikit-appkit/",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/steipete-me/2025/06/2025-06-11-automatic-observation-tracking-in-uikit-and-appkit-the.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/steipete-me/2025/06/2025-06-11-automatic-observation-tracking-in-uikit-and-appkit-the.md"
  },
  "id": "urn:aggr:item:79062a9d4f3c8f319151b971c9c50a35645ab600",
  "isBasedOn": {
    "@id": "https://steipete.me/posts/2025/automatic-observation-tracking-uikit-appkit/"
  },
  "mainEntity": {
    "@id": "https://steipete.me/posts/2025/automatic-observation-tracking-uikit-appkit/",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-11-automatic-observation-tracking-in-uikit-and-appkit-the/#webpage"
    },
    "dateModified": "2025-06-11T09:00:00+00:00",
    "datePublished": "2025-06-11T09:00:00+00:00",
    "headline": "Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention",
    "timeRequired": "PT6M",
    "url": "https://steipete.me/posts/2025/automatic-observation-tracking-uikit-appkit/",
    "wordCount": 1345
  },
  "name": "Archived snapshot: Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention",
  "preview": null,
  "reading_minutes": 6,
  "replicated_at": null,
  "source": "steipete-me",
  "source_name": "Peter Steinberger",
  "summary": "Discover how iOS 18's hidden automatic observation tracking brings SwiftUI-like reactive programming to UIKit and AppKit, making your UI code cleaner and more maintainable.",
  "tags": [],
  "temporalCoverage": "2026-09-27T19:29:15.293681927+00:00",
  "title": "Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention",
  "url": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-06-11-automatic-observation-tracking-in-uikit-and-appkit-the/",
  "word_count": 1345
}