【Swift 5】iPhone也能夠有圖形解鎖?
話說iPhone創新總是讓人耳目一新,尤其是「指紋辨識 - Touch ID」跟「臉部辨識 - Face ID」可以算是Android手機們「模仿」的對象之一,但是Android手機圖形解鎖反而在iPhone沒有,這真的是很讓人奇怪的地方,或許是「生物辨識」的安全性比圖形密碼來的高吧?在這裡呢,我們要仿製一個簡單圖形鎖,話不多說,我們就來試試看吧。
圖形解鎖
成果展示
- 首先,我們要做一個長得像這樣的圖形鎖
畫面結構
- 畫面結構如下,我們要利用UICollectionView做一個「正方形」的圖形鎖
// MARK: - 主工具
extension ViewController {
/// 圖形鎖的外觀 (圓形的)
private func lockCell(with collectionView: UICollectionView, for indexPath: IndexPath) -> UICollectionViewCell {
let lockCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
lockCell.tag = indexPath.row
lockCell.layer.cornerRadius = lockCell.bounds.height / 2
lockCell.layer.borderWidth = 3
lockCell.layer.borderColor = lockCellBorderColor(for: indexPath)
return lockCell
}
}
// MARK: - 小工具
extension ViewController {
/// 圖形鎖的數量 (3 x 3)
private func lockCellCount(with row: Int) -> Int {
return row * row
}
/// 圖形鎖的平均寬度
private func lockCellWidth(with width: CGFloat, for row: Int) -> CGFloat {
let cellWidth = width / CGFloat(row * 2 - 1)
let cellWidthString = String(format: "%.2f", cellWidth)
return CGFloat(Float(cellWidthString)!)
}
/// 圖形鎖的平均大小
private func lockCellSize(with width: CGFloat, for row: Int) -> CGSize {
let cellWidth = lockCellWidth(with: width, for: row)
return CGSize(width: cellWidth, height: cellWidth)
}
/// 圖形鎖的外框顏色 (選到的 / 沒選到)
private func lockCellBorderColor(for indexPath: IndexPath) -> CGColor {
let cellBorderColor = selectedPassword.contains(indexPath.row) ? UIColor.green.cgColor : UIColor.white.cgColor
return cellBorderColor
}
}
自定義Protocol
- 在這裡我們要自定義LockCollectionView來覆寫touchesMoved跟touchesEnded這兩個方法,跟LockCollectionViewDelegate來處理手指滑動的反應。在這裡主要的重點就是,怎麼樣才算「被選到呢」?就是利用它「indexPathForItem(at: touchPoint)」來測試Item有沒有被點到。
import UIKit
protocol LockCollectionViewDelegate: class {
func move(to point: CGPoint) /// 手指滑動時的反應
func moveEnded() /// 手指滑動完成後的反應
func selectedItem(at indexPath: IndexPath) /// 選到Item時的反應
}
class LockCollectionView: UICollectionView {
weak var lockCollectionViewDelegate: LockCollectionViewDelegate?
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) {
lockCollectionViewDelegate?.selectedItem(at: indexPath); return
}
lockCollectionViewDelegate?.move(to: touchPoint)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
lockCollectionViewDelegate?.moveEnded()
}
}
畫線
- 畫線主要的是利用CAShapeLayer去處理,從Item中點開始畫,而路徑是跟UIBezierPath做配合,然後記錄Password的值,其中比較要注意的是移動時的線是暫時的,會被清掉,而完成時的線會被記錄下來,在這裡特地把兩條線的顏色分開,以利說明。
// MARK: - 主工具
extension ViewController {
/// 畫圖形鎖的線 (完成時 - 紅色)
private func drawLockLayerForSelected(to point: CGPoint) {
if let _currentPoint = currentPoint {
let layerPath = lockShapeLayerPath(from: _currentPoint, to: point)
let lockShapeLayer = lockShapeLayerMaker(for: layerPath, color: .red)
lineLayers.append(lockShapeLayer)
view.layer.addSublayer(lockShapeLayer)
}
currentPoint = point
}
/// 畫圖形鎖的線 (移動時 - 綠色)
private func drawLockLayerForMove(to point: CGPoint) {
if let _currentPoint = currentPoint {
let layerPath = lockShapeLayerPath(from: _currentPoint, to: point)
if (moveLayer == nil) {
moveLayer = lockShapeLayerMaker(for: layerPath, color: .green)
view.layer.addSublayer(moveLayer!)
return
}
moveLayerSetting(for: layerPath, color: .green)
}
}
/// 產生畫線的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 lockCell = lockCollectionView.cellForItem(at: indexPath)
else {
return
}
selectedPassword.append(indexPath.row)
drawLockLayerForSelected(to: lockCell.center)
moveLayer?.removeFromSuperlayer()
moveLayer = nil
playSystemSound(soundID: 1520)
lockCollectionView.reloadItems(at: [indexPath])
}
}
// MARK: - 小工具
extension ViewController {
/// 畫線的路徑
private func lockShapeLayerPath(from point1: CGPoint, to point2: CGPoint) -> UIBezierPath {
let bezierPath = UIBezierPath()
bezierPath.move(to: point1)
bezierPath.addLine(to: point2)
return bezierPath
}
}
比對密碼
- 比對密碼就相當簡單了,直接就使用「!=」就可以了。另外也可以改變Row的數量跟Lock的類型。
// MARK: - Main
class ViewController: UIViewController {
/// 改變Row的數量
@IBAction func changeLockRows(_ sender: UIStepper) {
if (finalPassword.count != 0) { showMessageAlert("密碼請重新設定") }
lockRowCount = Int(sender.value)
clearAllData()
finalPassword = []
}
/// 改變Lock的類型
@IBAction func changeLockType(_ sender: UISegmentedControl) {
guard let type = LockType(rawValue: sender.selectedSegmentIndex) else { return }
lockType = type
switch type {
case .setting: lockCollectionView.backgroundColor = .blue
case .unlock: lockCollectionView.backgroundColor = .purple
}
}
}
// MARK: - 主工具
extension ViewController {
/// 比對密碼
private func checkPassword() {
if finalPassword.count > 0 {
let message = (finalPassword != selectedPassword) ? "答錯了" : "答對了"
showMessageAlert(message)
}
}
}
範例程式碼下載
後記
- 其實會有這篇文章只是因為好久沒有寫Swift了,加上看到了這篇文章,就決定要來抄一下,原文寫得很棒,大家也可以去看看,個人只是改一下下而已。另外,個人在文字的著墨上不多,大都是以Code來說明,希望大家能多多包含。