iOS双指静态触摸时touchesEnded未触发的多触点问题
iOS多点触摸静态场景下touchesEnded未触发问题排查与修复
问题描述
开发的iOS多点触摸应用中,双指静态触摸屏幕时对应Pad正常点亮并触发functionTouchesBegin回调;但抬起任意一根手指时,对应Pad未熄灭,touchesEnded内的functionTouchesEnded回调也未执行。仅当移动或抬起另一根手指时,才会触发之前抬起手指的touchesEnded方法,Pad才熄灭。该问题仅在双指静态触摸(未触发touchesMoved)时出现,其他触摸数量下功能正常。调试发现保持另一根手指静止并抬起其中一根时,程序完全未进入touchesEnded方法,尝试线程安全数据结构存储触摸状态后问题仍存在。
核心问题分析
- 回调代码错误:提供的代码框架中,
touchesEnded方法内错误调用了functionTouchesBegin而非functionTouchesEnded,直接导致抬起手指时触发错误逻辑。 - 触摸跟踪缺失唯一标识绑定:未使用
UITouch的identifier关联触摸点与Pad状态,静态触摸场景下系统事件优化时易出现状态映射混乱。 - layoutSubviews不当重置状态:
layoutSubviews中调用resetTouchStates(),而该方法会在视图布局变化时频繁触发,可能在触摸过程中意外重置状态,干扰事件处理。 - iOS触摸事件优化机制:当视图无持续绘制需求(如静态触摸无移动),系统可能延迟或合并触摸结束事件,直到其他交互触发视图更新。
修复方案与代码修正
1. 修正回调调用错误
在touchesEnded方法中替换错误的回调调用:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { for touch in touches { // 获取触摸对应的Pad索引逻辑 if let padIndex = getPadIndex(for: touch) { // 修正为正确的结束回调 functionTouchesEnded?(padIndex) updateTouchState(oldIndex: padIndex, newIndex: nil) } } setNeedsDisplay() }
2. 使用UITouch.identifier跟踪触摸状态
通过触摸唯一标识绑定Pad状态,确保线程安全:
// 替换原touchStatus,用字典存储触摸ID到Pad索引的映射 private var touchToPadMap: [Int: Int] = [:] private let stateQueue = DispatchQueue(label: "com.youapp.touchQueue", qos: .userInteractive, attributes: .concurrent) // 线程安全的状态更新方法 private func updateTouchState(touchID: Int, padIndex: Int?) { stateQueue.async(flags: .barrier) { if let index = padIndex { self.touchToPadMap[touchID] = index } else { self.touchToPadMap.removeValue(forKey: touchID) } } } // touchesBegan中绑定触摸ID与Pad索引 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { for touch in touches { let touchID = touch.identifier if let padIndex = calculatePadIndex(for: touch) { functionTouchesBegin?(padIndex) updateTouchState(touchID: touchID, padIndex: padIndex) } } setNeedsDisplay() } // touchesEnded中通过触摸ID获取对应Pad索引 override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { for touch in touches { let touchID = touch.identifier stateQueue.sync { if let padIndex = self.touchToPadMap[touchID] { functionTouchesEnded?(padIndex) self.updateTouchState(touchID: touchID, padIndex: nil) } } } setNeedsDisplay() }
3. 调整layoutSubviews逻辑
移除触摸过程中会干扰状态的重置操作:
override func layoutSubviews() { super.layoutSubviews() calculatePadFrames() invalidateCache() // 移除resetTouchStates(),避免触摸中丢失状态 }
4. 强制触发视图持续刷新(可选)
针对静态触摸场景,启动CADisplayLink保持视图刷新,确保系统及时分发触摸结束事件:
private var displayLink: CADisplayLink? override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesBegan(touches, with: event) if displayLink == nil { displayLink = CADisplayLink(target: self, selector: #selector(keepRefreshing)) displayLink?.add(to: .main, forMode: .common) } // 其他触摸逻辑... } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesEnded(touches, with: event) if let allTouches = event?.allTouches, allTouches.isEmpty { displayLink?.invalidate() displayLink = nil } // 其他触摸逻辑... } @objc private func keepRefreshing() { setNeedsDisplay() }
验证要点
- 测试双指静态触摸后单指抬起,确认
touchesEnded立即触发,对应Pad熄灭 - 检查多触摸场景下状态映射是否正确,无串扰
- 确认视图布局变化时不会影响当前触摸状态
内容的提问来源于stack exchange,提问作者Silhouette
相关产品推荐
相关产品推荐

