SwiftUI style recomposition in AppKit/UIKit

Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention

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!

Rather than quote the full article–which I think is pretty damned neat (and an admission that SwiftUI doesn’t handle all UI cases; there are times when you need to build your app using AppKit or UIKit)–I’ll summarize the key points here:

New reactive behavior in UIKit/AppKit: iOS 18 and macOS 15 include a hidden feature that automatically tracks property access on @Observable model objects and updates UI views when those properties change — similar to how SwiftUI’s reactive system works. It eliminates the need for manual UI refresh calls like setNeedsDisplay() or calling update methods explicitly.

How it works: When you read observable properties inside specific lifecycle methods (such as viewWillLayoutSubviews(), layoutSubviews(), or, on iOS 26+, the new updateProperties()), the system tracks those property accesses. Later, if those properties change anywhere in the app, the framework automatically re-invokes the method to refresh the UI.

For example, from some code I’m working on:

	override func viewWillLayout()
	{
		super.viewWillLayout()
		
		// Hook up ImportViewModel to our state
		reloadCameraMenu(importViewModel.cameras)
	}

As importViewModel is declared:

@Observable @MainActor
class ImportViewModel: NSObject, ICDeviceBrowserDelegate
{
	var cameras: [CameraSource] = []
	...
}

Any changes made to the cameras array will automatically trigger viewWillLayout to re-run.

Enabling the feature: On iOS 18 and macOS 15 this automatic tracking isn’t enabled by default but can be turned on by adding UIObservationTrackingEnabled (iOS) or NSObservationTrackingEnabled (macOS) as true in your app’s Info.plist. On iOS 26, macOS 26 and later it’s enabled by default.

Note: watchOS and tvOS, as well as iPadOS, are basically UIKit, so these changes should also be available there as well.

Where Observation Tracking Works: Generally you want to put your observation code in viewWillLayoutSubviews in UIKit, or viewWillLayout in AppKit. But observation is also supported in all of the methods listed below. Also note that updateProperties() is a new method in iOS 26/macOS 26 created for this purpose.

UIKit:

  • UIView
    • updateProperties() (iOS 26+)
    • layoutSubviews()
    • updateConstrants()
    • draw(_:)
  • UIViewController
    • updateProperties() (iOS 26+)
    • viewWillLayoutSubviews()
    • viewDidLayoutSubviews()
    • updateViewConstraints()
    • updateContentUnavailableConfiguration(using:)
  • UIPresentationController
    • containerViewWillLayoutSubviews()
    • containerViewDidLayoutSubviews()
  • UIButton
    • updateConfiguration() (iOS 26+)
    • When executing the configurationUpdateHandler
  • UICollectionViewCell, UITableViewCell, UITableViewHeaderFooterView
    • updateConfiguration(using:) (iOS 26+)
    • When executing the configurationUpdateHandler

AppKit: (Note: I’m unable to find the Apple documentation, but I’ve verified these indeed work.)

  • NSView
    • updateConstraints()
    • layout()
    • updateLayer()
    • draw(_:)
  • NSViewController
    • updateViewConstraints()
    • viewWillLayout()
    • viewDidLayout()

Gotchas: Of course with great power comes great responsibility. Rather than go into detail, here’s a quick list of things to watch out for:

  • Only in specific methods: Only property access inside supported lifecycle/update methods (e.g., viewWillLayoutSubviews(), layoutSubviews(), updateProperties(), cell configuration handlers) is tracked. Properties accessed elsewhere won’t trigger automatic updates.
  • Execution frequency: These methods may run often (e.g., on every layout pass). If you do expensive computations there, performance can suffer; consider caching results.
  • Memory retention: Observable objects are retained while being observed, which can lead to retain cycles if you’re not careful with references.
  • Thread safety: Although @Observable itself is designed to be thread-safe, updating observable properties from background threads can produce inconsistent UI states. Mutations should generally occur on the main thread.
  • Avoid artificial property access: Pre-accessing all properties just to “force” observation creates unnecessary dependencies and undermines the efficiency of automatic tracking. Only access what the UI actually needs.

As they say, read the original article for more information. And of course, when in doubt, refer to the original Apple Documentation.

Leave a comment