對於有經驗的開發者,你可能已使用過 Core Graphics API 來繪製形狀與物件。這是一個非常強大的框架,可於建立向量圖。在 SwiftUI 中,它也提供幾個向量繪圖 API,可供開發者繪製線條與形狀。
在本章中,你將學習如何使用 Path
與內建的 Shape
(如 Circle
與 RoundedRectangle
), 來繪製線條、圓弧、圓餅圖與環圈圖。下列是我將要介紹的主題:
Shape
協定?如何遵循這個協定來繪製出自訂的形狀? 圖8.1 列出了我們在後面的小節中所要建立的一些形狀與圖表。
在 SwiftUI 中,你可使用 Path 繪製線條與形狀。如果你參考 Apple 的文件 (https://developer.apple.com/documentation/swiftui/path) , Path
是一個包含 2D 形狀輪廓的結構,基本上,線條與形狀是以路徑逐步描繪。以圖 8.2 為例,這是我們要在螢幕上繪製的矩形。
請敘說你要如何逐步繪製正方形呢?你可能會提供下列的描述:
這就是所謂的 Path
。如果將上面的步驟寫成程式碼,程式碼如下所示:
Path() { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 300, y: 20))
path.addLine(to: CGPoint(x: 300, y: 200))
path.addLine(to: CGPoint(x: 20, y: 200))
}
.fill(.green)
這裡初始化一個 Path
,並在閉包中提供詳細的說明。你可以呼叫 move(to:)
方法移動至一個特定的座標。要從目前的點畫一條線到特定的點,則可以呼叫 addLine(to:)
方法。預設上,iOS 會以預設的前景色(即黑色)來填滿路徑,若填滿其他顏色,則可以使用 .fill
修飾器,並設定為其顏色。
你可以使用 「App」模板建立一個新專案來測試程式碼。將專案命名為 SwiftUIShape
(或你喜歡的任何名稱),然後在 body
輸入上列的程式碼片段,預覽畫布即會顯示出一個綠色矩形,如圖 8.3 所示。
你不需要以顏色填滿整個區域,如果你只想繪製線條的話,則可以使用 .stroke
修飾器,並指定線條的寬度與顏色,如圖 8.4 所示。
因為我們沒有指定將線條繪製到原點的步驟,所以顯示為一個開放路徑。要封閉路徑的話,你可以在 Path
閉包的結尾處呼叫 closeSubpath()
方法,此方法會自動將目前點與起點連接起來。
Path
提供了多個內建的 API 來幫助你繪製不同的形狀。你不只能夠畫出直線,還可以使用 addQuadCurve
、addCurve
與 addArc
方法來繪製出曲線與圓弧。例如:你想要在矩形頂部繪製出一個圓頂,如圖 8.6 所示。
程式碼可以這樣編寫:
Path() { path in
path.move(to: CGPoint(x: 20, y: 60))
path.addLine(to: CGPoint(x: 40, y: 60))
path.addQuadCurve(to: CGPoint(x: 210, y: 60), control: CGPoint(x: 125, y: 0))
path.addLine(to: CGPoint(x: 230, y: 60))
path.addLine(to: CGPoint(x: 230, y: 100))
path.addLine(to: CGPoint(x: 20, y: 100))
}
.fill(Color.purple)
addQuadCurve
方法可以讓你透過定義一個控制點(control point )來繪製曲線。參考圖 8.6,(40, 60) 與(210, 60) 就是所謂的「錨點」(anchor point ),(125, 0) 則是計算建立圓頂形狀的控制點,我不打算在這裡討論有關繪製曲線的數學,你可嘗試更改控制點的值來查看效果。簡單而言,該控制點控制如何繪制曲線。如果你將控制點放在更靠近矩形頂部的位置(例如:125, 30),則會繪製出不圓的外觀。
如果要畫出形狀的邊框,並同時以顏色填滿形狀,該怎麼做呢? fill
與 stroke
修飾器無法並行使用,不過你可以使用 ZStack
來達到相同的效果,程式碼如下所示:
ZStack {
Path() { path in
path.move(to: CGPoint(x: 20, y: 60))
path.addLine(to: CGPoint(x: 40, y: 60))
path.addQuadCurve(to: CGPoint(x: 210, y: 60), control: CGPoint(x: 125, y: 0))
path.addLine(to: CGPoint(x: 230, y: 60))
path.addLine(to: CGPoint(x: 230, y: 100))
path.addLine(to: CGPoint(x: 20, y: 100))
}
.fill(Color.purple)
Path() { path in
path.move(to: CGPoint(x: 20, y: 60))
path.addLine(to: CGPoint(x: 40, y: 60))
path.addQuadCurve(to: CGPoint(x: 210, y: 60), control: CGPoint(x: 125, y: 0))
path.addLine(to: CGPoint(x: 230, y: 60))
path.addLine(to: CGPoint(x: 230, y: 100))
path.addLine(to: CGPoint(x: 20, y: 100))
path.closeSubpath()
}
.stroke(Color.black, lineWidth: 5)
}
我們使用相同的路徑建立兩個 Path
物件,然後使用 ZStack
來讓一個 Path
物件疊在另一個 Path
物件上面。下面是使用 fill
填滿紫色的圓頂矩形,並以黑色邊框疊在上面,如圖 8.7 所示。
SwiftUI 為開發者提供了一個方便的 API 來繪製圓弧,該 API 對於組合各種形狀和物件(包含圓餅圖)非常有用。要繪製圓弧,你可以撰寫程式碼如下:
Path { path in
path.move(to: CGPoint(x: 200, y: 200))
path.addArc(center: .init(x: 200, y: 200), radius: 150, startAngle: .degrees(0), endAngle: .degrees(90), clockwise: true)
}
.fill(.green)
如果你將程式碼放入 body 中,則會在預覽畫布中看到一個填滿綠色的圓弧,如圖 8.8 所示。
在上列的程式碼中,我們先至起點 (200, 200),然後呼叫 addArc
來建立圓弧。addArc
方法接受幾個參數:
如果只看「startAngle」與「endAngle」等兩個參數的名稱,應該會對其含義有點困惑, 圖 8.9 可讓你更加了解這些參數的含義。
使用 addArc
可輕鬆建立不同色扇形的圓餅圖,你只需要以 ZStack
來重疊不同的扇形即可。組成其圖的各個扇形都有不同 startAngle
值與 endAngle
值,下列是程式碼片段:
ZStack {
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(0), endAngle: .degrees(190), clockwise: true)
}
.fill(.yellow)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(190), endAngle: .degrees(110), clockwise: true)
}
.fill(.teal)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(110), endAngle: .degrees(90), clockwise: true)
}
.fill(.blue)
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90), endAngle: .degrees(360), clockwise: true)
}
.fill(.purple)
}
這將渲染出一個具有四個扇形的圓餅圖,如果你需要更多的扇形,則只要使用不同角度值來建立其他的路徑物件即可。順帶一提,我使用的顏色是來自 iOS 所提供的標準顏色物件。你可以至下列的網址來了解完整的顏色物件:https://developer.apple.com/documentation/uikit/uicolor/standard_colors.
有時,你可能想從圓餅圖切分出來,以突顯特定的扇形。舉例而言,要以紫色突顯扇形時,你可以應用 offset
修飾器來改變扇形的位置:
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90), endAngle: .degrees(360), clockwise: true)
}
.fill(.purple)
.offset(x: 20, y: 20)
或者,你可以疊加一個邊框來進一步吸引人們目光。如果你要在突顯的扇形上加入標籤,則可以疊上一個 Text
視圖,如下所示:
Path { path in
path.move(to: CGPoint(x: 187, y: 187))
path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90), endAngle: .degrees(360), clockwise: true)
path.closeSubpath()
}
.fill(.purple)
.offset(x: 20, y: 20)
.overlay(
Text("25%")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundColor(.white)
.offset(x: 80, y: -110)
)
該路徑有與紫色扇形相同的起點角度與終點角度,但是它只僅繪製邊框及加入一個文字視圖,以使扇形突出,圖 8.10 為最後的結果。
在我們深入了解 Shape
協定之前,我們先從一個簡單的作業來開始。根據所學,使用 Path
繪製下列的形狀,如圖8.11 所示。
請先不要看解答,試著自己做看看。
好,要建立一個像這樣的形狀,你可使用 addLine
與 addQuadCurve
來建立一個 Path
:
Path() { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addQuadCurve(to: CGPoint(x: 200, y: 0), control: CGPoint(x: 100, y: -20))
path.addLine(to: CGPoint(x: 200, y: 40))
path.addLine(to: CGPoint(x: 200, y: 40))
path.addLine(to: CGPoint(x: 0, y: 40))
}
.fill(Color.green)
如果你閱讀過 Path
的文件,則可能找到另一個名為 addRect
的函數,該函數可以讓你以特定的寬度與高度來繪製矩形。因此,下面是替代的解決方案:
Path() { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addQuadCurve(to: CGPoint(x: 200, y: 0), control: CGPoint(x: 100, y: -20))
path.addRect(CGRect(x: 0, y: 0, width: 200, height: 40))
}
.fill(Color.green)
現在,我們來討論一下 Shape
協定,這個協定非常簡單,只有一個需求,當你使用它時,你必須實作下列函數:
func path(in rect: CGRect) -> Path
那麼,我們何時需要使用 Shape
協定呢?試問你如何重新使用剛建立的 Path
呢?例如: 你想要建立一個圓頂(Dome)形狀、大小彈性的按鈕,該如何實作呢?
再看一下上列的程式碼,你以絕對座標與尺寸來建立一個路徑。為了建立相同但大小可變的形狀,則可以建立一個結構來採用 Shape
協定,並實作 path(in:)
函數。當 path(in:)
函數被框架呼叫時,你將獲得 rect
的大小,然後可在 rect
中繪製路徑。
我們來了解如何建立圓頂形狀,如此你便能更了解 Shape
協定。
struct Dome: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addQuadCurve(to: CGPoint(x: rect.size.width, y: 0), control: CGPoint(x: rect.size.width/2, y: -(rect.size.width * 0.1)))
path.addRect(CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height))
return path
}
}
使用該協定後,我們會獲得用於繪製路徑的矩形區城,我們從 rect
可以找到矩形區域的寬度與高度來計算控制點,並繪製矩形底座。
藉由這個形狀,你就可以使用它來建立各種 SwiftUI
控制元件。舉例而言,你可以建立一個具有圓頂形狀的按鈕,如下所示:
Button(action: {
// 執行動作
}) {
Text("Test")
.font(.system(.title, design: .rounded))
.bold()
.foregroundColor(.white)
.frame(width: 250, height: 50)
.background(Dome().fill(Color.red))
}
我們將圓頂形狀作為按鈕的背景,其寬度與高度是基於指定的框架大小。
在前面,我們使用 Shape
協定自訂了一個形狀。而 SwiftUI
其實有幾種內建形狀,如圓形( Circle
)、矩形( Rectangle
)、圓角矩形( RoundedRectangle
)與橢圓( Ellipse
)等, 如果你不想要太花俏的話,這些形狀已經足以建立一些常見的物件了。
舉例而言,你要建立一個如圖 8.13 所示的「停止」按鈕,此按鈕是由一個圓角矩形與一個圓形所組成,你可以撰寫程式碼如下:
Circle()
.foregroundColor(.green)
.frame(width: 200, height: 200)
.overlay(
RoundedRectangle(cornerRadius: 5)
.frame(width: 80, height: 80)
.foregroundColor(.white)
)
這裡,我們初始化一個 Circle
視圖,然後將一個 RoundedRectangle
視圖疊在上面。
透過內建形狀的混搭,你可以為應用程式建立各種類型的向量式(vector-based )UI 控制元件。我再舉另一個例子,圖 8.14 為一個使用 Circle
建立的進度指示器。
這個進度指示器其實是由兩個圓形所組成,下方是一個灰色圓環,而在灰色圓環的上方則是一個開口圓環,指示完成的進度。你可以在 ContentView
中撰寫程式碼,如下所示:
struct ContentView: View {
private var purpleGradient = LinearGradient(gradient: Gradient(colors: [ Color(red: 207/255, green: 150/255, blue: 207/255), Color(red: 107/255, green: 116/255, blue: 179/255) ]), startPoint: .trailing, endPoint: .leading)
var body: some View {
ZStack {
Circle()
.stroke(Color(.systemGray6), lineWidth: 20)
.frame(width: 300, height: 300)
}
}
}
我們使用 stroke
修飾器來畫出圓環的輪廓,若是你喜歡較粗(或較細)的線條,則可以調整 lineWidth
參數。而 purpleGradient
屬性定義了紫色漸層,我們稍後在繪製開口圓環時會使用它。
現在,在 ZStack
中插入下列的程式碼,以建立開口圓環:
Circle()
.trim(from: 0, to: 0.85)
.stroke(purpleGradient, lineWidth: 20)
.frame(width: 300, height: 300)
.overlay {
VStack {
Text("85%")
.font(.system(size: 80, weight: .bold, design: .rounded))
.foregroundColor(.gray)
Text("Complete")
.font(.system(.body, design: .rounded))
.bold()
.foregroundColor(.gray)
}
}
建立一個開口圓環的技巧是加上一個 trim
修飾器。你可指定 from
值與 to
值,以指示要顯示圓環的哪一個部分,在這個範例中,我們想要顯示 85% 的進度,所以設定 from
的值為「0」、to 的值為「0.85」。
為了顯示完成百分比( completion percentage),我們將一個文字視圖疊在圓環的中間, 如圖 8.16 所示。
最後要示範的是環圈圖,如果你完全了解 trim
修飾器的用法,那麼你可能已經知道我們將如何實作環圈圖了。處理 trim
修飾器的值,我們可以將圓環切分成多段。
這是我們用來建立環圈圖的技巧,程式碼如下所示:
ZStack {
Circle()
.trim(from: 0, to: 0.4)
.stroke(Color(.systemBlue), lineWidth: 80)
Circle()
.trim(from: 0.4, to: 0.6)
.stroke(Color(.systemTeal), lineWidth: 80)
Circle()
.trim(from: 0.6, to: 0.75)
.stroke(Color(.systemPurple), lineWidth: 80)
Circle()
.trim(from: 0.75, to: 1)
.stroke(Color(.systemYellow), lineWidth: 90)
.overlay(
Text("25%")
.font(.system(.title, design: .rounded))
.bold()
.foregroundColor(.white)
.offset(x: 80, y: -100)
)
}
.frame(width: 250, height: 250)
第一段圓弧只顯示圓環的 40%,第二段圓弧顯示圓環的 20%,不過請注意 from
值是「0.4」,而不是「0」,這可以讓第二段圓弧連接第一段圓弧。
對於最後一個圓弧,我故意把線寬設得大一點,以使該段圓弧突出,如圖8.17 所示。如果你不喜歡這樣的設計,則可以將 linewidth
值由「90」改為「80」。
我希望你喜歡本章內容,並愛上範例專案。藉由框架所提供的繪圖 API,你可以輕鬆為應用程式建立自訂形狀。Path 與 Shape 的運用還有很多,我僅介紹本章中的一些技巧,但請試著運用所學來施展一些魔法吧 !
在本章所準備的範例檔中,有完整的專案可以下載: