惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
博客园 - Franky
V
V2EX
爱范儿
爱范儿
J
Java Code Geeks
小众软件
小众软件
Last Week in AI
Last Week in AI
The Cloudflare Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hugging Face - Blog
Hugging Face - Blog
T
The Blog of Author Tim Ferriss
酷 壳 – CoolShell
酷 壳 – CoolShell
The Register - Security
The Register - Security
GbyAI
GbyAI
Vercel News
Vercel News
Y
Y Combinator Blog
腾讯CDC
F
Fortinet All Blogs
I
InfoQ
N
Netflix TechBlog - Medium
B
Blog RSS Feed
D
Docker
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
量子位
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
The GitHub Blog
The GitHub Blog
Microsoft Security Blog
Microsoft Security Blog
V
Visual Studio Blog
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
Blog — PlanetScale
Blog — PlanetScale
H
Help Net Security
云风的 BLOG
云风的 BLOG
A
About on SuperTechFans
Scott Helme
Scott Helme
T
Tor Project blog
U
Unit 42
Google Online Security Blog
Google Online Security Blog
PCI Perspectives
PCI Perspectives
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
aimingoo的专栏
aimingoo的专栏
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Stack Overflow Blog
Stack Overflow Blog
Security Archives - TechRepublic
Security Archives - TechRepublic

Peter Steinberger

OpenClaw, OpenAI and the future | Peter Steinberger Shipping at Inference-Speed | Peter Steinberger The Signature Flicker | Peter Steinberger Just Talk To It - the no-bs Way of Agentic Engineering | Peter Steinberger Claude Code Anonymous | Peter Steinberger Live Coding Session: Building Arena | Peter Steinberger My Current AI Dev Workflow | Peter Steinberger Essential Reading for Agentic Engineers - August 2025 | Peter Steinberger Just One More Prompt | Peter Steinberger Poltergeist: The Ghost That Keeps Your Builds Fresh | Peter Steinberger Don't read this Startup Slop | Peter Steinberger Essential Reading for Agentic Engineers - July 2025 | Peter Steinberger Self-Hosting AI Models After Claude's Usage Limits | Peter Steinberger Logging Privacy Shenanigans | Peter Steinberger VibeTunnel's first AI-anniversary | Peter Steinberger Making AppleScript Work in macOS CLI Tools: The Undocumented Parts | Peter Steinberger Peekaboo 2.0 – Free the CLI from its MCP shackles | Peter Steinberger Command your Claude Code Army, Reloaded | Peter Steinberger Essential Reading for Agentic Engineers | Peter Steinberger Slot Machines for Programmers: How Peter Builds Apps 20x Faster with AI | Peter Steinberger My AI Workflow for Understanding Any Codebase | Peter Steinberger stats.store: Privacy-First Sparkle Analytics | Peter Steinberger Showing Settings from macOS Menu Bar Items: A 5-Hour Journey | Peter Steinberger VibeTunnel: Turn Any Browser into Your Mac's Terminal | Peter Steinberger Vibe Meter 2.0: Calculating Claude Code Usage with Token Counting | Peter Steinberger llm.codes: Make Apple Docs AI-Readable | Peter Steinberger Peekaboo MCP – lightning-fast macOS screenshots for AI agents | Peter Steinberger Migrating 700+ Tests to Swift Testing: A Real-World Experience | Peter Steinberger Commanding Your Claude Code Army | Peter Steinberger Code Signing and Notarization: Sparkle and Tears | Peter Steinberger Vibe Meter: Monitor Your AI Costs | Peter Steinberger Claude Code is My Computer | Peter Steinberger Stop Over-thinking AI Subscriptions | Peter Steinberger Introducing Demark: HTML in. MD out. Blink-fast. | Peter Steinberger The Future of Vibe Coding: Building with AI, Live and Unfiltered | Peter Steinberger MCP Best Practices | Peter Steinberger Finding My Spark Again | Peter Steinberger Top-Level Menu Visibility in SwiftUI for macOS | Peter Steinberger Fixing keyboardShortcut in SwiftUI | Peter Steinberger Supporting Both Tap and Long Press on a Button in SwiftUI | Peter Steinberger On Using Apple Silicon Mac Mini for Continuous Integration | Peter Steinberger Apple Silicon M1: A Developer's Perspective | Peter Steinberger Gardening Your Twitter: Curating Your Timeline | Peter Steinberger Gardening Your Twitter: Growing Your Followers | Peter Steinberger Forbidden Controls in Catalyst: Optimize Interface for Mac | Peter Steinberger Disabling Keyboard Avoidance in SwiftUI's UIHostingController | Peter Steinberger The State of SwiftUI | Peter Steinberger Logging in Swift | Peter Steinberger Building with Swift Trunk Development Snapshots | Peter Steinberger Calling Super at Runtime in Swift | Peter Steinberger zld — A Faster Version of Apple's Linker | Peter Steinberger How to Fix LLDB: Couldn't IRGen Expression | Peter Steinberger Updating macOS on a Hackintosh | Peter Steinberger InterposeKit — Elegant Swizzling in Swift | Peter Steinberger The Great Mac Catalyst Text Input Crash Hunt | Peter Steinberger Jailbreaking for iOS Developers | Peter Steinberger Network Kernel Core Dump | Peter Steinberger How to macOS Core Dump | Peter Steinberger Kernel Panics and Surprise boot-args | Peter Steinberger The LG UltraFine 5K, kernel_task, and Me | Peter Steinberger Let's Try This Again | Peter Steinberger How We Work at PSPDFKit | Peter Steinberger Swizzling in Swift | Peter Steinberger WWDC for First-Timers, 2019 Edition | Peter Steinberger Challenges of Adopting Drag and Drop | Peter Steinberger Marzipan: Porting iOS Apps to the Mac | Peter Steinberger How to Use Slack and Not Go Crazy | Peter Steinberger Hardcore Debugging - Heavy Weapons for Hard Bugs | Peter Steinberger Binary Frameworks in Swift | Peter Steinberger Even Swiftier Objective-C | Peter Steinberger The Case for Deprecating UITableView | Peter Steinberger Running tests with Clang Address Sanitizer | Peter Steinberger UI testing on iOS, without busy waiting | Peter Steinberger Hiring a distributed team | Peter Steinberger Writing Good Bug Reports | Peter Steinberger Real-time collaboration, Apple, and you | Peter Steinberger Converting Xcode Test Runs to JUnit, the Fast Way | Peter Steinberger Efficient iOS Version Checking | Peter Steinberger Investigating Thread Safety of UIImage | Peter Steinberger Swifty Objective-C | Peter Steinberger Running UI Tests on iOS With Ludicrous Speed | Peter Steinberger A Pragmatic Approach to Cross-Platform | Peter Steinberger Surprises with Swift Extensions | Peter Steinberger Using ccache for Fun and Profit | Peter Steinberger UITableViewController designated initializer woes | Peter Steinberger Researching ResearchKit | Peter Steinberger The curious case of rotation with multiple windows on iOS 8 | Peter Steinberger UIKit Debug Mode | Peter Steinberger Retrofitting containsString: on iOS 7 | Peter Steinberger A Story About Swizzling "the Right Way™" and Touch Forwarding | Peter Steinberger Hacking with Aspects | Peter Steinberger Fixing UITextView On iOS 7 | Peter Steinberger Fixing What Apple Doesn't | Peter Steinberger How To Inspect The View Hierarchy Of Third-Party Apps | Peter Steinberger Fixing UISearchDisplayController On iOS 7 | Peter Steinberger Smart Proxy Delegation | Peter Steinberger Adding Keyboard Shortcuts To UIAlertView | Peter Steinberger How To Center Content Within UIScrollView | Peter Steinberger UIAppearance for Custom Views | Peter Steinberger Hacking Block Support Into UIMenuItem | Peter Steinberger
Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention | Peter Steinberger
Peter Steinberger · 2025-06-11 · via Peter Steinberger

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!

Remember 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.

The Problem We’ve All Faced

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:

class ProfileViewController: UIViewController {
    var user: User? {
        didSet {
            updateUI()
        }
    }
    
    func updateUI() {
        nameLabel.text = user?.name
        avatarImageView.image = user?.avatar
        // ... 20 more lines of manual updates
        setNeedsLayout()
    }
}

Forgot to call updateUI()? Enjoy your stale UI. Called it too often? Hello, performance issues. It’s tedious and error-prone.

Enter Automatic Observation Tracking

With the new observation framework, this entire pattern becomes obsolete. Here’s the same code with automatic tracking:

import Observation

@Observable
class User {
    var name: String = ""
    var avatar: UIImage?
    var unreadCount = 0
    
    var hasUnread: Bool {
        unreadCount > 0
    }
}

class ProfileViewController: UIViewController {
    let user = User()
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        // UIKit tracks these property accesses automatically!
        nameLabel.text = user.name
        avatarImageView.image = user.avatar
        badgeView.isHidden = !user.hasUnread
    }
}

