I think there's a logic error in Apple SwiftUI tutorial Scrumdinger.

Hi.

In Apple's SwiftUI Tutorial Scrumdinger, there's a logic that read and save a file in asynchronous function to keep the App's UI responsive.

The tutorial says,

Reading from the file system can be slow. To keep the interface responsive, you’ll write an asynchronous function to load data from the file system. Making the function asynchronous lets the system efficiently prioritize updating the user interface instead of sitting idle and waiting while the file system reads data.

And here's code.

@MainActor
class ScrumStore: ObservableObject {
    @Published var scrums: [DailyScrum] = []

    func load() async throws {
        let task = Task<[DailyScrum], Error> {
            let fileURL = try Self.fileURL()
            guard let data = try? Data(contentsOf: fileURL) else {
                return []
            }
            let dailyScrums = try JSONDecoder().decode([DailyScrum].self, from: data)
            return dailyScrums
        }
        let scrums = try await task.value
    }

    func save(scrums: [DailyScrum]) async throws {
        let task = Task {
            let data = try JSONEncoder().encode(scrums)
            let outfile = try Self.fileURL()
            try data.write(to: outfile)
        }
        _ = try await task.value
    }
}

But the problem i think here is that the ScrumStore is Mainactor isolated annotated, the 'load' and 'save' method execute their Tasks in the main thread, so I think there's not much benefit we can get comparing using a synchronous method. The proper way I think here is that use 'Task.detached' initializer instead of 'Task' initializer, so that the Task doesn't isolated in MainActor context. If there's anything I understand wrong or miss, please let me know.

Apple's Scrumdinger Tutorial Link

I think there's not much benefit we can get comparing using a synchronous method.

Agreed. I’d appreciate you filing a bug against the tutorial. Please post your bug number, just for the record.

If you’re unsure about this stuff, one way to check your logic is to access a property that’s isolated to the main actor. For example, I added this to the ScrumStore class:

var counter = 0

and then added:

self.counter += 1

to each task body. Note how the compiler doesn’t complain, even with strict concurrency checking enabled. Now if I change the task within save(scrums:) to use Task.detache(…), the compiler complains with Main actor-isolated property 'counter' can not be mutated from a Sendable closure.

The proper way I think here is that use Task.detached initializer

That’s one way to do it. As to whether it’s the proper way, that kinda depends. My general advice is that you avoid Task, either detached or not, because it’s unstructured. Given that the load() and save(scrums:) method are already async, you can take advantage of structured concurrency. In that case that’s as simple as calling an async function in some context that’s not main-actor-isolated.

ps If you’re still coming up to speed on this, I strongly recommend WWDC 2021 Session 10134 Explore structured concurrency in Swift. Specifically, the chart at 26:22 is super useful.

Share and Enjoy

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

I think there's a logic error in Apple SwiftUI tutorial Scrumdinger.
 
 
Q