CAShapeLayer UIBezierPath

透過CAShapeLayer和UIBezierPath製作簡易簽名板

林哲緯 2020/02/20 10:19:44
1956

 

・前言:

在專案中,經常需要使用者的簽名圖檔,尤其是金融相關的案子更是基本需求,不論是簽署保單、同意書簽署等,相關功能都相當常見,下面就將透過CAShapeLayerUIBezierPath做一個簽名板。

 

 

 

・建立專案:

打開Xcode -> File -> New -> Project ...

 

 

選擇iOS -> Single View App -> Next

 

輸入專案名稱:SignatureDemo -> Next...

 

 

這樣就建立了Demo 的專案了

 

 

 

 

・實作專案:

 

首先,新增一個View,File -> New -> File...

 


 

選擇iOS -> Cocoa Touch Class -> Next

 

輸入View名稱:SignatureView -> Next

 

 

 

這個 SignatureView就是我們的簽名板,等等我們就是要將功能實作在這裡

 

在實作簽名板功能之前,我們先來對ViewController做一些相關設定

 

 

點選 Main.Storyboard -> ViewCotroller -> 點選+號 -> 選取View -> 將View拖拉到ViewController中

如下圖所示:

 

 

將剛剛加入的View 的Custom Class 中的Class設定為SignatureView

並加入適當的Constraints,我這裡設定的是:對上距離0、對左距離0、對右距離0、對下距離100

如下圖所示:

 

 

加入SignatureView之後,再加入兩個Button

這兩個Button的用途分別為清除簽名以及將簽名存入手機相簿

 

點選 Main.Storyboard -> ViewCotroller -> 點選+號 -> 選取Button -> 將Button拖拉到ViewController中

如下圖所示:

 

加入適當的Constraints和Button的外觀

我這邊設定的是:(清除簽名按鈕)對上距離10、對左距離0、對右距離0、高度40

(存入手機相簿按鈕)對上距離5、對左距離0、對右距離0、高度40

如下圖所示:

將需要使用的元件新增且把樣式設定完畢後,再來就是要加入元件和程式的關聯了(IBOutlet & IBAction func)

 

加入SignatureView與程式的關聯:

點選左側SignatureView並按下滑鼠右鍵不放,將游標拖移到" class ViewController: UIViewController{ "這行底下後放開滑鼠右鍵,就會像下圖一樣跳出一個框框,輸入SignatureView的名字,我這邊命名為signatureView(注意:這裡的名稱開頭為小寫)這樣就建立了SignatureView和程式的關聯了

(宣告了一個Class為SignatureView的物件,變數名稱叫做signatureView

 

如下圖所示:

 

 

 

加入Button與程式的關聯:

和剛剛SignatureView的做法一樣,點選左側清除簽名的Button並按下滑鼠右鍵不放,將游標拖移到第19行Override func viewDidLoad()這個方法的結束點 " }" 下面一行後放開滑鼠右鍵,一樣會跳出一個小框框,輸入這個方法的名稱,我這裡設定為clearButtonPressed,另外一個存入手機相簿的Button做法也是一樣,我這裡將他的方法命名為saveButtonPressed

 

如下圖所示:

 

 

這樣就完成元件與程式的關聯了,再來就是要寫程式囉。

 

・實作簽名板:

 

在開始實作之前,我們先思考一下實作簽名的功能

 

1.這次的簽名板是設定只能一隻筆簽名

 

2.簽名的時候不能超過簽名板的範圍

 

3.簽名的顏色

 

4.筆的寬度

 

5.顯示簽名

 

6.清除簽名

 

7.將簽名存入手機相簿

 

 

將以上這些功能是簽名板所需的基本功能,一一實作後就能完成簡易的簽名板囉

 

 

1.這次的簽名板是設定只能一隻筆簽名:

 

因為ios預設是支援多點觸碰,所以只要將簽名板(SignatureView)的多點觸碰關閉就好囉。

//將signatureView的多點觸碰功能關閉
signatureView.isMultipleTouchEnabled = false

 

2.簽名的時候不能超過簽名板的範圍:

 

簽名的時候超出簽名板的範圍就無效囉。

//設定只顯示signatureView的範圍,超過的裁切掉
signatureView.clipsToBounds = true

 

點選ViewController.Swift -> 在Override func ViewDidLoad()這個方法底下加入上面這兩項設定

 

接著點選SignatureView.Swift 做以下設定

 

3.簽名的顏色:

4.筆的寬度:

 

對於兩項功能,我們先對簽名板(SignatureView)新增一些屬性

 

class SignatureView: UIView {
    
    var lineColor = UIColor.black       //預設簽名顏色為黑色
    var lineWidth:CGFloat = 2           //預設簽名寬度為2
    var path:UIBezierPath?              //紀錄簽名路徑
    var touchPoint:CGPoint?             //所有簽名座標
    var startPoint:CGPoint?             //開始簽名座標

}

 

5.顯示簽名

顯示簽名的功能,我們拆解其功能為:開始簽名、簽名、顯示

首先我們先override這兩項UIView的方法

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        //開始簽名
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        //正在簽名
    }

 

