使用 #Preview 預覽 UIBezierPath 的繪圖結果
從 Xcode 15 開始,我們可以利用 #Preview 預覽 App 畫面,因此我們可以利用它一邊用 UIBezierPath 繪圖,一邊從預覽畫面觀看繪製的結果。
接下來讓我們一邊說明 UIBezierPath 繪圖的原理,一邊介紹如何利用 #Preview 預覽繪製的結果。
加入 App 的預覽畫面
建立 iOS App 專案,Interface 選擇 storyboard,在 ViewController.swift 的下方加入以下程式,此段程式將讓右邊出現 App 的預覽畫面。
#Preview {
UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()!
}
在 function viewDidLoad 裡繪圖
剛剛我們順利地看到了預覽畫面,但是它卻是一片空白。這是當然的,因為我們還沒有繪製任何圖案。
接下來我們將在 function viewDidLoad 裡利用 UIBezierPath 繪圖。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
利用 UIBezierPath 繪製路徑
在繪圖之前請先參考以下文章的教學,了解利用 UIBezierPath 繪製路徑的原理。
以下我們在 viewDidLoad 裡利用 UIBezierPath 繪製三角形的路徑。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 100, y: 0))
path.addLine(to: CGPoint(x: 100, y: 100))
path.close()
}
生成能顯示在畫面上的形狀,CAShapeLayer
IBezierPath 繪製的路徑是虛擬的,無法顯示在 App 畫面上,就像我們告白時用手在空氣中畫愛心一樣。因此接下來我們要產生能顯示在畫面上的 CAShapeLayer。
CAShapeLayer 代表某種形狀,我們透過它的 path 設定形狀。然而 triangleLayer.path 的型別是 CGPath,因此以下程式將產生錯誤。
我們必須利用 path.cgPath 讀取 CGPath 型別的三角形路徑後再存入 triangleLayer.path。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 100, y: 0))
path.addLine(to: CGPoint(x: 100, y: 100))
path.close()
let triangleLayer = CAShapeLayer()
triangleLayer.path = path.cgPath
}
將 CAShapeLayer 加到 view 上
此時預覽畫面還是一片空白,因為它顯示的是空白的 view,而不是我們剛剛產生的 triangleLayer。因此我們還要加入關鍵的一行,利用 view.layer.addSublayer(triangleLayer)
將 triangleLayer 加到 view 上。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 100, y: 0))
path.addLine(to: CGPoint(x: 100, y: 100))
path.close()
let triangleLayer = CAShapeLayer()
triangleLayer.path = path.cgPath
view.layer.addSublayer(triangleLayer)
}
預覽繪製的形狀
Cool,我們成功地在右邊的預覽畫面看到三角形。
而且更令人開心的,當我們對繪製的程式做任何修改時,馬上就能在右邊的預覽畫面看到結果,實現一邊以程式畫圖,一邊從預覽畫面觀看結果的夢想。
不過有時它會忘了自動更新,此時可按下方的三角形強迫它更新。
設定 CAShapeLayer 填滿形狀的顏色
CAShapeLayer 預設以黑色填滿,我們可以設定 CAShapeLayer 的 fillColor,改變它填滿的顏色。由於 fillColor 的型別是 CGColor,所以我們需要給它型別 CGColor 的東西。
triangleLayer.fillColor = CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1)
繪製線條
有時我們希望繪製線條,而不是填滿的形狀。此時可透過 CAShapeLayer 的 strokeColor 設定邊框的顏色,lineWidth 設定邊框的寬度。(stroke 描述繪圖的筆畫,因此 strokeColor 表示 UIBezierPath 畫出的線條顏色。)
let triangleLayer = CAShapeLayer()
triangleLayer.path = path.cgPath
triangleLayer.strokeColor = CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1)
triangleLayer.lineWidth = 10
view.layer.addSublayer(triangleLayer)
不過此時三角形的中間卻是黑色的,因為 CAShapeLayer 預設會填滿黑色。若想只有線條,我們可以將 fillColor 設為代表透明的 clear color。
let triangleLayer = CAShapeLayer()
triangleLayer.path = path.cgPath
triangleLayer.strokeColor = CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1)
triangleLayer.fillColor = UIColor.clear.cgColor
triangleLayer.lineWidth = 10
view.layer.addSublayer(triangleLayer)
加入多個 layer
我們可利用 addSublayer 加入多個 layer,比方以下例子,加入一個藍色的實心三角形和紅色的空心三角形。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// 藍色實心三角形
var path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 50))
path.addLine(to: CGPoint(x: 100, y: 0))
path.addLine(to: CGPoint(x: 100, y: 100))
path.close()
let blueTriangleLayer = CAShapeLayer()
blueTriangleLayer.path = path.cgPath
blueTriangleLayer.fillColor = CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1)
view.layer.addSublayer(blueTriangleLayer)
// 紅色空心三角形
path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 150))
path.addLine(to: CGPoint(x: 100, y: 0))
path.addLine(to: CGPoint(x: 200, y: 200))
path.close()
let redTriangleLayer = CAShapeLayer()
redTriangleLayer.path = path.cgPath
redTriangleLayer.fillColor = UIColor.clear.cgColor
redTriangleLayer.lineWidth = 10
redTriangleLayer.strokeColor = CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1)
view.layer.addSublayer(redTriangleLayer)
}
由於我們先加入 blueTriangleLayer,再加入 redTriangleLayer,因此 redTriangleLayer 會蓋住 blueTriangleLayer,愈後面加入的在愈上層,就像疊疊樂一樣。
繪製特定形狀的三個步驟
透過剛剛的例子,我們了解繪製特定形狀可透過以下三個步驟實現:
- 利用 UIBezierPath 繪製形狀的路徑。
- 產生 CAShapeLayer,將它變成 UIBezierPath 繪製的形狀。
- 利用 addSublayer 加入 CAShapeLayer。
UIView & CAShapeLayer
UIView & CAShapeLayer 都能顯示在畫面上,因此設計 App 畫面時,我們可以利用一個個 UIView 堆疊畫面,也可以利用 CAShapeLayer。一個 view 上可以呼叫 addSubview 加入很多 view 當它的 subview,一個 view 上也可以呼叫 addSublayer 加入很多 layer 當它的 sublayer。
以下範例我們在 view 上依序加入 greenView,redTriangleLayer,blueTriangleLayer,yellowView,因此它們的階層順序將是綠色正方形在最底層,黃色長方形在最上層。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// 綠色正方形
let greenView = UIView(frame: CGRect(x: 10, y: 10, width: 150, height: 150))
greenView.backgroundColor = UIColor.green
view.addSubview(greenView)
// 藍色實心三角形
var path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 50))
path.addLine(to: CGPoint(x: 100, y: 0))
path.addLine(to: CGPoint(x: 100, y: 100))
path.close()
let blueTriangleLayer = CAShapeLayer()
blueTriangleLayer.path = path.cgPath
blueTriangleLayer.fillColor = CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1)
view.layer.addSublayer(blueTriangleLayer)
// 紅色空心三角形
path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 150))
path.addLine(to: CGPoint(x: 100, y: 0))
path.addLine(to: CGPoint(x: 200, y: 200))
path.close()
let redTriangleLayer = CAShapeLayer()
redTriangleLayer.path = path.cgPath
redTriangleLayer.fillColor = UIColor.clear.cgColor
redTriangleLayer.lineWidth = 10
redTriangleLayer.strokeColor = CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1)
view.layer.addSublayer(redTriangleLayer)
// 黃色長方形
let yellowView = UIView(frame: CGRect(x: 60, y: 70, width: 80, height: 50))
yellowView.backgroundColor = UIColor.yellow
view.addSubview(yellowView)
}
通常繪製複雜的視覺效果,比方特別的形狀或漸層時,比較適合用 layer。關於 view 和 layer 的相關說明,可參考以下 AI 的說明。
運用 UIBezierPath 繪製可愛圖案
了解如何繪製簡單的三角形後,接下來讓我們進一步挑戰參考圖片繪製可愛的圖案。
準備繪製的圖片
以可愛的星星弟弟為例。
https://www.pinterest.com/pin/331014641344432049/
建議圖片先縮小到小於 iPhone 螢幕的尺寸,這樣繪製時才不會超出 iPhone 的螢幕。在 Mac 上修改圖片尺寸的方法可參考以下連結。
將圖片上傳到查詢座標顏色的網站 iview
參考圖片繪圖時最困難的莫過於如何得知圖片裡形狀的座標和顏色。不過別擔心,感謝方便的 iview 網站,我們只要將圖片上傳,即可查詢圖片的座標顏色。(ps: 如果覺得圖片太小不容易查座標,可按 cmd + 將網頁放大。)
https://yangcha.github.io/iview/iview.html
ps: 其它獲取顏色的方法。
依據網站標示的座標顏色繪製星星
繪製時建議先繪製線條,方便我們從右邊的預覽觀看形狀。因此我們先設定 CAShapeLayer 的 strokeColor & lineWidth
,並將 fillColor 設為透明。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let path = UIBezierPath()
path.move(to: CGPoint(x: 182, y: 27))
path.addLine(to: CGPoint(x: 224, y: 120))
path.addLine(to: CGPoint(x: 322, y: 139))
path.addLine(to: CGPoint(x: 258, y: 217))
path.addLine(to: CGPoint(x: 270, y: 326))
path.addLine(to: CGPoint(x: 182, y: 282))
let starLayer = CAShapeLayer()
starLayer.path = path.cgPath
starLayer.fillColor = UIColor.clear.cgColor
starLayer.strokeColor = UIColor.black.cgColor
starLayer.lineWidth = 5
view.layer.addSublayer(starLayer)
}
形狀畫好後,接著要移除線條或填滿顏色都可以。比方我們想貪心地同時擁有線條跟填滿顏色,完整程式如下:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let path = UIBezierPath()
path.move(to: CGPoint(x: 182, y: 27))
path.addLine(to: CGPoint(x: 224, y: 120))
path.addLine(to: CGPoint(x: 322, y: 139))
path.addLine(to: CGPoint(x: 258, y: 217))
path.addLine(to: CGPoint(x: 270, y: 326))
path.addLine(to: CGPoint(x: 182, y: 282))
path.addLine(to: CGPoint(x: 90, y: 326))
path.addLine(to: CGPoint(x: 101, y: 221))
path.addLine(to: CGPoint(x: 28, y: 143))
path.addLine(to: CGPoint(x: 128, y: 121))
path.close()
let starLayer = CAShapeLayer()
starLayer.path = path.cgPath
starLayer.fillColor = UIColor.yellow.cgColor
starLayer.strokeColor = UIColor.black.cgColor
starLayer.lineWidth = 5
view.layer.addSublayer(starLayer)
}
現在我們已經成功畫出星星,但要成為下圖可愛的星星弟弟還有滿大的距離。仔細觀察下圖,星星弟弟有一些形狀和線條是曲線,可不是單純的直線呢。
繪製彎曲的曲線
- 畫出某個弧度的曲線
使用 addQuadCurve。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 50))
path.addQuadCurve(to: CGPoint(x: 50, y: 200), controlPoint: CGPoint(x: 150, y: 125))
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.lineWidth = 3
layer.strokeColor = UIColor.black.cgColor
layer.fillColor = UIColor.clear.cgColor
view.layer.addSublayer(layer)
}
如下圖所示,controlPoint 控制曲線的弧度。剛剛 addQuadCurve(to: CGPoint(x: 50, y: 200), controlPoint: CGPoint(x: 150, y: 125))
的參數 to 對應下圖的 C 點,controlPoint 對應 B 點,那麼 A 點呢 ? A 點是在呼叫 addQuadCurve 前畫到的位置,因此是從 (50, 50) 開始。
以下為剛剛的 controlPoint 改成 (10, 125) & (300, 125) 的例子。
使用 addCurve(to:controlPoint1:controlPoint2:)。
有兩個 control 點。
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 50))
path.addCurve(to: CGPoint(x: 50, y: 200), controlPoint1: CGPoint(x: 150, y: 125), controlPoint2: CGPoint(x: 10, y: 160))
- 繪製剛好在圓上的圓弧
利用 UIBezierPath 的 init(arcCenter:radius:startAngle:endAngle:clockwise:) 繪製圓弧。
比方以下程式可畫出只能看不能吃的西瓜。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let aDegree = Double.pi / 180
let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 40, startAngle: aDegree * 0, endAngle: aDegree * 180, clockwise: true)
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.fillColor = UIColor.red.cgColor
view.layer.addSublayer(layer)
}
參數說明:
arcCenter: 圓心座標。
radius: 半徑。
startAngle & endAngle: 圓弧開始和結束的角度。圓形有 360 度,如下圖所示,在程式裡我們以 2 pi 表示 360 度,1 pi 表示 180 度。
我們都知道 pi 是 3.1415926… ,可是我們只記得前面幾位,不記得後面的數字。沒關係,iOS SDK 已經幫我們定義好,透過 Double.pi 或 CGFloat.pi 即可取得。
因此 1 度是 pi / 180,我們利用 let aDegree = Double.pi / 180
算出一度的大小,存在常數 aDegree。而角度 0 度的位置在右邊,180 度在左邊。startAngle 傳入 aDegree * 0,endAngle 傳入 aDegree * 180時,表示從 0 度畫到 180 度。不過由於這個例子剛好整除,所以也可以簡化成傳入 0 & Double.pi。
let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 40, startAngle: 0, endAngle: .pi, clockwise: true)
clockwise: 是否為順時針。畫圓時可以順時針,也可以逆時針。剛剛的例子, 參數 clockwise 傳入 true,因此我們順時針從 0 度畫到 180 度,產生西瓜形狀的下半圓。
其它例子:
逆時針從 10 度畫到 150 度。
let aDegree = Double.pi / 180
let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 40, startAngle: aDegree * 10, endAngle: aDegree * 150, clockwise: false)
值得注意的,剛剛方法繪製的形狀填滿顏色時將以圓弧和圓弧兩端點連線包含的區塊,若想畫出從圓心連到圓弧兩端點的扇形,則須先用 move 移動到圓心,然後再呼叫 addArc(withCenter:radius:startAngle:endAngle:clockwise:)
加入圓弧,例如以下例子:
let aDegree = Double.pi / 180
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 50))
path.addArc(withCenter: CGPoint(x: 50, y: 50), radius: 40, startAngle: aDegree * 10, endAngle: aDegree * 150, clockwise: false)
繪製馬蹄形。
let aDegree = Double.pi / 180
// 畫出比較大的外圓弧,從 (0, 50) 到 (100, 50)
let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 50, startAngle: aDegree * 180, endAngle: aDegree * 0, clockwise: true)
// 畫出右下方的橫線,從 (100, 50) 連到 (75, 50)
path.addLine(to: CGPoint(x: 75, y: 50))
// 畫出比較小的內圓弧,從 (75, 50) 到 (25, 50)
path.addArc(withCenter: CGPoint(x: 50, y: 50), radius: 25, startAngle: aDegree * 0, endAngle: aDegree * 180, clockwise: false)
path.close()
關於曲線繪製的更多說明,也可以參考 Jaba 同學的文章。
合併 path 繪製多個形狀
若是想繪製的多個形狀是同一個樣式,我們也可以用一個 UIBezierPath & CAShapeLayer 繪製,而不用分成多個。如以下程式所示,當我們第一次呼叫 path.close
時代表畫完第一個三角形的 path,接著呼叫 path.move
將移動畫筆,準備畫第二個形狀。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let path = UIBezierPath()
// 繪製第一個三角形的 path
path.move(to: CGPoint(x: 0, y: 50))
path.addLine(to: CGPoint(x: 100, y: 0))
path.addLine(to: CGPoint(x: 100, y: 100))
path.close()
// 繪製第二個三角形的 path
path.move(to: CGPoint(x: 50, y: 150))
path.addLine(to: CGPoint(x: 120, y: 20))
path.addLine(to: CGPoint(x: 200, y: 200))
path.close()
let triangleLayer = CAShapeLayer()
triangleLayer.path = path.cgPath
triangleLayer.fillColor = UIColor.clear.cgColor
triangleLayer.lineWidth = 3
triangleLayer.strokeColor = CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1)
view.layer.addSublayer(triangleLayer)
}
利用 CGAffineTransform 控制元件的縮放,位移和旋轉
需要繪製對稱圖形,比方星星,或是繪製多個一樣形狀的圖案,可研究利用 CGAffineTransform 控制元件的縮放,位移和旋轉。
將 view 變成任意形狀的三種方法
剛剛我們利用 addSublayer 加入不同的形狀,在 iOS 上將 view 變成任意形狀的方法不只一種,比方利用 mask & 定義 function draw 也可以,有興趣的朋友可參考以下連結。
將 SVG 變成 UIBezierPath
同學作品集
剛剛的星星弟弟只是小 case,同學們都用了無與輪比的耐心以 Swift 畫出一個個精彩的作品,有興趣的朋友可參考以下連結慢慢欣賞。