12 C
Canberra
Thursday, October 30, 2025

Observing properties on an @Observable class exterior of SwiftUI views – Donny Wals


Revealed on: January 21, 2025

On iOS 17 and newer, you might have entry to the Observable macro. This macro might be utilized to lessons, and it permits SwiftUI to formally observe properties on an observable class. If you wish to be taught extra about Observable or if you happen to’re searching for an introduction, undoubtedly go forward and take a look at my introduction to @Observable in SwiftUI.

On this publish, I want to discover how one can observe properties on an observable class. Whereas the ObservableObject protocol allowed us to simply observe printed properties, we do not have one thing like that with Observable. Nevertheless, that does not imply we can not observe observable properties.

A easy remark instance

The Observable macro was constructed to lean right into a operate referred to as WithObservationTracking. The WithObservationTracking operate permits you to entry state in your observable. The observable will then monitor the properties that you have accessed within that closure. If any of the properties that you have tried to entry change, there is a closure that will get referred to as. This is what that appears like.

@Observable
class Counter {
  var depend = 0
}

class CounterObserver {
  let counter: Counter

  init(counter: Counter) {
    self.counter = counter
  }

  func observe() {
    withObservationTracking { 
      print("counter.depend: (counter.depend)")
    } onChange: {
      self.observe()
    }
  }
}

Within the observe operate that’s outlined on CounterObserver, I entry a property on the counter object.

The best way remark works is that any properties that I entry within that first closure can be marked as properties that I am keen on. So if any of these properties change, on this case there’s just one, the onChange closure can be referred to as to tell you that there have been modifications made to a number of properties that you have accessed within the first closure.

How withObservationTracking may cause points

Whereas this appears easy sufficient, there are literally a number of irritating hiccups to take care of while you work with remark monitoring. Observe that in my onChange I name self.observe().

It’s because withObservationTracking solely calls the onChange closure as soon as. So as soon as the closure is named, you don’t get notified about any new updates. So I must name observe once more to as soon as extra entry properties that I am keen on, after which have my onChange fireplace once more when the properties change.

The sample right here basically is to utilize the state you’re observing in that first closure.

For instance, if you happen to’re observing a String and also you need to carry out a search motion when the textual content modifications, you’ll try this within withObservationTracking‘s first closure. Then when modifications happen, you possibly can re-subscribe from the onChange closure.

Whereas all of this isn’t nice, the worst half is that onChange is named with willSet semantics.

Which means that the onChange closure is named earlier than the properties you’re keen on have modified so you are going to at all times have entry to the outdated worth of a property and never the brand new one.

You can work round this by calling observe from a name to DispatchQueue.most important.async.

Getting didSet semantics when utilizing withObservationTracking

Since onChange is named earlier than the properties we’re keen on have up to date we have to postpone our work to the subsequent runloop if we need to get entry to new values. A standard approach to do that is by utilizing DispatchQueue.most important.async:

func observe() {
  withObservationTracking { 
    print("counter.depend: (counter.depend)")
  } onChange: {
    DispatchQueue.most important.async {
      self.observe()
    }
  }
}

The above isn’t fairly, nevertheless it works. Utilizing an method primarily based on what’s proven right here on the Swift boards, we will transfer this code right into a helper operate to scale back boilerplate:

public func withObservationTracking(execute: @Sendable @escaping () -> Void) {
    Commentary.withObservationTracking {
        execute()
    } onChange: {
        DispatchQueue.most important.async {
            withObservationTracking(execute: execute)
        }
    }
}

The utilization of this operate within observe() would look as follows:

func observe() {
  withObservationTracking { [weak self] in
    guard let self else { return }
    print("counter.depend: (counter.depend)")
  }
}

With this easy wrapper that we wrote, we will now go a single closure to withObservationTracking. Any properties that we have accessed within that closure are actually robotically noticed for modifications, and our closure will maintain operating each time considered one of these properties change. As a result of we’re capturing self weakly and we solely entry any properties when self remains to be round, we additionally help some type of cancellation.

Observe that my method is moderately totally different from what’s proven on the Swift boards. It is impressed by what’s proven there, however the implementation proven on the discussion board really does not help any type of cancellation. I figured that including a bit of little bit of help for cancellation was higher than including no help in any respect.

Commentary and Swift 6

Whereas the above works fairly respectable for Swift 5 packages, if you happen to attempt to use this within a Swift 6 codebase, you may really run into some points… As quickly as you activate the Swift 6 language mode you’ll discover the next error:

func observe() {
  withObservationTracking { [weak self] in
    guard let self else { return }
    // Seize of 'self' with non-sendable sort 'CounterObserver?' in a `@Sendable` closure
    print("counter.depend: (counter.depend)")
  }
}

The error message you’re seeing right here tells you that withObservationTracking desires us to go an @Sendable closure which implies we will’t seize non-Sendable state (learn this publish for an in-depth clarification of that error). We will’t change the closure to be non-Sendable as a result of we’re utilizing it within the onChange closure of the official withObservationTracking and as you may need guessed; onChange requires our closure to be sendable.

In lots of instances we’re in a position to make self Sendable by annotating it with @MainActor so the item at all times runs its property entry and features on the primary actor. Generally this isn’t a foul thought in any respect, however once we try to apply it on our instance we obtain the next error:

@MainActor
class CounterObserver {
  let counter: Counter

  init(counter: Counter) {
    self.counter = counter
  }

  func observe() {
    withObservationTracking { [weak self] in
      guard let self else { return }
      // Most important actor-isolated property 'counter' can't be referenced from a Sendable closure
      print("counter.depend: (counter.depend)")
    }
  }
}

We will make our code compile by wrapping entry in a Activity that additionally runs on the primary actor however the results of doing that’s that we’d asynchronously entry our counter and we’ll drop incoming occasions.

Sadly, I haven’t discovered an answer to utilizing Commentary with Swift 6 on this method with out leveraging @unchecked Sendable since we will’t make CounterObserver conform to Sendable for the reason that @Observable class we’re accessing can’t be made Sendable itself (it has mutable state).

In Abstract

Whereas Commentary works improbable for SwiftUI apps, there’s lots of work to be achieved for it to be usable from different locations. Total I believe Mix’s publishers (and @Revealed particularly) present a extra usable method to subscribe to modifications on a particular property; particularly while you need to use the Swift 6 language mode.

I hope this publish has proven you some choices for utilizing Commentary, and that it has shed some mild on the problems you would possibly encounter (and how one can work round them).

Should you’re utilizing withObservationTracking efficiently in a Swift 6 app or package deal, I’d like to hear from you.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

[td_block_social_counter facebook="tagdiv" twitter="tagdivofficial" youtube="tagdiv" style="style8 td-social-boxed td-social-font-icons" tdc_css="eyJhbGwiOnsibWFyZ2luLWJvdHRvbSI6IjM4IiwiZGlzcGxheSI6IiJ9LCJwb3J0cmFpdCI6eyJtYXJnaW4tYm90dG9tIjoiMzAiLCJkaXNwbGF5IjoiIn0sInBvcnRyYWl0X21heF93aWR0aCI6MTAxOCwicG9ydHJhaXRfbWluX3dpZHRoIjo3Njh9" custom_title="Stay Connected" block_template_id="td_block_template_8" f_header_font_family="712" f_header_font_transform="uppercase" f_header_font_weight="500" f_header_font_size="17" border_color="#dd3333"]
- Advertisement -spot_img

Latest Articles