如何在ViewModel中取消长期任务?

huangapple go评论51阅读模式
英文:

How to cancel a long term Task in ViewModel?

问题

以下是翻译好的部分:

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.

这是一个简单的测试,但并不简单。HomeView将TestView显示为一个sheet,TestView将几乎所有操作(不包括.task、onReceive等)隐藏到TestViewModel中,TestViewModel将检测设备方向并在TestView上显示它。当关闭TestView时,停止检测。

Almost works fine but when dismiss TestView, print("TestViewModel deinit.") not happened, and the detection is still working.

几乎一切正常,但当关闭TestView时,"TestViewModel deinit."并未发生,检测仍在运行。

I think Task have a reference of TestViewModel, that causes TestViewModel cannot release. But how to fix?

我认为Task引用了TestViewModel,导致TestViewModel无法释放。但如何修复呢?

OK, next question is how to cancel the Task in TestViewModel (not in TestView) when dismiss TestView?

好的,下一个问题是在关闭TestView时如何取消TestViewModel中的Task(而不是在TestView中)?

Any suggestion?

有什么建议吗?

英文:

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
        }
    }
}

答案1

得分: -1

你需要自行管理任务的取消:在TaskViewModel中保留对后台任务的引用,并在对象销毁时取消任务。

@MainActor
final class TestViewModel: ObservableObject {
    @Published var isPortrait = false
    private var task: Task<Void, Never>? = nil
    
    init() {
        print("TestViewModel init.")
        setup()
    }
    
    deinit {
        print("TestViewModel deinit.")
        task?.cancel()
        task = nil
    }
    
    func setup() {
        guard task == nil else { return }
        
        task = Task { [weak self] in
            let sequence = NotificationCenter.default.notifications(
                named: UIDevice.orientationDidChangeNotification
            )
            .map { _ in
                await UIDevice.current.orientation
            }
            
            for await value in sequence {
                if Task.isCancelled {
                    print("Task cancelled")
                    return
                }
                
                print("orientationDidChangeNotification changed, orientation = \(value).")
                self?.isPortrait = value == .portrait
            }
            
            print("Observation ended")
        }
    }
}

正如你所猜测的,必须在启动后台任务时将self视为弱引用,否则会在内存中保持两个对象的引用循环。

我没有找到很好文档化的一点是,由.notification()方法返回的异步迭代器似乎处理了取消,因此,当任务被取消时,迭代器会返回nil,并且for循环会结束。

话虽如此,.task()视图修饰符是实现你尝试做的事情的最便捷和更安全的方式。它专门为此目的设计。

英文:

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.

@MainActor
final class TestViewModel: ObservableObject {
    @Published var isPortrait = false
    private var task: Task<Void, Never>? = nil
    
    init() {
        print("TestViewModel init.")
        setup()
    }
    
    deinit {
        print("TestViewModel deinit.")
        task?.cancel()
        task = nil
    }
    
    func setup() {
        guard task == nil else { return }
        
        task = Task { [weak self] in
            let sequence = NotificationCenter.default.notifications(
                named: UIDevice.orientationDidChangeNotification
            )
            .map { _ in
                await UIDevice.current.orientation
            }
            
            for await value in sequence {
                if Task.isCancelled {
                    print("Task cancelled")
                    return
                }
                
                print("orientationDidChangeNotification changed, orientation = \(value).")
                self?.isPortrait = value == .portrait
            }
            
            print("Observation ended")
        }
    }
}

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.

huangapple
  • 本文由 发表于 2023年2月24日 15:58:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/75553924.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定