【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來覆寫touchesMovedtouchesEnded這兩個方法,跟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來說明,希望大家能多多包含。