前言
TTGPuzzleVerify 是一个拼图验证控件。最早写它的时候,重点是“怎么从图片里抠出一块拼图、怎么做镂空、怎么判断拖动是否成功”。那篇旧文章里也主要讲的是 UIBezierPath、CAShapeLayer.mask 和手势判断。
这次更新到 2.0.0,核心思路没有变,但项目本身变得完整了很多:Swift 原生实现、Objective-C 可调用、SwiftUI 可包装、统一配置对象、更多验证行为、状态机、失败反馈、结果指标等等。

Github: https://github.com/zekunyan/TTGPuzzleVerify
更新
2.0.0 这次主要做了这些事情:
- 迁移到 Swift:核心类
TTGPuzzleVerifyView用 Swift 实现,同时通过@objc暴露给 Objective-C。 - 统一配置:新增
TTGPuzzleVerifyConfiguration,把形状、尺寸、容差、拖动方向、自动吸附、重试次数等放到一起。 - 更多形状:内置 classic、square、circle,并支持自定义
UIBezierPath。 - 拖动方向控制:支持水平、垂直、双轴拖动。
- 状态机:公开 idle、dragging、verified、failed、locked 状态。
- 结果指标:成功/失败回调里带上偏移、耗时、拖动距离、交互次数等数据。
- SwiftUI / UIKit / Objective-C 共用核心:不同集成方式不再各写一套逻辑。

快速使用
UIKit 里最基本的配置如下:
import TTGPuzzleVerify
let puzzleView = TTGPuzzleVerifyView()
puzzleView.image = UIImage(named: "background")
let configuration = TTGPuzzleVerifyConfiguration()
configuration.puzzlePattern = .circlePattern
configuration.puzzleSize = CGSize(width: 92, height: 92)
configuration.allowedAxes = .horizontal
configuration.verificationTolerance = 7
puzzleView.applyConfiguration(configuration)
puzzleView.puzzleBlankPosition = CGPoint(x: 210, y: 20)
puzzleView.resetVerification()

如果在 SwiftUI 里用,也就是包一层 UIViewRepresentable:
struct PuzzleVerifyRepresentable: UIViewRepresentable {
@Binding var isVerified: Bool
func makeUIView(context: Context) -> TTGPuzzleVerifyView {
let view = TTGPuzzleVerifyView()
view.image = UIImage(named: "background")
let configuration = TTGPuzzleVerifyConfiguration()
configuration.puzzlePattern = .classicPattern
configuration.puzzleSize = CGSize(width: 86, height: 86)
configuration.allowedAxes = .both
configuration.autoSnapWhenWithinTolerance = true
view.applyConfiguration(configuration)
view.puzzleBlankPosition = CGPoint(x: 220, y: 96)
view.verificationChangeBlock = { _, verified in
isVerified = verified
}
return view
}
}
配置对象
旧版很多属性是直接设置到 view 上的,这当然简单,但当行为越来越多以后,调用处会比较散。新版增加了 TTGPuzzleVerifyConfiguration:
let configuration = TTGPuzzleVerifyConfiguration()
configuration.puzzlePattern = .classicPattern
configuration.puzzleSize = CGSize(width: 100, height: 100)
configuration.verificationTolerance = 8
configuration.allowedAxes = .both
configuration.autoSnapWhenWithinTolerance = true
configuration.recordsTrack = true
configuration.maxRetryCount = 3
puzzleView.applyConfiguration(configuration)
这里最重要的几个参数:
puzzlePattern:拼图形状。puzzleSize:拼图块大小。verificationTolerance:验证容差,避免要求用户像素级对齐。allowedAxes:限制水平、垂直或双轴拖动。autoSnapWhenWithinTolerance:进入容差范围时自动吸附到正确位置。maxRetryCount:失败次数达到上限后进入 locked 状态。
这个配置对象的好处是,产品层可以维护几套不同策略:比如登录页更严格,活动页更轻一点,或者调试环境打开 track 记录。
状态和结果
拼图验证不只是“成功/失败”两个布尔值。实际接到业务里以后,常常还需要知道失败了几次、拖动过程是否异常、耗时多久、偏差是多少。
新版公开了状态:
public enum TTGPuzzleVerifyState: Int {
case idle
case dragging
case verified
case failed
case locked
}
完成时会产生 TTGPuzzleVerifyResult,里面包括:
isVerifiedpuzzlePositionblankPositionxOffset/yOffsetelapsedTimedragDistanceinteractionCount
注意,这些指标不是为了在客户端做“安全风控”,客户端本身不适合承担这个职责。它更适合用于体验分析、调试、轻量行为判断,真正的安全策略还是应该服务端参与。
实现回顾
拼图控件核心还是三个层:
- 背景图:完整图片。
- 镂空图:用 even-odd mask 从背景中抠掉拼图区域。
- 拼图块:同一张图片通过 puzzle path mask 裁出来。
旧文章里讲过 mask 的基本思路,这次重构后,重点是把这些能力封装成可以稳定复用的状态机:配置变化时更新 path 和 mask,拖动时更新拼图位置,结束时根据容差和轴向策略决定 verified/failed/locked。
在 API 上,customPuzzlePatternPath 仍然保留给自定义形状:
configuration.puzzlePattern = .customPattern
puzzleView.customPuzzlePatternPath = myBezierPath
puzzleView.applyConfiguration(configuration)
这样内置形状不够时,外部可以继续提供自己的 UIBezierPath,控件本身只负责把路径缩放、移动、应用到 mask 和验证逻辑里。
总结
TTGPuzzleVerify 2.0.0 更像是把一个“有趣的拼图 Demo 控件”整理成了一个能在项目里使用的组件:Swift 化、配置化、状态清晰、回调信息更完整,也兼顾了 UIKit、SwiftUI 和 Objective-C。
当然,拼图验证这种东西也要看使用场景。它可以增加交互门槛、减少误触或低成本刷操作,但不要把它当成完整安全方案。控件负责交互体验,安全策略还是要回到整体系统设计里。