【问题标题】:Implement PencilKit undo functionality using SwiftUI使用 SwiftUI 实现 PencilKit 撤消功能
【发布时间】:2020-06-20 21:21:10
【问题描述】:

编辑:感谢一些反馈,我已经能够部分工作(更新代码以反映当前更改)。

即使应用程序似乎按预期工作,我仍然收到“正在修改状态...”警告。如何在updateUIView 中更新视图的绘图并使用canvasViewDrawingDidChange 将新绘图推送到堆栈上而不会导致此问题?我已经尝试将它包装在一个调度调用中,但这只会创建一个无限循环。


我正在尝试在 UIViewRepresentable (PKCanvasView) 中实现撤消功能。我有一个名为 WriterView 的父 SwiftUI 视图,它包含两个按钮和画布。

这是父视图:

struct WriterView: View {
    @State var drawings: [PKDrawing] = [PKDrawing()]

    var body: some View {
        VStack(spacing: 10) {
            Button("Clear") {
                self.drawings = []
            }
            Button("Undo") {
                if !self.drawings.isEmpty {
                    self.drawings.removeLast()
                }
            }
            MyCanvas(drawings: $drawings)
        }
    }
}

这是我如何实现我的UIViewRepresentable

struct MyCanvas: UIViewRepresentable {
    @Binding var drawings: [PKDrawing]

    func makeUIView(context: Context) -> PKCanvasView {
        let canvas = PKCanvasView()
        canvas.delegate = context.coordinator
        return canvas
    }

    func updateUIView(_ uiView: PKCanvasView, context: Context) {
        uiView.drawing = self.drawings.last ?? PKDrawing()
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self._drawings)
    }

    class Coordinator: NSObject, PKCanvasViewDelegate {
        @Binding drawings: [PKDrawing]

        init(_ drawings: Binding<[PKDrawing]>) {
            self._drawings = drawings
        }

        func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
            drawings.append(canvasView.drawing)
        }
    }
}

我收到以下错误:

[SwiftUI] 在视图更新期间修改状态,这将导致未定义的行为。

大概是由我的协调员确实更改功能引起的,但我不知道如何解决这个问题。解决这个问题的最佳方法是什么?

谢谢!

