[Swift] 利用CAShapeLayer完成iOS圖形密碼驗證
!!!文末含有當初遇到的大坑!!!
利用collectionView完成此功能
先建立一個collectionView的類別以及該類別的delegate
delegate含有移動時、選到cell後、結束滑動時動作,這三個function
protocol GestureCollectionViewDelegate: AnyObject {
func move(point: CGPoint) /// 手指滑動時的反應
func selectedItem(indexPath: IndexPath) //選取到cell時動作
func moveEnded() //結束滑動時動作
}
class GestureCollectionView: UICollectionView {
weak var gestureCollectionViewDelegate:GestureCollectionViewDelegate?
open override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touchPoint = touches.first?.location(in: self) else { return }
if let indexPath = self.indexPathForItem(at: touchPoint) {
gestureCollectionViewDelegate?.selectedItem(indexPath: indexPath); return
}
gestureCollectionViewDelegate?.move(point: touchPoint)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
gestureCollectionViewDelegate?.moveEnded()
}
}
//
// GesturePassword_ViewController.swift
// BOT_iOS
//
// Created by allenhung on 2021/7/28.
//
import UIKit
enum GesturePasswordType: Int {
case setting = 0
case unlock = 1
}
class GesturePassword_ViewController: UIViewController {
@IBOutlet weak var gestureCollectionView: GestureCollectionView!
private var selectedColor = #colorLiteral(red: 0.7843137255, green: 0.1450980392, blue: 0.3294117647, alpha: 1)
private var unSelectedColor = #colorLiteral(red: 0.6677469611, green: 0.6677629352, blue: 0.6677542925, alpha: 1)
//目前模式
private var gesturePasswordType = GesturePasswordType.setting
/// 目前滑到的最後一個點
private var currentPoint: CGPoint?
//當前畫出的密碼
private var selectedPassword = [Int]()
// 用來管理畫在 View 上的 Layer
private var lineLayers = [CAShapeLayer]() {
didSet {
print(lineLayers.count)
}
}
//設定一行一列有幾個解鎖點
private var rowCount = 3
//跟著手指移動的layer
private var moveLayer: CAShapeLayer?
override func viewDidLoad() {
super.viewDidLoad()
setupFakeGesturePassword()
setupUI()
}
@IBAction func changeType(_ sender: UISegmentedControl) {
guard let type = GesturePasswordType(rawValue: sender.selectedSegmentIndex) else { return }
gesturePasswordType = type
}
//手勢事件
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
gestureCollectionView.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
gestureCollectionView.touchesEnded(touches, with: event)
}
}
extension GesturePassword_ViewController{
private func setupUI(){
setupTitle()
setupGestureCollection()
}
private func setupTitle(){
self.title = "圖形登入"
}
private func setupGestureCollection(){
gestureCollectionView.dataSource = self
gestureCollectionView.delegate = self
gestureCollectionView.gestureCollectionViewDelegate = self
gestureCollectionView.isUserInteractionEnabled = false
}
private func setupFakeGesturePassword(){
UserDefaults.standard.set([0,1,2,3,4,5,6,7,8], forKey:"gesturePassword")
}
}
extension GesturePassword_ViewController{
/// 圖形鎖的外框顏色 (選到的 / 沒選到)
private func gestureCollectionViewCellBorderColor(for indexPath: IndexPath) -> UIColor {
let cellBorderColor = selectedPassword.contains(indexPath.row) ? selectedColor : unSelectedColor
return cellBorderColor
}
/// 畫線的路徑
private func shapeLayerPath(from point1: CGPoint, to point2: CGPoint) -> UIBezierPath {
let bezierPath = UIBezierPath()
bezierPath.move(to: point1)
bezierPath.addLine(to: point2)
return bezierPath
}
/// 清除所有資料 (Layer / Password)
private func clearAllData() {
lineLayers.forEach { (layer) in
layer.removeFromSuperlayer()
}
moveLayer?.removeFromSuperlayer()
lineLayers.removeAll()
selectedPassword.removeAll()
moveLayer = nil
currentPoint = nil
gestureCollectionView.reloadSections(IndexSet(integer: 0))
}
/// 設定畫線的Layer (有舊的就用舊的,不然就產生新的)
private func lockLayerSetting(_ layer: CAShapeLayer? = nil, for path: UIBezierPath, color: UIColor) -> CAShapeLayer {
let _layer = (layer != nil) ? layer! : CAShapeLayer()
_layer.frame = gestureCollectionView.bounds
_layer.position = gestureCollectionView.center
_layer.fillColor = nil
_layer.lineWidth = 10
_layer.opacity = 0.5
_layer.strokeColor = color.cgColor
_layer.lineCap = .round
_layer.path = path.cgPath
return _layer
}
}
//MARK:-主要畫線的方法
extension GesturePassword_ViewController{
/// 畫圖形鎖的線 (完成時 )
private func drawLockLayerForSelected(to point: CGPoint) {
if let _currentPoint = currentPoint {
let layerPath = shapeLayerPath(from: _currentPoint, to: point)
let lockShapeLayer = lockShapeLayerMaker(for: layerPath, color: selectedColor)
lineLayers.append(lockShapeLayer)
view.layer.addSublayer(lockShapeLayer)
}
currentPoint = point
}
/// 畫圖形鎖的線 (移動時 )
private func drawLockLayerForMove(to point: CGPoint) {
if let _currentPoint = currentPoint {
let layerPath = shapeLayerPath(from: _currentPoint, to: point)
if (moveLayer == nil) {
moveLayer = lockShapeLayerMaker(for: layerPath, color: selectedColor)
view.layer.addSublayer(moveLayer!)
return
}
moveLayerSetting(for: layerPath, color: selectedColor)
}
}
/// 產生畫線的Layer
private func lockShapeLayerMaker(for path: UIBezierPath, color: UIColor) -> CAShapeLayer {
return lockLayerSetting(for: path, color: color)
}
/// 設定moveLayer
private func moveLayerSetting(for path: UIBezierPath, color: UIColor) {
_ = lockLayerSetting(moveLayer, for: path, color: color)
}
/// 記錄選到的Password的值 (畫線)
private func appendPassword(at indexPath: IndexPath) {
guard !selectedPassword.contains(indexPath.row),
let gestureCollectionViewCell = gestureCollectionView.cellForItem(at: indexPath)
else {
return
}
selectedPassword.append(indexPath.row)
drawLockLayerForSelected(to: gestureCollectionViewCell.center)
moveLayer?.removeFromSuperlayer()
moveLayer = nil
gestureCollectionView.reloadItems(at: [indexPath])
}
}
//設定每個解鎖鈕的間隔距離
extension GesturePassword_ViewController:UICollectionViewDelegateFlowLayout{
//設定cell間隔
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.bounds.width / CGFloat(rowCount * 2 - 1)
return CGSize(width: width, height: width)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
let width = collectionView.bounds.width / CGFloat(rowCount * 2 - 1)
return width
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
let width = collectionView.bounds.width / CGFloat(rowCount * 2 - 1)
return width
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets.zero
}
}
extension GesturePassword_ViewController:UICollectionViewDataSource {
//設定幾乘幾的cell
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return rowCount * rowCount
}
//設定cell樣式
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GestureCollectionViewCell", for: indexPath) as? GestureCollectionViewCell{
cell.tag = indexPath.row
cell.imageView.tintColor = gestureCollectionViewCellBorderColor(for: indexPath)
return cell
}
return UICollectionViewCell()
}
}
//實現GestureCollectionViewDelegater
extension GesturePassword_ViewController:GestureCollectionViewDelegate{
func move(point: CGPoint) {
drawLockLayerForMove(to: point)
}
func selectedItem(indexPath: IndexPath) {
appendPassword(at: indexPath)
}
func moveEnded() {
switch gesturePasswordType {
case .setting:
if selectedPassword.count >= 4 {
UserDefaults.standard.set(self.selectedPassword, forKey:"gesturePassword")
self.showCustomAlertWithIcon(message: "設定圖形密碼成功", buttonTitle: "確認"
, tapButtonAcction: {
self.dismiss(animated: true, completion: nil)
})
} else {
self.showCustomAlertWithIcon(message: "圖形密碼需至少連線4個\n請重新設定", buttonTitle: "確認"
, tapButtonAcction: {
self.dismiss(animated: true, completion: nil)
})
}
case .unlock:
if selectedPassword == UserDefaults.standard.object(forKey: "gesturePassword") as! [Int]{
self.showCustomAlertWithIcon(message: "解鎖成功", buttonTitle: "確認",icon: "checkmark.circle"
, tapButtonAcction: {
self.dismiss(animated: true, completion: nil)
})
} else {
lineLayers.forEach { (layer) in
layer.strokeColor = UIColor.red.cgColor
}
gestureCollectionView.visibleCells.forEach { (cell) in
cell.layer.borderColor = UIColor.red.cgColor
}
moveLayer?.strokeColor = UIColor.red.cgColor
self.showCustomAlertWithIcon(message: "解鎖失敗", buttonTitle: "確認",icon: "multiply.circle"
, tapButtonAcction: {
self.dismiss(animated: true, completion: nil)
})
}
}
clearAllData()
}
}
*需要注意
如果將collectionView用view包起來的話,會造成collectionView的中心點座標偏移,畫出來的線就不會在cell的中心,當初花了很久時間才發現這個問題