手势解锁
基本逻辑
手势解锁在应用中都很常见,手势基本的连接点一般都是9个。实现逻辑一般都不难,主要是对滑动过程中,手势所到的点跟本身9个链接点的位置判断并且使用贝塞尔曲线在连接点之间添加连线,最后根据链接点的内容进行输出验证。详看以下代码:
1、解锁视图
新建一个名为gesUnLockView
类,继承自UIView
,重写init(frame:)
并且给视图添加UIPanGestureRecognizer
手势识别。
class gesUnlockView: UIView {
let topMargin: CGFloat = 150 //在视图中相对y方向的距离
var seletedArr = [UIButton]()
var currentP: CGPoint? //当前位置
weak var delegate:LockViewDelegate?
super.init(frame: frame){
let tapGes = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_ :)))
addGestureRecognizer(tapGes)
}
}
新增一个名为setButtons
的方法,该方法的作用主要是新增9个按钮,为按钮设置不同状态下的图片。到时候我们链接的时候,如果手势经过的点在按钮内的话,则设置该按钮为选中状态.
private func setButtons() {
for i in 0..<10 {
print("i -- \(i)")
let btn = UIButton(type: .custom)
btn.isUserInteractionEnabled = false
btn.setImage(UIImage(systemName: "circle.circle"), for: .normal) //使用SFSymbols图片
btn.setImage(UIImage(systemName: "circle.circle.fill"), for: .selected)
btn.tag = i + 1 //设置按钮的tag 到时候密码校验的时候其实就是用一连串的tag组合起来跟最初设定的密码做对比
addSubview(btn)
}
}
重写View的布局方法layoutSubviews()
,也可以这个方法主要是对按钮进行布局,布局成3*3 9个链接点的矩阵,代码如下
override func layoutSubviews() {
super.layoutSubViews()
//设置行数
let cols = 3
var x: CGFloat = 0 //按钮在x轴总的间距
var y: CGFloat = 0 //按钮在x轴总的间距
let w: CGFloat = 74 //按钮的宽
let h: CGFloat = 74
let margin = (bounds.size.width - CGFloat(cols * w)) / CGFloat(cols + 1) //设置间距 间距 = 屏幕的宽度 - 每行按钮的个数*按钮的宽度
var col: CGFloat = 0 //记录列间距 % 行数 比如我们9个按钮,那么0-2 % 3 都为0 则第一行的三个按钮在布局的时候就不需要加上间距 ,第二行的三个俺就则为3-5 % 3 = 1 则间距就为(margin + w) * 1 以此类推
var row: CGFloat = 0 //行间距 作用如上
for i in 0..<count-1 {
let btn: UIButton = self.subviews[i] as! UIButton
col = CGFloat(i % Int(cols))
row = CGFloat(i / Int(cols))
x = margin + col * (margin + w)
y = row * (margin + w)
btn.frame = CGRect(x: x, y: y + topMargin , width: w, height: h)
}
}
声明协议
该协议主要是在手势选择结束时返回所有链接的button的tag,也就是手势所经过的点所代表的数
protocol gesUnlockView: AnyObject {
func didPanEnded(password: String)
}
实现手势方法
手势的方法主要是用来确定手势有经过的按钮,如果有经过则设置按钮的为选中状态,并将选中的按钮加入到数组中,然后将按钮的tag拼接成字符串返回,并且将俺就加入到数组中 ,具体实现如下:
@objc private func panGesture(_ ges: UIPanGestureRecognizer) {
currentP = ges.location(in: self)//获取当前手势所在的位置
self.subviews.forEach { btn in //遍历子视图,也就是9个按钮
guard let btn1 = btn as? UIButton else { return }
guard let position = currentP else { return }
if btn1.frame.contains(position) && !btn1.isSelected { //判断按钮的frame内是否包含手势所处的位置
btn1.isSelected = true
self.seletedArr.append(btn1)//将那就加入数组
}
}
//告诉视图需要重新绘制
setNeedsDisplay() //这一句必须要调用,否则添加贝塞尔曲线不会重绘,也就看不到连接线
if ges.state == .ended {
self.seletedArr.forEach { btn in
btn.isSelected = false
strArr.append("\(btn.tag)") //选择结束后 将tag添加到数组返回
}
self.delegate?.didPanEnded(password: strArr.joined())
self.seletedArr.removeAll() //移除所有添加的按钮
}
}
绘制贝塞尔曲线
这一步主要是重写draw
方法,根据所选中的按钮添加贝塞尔曲线。实现如下:
override func draw(_ rect: CGRect) {
if self.seletedArr.count == 0 {return}
let bezierPath = UIBezierPath()
for i in 0..<seletedArr.count { //遍历按钮字典
let btn = self.seletedArr[i]
if i == 0 {
bezierPath.move(to: btn.center) //如果是第一个点,则将其设置为贝塞尔曲线的起点
} else {
bezierPath.addLine(to: btn.center)
}
}
//链接手势所到达的点
guard let currentP = currentP else { return }
bezierPath.addLine(to: currentP)
UIColor.systemPink.set()
bezierPath.lineWidth = 4
bezierPath.lineJoinStyle = .round
bezierPath.lineCapStyle = .round
bezierPath.stroke()
}
最后
在对应的控制器中实现代理方法并获取代理返回值进行比对。如果跟预设的相同则可验证手势正确,否则验证失败。在这个demo里并未对手势的链接点个数和链接次数进行限制,以及链接错误的提示。小伙伴们可以发散思维,实现对应的逻辑。
Demo地址:gestureUnlockDemo