@Observable in command line app

I have a problem with the following code, I am not being notified of changes to the progress property of my Job object, which is @Observable... This is a command-line Mac application (the same code works fine in a SwiftUI application).

I must have missed something?

do {
    let job = AsyncJob()
    
    withObservationTracking {
        let progress = job.progress
    } onChange: {
        print("Current progress: \(job.progress)")
    }
    
    let _ = try await job.run()
    
    print("Done...")
} catch {
    print(error)
}

I Try this without any success:

@main
struct MyApp {
    static func main() async throws {
        // my code here
    }
}
Answered by DTS Engineer in 810612022

This is working for me.

The snippet you posted is missing the bits necessary to run it, so I filled those in as best I could. Pasted in at the end you’ll find the resulting code. I built this with Xcode 16.0 and ran it on macOS 14.7. Here’s what I see:

apply, 0
will do some work
change
did do some work
will do some work
did do some work
will do some work
did do some work

As you can see, the change handler fired when the run() method updated progress.

It only fired once, but such is the nature of withObservationTracking(_:onChange:): You have to re-observe each time.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


import Foundation
import Observation

@Observable
class AsyncJob {
    var progress: Int = 0

    func run() async throws {
        for _ in 0..<3 {
            print("will do some work")
            try await Task.sleep(for: .seconds(1))
            self.progress += 1
            print("did do some work")
        }
    }
}

func main() async {
    do {
        let job = AsyncJob()
        
        withObservationTracking {
            print("apply, \(job.progress)")
        } onChange: {
            print("change")
        }
        
        try await job.run()
    } catch {
        print(error)
    }
}

await main()
Accepted Answer

This is working for me.

The snippet you posted is missing the bits necessary to run it, so I filled those in as best I could. Pasted in at the end you’ll find the resulting code. I built this with Xcode 16.0 and ran it on macOS 14.7. Here’s what I see:

apply, 0
will do some work
change
did do some work
will do some work
did do some work
will do some work
did do some work

As you can see, the change handler fired when the run() method updated progress.

It only fired once, but such is the nature of withObservationTracking(_:onChange:): You have to re-observe each time.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


import Foundation
import Observation

@Observable
class AsyncJob {
    var progress: Int = 0

    func run() async throws {
        for _ in 0..<3 {
            print("will do some work")
            try await Task.sleep(for: .seconds(1))
            self.progress += 1
            print("did do some work")
        }
    }
}

func main() async {
    do {
        let job = AsyncJob()
        
        withObservationTracking {
            print("apply, \(job.progress)")
        } onChange: {
            print("change")
        }
        
        try await job.run()
    } catch {
        print(error)
    }
}

await main()

@Quinn Thank you for your clear and precise answer, which helped me understand my mistake: I had misunderstood how withObservationTracking works. It only observes the state change of the observed property once (I thought it was doing so continuously).

Does the following code seem correct to you? (I added the observe method)

import Foundation
import Observation

@Observable
class AsyncJob {
    var progress: Int = 0

    func run() async throws {
        for _ in 0..<3 {
            print("will do some work")
            try await Task.sleep(for: .seconds(1))
            self.progress += 1
            print("did do some work")
        }
    }
}

func observe(job: AsyncJob) {
    withObservationTracking {
        print("apply, \(job.progress)")
    } onChange: {
        observe(job: job)
    }
}

func main() async {
    do {
        let job = AsyncJob()
        
        observe(job: job)
        
        try await job.run()
    } catch {
        print(error)
    }
}

await main()

Yeah, that’s a common pitfall.

Does the following code seem correct to you?

Yes. And no (-: More below.

I added the observe method

Yeah, this recursive structure is the standard way of handling this.

The only problem with your code is that it falls foul of Swift 6’s data race checking:

observe(job: job)
          // ^ Capture of 'job' with non-sendable type 'AsyncJob' in a `@Sendable` closure

It’s hard for me to recommend a fix for that, because it’s tied into your overall concurrency strategy.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you again for your response! Indeed, you are right, concurrent access management is missing.

Does this seem correct to you? (It's not easy, the subject seems complex to me, I can't use NSLock in an asynchronous context or an Actor in this case...) What would you recommend in an architecture of this type?

import Foundation
import Observation

@Observable
class AsyncJob: @unchecked Sendable {
    var progress: Int = 0

    let serialQueue = DispatchQueue(label: "serial.queue")
    
    func run() async throws {
        for _ in 0..<3 {
            try await Task.sleep(for: .seconds(1))
           
            serialQueue.async {
                self.progress += 1
            }
        }
    }
}

func observe(job: AsyncJob) {
    withObservationTracking {
        print("apply, \(job.progress)")
    } onChange: {
        observe(job: job)
    }
}

func main() async {
    do {
        let job = AsyncJob()
        
        observe(job: job)
        
        try await job.run()
    } catch {
        print(error)
    }
}

await main()

That seems rather convoluted )-:

But you’re right, this isn’t necessarily easy. That’s because there are a wide range of potential ways to integrate this, and the best approach is going to depend on the concurrent strategy of the observer.

Can you explain more about how you’re dealing with concurrency in the wider program?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

The topic is very interesting, and I humbly acknowledge that I haven’t grasped all the subtleties of Swift 6 (I’ll need to improve my skills on this subject).

Here’s more information: my application takes input data and generates an output file. The file generation (which is complex) is carried out on a secondary thread (old school with GCD). Compatibility with Swift Concurrency is handled through a facade using withCheckedThrowingContinuation.