在 override func touchesBegan(_ touchL Set<UITouch>, with event: UIEvent?)這個方法中將第一個座標指定給startPoint

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        //開始簽名,將第一個接觸到的座標指定給startPoint
        startPoint = touches.first?.location(in: self)
    }

 

再透過 override func touchMoved(_ touches: Set<UITouch>, with event: UIEvent?)這個方法將所有簽名時所接觸到的座標指定給touchPoint

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        //正在簽名,將所有簽名時的座標指定給touchPoint
        touchPoint = touches.first?.location(in: self)
        
    }

 

接著,將取到的touchPoint座標和startPoint座標之間畫一條線,並記錄在path裡,畫完之後再將touchPoint座標指定給startPoint座標再繼續畫線

startPoint --> touchPoint(startPoint) --> touchPoint(startPoint) --> touchPoint

 

 override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        //正在簽名,將所有簽名時的座標指定給touchPoint
        touchPoint = touches.first?.location(in: self)
        //初始化path,在startPoint到touchPoint畫一條線
        //可以想像成move是開始的座標,addline是下一個的座標
        //畫完之後再將下一個座標指定成為開始的座標,這樣就能畫出連續的線
        // startPoint --> touchPoint (startPoint) --> touchPoint (startPoint) --> touchPoint
        path = UIBezierPath()
        path?.move(to: startPoint ?? CGPoint.init())
        path?.addLine(to: touchPoint ?? CGPoint.init())
        startPoint = touchPoint
        
    }

 

再來將所有路徑透過CAShapeLayer畫出

 

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        //正在簽名,將所有簽名時的座標指定給touchPoint
        touchPoint = touches.first?.location(in: self)
        //初始化path,在startPoint到touchPoint畫一條線
        //可以想像成move是開始的座標,addline是下一個的座標
        //畫完之後再將下一個座標指定成為開始的座標,這樣就能畫出連續的線
        // startPoint --> touchPoint (startPoint) --> touchPoint (startPoint) --> touchPoint
        path = UIBezierPath()
        path?.move(to: startPoint ?? CGPoint.init())
        path?.addLine(to: touchPoint ?? CGPoint.init())
        startPoint = touchPoint
        draw()
    }
    func draw() {
        let shape = CAShapeLayer()              //初始化 CAShapeLayer()
        shape.path = path?.cgPath               //設定 CAShapeLayer要畫出的路徑
        shape.strokeColor = lineColor.cgColor   //設定線條的顏色
        shape.lineWidth = lineWidth             //設定線條的寬度
        self.layer.addSublayer(shape)           //將設定的屬性增加到SignatureView的layer
        self.setNeedsLayout()                   //更新畫面
    }

 

這樣就完成了簽名板的基本功能了

6.清除簽名

 

實作了簽名板功能後,萬一不小心手誤簽錯了該怎麼,我們只要將所有路徑及SignatureView的Layer清空就可以了

 

    func clearView() {
        path?.removeAllPoints()                 //移除UIBezierPath所有座標
        self.layer.sublayers = nil              //將SignatureView的layer清除
        self.setNeedsDisplay()                  //更新畫面
    }

 

 

 

7.將簽名存入手機相簿

 

簽完名之後透過螢幕快照的方式將簽名檔存入手機相簿內

 

一定要做這個步驟:取得存取相簿權限,在Info.plist加入NSPhotoLibraryAddUsageDescription

	<key>NSPhotoLibraryAddUsageDescription</key>
	<string>取得相機權限</string>

回到ViewController.Swift 將螢幕快照的方法加入saveButtonPressed這個方法裡面,

也就是說按下“存入手機相簿”的按鈕的時候,執行螢幕快照的功能

 

@IBAction func saveButtonPressed(_ sender: Any) {
        var screenshotImage :UIImage?
        let layer = UIApplication.shared.windows.first?.layer
        let scale = UIScreen.main.scale
        UIGraphicsBeginImageContextWithOptions(layer!.frame.size, false, scale);
        guard let context = UIGraphicsGetCurrentContext() else { return }
        layer!.render(in:context)
        screenshotImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        if let image = screenshotImage {
            UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
            popAlert()
        }
    }
    func popAlert() {
        let controller = UIAlertController(title: "提示", message: "已存入手機相簿", preferredStyle: .alert)
        let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
            self.signatureView.clearView()
        }
        controller.addAction(okAction)
        present(controller, animated: true, completion: nil)
    }

 

螢幕快照成功後跳出alert提醒使用者儲存成功

結論:

在IOS的App開發中一定很多種製作簽名版的方式,

透過CAShapeLayerUIBezierPath所製作的簽名板只是其中一項

也許將來Apple 會直接推出簽名版的API供開發者使用也說不定

就讓我們期待吧!

 

 

參考資料:

https://developer.apple.com/documentation/uikit/uibezierpath

https://developer.apple.com/documentation/quartzcore/cashapelayer

https://stackoverflow.com/questions/25448879/how-do-i-take-a-full-screen-screenshot-in-swift

 

 

 

 

林哲緯