GlobalActor指令不能保证该演员会调用功能

发布于 2025-02-13 22:17:09 字数 1559 浏览 1 评论 0原文

假设我已经定义了一个全球演员:

@globalActor actor MyActor {
    static let shared = MyActor()
}

并且我有一个类,其中几种方法需要采取以下操作:

class MyClass {

    @MyActor func doSomething(undoManager: UndoManager) {

        // Do something here
   
        undoManager?.registerUndo(withTarget: self) { 
            $0.reverseSomething(undoManager: UndoManager)
        }
    }

    @MyActor func reverseSomething(undoManager: UndoManager) {

        // Do the reverse of something here

        print(\(Thread.isMainThread) /// Prints true when called from undo stack
   
        undoManager?.registerUndo(withTarget: self) { 
            $0.doSomething(undoManager: UndoManager)
        }
    }
}

假设代码从swiftui视图中调用:

struct MyView: View {
   @Environment(\.undoManager) private var undoManager: UndoManager?
   let myObject: MyClass

   var body: some View {
        Button("Do something") { myObject.doSomething(undoManager: undoManager) }
   }
}

请注意,当操作被撤销时,“反向反向” func是打电话给主要线程。防止这种情况包裹在任务中的正确方法是正确的吗?如:

    @MyActor func reverseSomething(undoManager: UndoManager) {

        // Do the reverse of something here

        print(\(Thread.isMainThread) /// Prints true
   
        undoManager?.registerUndo(withTarget: self) { 
            Task { $0.doSomething(undoManager: UndoManager) }
        }
    }

Assuming I have defined a global actor:

@globalActor actor MyActor {
    static let shared = MyActor()
}

And I have a class, in which a couple of methods need to act under this:

class MyClass {

    @MyActor func doSomething(undoManager: UndoManager) {

        // Do something here
   
        undoManager?.registerUndo(withTarget: self) { 
            $0.reverseSomething(undoManager: UndoManager)
        }
    }

    @MyActor func reverseSomething(undoManager: UndoManager) {

        // Do the reverse of something here

        print(\(Thread.isMainThread) /// Prints true when called from undo stack
   
        undoManager?.registerUndo(withTarget: self) { 
            $0.doSomething(undoManager: UndoManager)
        }
    }
}

Assume the code gets called from a SwiftUI view:

struct MyView: View {
   @Environment(\.undoManager) private var undoManager: UndoManager?
   let myObject: MyClass

   var body: some View {
        Button("Do something") { myObject.doSomething(undoManager: undoManager) }
   }
}

Note that when the action is undone the 'reversing' func it is called on the MainThread. Is the correct way to prevent this to wrap the undo action in a task? As in:

    @MyActor func reverseSomething(undoManager: UndoManager) {

        // Do the reverse of something here

        print(\(Thread.isMainThread) /// Prints true
   
        undoManager?.registerUndo(withTarget: self) { 
            Task { $0.doSomething(undoManager: UndoManager) }
        }
    }

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

血之狂魔 2025-02-20 22:17:10

令我惊讶的是,编译器不会在同步的非分离上下文(即关闭)中引起全局演员为“ myActor”的实例方法的警告。看来,编译器被隔离方法中的闭合语法混淆了。

无论如何,您可以将其包裹在任务中,并且应该在适当的演员上运行:

@MyActor func doSomething(undoManager: UndoManager) {
    // Do something here

    undoManager.registerUndo(withTarget: self) { target in
        Task { @MyActor in
            target.reverseSomething(undoManager: undoManager)
        }
    }
}

就说是说,我发现了从背景中使用它时不稳定的undomanager行为线程(即,不在主要演员上)。

因此,尤其是因为撤消/重做是通常是从UI(在主线程上)启动的行为,所以我将其保留在主线程上,并且仅在另一个演员上运行所需的工作。例如:

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    @State var input: String = ""

    var body: some View {
        VStack {
            TextField(text: $input) {
                Text("enter value")
            }

            Button("Add record") {
                viewModel.addAndPrepareUndo(for: input)
                input = ""
            }.disabled(input.isEmpty)

            Button("Undo") {
                viewModel.undo()
            }.disabled(!viewModel.canUndo)

            Button("Redo") {
                viewModel.redo()
            }.disabled(!viewModel.canRedo)
        }
        .padding()
    }
}

@globalActor actor MyGlobalActor {
    static let shared = MyGlobalActor()
}

@MainActor
class ViewModel: ObservableObject {
    @MyGlobalActor
    var values: [String] = []

    @Published var canUndo = false
    @Published var canRedo = false

    private var undoManager = UndoManager()

    func undo() {
        undoManager.undo()
        updateUndoStatus()
    }

    func redo() {
        undoManager.redo()
        updateUndoStatus()
    }

    func updateUndoStatus() {
        canUndo = undoManager.canUndo
        canRedo = undoManager.canRedo
    }

    func addAndPrepareUndo(for newValue: String) {
        undoManager.registerUndo(withTarget: self) { [weak self] target in
            guard let self else { return }
            self.removeAndPrepareRedo(for: newValue)
        }
        updateUndoStatus()

        Task { @MyGlobalActor in
            values.append(newValue)
            print(#function, values)
        }
    }

    func removeAndPrepareRedo(for revertValue: String) {
        undoManager.registerUndo(withTarget: self) { [weak self] target in
            guard let self else { return }
            self.addAndPrepareUndo(for: revertValue)
        }
        updateUndoStatus()

        Task { @MyGlobalActor in
            values.removeLast()
            print(#function, values)
        }
    }
}

现在,这是一个人为的例子(对于这个简单的事情,我们不会在全球演员上只有一个数组),但希望它说明了这个想法。

或者,您可以使用非全球演员:

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    @State var input: String = ""

    var body: some View {
        VStack {
            TextField(text: $input) {
                Text("enter value")
            }

            Button("Add record") {
                viewModel.addAndPrepareUndo(for: input)
                input = ""
            }.disabled(input.isEmpty)

            Button("Undo") {
                viewModel.undo()
            }.disabled(!viewModel.canUndo)

            Button("Redo") {
                viewModel.redo()
            }.disabled(!viewModel.canRedo)
        }
        .padding()
    }
}

@MainActor
class ViewModel: ObservableObject {
    var model = Model()

    @Published var canUndo = false
    @Published var canRedo = false

    private var undoManager = UndoManager()

    func undo() {
        undoManager.undo()
        updateUndoStatus()
    }

    func redo() {
        undoManager.redo()
        updateUndoStatus()
    }

    func updateUndoStatus() {
        canUndo = undoManager.canUndo
        canRedo = undoManager.canRedo
    }

    func addAndPrepareUndo(for newValue: String) {
        undoManager.registerUndo(withTarget: self) { [weak self] target in
            guard let self else { return }
            self.removeAndPrepareRedo(for: newValue)
        }
        updateUndoStatus()

        Task {
            await model.append(newValue)
            await print(#function, model.values())
        }
    }

    func removeAndPrepareRedo(for revertValue: String) {
        undoManager.registerUndo(withTarget: self) { [weak self] target in
            guard let self else { return }
            self.addAndPrepareUndo(for: revertValue)
        }
        updateUndoStatus()

        Task {
            await model.removeLast()
            await print(#function, model.values())
        }
    }
}

actor Model {
    private var strings: [String] = []

    func append(_ string: String) {
        strings.append(string)
    }

    func removeLast() {
        strings.removeLast()
    }

    func values() -> [String] {
        strings
    }
}

I am surprised that the compiler does not generate a warning about calling a global actor 'MyActor'-isolated instance method in a synchronous nonisolated context (i.e. the closure). It would appear that the compiler is confused by the closure syntax within an actor isolated method.

Anyway, you can wrap it in a Task and it should run that on the appropriate actor:

@MyActor func doSomething(undoManager: UndoManager) {
    // Do something here

    undoManager.registerUndo(withTarget: self) { target in
        Task { @MyActor in
            target.reverseSomething(undoManager: undoManager)
        }
    }
}

That having been said, I have found erratic UndoManager behavior when using it from a background thread (i.e., not on the main actor).

So, especially because undo/redo is behavior generally initiated from the UI (on the main thread), I would keep it on the main thread, and only run the desired work on another actor. E.g.:

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    @State var input: String = ""

    var body: some View {
        VStack {
            TextField(text: $input) {
                Text("enter value")
            }

            Button("Add record") {
                viewModel.addAndPrepareUndo(for: input)
                input = ""
            }.disabled(input.isEmpty)

            Button("Undo") {
                viewModel.undo()
            }.disabled(!viewModel.canUndo)

            Button("Redo") {
                viewModel.redo()
            }.disabled(!viewModel.canRedo)
        }
        .padding()
    }
}

@globalActor actor MyGlobalActor {
    static let shared = MyGlobalActor()
}

@MainActor
class ViewModel: ObservableObject {
    @MyGlobalActor
    var values: [String] = []

    @Published var canUndo = false
    @Published var canRedo = false

    private var undoManager = UndoManager()

    func undo() {
        undoManager.undo()
        updateUndoStatus()
    }

    func redo() {
        undoManager.redo()
        updateUndoStatus()
    }

    func updateUndoStatus() {
        canUndo = undoManager.canUndo
        canRedo = undoManager.canRedo
    }

    func addAndPrepareUndo(for newValue: String) {
        undoManager.registerUndo(withTarget: self) { [weak self] target in
            guard let self else { return }
            self.removeAndPrepareRedo(for: newValue)
        }
        updateUndoStatus()

        Task { @MyGlobalActor in
            values.append(newValue)
            print(#function, values)
        }
    }

    func removeAndPrepareRedo(for revertValue: String) {
        undoManager.registerUndo(withTarget: self) { [weak self] target in
            guard let self else { return }
            self.addAndPrepareUndo(for: revertValue)
        }
        updateUndoStatus()

        Task { @MyGlobalActor in
            values.removeLast()
            print(#function, values)
        }
    }
}

Now, this is a somewhat contrived example (for something this simple, we wouldn't have a simply array on a global actor), but hopefully it illustrates the idea.

Or, you can use a non-global actor:

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    @State var input: String = ""

    var body: some View {
        VStack {
            TextField(text: $input) {
                Text("enter value")
            }

            Button("Add record") {
                viewModel.addAndPrepareUndo(for: input)
                input = ""
            }.disabled(input.isEmpty)

            Button("Undo") {
                viewModel.undo()
            }.disabled(!viewModel.canUndo)

            Button("Redo") {
                viewModel.redo()
            }.disabled(!viewModel.canRedo)
        }
        .padding()
    }
}

@MainActor
class ViewModel: ObservableObject {
    var model = Model()

    @Published var canUndo = false
    @Published var canRedo = false

    private var undoManager = UndoManager()

    func undo() {
        undoManager.undo()
        updateUndoStatus()
    }

    func redo() {
        undoManager.redo()
        updateUndoStatus()
    }

    func updateUndoStatus() {
        canUndo = undoManager.canUndo
        canRedo = undoManager.canRedo
    }

    func addAndPrepareUndo(for newValue: String) {
        undoManager.registerUndo(withTarget: self) { [weak self] target in
            guard let self else { return }
            self.removeAndPrepareRedo(for: newValue)
        }
        updateUndoStatus()

        Task {
            await model.append(newValue)
            await print(#function, model.values())
        }
    }

    func removeAndPrepareRedo(for revertValue: String) {
        undoManager.registerUndo(withTarget: self) { [weak self] target in
            guard let self else { return }
            self.addAndPrepareUndo(for: revertValue)
        }
        updateUndoStatus()

        Task {
            await model.removeLast()
            await print(#function, model.values())
        }
    }
}

actor Model {
    private var strings: [String] = []

    func append(_ string: String) {
        strings.append(string)
    }

    func removeLast() {
        strings.removeLast()
    }

    func values() -> [String] {
        strings
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文