That’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.

Where Observation Tracking Works

The 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.

View the complete list of supported methods

UIView

UIViewController

UIPresentationController

UIButton

NSView (AppKit)

NSViewController (AppKit)

Enabling the Magic

Here’s where it gets interesting. This feature isn’t enabled by default (yet). You need to add a key to your Info.plist:

For UIKit (iOS 18+)

<key>UIObservationTrackingEnabled</key><true/>

For AppKit (macOS 15+)

<key>NSObservationTrackingEnabled</key><true/>

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.

iOS 26 and Beyond

iOS 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.

class MyView: UIView {
    let model: MyModel
    
    override func updateProperties() {
        super.updateProperties()
        // This runs before layoutSubviews for even better performance
        backgroundColor = model.backgroundColor
        layer.cornerRadius = model.cornerRadius
    }
}

This method is specifically designed for property updates and runs before layoutSubviews, allowing for more efficient updates and clearer separation of concerns.

Just like the layout system has setNeedsLayout() and layoutIfNeeded(), the property update system provides setNeedsUpdateProperties() and 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.

Apple’s automatic trait tracking documentation 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.

The Gotchas

Of course, it’s not all roses. Here are a few things to watch out for:

  • Observation happens in specific methods: Only properties accessed in the supported methods (see list above) are tracked
  • Timing matters: If you’re doing expensive computations, consider caching results since these methods can be called frequently
  • Memory considerations: Observable objects are retained while being observed, so be mindful of retain cycles
  • 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

A Pattern to Avoid

You might be tempted to create a method that pre-accesses all observable properties:

// ❌ Don't do this
override func trackObservableProperties() {
    // Accessing all properties upfront
    _ = model.backgroundColor
    _ = model.cornerRadius
    _ = model.title
    // ... etc
}

This is an anti-pattern for two reasons:

  1. 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.

  2. Fragility: You’re maintaining a duplicate list of properties that can easily fall out of sync with your actual UI code.

Instead, access properties directly where they’re used:

// ✅ Do this
override func layoutSubviews() {
    super.layoutSubviews()
    // Only accessed properties create dependencies
    if model.isOptionEnabled {
        view.foo = model.bar  // Only bar is observed
    } else {
        view.foo = model.baz  // Only baz is observed
    }
}

This way, only the properties actually affecting your UI get observation dependencies, making your code both more efficient and maintainable.

Performance Considerations

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.

Complete Example Project

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.

All the code snippets in this post come from a fully working example project. Check it out on GitHub: ObservationTrackingExample

The Missing Piece: Custom Traits

If 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)

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 excellent deep dive into custom traits if you want the full story.

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.

View the complete Example

Let’s build an app-wide state container that any view can access and observe:

import UIKit
import Observation

@Observable 
class AppModel {
    var currentUser: User?
    var theme: Theme = .light
    var isOnline = true
    
    // Add more app-wide state as needed
}

// Define a custom trait for your app model
struct AppModelTrait: UITraitDefinition {
    static let defaultValue: AppModel? = nil
}

// Add convenient accessors
extension UITraitCollection {
    var appModel: AppModel? {
        self[AppModelTrait.self]
    }
}

extension UIMutableTraits {
    var appModel: AppModel? {
        get { self[AppModelTrait.self] }
        set { self[AppModelTrait.self] = newValue }
    }
}

Injecting the Model

At your app’s root (usually in your scene delegate or root view controller), inject the model:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    let appModel = AppModel()
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        window = UIWindow(windowScene: windowScene)
        let rootViewController = MainTabBarController()
        
        // Inject the app model into the trait system
        rootViewController.traitOverrides.appModel = appModel
        
        window?.rootViewController = rootViewController
        window?.makeKeyAndVisible()
    }
}

Observing Changes Anywhere

Now the magic part - any view controller in your hierarchy can access and observe the model:

class ProfileViewController: UIViewController {
    let nameLabel = UILabel()
    let statusIndicator = UIView()
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        guard let model = traitCollection.appModel else { return }
        
        // These automatically update when model properties change!
        nameLabel.text = model.currentUser?.name ?? "Guest"
        statusIndicator.backgroundColor = model.isOnline ? .systemGreen : .systemRed
        
        // Theme updates
        view.backgroundColor = model.theme.backgroundColor
        nameLabel.textColor = model.theme.textColor
    }
}

No delegates. No notifications. No manual updates. Change appModel.currentUser anywhere in your app, and every view observing it updates automatically.

Resources