【问题讨论】:

    标签: swiftui uiviewrepresentable pencilkit


    【解决方案1】:

    我终于(意外地)想出了如何使用 UndoManager 来做到这一点。我仍然不确定为什么这确实有效,因为我从来不用打电话给self.undoManager?.registerUndo()。如果您了解我为什么不需要注册活动,请发表评论。

    这是我的工作父视图:

    struct Writer: View {
        @Environment(\.undoManager) private var undoManager
        @State private var canvasView = PKCanvasView()
        
        var body: some View {
            VStack(spacing: 10) {
                Button("Clear") {
                    canvasView.drawing = PKDrawing()
                }
                Button("Undo") {
                    undoManager?.undo()
                }
                Button("Redo") {
                    undoManager?.redo()
                }
                MyCanvas(canvasView: $canvasView)
            }
        }
    }
    

    这是我的工作子视图:

    struct MyCanvas: UIViewRepresentable {
        @Binding var canvasView: PKCanvasView
        
        func makeUIView(context: Context) -> PKCanvasView {
            canvasView.drawingPolicy = .anyInput
            canvasView.tool = PKInkingTool(.pen, color: .black, width: 15)
            return canvasView
        }
    
        func updateUIView(_ canvasView: PKCanvasView, context: Context) { }
    }
    

    这当然感觉更像是 SwiftUI 的预期方法,并且肯定比我之前所做的尝试更优雅。

    【讨论】:

    • 这看起来是一个更好的解决方案。我在swiftwithmajid.com/2019/08/21/… 看到了对环境的一个很好的解释,它基本上说这是一个预定义的系统范围的环境变量,它是为每个视图传递的。一切都为你完成。在我自己的代码中,在 iPad 上,绘图工具内置了撤消/重做按钮,但在 iPhone 上没有。在 Mac (catalist) 上根本没有工具,你必须自己做,所以我认为这就是你编码的目的。
    • 唯一可以改进的方法是清除按钮可以撤消......但我现在不知道该怎么做:sad:
    【解决方案2】:

    只是为了完整性,如果你想显示 PKToolPicker,这里是我的 UIViewRepresentable。

    import Foundation
    import SwiftUI
    import PencilKit
    
    struct PKCanvasSwiftUIView : UIViewRepresentable {
    
    let canvasView = PKCanvasView()
    
    #if !targetEnvironment(macCatalyst)
    let coordinator = Coordinator()
    
    class Coordinator: NSObject, PKToolPickerObserver {
        // initial values 
        var color = UIColor.black
        var thickness = CGFloat(30)
    
        func toolPickerSelectedToolDidChange(_ toolPicker: PKToolPicker) {
            if toolPicker.selectedTool is PKInkingTool {
                let tool = toolPicker.selectedTool as! PKInkingTool
                self.color = tool.color
                self.thickness = tool.width
            }
        }
        func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
            if toolPicker.selectedTool is PKInkingTool {
                let tool = toolPicker.selectedTool as! PKInkingTool
                self.color = tool.color
                self.thickness = tool.width
            }
        }
    }
    func makeCoordinator() -> PKCanvasSwiftUIView.Coordinator {
        return Coordinator()
    }
    #endif
    
    func makeUIView(context: Context) -> PKCanvasView {
        canvasView.isOpaque = false
        canvasView.backgroundColor = UIColor.clear
        canvasView.becomeFirstResponder()
        #if !targetEnvironment(macCatalyst)
        if let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first,
            let toolPicker = PKToolPicker.shared(for: window) {
            toolPicker.addObserver(canvasView)
            toolPicker.addObserver(coordinator)
            toolPicker.setVisible(true, forFirstResponder: canvasView)
        }
        #endif
        return canvasView
    }
    
    func updateUIView(_ uiView: PKCanvasView, context: Context) {
    
    }
    } 
    

    【讨论】:

      【解决方案3】:

      我认为错误可能来自私有 func clearCanvas() 和私有函数 undoDrawing()。试试这个看看它是否有效:

      private func clearCanvas() {
       DispatchQueue.main.async {
          self.drawings = [PKDrawing()]
       }
      }
      

      同样适用于 undoDrawing()。

      如果是来自 canvasViewDrawingDidChange,同样的技巧。

      【讨论】:

      • 不幸的是,这没有帮助。我尝试通过删除清除和撤消功能来隔离问题。这似乎是updateUIViewcanvasViewDrawingDidChange 的一些问题。我想在某些时候我需要将新绘图推送到堆栈上,而不是在身体再次渲染时?
      • 我注意到你有 "[PKDrawing()]" ,应该是 [PKDrawing]()
      【解决方案4】:

      我有一些工作要做:

      struct MyCanvas: UIViewRepresentable {
      @Binding var drawings: [PKDrawing]
      
      func makeUIView(context: Context) -> PKCanvasView {
          let canvas = PKCanvasView()
          canvas.delegate = context.coordinator
          return canvas
       }
      
      func updateUIView(_ canvas: PKCanvasView, context: Context) { }
      
      func makeCoordinator() -> Coordinator {
          Coordinator(self._drawings)
      }
      
      class Coordinator: NSObject, PKCanvasViewDelegate {
          @Binding var drawings: [PKDrawing]
      
          init(_ drawings: Binding<[PKDrawing]>) {
              self._drawings = drawings
          }
      
          func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
              self.drawings.append(canvasView.drawing)
          }
      }
      }
      

      【讨论】:

      • 这几乎可以工作。该视图没有反映绘图数组,因此我将uiView.drawing = self.drawings.last ?? PKDrawing() 添加到updateUIView 函数中。即使它现在似乎可以工作,它仍然给我同样的警告。你有什么建议吗?
      • 我尝试了很多方法,例如使用@ObservedObject,但没有任何效果。我无法摆脱警告。
      【解决方案5】:

      我认为我使用不同的方法可以在没有警告的情况下工作。

      struct ContentView: View {
      
      let pkCntrl = PKCanvasController()
      
      var body: some View {
          VStack(spacing: 10) {
              Button("Clear") {
                  self.pkCntrl.clear()
              }
              Spacer()
              Button("Undo") {
                  self.pkCntrl.undoDrawing()
              }
              Spacer()
              MyCanvas(cntrl: pkCntrl)
          }
      }
      
      }
      
      struct MyCanvas: UIViewRepresentable {
      var cntrl: PKCanvasController
      
      func makeUIView(context: Context) -> PKCanvasView {
          cntrl.canvas = PKCanvasView()
          cntrl.canvas.delegate = context.coordinator
          cntrl.canvas.becomeFirstResponder()
          return cntrl.canvas
      }
      
      func updateUIView(_ uiView: PKCanvasView, context: Context) { }
      
      func makeCoordinator() -> Coordinator {
          Coordinator(self)
      }
      
      class Coordinator: NSObject, PKCanvasViewDelegate {
          var parent: MyCanvas
      
          init(_ uiView: MyCanvas) {
              self.parent = uiView
          }
      
          func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
              if !self.parent.cntrl.didRemove {
                  self.parent.cntrl.drawings.append(canvasView.drawing)
              }
          }
      }
      }
      
      class PKCanvasController {
      var canvas = PKCanvasView()
      var drawings = [PKDrawing]()
      var didRemove = false
      
      func clear() {
          canvas.drawing = PKDrawing()
          drawings = [PKDrawing]()
      }
      
      func undoDrawing() {
          if !drawings.isEmpty {
              didRemove = true
              drawings.removeLast()
              canvas.drawing = drawings.last ?? PKDrawing()
              didRemove = false
          }
      }
      }
      

      【讨论】:

      • 非常感谢!这很有效,看起来相当干净/直接。
      • 这是一个小错误,但我认为这是一个很好的起点。当您撤消,再写一些,然后再次按撤消时,会出现一些奇怪的问题。
      • 奇怪的问题来自于我们没有撤消单个笔划,更像是撤消屏幕截图。一张PKDrawing不是笔画,是当时所有的笔画。
      猜你喜欢
      • 2011-05-27
      • 1970-01-01
      • 2021-08-05
      • 2012-02-08
      • 2011-05-11
      • 2011-05-01
      • 2011-05-24
      • 2011-12-16
      • 1970-01-01
      相关资源
      最近更新 更多