This is simple test but not simple. HomeView show TestView as a sheet, TestView will hide almost actions (no .task, onReceive...) into TestViewModel, TestViewModel will detect device orientation and show it on TestView. when dismiss TestView, stop the detection.
Almost works fine but when dismiss TestView, print("TestViewModel deinit.") not happened, and the detection is still working.
I think Task have a reference of TestViewModel, that causes TestViewModel cannot release. But how to fix?
OK, next question is how to cancel the Task in TestViewModel(not in TestView) when dismiss TestView?
Any suggestion?
struct HomeView: View {
@State var showTestView = false
var body: some View {
Button("Show Test View") {
showTestView = true
}.sheet(isPresented: $showTestView) {
TestView()
}
}
}
struct TestView: View {
@StateObject private var vm = TestViewModel()
@Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Text("isPortrait = \(vm.isPortrait.description)")
Button("Dismiss") {
dismiss()
}
}
.onDisappear {
print("TestView onDisappear.")
}
}
}
@MainActor
class TestViewModel: ObservableObject {
@Published var isPortrait = false
init() {
print("TestViewModel init.")
setup()
}
deinit {
print("TestViewModel deinit.")
}
func setup() {
Task {
await observeNotification()
}
}
private func observeNotification() async {
let sequence = NotificationCenter.default.notifications(named: UIDevice.orientationDidChangeNotification)
.map { _ in await UIDevice.current.orientation }
for await value in sequence {
print("orientationDidChangeNotification changed, orientation = \(value).")
isPortrait = value == .portrait
}
}
}
You need to manage the cancellation of the task yourself: keep a reference to the background task in
TaskViewModel
and cancel the task when the object is destroyed.As you guessed, you must treat
self
as a weak reference inside the fire up background task, otherwise a reference cycle will keep both objects alive in memory.A behavior I did not find well documented is that the async iterator returned by the
.notification()
method seems to handle cancellation, as such, the iterator returns nil when the task is cancelled and the for loop is ended.That being said, the
.task()
view modifier is the most convenient and a safer way to achieve what you are trying to do. It is designed specifically for this purpose.