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

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


Printed on: January 21, 2025

On iOS 17 and newer, you could have entry to the Observable macro. This macro may be utilized to courses, and it permits SwiftUI to formally observe properties on an observable class. If you wish to be taught extra about Observable or for those who’re on the lookout 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. Nonetheless, that does not imply we can’t observe observable properties.

A easy remark instance

The Observable macro was constructed to lean right into a operate known as WithObservationTracking. The WithObservationTracking operate lets you 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 known 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 way in which remark works is that any properties that I entry within that first closure can be marked as properties that I am excited by. So if any of these properties change, on this case there’s just one, the onChange closure can be known 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 could cause points

Whereas this seems 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 is because withObservationTracking solely calls the onChange closure as soon as. So as soon as the closure known as, 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 excited by, 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, for those who’re observing a String and also you wish to carry out a search motion when the textual content modifications, you’d 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 known as with willSet semantics.

Because of this the onChange closure known as earlier than the properties you’re excited by have modified so you are going to at all times have entry to the previous worth of a property and never the brand new one.

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

Getting didSet semantics when utilizing withObservationTracking

Since onChange known as earlier than the properties we’re excited by have up to date we have to postpone our work to the following runloop if we wish to get entry to new values. A standard means to do that is through the use of DispatchQueue.important.async:

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

The above isn’t fairly, but it surely 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.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 straightforward 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 hold operating each time one in all these properties change. As a result of we’re capturing self weakly and we solely entry any properties when self continues to be round, we additionally assist some type of cancellation.

Observe that my method is moderately completely 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 truly would not assist any type of cancellation. I figured that including a bit of little bit of assist for cancellation was higher than including no assist in any respect.

Commentary and Swift 6

Whereas the above works fairly first rate for Swift 5 packages, for those who attempt to use this within a Swift 6 codebase, you may truly 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 needs 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 circumstances 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 capabilities on the principle actor. Typically this isn’t a nasty concept in any respect, however after 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 }
      // Principal 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 Process that additionally runs on the principle 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 executed for it to be usable from different locations. General I feel Mix’s publishers (and @Printed specifically) present a extra usable option to subscribe to modifications on a particular property; particularly while you wish 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).

In the event you’re utilizing withObservationTracking efficiently in a Swift 6 app or bundle, I’d like to hear from you.

author avatar
roosho Senior Engineer (Technical Services)
I am Rakib Raihan RooSho, Jack of all IT Trades. You got it right. Good for nothing. I try a lot of things and fail more than that. That's how I learn. Whenever I succeed, I note that in my cookbook. Eventually, that became my blog. 
rooshohttps://www.roosho.com
I am Rakib Raihan RooSho, Jack of all IT Trades. You got it right. Good for nothing. I try a lot of things and fail more than that. That's how I learn. Whenever I succeed, I note that in my cookbook. Eventually, that became my blog. 

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here


Latest Articles

author avatar
roosho Senior Engineer (Technical Services)
I am Rakib Raihan RooSho, Jack of all IT Trades. You got it right. Good for nothing. I try a lot of things and fail more than that. That's how I learn. Whenever I succeed, I note that in my cookbook. Eventually, that became my blog.