You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI+PencilKit中绘图与手势无法同时响应问题求助

SwiftUI + PencilKit 手势冲突问题:按住按钮切换画笔工具失效

需求

在SwiftUI与PencilKit混合开发的项目中,需要实现一个功能:按住按钮时切换PKCanvasView的PKInkingTool,松开按钮后恢复原画笔工具。

现有代码实现

封装的CanvasView(UIViewRepresentable)

import PencilKit
import SwiftUI

struct CanvasView: UIViewRepresentable {
    
    var canvas = PKCanvasView()

    private var pen = PKInkingTool(.pen, color: .black , width: 2)
    private var eraser = PKInkingTool(.pen, color: .white , width: 2)
    private var marker = PKInkingTool(.marker, color: .white , width: 2)
    private var corrector = PKInkingTool(.marker, color: .white , width: 2)
    private var currentTool: PKInkingTool
    private var width: CGFloat = 2
    
    
    init() {
        self.currentTool = pen
    }
    
    func makeUIView(context: Context) -> PKCanvasView {
        canvas.drawingGestureRecognizer.cancelsTouchesInView = false
        canvas.tool = pen
        return canvas
    }
    
    func updateUIView(_ uiView: PKCanvasView, context: Context) {
        DispatchQueue.main.async {
            uiView.becomeFirstResponder()
        }
    }
    
    mutating func setPen(color: Color) {
        pen.color = color.uiColor()
        pen.width = width
        currentTool = pen
        canvas.tool = currentTool
    }
    
    mutating func setCorrector(color: Color) {
        corrector.color = color.uiColor()
        corrector.width = width
        currentTool = corrector
        canvas.tool = currentTool
    }
    
    mutating func setMarker(color: Color) {
        marker.color = color.uiColor()
        marker.width = width
        currentTool = marker
        canvas.tool = marker
    }
    
    mutating func setWidth(width: CGFloat) {
        self.width = width
        currentTool.width = width
        canvas.tool = currentTool
    }
    
    mutating func setEraser(width: CGFloat) {
        eraser.width = width
        canvas.tool = eraser
    }
    
    func setLasso(){
        canvas.tool = PKLassoTool()
    }
    
    func setMagicButtonTool(tool: PKInkingTool) {
        var newTool = tool
        newTool.width = width
        canvas.tool = newTool
    }
    
    func restoreTool() {
        canvas.tool = currentTool
    }
    
}

切换按钮MagicButtonView

import SwiftUI

struct MagicButtonView: View {
    
    // MARK: - View objects
    @Environment(\.mainWindowSize) private var mainWindowSize
    
    // MARK: - Public properties
    var icon: Image
    var color: Color
    var action: (_ isTapped: Bool) -> Void
    
    @State var isTapped: Bool = false
    // MARK: - View body
    var body: some View {
        HStack {
            Image.iconNotesPencil
                .resizable()
                .padding(.all, 18)
        }
        .background(Circle()
            .foregroundColor(color)
            .frame(
                width:  mainWindowSize.proportionalWidth(with: .widthSizeRatio),
                height: mainWindowSize.proportionalHeight(with: .heightSizeRatio))
                .overlay(
                    Circle()
                        .stroke(
                            Color.colorBackgroundGray,
                            lineWidth: 2
                        )
                )
        )
        .frame(
            width:  mainWindowSize.proportionalWidth(with: .widthSizeRatio),
            height: mainWindowSize.proportionalHeight(with: .heightSizeRatio))
        .gesture(
            DragGesture(minimumDistance: 0.01)
                .onChanged({ _ in
                    if !isTapped {
                        isTapped = true
                        action(isTapped)
                    }
                })
                .onEnded({ _ in
                    isTapped = false
                    action(isTapped)
                })
        )
    }
}

父视图调用方式

MagicButtonView(icon: .iconNotesPencil, color: .red, action: { isTapped in
            if isTapped {
                canvas.setMagicButtonTool(
                    tool: PKInkingTool(
                        .pen,
                        color: .red,
                        width: 2
                    )
                )
            } else {
                canvas.restoreTool()
            }
        }),

问题现象

  • 点击MagicButtonView时,CanvasView完全无法响应任何UIGestureRecognizer事件
  • 操作CanvasView时,MagicButtonView的SwiftUI手势也无法触发

推测方向

问题源于SwiftUI与UIKit视图混合导致的手势事件抢占,或是Apple Pencil防误触机制干扰,但GoodNotes等同类应用可实现类似功能,说明存在可行的修复方案。

