Just when I thought I'd understood Swift Concurrency fairly well, I came up with an example project that made me question my knowledge of several of its mechanics.
Given the following ViewModel that is observed by a SwiftUI View, and compiled with Swift 6:
protocol ContentViewModel {
var content: String { get }
var buttonTitle: String { get }
var showLoadingIndicator: Bool { get }
@MainActor func onButtonPress() async throws
}
@Observable final class DefaultContentViewModel: ContentViewModel {
let interactor: any ContentInteractor
var content: String = "Lorem ipsum."
let buttonTitle: String = "Perform Something"
var showLoadingIndicator: Bool = false
init(interactor: any ContentInteractor) {
self.interactor = interactor
NSLog("DefaultContentViewModel initialized.")
}
func onButtonPress() async throws {
showLoadingIndicator = true
Task { [weak self] in // 1, 2, 3
guard let content = self?.content else { return }
self?.content = try await self?.interactor.loadContent(for: content) ?? ""
self?.showLoadingIndicator = false
}
}
}
I am not able to explain these points:
- Does a weak reference to
self
actually avoid unwanted behavior here? I have been seeing this a lot in code recently. However, my vague understanding of the documentation is that this is unnecessary. - The wrapping
Task
fixes the error "Sending 'self.interactor' risks causing data races". This is the main part that I do not understand. Doesn't the Task use the same concurrency context as the async function that creates it? - How exactly is this different to a detached Task and why are both an option?
I believe my main source of confusion is how the word "context" is thrown around in discussions surrounding Swift Concurrency - by myself included.