The processes performed on the secondary thread (file creation) require configuration data that is used solely in a read-only way (the secondary thread does not modify this data, which is encapsulated in an object).

In short, it looks something like this (names have been simplified for the example):

  • Job (file creation on a secondary thread using GCD)
  • AsyncJob (a facade over Job to ensure compatibility with Swift Concurrency)
  • Configuration (data needed for the job processing)

Code example:

do {
	let configuration = Configuration(...)

	let job = AsyncJob(configuration)

	try await job.run()
} catch {
	print(error)
}

To be honest, I haven’t established a specific strategy regarding concurrent access in my application, as the configuration is not modified and is only used by AsyncJob (but perhaps this is a mistake on my part?).

In light of our discussion, I’m wondering what would be the best approach if I had multiple jobs using shared data that could be modified?

I haven’t grasped all the subtleties of Swift 6

I think that’s fair to say of the entire Swift community. We’re all coming up to speed on this together.

It’s also fair to say that some of the pieces are still missing. One of those things is a really good solution to the issue of progress reporting (-:

However, in this case I think you’ll be OK. I see two significant challenges here:

  • Configuration

  • Progress reporting

Lemme tackle each in turn.


You need to create a configuration value in Swift Concurrency Land™ and use it in GCD Land™. If I were in your shoes I’d give your configuration value semantics. And the easiest way is to make it a struct. A typical struct is sendable because all of its components are sendable.

This assumes that the configuration is fairly straightforward. If it’s complex, or very large, you may need something more complex.


With regards progress, this is one are where Swift concurrency is weak right now. However, there’s a reasonable solution for your setup, namely to create an async stream:

  • Have Swift Land create the stream using AsyncStreammakeStream(of:bufferingPolicy:).

  • And then monitor it like it would any other stream of events.

  • Then have GCD Land issue progress updates to the stream.

This works because the continuation for the stream is sendable, so you can freely pass it off to GCD Land.

The biggest complication with AsyncStream is the lack of flow control, but that’s not an issue for progress reporting because the receiver only cares about the latest value. So, you can create yous stream like this:

let (stream, continuation) = AsyncStream.makeStream(of: Int.self, bufferingPolicy: .bufferingNewest(1))

Another concern is cancellation. What you do about that depends on whether you have an existing cancellation mechanism. If you do, you can continue using it and just ignore the whole issue of cancellation. If not, you could use the stream as you cancellation mechanism.

Lemme see if I can put something together that’ll illustrate this…

OK, yeah, see the code below.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


import Foundation

class Job {
    func run(
        progressHandler: @Sendable @escaping (Int) -> Void,
        completionHandler: @Sendable @escaping () -> Void
    ) {
        // IMPORTANT: I’m using a global concurrent queue here because it’s a
        // simple test project. In Real Code™ it’s best to avoid those.  See
        // “Avoid Dispatch Global Concurrent Queues" for the details.
        //
        // <https://developer.apple.com/forums/thread/711736>
        DispatchQueue.global().async {
            for i in 0..<5 {
                progressHandler(i)
                Thread.sleep(forTimeInterval: 1.0)
            }
            completionHandler()
        }
    }
}

class AsyncJob {

    init() {
        (self.progress, self.progressContinuation) = AsyncStream.makeStream(of: Int.self, bufferingPolicy: .bufferingNewest(1))
    }

    let progress: AsyncStream<Int>
    private let progressContinuation: AsyncStream<Int>.Continuation

    func run() async {
        // Note that I create a local copy of `progressContinuation` here so
        // that the various closures below capture this sendable value, not
        // `self`.
        let progressContinuation = self.progressContinuation
        await withCheckedContinuation { continuation in
            let j = Job()
            j.run(
                progressHandler: { progressContinuation.yield($0) },
                completionHandler: { continuation.resume() }
            )
        }
    }
}

@MainActor
func main() async {
    let j = AsyncJob()
    Task {
        for await i in j.progress {
            print("progress: \(i)")
        }
    }
    print("will run")
    await j.run()
    print("did run")
}

await main()

Thank you again for your help! Your advice will allow me to improve my architecture and my vision for Swift 6.

I really appreciate your approach, and the use of AsyncStream (which I wasn’t familiar with) seems well-suited for event transmission in an async-await context.

My application handles cancellation at the lowest level using the cancel() method of the Job object. The facade (AsyncJob object) exposes the same API. The implementation of cancellation is done with a traditional approach, which involves constantly checking if the user has requested cancellation of the job within the processing code block.

Otherwise, I usually manage cancellation in an async-await context by keeping a reference to the task and calling task?.cancel.

What approach do you recommend?

What approach do you recommend?

If you’re using Swift concurrency, I generally recommend that you use its cancellation support. However, things get tricky when you’re bridging to existing code that uses some other cancellation model. That’s why I brought this up.

You can absolutely do this in your bridge; you just have to deploy a cancellation handler:

await withTaskCancellationHandler {
    … start the operation …
} onCancel: {
    … pass cancellation through to the operation ;
}

However, it introduces another complication when it comes to concurrency, because the cancellation handler has to be sendable.

So, if you’re already taking care of cancellation via some other mechanism, that’s cool. If not, you have to factor cancellation into your approach, making sure there’s some way for that sendable cancellation handler to do that necessary cancellation.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you, you've helped me a lot!!

@Observable in command line app
 
 
Q