SwiftUI 使用InsettableShape畫正多邊形
xcode 13.4.1, swift 5.5, iOS 15.4
2022-08-13
SwiftUI 提供了Shape和InsettableShape兩個元件,使用這兩個元件可以大大簡化在繪圖的複雜度。雖然使用Shape就可以用來繪畫,但當stroke的lineWidth很寛時,會有一半的線條超出所顯示的範圍,就可以SwiftUI就提供了另一個prototype InsettableShape,只要實現InsettableShape的inset(by amount: CGFloat)就可以解決以上的問題。
1. 正多邊形的所有頂點是平均分布在用一個圖上,計算如下。
假計現在要畫一個5邊形,圓心在(x0, y0),圓的半徑為R,5個頂點平均分布在一個圓上,就是每兩個頂點之間相隔2π/5,所以第i個頂點會落在的角度為2π * i / 5上,
位置就落在(x : x0 + cos(2π * i / 5) * R, y : y0 + sin(2π * i / 5) * R)
最後把這些點依次連成一個閉圖。
import SwiftUI
struct Polygon : Shape {
let numOfPoints : Int
func path(in rect: CGRect) -> Path {
let radius = min(rect.width, rect.height)/2
let center = CGPoint(x : rect.width/2, y:rect.height/2)
let eachOfAngle = 2.0*Double.pi/Double(numOfPoints)
let points = (0..<numOfPoints).map { index -> CGPoint in
let angle = -Double.pi/2.0 + eachOfAngle*Double(index)
return CGPoint(x : center.x + cos(angle) * radius, y : center.y + sin(angle) * radius)
}
return Path { path in
path.addLines(points)
path.closeSubpath()
}
}
}
struct PolygonExample : View {
var body: some View {
Polygon(numOfPoints: 5)
}
}
程式解釋:
numOfPoints:正多邊形的邊數
2. 自定議的Polygon可以使用Shape和InsettableShape已經提供的一些方法,比如stroke的顏色和寬度。
import SwiftUI
struct Polygon : Shape {
let numOfPoints : Int
func path(in rect: CGRect) -> Path {
let radius = min(rect.width, rect.height)/2
let center = CGPoint(x : rect.width/2, y:rect.height/2)
let eachOfAngle = 2.0*Double.pi/Double(numOfPoints)
let points = (0..<numOfPoints).map { index -> CGPoint in
let angle = -Double.pi/2.0 + eachOfAngle*Double(index)
return CGPoint(x : center.x + cos(angle) * radius, y : center.y + sin(angle) * radius)
}
return Path { path in
path.addLines(points)
path.closeSubpath()
}
}
}
struct PolygonExample : View {
var body: some View {
Polygon(numOfPoints: 5)
.stroke(.blue, lineWidth: 50)
}
}
3. 由上圖看到若線條寛度太寛會出現半條線條在外面,為了解決這個問題,將Polygon由承繼Shape改成承繼InsettableShape,並實現inset(by amount: CGFloat)就可以解決以上的問題,實作如下。
import SwiftUI
struct Polygon : InsettableShape {
let numOfPoints : Int
var insetAmount: CGFloat = 0
func path(in rect: CGRect) -> Path {
let radius = min(rect.width, rect.height)/2-insetAmount
let center = CGPoint(x : rect.width/2, y:rect.height/2)
let eachOfAngle = 2.0*Double.pi/Double(numOfPoints)
let points = (0..<numOfPoints).map { index -> CGPoint in
let angle = -Double.pi/2.0 + eachOfAngle*Double(index)
return CGPoint(x : center.x + cos(angle) * radius, y : center.y + sin(angle) * radius)
}
return Path { path in
path.addLines(points)
path.closeSubpath()
}
}
func inset(by amount: CGFloat) -> some InsettableShape {
var polygon = self
polygon.insetAmount += amount
return polygon
}
}
struct PolygonExample : View {
var body: some View {
Polygon(numOfPoints: 5)
.inset(by: 25)
.stroke(.blue, lineWidth: 50)
}
}
4. 之後我們就可以覆用這個自創的元件。
import SwiftUI
struct PolygonExample : View {
var body: some View {
VStack {
HStack {
Polygon(numOfPoints: 5)
.inset(by: 5)
.stroke(.blue, lineWidth: 10)
Polygon(numOfPoints: 6)
.inset(by: 5)
.fill(.teal)
}
HStack {
Polygon(numOfPoints: 8)
.inset(by: 5)
.stroke(.orange, lineWidth: 10)
Polygon(numOfPoints: 10)
.inset(by: 5)
.stroke(.teal, lineWidth: 10)
.overlay(
Polygon(numOfPoints: 10)
.inset(by: 12)
.fill(.blue.opacity(0.5))
)
}
}
}
}