手勢解鎖
基本邏輯
手勢解鎖在應用中都很常見,手勢基本的連接點一般都是9個。實現(xiàn)邏輯一般都不難,主要是對滑動過程中,手勢所到的點跟本身9個鏈接點的位置判斷并且使用貝塞爾曲線在連接點之間添加連線,最后根據(jù)鏈接點的內(nèi)容進行輸出驗證。詳看以下代碼:
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個按鈕,為按鈕設(shè)置不同狀態(tài)下的圖片。到時候我們鏈接的時候,如果手勢經(jīng)過的點在按鈕內(nèi)的話,則設(shè)置該按鈕為選中狀態(tài).
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 //設(shè)置按鈕的tag 到時候密碼校驗的時候其實就是用一連串的tag組合起來跟最初設(shè)定的密碼做對比
addSubview(btn)
}
}
重寫View的布局方法layoutSubviews(),也可以這個方法主要是對按鈕進行布局,布局成3*3 9個鏈接點的矩陣,代碼如下
override func layoutSubviews() {
super.layoutSubViews()
//設(shè)置行數(shù)
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) //設(shè)置間距 間距 = 屏幕的寬度 - 每行按鈕的個數(shù)*按鈕的寬度
var col: CGFloat = 0 //記錄列間距 % 行數(shù) 比如我們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)
}
}
聲明協(xié)議
該協(xié)議主要是在手勢選擇結(jié)束時返回所有鏈接的button的tag,也就是手勢所經(jīng)過的點所代表的數(shù)
protocol gesUnlockView: AnyObject {
func didPanEnded(password: String)
}
實現(xiàn)手勢方法
手勢的方法主要是用來確定手勢有經(jīng)過的按鈕,如果有經(jīng)過則設(shè)置按鈕的為選中狀態(tài),并將選中的按鈕加入到數(shù)組中,然后將按鈕的tag拼接成字符串返回,并且將俺就加入到數(shù)組中 ,具體實現(xiàn)如下:
@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內(nèi)是否包含手勢所處的位置
btn1.isSelected = true
self.seletedArr.append(btn1)//將那就加入數(shù)組
}
}
//告訴視圖需要重新繪制
setNeedsDisplay() //這一句必須要調(diào)用,否則添加貝塞爾曲線不會重繪,也就看不到連接線
if ges.state == .ended {
self.seletedArr.forEach { btn in
btn.isSelected = false
strArr.append("\(btn.tag)") //選擇結(jié)束后 將tag添加到數(shù)組返回
}
self.delegate?.didPanEnded(password: strArr.joined())
self.seletedArr.removeAll() //移除所有添加的按鈕
}
}
繪制貝塞爾曲線
這一步主要是重寫draw方法,根據(jù)所選中的按鈕添加貝塞爾曲線。實現(xiàn)如下:
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) //如果是第一個點,則將其設(shè)置為貝塞爾曲線的起點
} 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()
}
最后
在對應的控制器中實現(xiàn)代理方法并獲取代理返回值進行比對。如果跟預設(shè)的相同則可驗證手勢正確,否則驗證失敗。在這個demo里并未對手勢的鏈接點個數(shù)和鏈接次數(shù)進行限制,以及鏈接錯誤的提示。小伙伴們可以發(fā)散思維,實現(xiàn)對應的邏輯。
Demo地址:gestureUnlockDemo