已尝试但无效的方案

  • 给MagicButtonView的手势改用.highPriorityGesture()/.simultaneousGesture()
  • 修改CanvasView的drawingPolicy
  • 设置canvas.drawingGestureRecognizer.cancelsTouchesInView=false
  • 调整按钮层级
  • 更换手势类型

解决方案

1. 给CanvasView添加手势代理,避开按钮区域

通过协调器让PKCanvasView的手势识别器在按钮所在区域不响应触摸事件,避免抢占按钮的手势:

struct CanvasView: UIViewRepresentable {
    // 保留原有属性...
    
    // 添加协调器处理手势代理
    class Coordinator: NSObject, UIGestureRecognizerDelegate {
        var parent: CanvasView
        var buttonFrame: CGRect = .zero // 存储按钮的全局坐标
        
        init(parent: CanvasView) {
            self.parent = parent
        }
        
        // 判断触摸点是否在按钮区域,不在才响应画布手势
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
            let touchLocation = touch.location(in: parent.canvas)
            let convertedButtonFrame = parent.canvas.convert(buttonFrame, from: nil)
            return !convertedButtonFrame.contains(touchLocation)
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    func makeUIView(context: Context) -> PKCanvasView {
        canvas.drawingGestureRecognizer.cancelsTouchesInView = false
        canvas.drawingGestureRecognizer.delegate = context.coordinator // 设置代理
        canvas.tool = pen
        return canvas
    }
    
    // 新增方法:更新按钮的全局frame
    func updateButtonFrame(_ frame: CGRect) {
        if let coordinator = makeCoordinator() as? Coordinator {
            coordinator.buttonFrame = frame
        }
    }
    
    // 保留原有方法...
}

2. 优化MagicButtonView的手势

改用LongPressGesture替代DragGesture,并设置高优先级确保手势优先触发:

struct MagicButtonView: View {
    // 保留原有属性...
    
    @State var isTapped: Bool = false
    
    var body: some View {
        HStack {
            Image.iconNotesPencil
                .resizable()
                .padding(.all, 18)
        }
        .background(Circle()
            .foregroundColor(color)
            .frame(
                width:  mainWindowSize.proportionalWidth(with: .widthSizeRatio),
                height: mainWindowSize.proportionalHeight(with: .heightSizeRatio))
                .overlay(
                    Circle()
                        .stroke(
                            Color.colorBackgroundGray,
                            lineWidth: 2
                        )
                )
        )
        .frame(
            width:  mainWindowSize.proportionalWidth(with: .widthSizeRatio),
            height: mainWindowSize.proportionalHeight(with: .heightSizeRatio))
        // 高优先级长按手势,最小触发时长设为0,按住即响应
        .highPriorityGesture(
            LongPressGesture(minimumDuration: 0)
                .onChanged { _ in
                    if !isTapped {
                        isTapped = true
                        action(isTapped)
                    }
                }
                .onEnded { _ in
                    isTapped = false
                    action(isTapped)
                }
        )
        // 同步手势确保触摸事件可传递给底层视图
        .simultaneousGesture(DragGesture(minimumDistance: 0))
    }
}

3. 在父视图中传递按钮的全局坐标给CanvasView

通过GeometryReader获取按钮的全局frame,传递给CanvasView让其手势识别器避开该区域:

struct ParentView: View {
    @State private var canvas = CanvasView()
    @State private var buttonFrame: CGRect = .zero
    
    var body: some View {
        ZStack {
            canvas
                .onAppear {
                    DispatchQueue.main.async {
                        canvas.canvas.becomeFirstResponder()
                    }
                }
            
            MagicButtonView(icon: .iconNotesPencil, color: .red, action: { isTapped in
                if isTapped {
                    canvas.setMagicButtonTool(
                        tool: PKInkingTool(
                            .pen,
                            color: .red,
                            width: 2
                        )
                    )
                } else {
                    canvas.restoreTool()
                }
            })
            .background(GeometryReader { proxy in
                Color.clear.onAppear {
                    buttonFrame = proxy.frame(in: .global)
                    canvas.updateButtonFrame(buttonFrame)
                }
            })
        }
    }
}

额外优化:调整画布的绘画策略

如果存在Pencil防误触问题,可设置画布仅响应Pencil输入,避免手指触摸干扰:

func makeUIView(context: Context) -> PKCanvasView {
    canvas.drawingGestureRecognizer.cancelsTouchesInView = false
    canvas.drawingGestureRecognizer.delegate = context.coordinator
    canvas.drawingPolicy = .pencilOnly // 添加此行
    canvas.tool = pen
    return canvas
}

内容的提问来源于stack exchange,提问作者Alvaro Sanz Rodrigo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 17:32:36