使用 SwiftUI 实现 PencilKit 撤消功能
Implement PencilKit undo functionality using SwiftUI
编辑:感谢一些反馈,我已经能够部分工作(更新代码以反映当前更改)。
即使该应用程序似乎按预期运行,我仍然收到 'Modifying state...' 警告。如何更新 updateUIView
中的视图绘图并使用 canvasViewDrawingDidChange
将新绘图推入堆栈而不导致此问题?我试过将它包装在调度调用中,但这只会创建一个无限循环。
我正在尝试在 UIViewRepresentable
(PKCanvasView
) 中实现撤消功能。我有一个名为 WriterView
的父 SwiftUI 视图,它包含两个按钮和 canvas。
这是父视图:
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] Modifying state during view update, this will cause undefined behavior.
大概是我的协调员的 did change 函数引起的,但我不确定如何解决这个问题。处理此问题的最佳方法是什么?
谢谢!
我认为错误可能来自私有函数 clearCanvas()
和私有函数 undoDrawing()。试试这个看看它是否有效:
private func clearCanvas() {
DispatchQueue.main.async {
self.drawings = [PKDrawing()]
}
}
与 undoDrawing() 类似。
如果是来自canvasViewDrawingDidChange,做同样的事情。
我正在处理这个问题:
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)
}
}
}
我想我可以使用不同的方法在没有警告的情况下工作。
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
}
}
}
我终于(无意中)弄清楚了如何使用 UndoManager 执行此操作。我仍然不确定 为什么 这行得通,因为我从来不需要调用 self.undoManager?.registerUndo()
。如果您理解为什么我从来不需要注册活动,请发表评论。
这是我的工作 parent 视图:
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)
}
}
}
这是我的工作 child 视图:
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 的预期方法,而且肯定比我之前所做的尝试更优雅。
只是为了完整性,如果你想显示 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({[=10=].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) {
}
}
编辑:感谢一些反馈,我已经能够部分工作(更新代码以反映当前更改)。
即使该应用程序似乎按预期运行,我仍然收到 'Modifying state...' 警告。如何更新 updateUIView
中的视图绘图并使用 canvasViewDrawingDidChange
将新绘图推入堆栈而不导致此问题?我试过将它包装在调度调用中,但这只会创建一个无限循环。
我正在尝试在 UIViewRepresentable
(PKCanvasView
) 中实现撤消功能。我有一个名为 WriterView
的父 SwiftUI 视图,它包含两个按钮和 canvas。
这是父视图:
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] Modifying state during view update, this will cause undefined behavior.
大概是我的协调员的 did change 函数引起的,但我不确定如何解决这个问题。处理此问题的最佳方法是什么?
谢谢!
我认为错误可能来自私有函数 clearCanvas() 和私有函数 undoDrawing()。试试这个看看它是否有效:
private func clearCanvas() {
DispatchQueue.main.async {
self.drawings = [PKDrawing()]
}
}
与 undoDrawing() 类似。
如果是来自canvasViewDrawingDidChange,做同样的事情。
我正在处理这个问题:
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)
}
}
}
我想我可以使用不同的方法在没有警告的情况下工作。
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
}
}
}
我终于(无意中)弄清楚了如何使用 UndoManager 执行此操作。我仍然不确定 为什么 这行得通,因为我从来不需要调用 self.undoManager?.registerUndo()
。如果您理解为什么我从来不需要注册活动,请发表评论。
这是我的工作 parent 视图:
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)
}
}
}
这是我的工作 child 视图:
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 的预期方法,而且肯定比我之前所做的尝试更优雅。
只是为了完整性,如果你想显示 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({[=10=].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) {
}
}