和 UIKit 相比,SwiftUI 還是不夠完善。儘管 iOS15 推出的 SwiftUI 已經是第 3 次修訂,但還是缺少了一些重要的視圖。
Apple 餘下的挑戰,最主要的就是盡可能讓開發者自然地進行過渡。因為大家都不想重寫 UI/UX 程式碼,公司絕對無法付出這麼多時間和精神。他們需要一種方法,來盡量重用已有的程式碼,但又不會有 UIKit 的包袱。
除此之外,SwiftUI 是個非常新的典範 (paradigm),調整了一些開發者之前已經掌握好的控件。因此,雖然他們在最近的 WWDC 21 中沒有詳細說明,但可以看出 Apple 正在嘗試達到一些中間點,像是一個名為 Canvas 的新視圖。這也可以說是 Apple 的一種妥協。
在 WWDC 21,Apple 把 SwiftUI 介紹作一種更有效的繪圖方式。它不但提升了 CPU 方面的效能,我認為它在程式碼遷移方面、和對 Core Graphics 的原生支援也更有效率,這些都是我們以前無法在 SwiftUI 中輕鬆做到的事,畢竟以前需要在 UIImages
和 Images
之間進行轉換。
什麼是 SwiftUI Canvas 視圖?
Canvas 視圖有點像是 SwiftUI 的 SpriteKit 視圖,因為兩者都是比較獨立的視圖。
比如說,我們不能將手勢 (gesture) 附加到 Canvas 視圖內的元素 (element),因為它是一個獨立的物件(當然我們還是可以將手勢添加到整個 Canvas 視圖中)。但與以前的情況相比,Canvas 視圖有一個好處,就是大大提高了繪製視圖的效率。更重要的是,Canvas 視圖原生支援所有 Core Graphics,也就是說,我們只要在 SwiftUI 界面進行更改,就可以將程式碼移植到視圖。讓我們一起構建上面的綠色時鐘,了解一下新的 Canvas 指令。
在 Canvas 視圖中繪製形狀
首先,讓我們繪製時鐘的形狀,就是一個簡單的圓形。當然你也可以繪製方形的時鐘,方形和圓形都是已有的基本圖形。我們會把它設置為紅色,並降低不透明度,所以它看起來是已褪色的紅色。我們會利用下面程式碼看到的 setLineDash
函數來繪製黑線,長度以弧度 (radian) 計算(對,這真的很不方便)。
struct Clock: View {
var body: some View {
Canvas { context, size in
context.withCGContext { cgContext in
let rect = CGRect(origin: .zero, size: size).insetBy(dx: 40, dy: 40)
let path = CGPath(ellipseIn: rect, transform: nil)
cgContext.addPath(path)
cgContext.setStrokeColor(UIColor.black.cgColor)
cgContext.setFillColor(UIColor.red.cgColor)
cgContext.setLineWidth(24)
cgContext.setAlpha(0.5)
cgContext.setLineDash(phase: 3, lengths: [2*Double.pi*15])
cgContext.drawPath(using: .eoFillStroke)
}
}.frame(width: 200, height: 200)
}
}
在 Canvas 視圖中混合 (Blend) 形狀顏色
接下來,我們的時鐘需要一個邊框,我選擇了綠色,所以我將它與紅色的時鐘混合在一起,為時鐘添加了一個邊框。
context.withCGContext { cgContext in
let rect = CGRect(origin: .zero, size: size).insetBy(dx: 20, dy: 20)
let path = CGPath(ellipseIn: rect, transform: nil)
cgContext.addPath(path)
cgContext.setStrokeColor(UIColor.black.cgColor)
cgContext.setFillColor(UIColor.green.cgColor)
cgContext.setLineWidth(10)
cgContext.setAlpha(0.5)
cgContext.drawPath(using: .eoFillStroke)
}
在 Canvas 視圖中繪製文本
接著,雖然不是一定需要,但我想利用渲染 (render) 添加一個文本,所以我在 Canvas 中添加了一個字詞。
context.withCGContext { cgContext in
let midPoint = CGPoint(x: size.width/2.0, y: size.height/2.0)
let text = Text("SwiftUI").font(.title).fontWeight(.heavy)
context.blendMode = GraphicsContext.BlendMode.softLight
context.draw(text, at: midPoint)
}
在 Canvas 視圖中繪製圖像
然後,我會添加一個從 thenounproject
下載的圖像,並混合到邊框內,這樣它就不會太突出了,雖然文本會有一點看不清楚。
context.withCGContext { cgContext in
context.blendMode = GraphicsContext.BlendMode.softLight
let rect3 = CGRect(origin: .zero, size: size).insetBy(dx: 10, dy: 10)
let image = Image("800x800")
context.draw(image, in: rect3)
}
在 Canvas 視圖中繪製線條
最後,我們簡單地從圖像的中心向外繪製兩條線,作為時鐘的時針和分針。請注意,這是另外兩個獨立的 Canvas 物件。因為我們會分別為兩條線製作動畫,因此它們需要分開處理。另外,我也更改了 zIndex
,以確保兩條線永遠都會顯示在時鐘圖像的上方,並稍微旋轉兩條線,以便我們可以看到線條。
Canvas { context, size in
context.withCGContext { cgContext in
let midPoint = CGPoint(x: size.width/2.0 - 64, y: size.height/2.0)
let nexPoint = CGPoint(x: size.width/2.0, y: size.height/2.0)
cgContext.move(to: nexPoint)
cgContext.setStrokeColor(UIColor.white.cgColor)
cgContext.setLineWidth(3)
cgContext.addLine(to: midPoint)
cgContext.drawPath(using: CGPathDrawingMode.eoFillStroke)
}
}.rotationEffect(.degrees(redHand))
.zIndex(1)
Canvas { context, size in
context.withCGContext { cgContext in
let midPoint = CGPoint(x: size.width/2.0, y: size.height/2.0)
let nexPoint = CGPoint(x: size.width/2.0 - 48, y: size.height/2.0)
cgContext.move(to: nexPoint)
cgContext.setStrokeColor(UIColor.red.cgColor)
cgContext.setLineWidth(3)
cgContext.addLine(to: midPoint)
cgContext.drawPath(using: CGPathDrawingMode.eoFillStroke)
}
}.rotationEffect(.degrees(whiteHand))
.zIndex(1)
製作動畫
現在,讓我們在時鐘添加四個數字,並創建兩個計時器 (timer) 連接到程式碼,來製作隨時間旋轉那兩條線的動畫。這就是你在文章開頭看到的動圖了!
這是既成事實——我在鐘面上添加了四個主要數字;然後通過創建兩個計時器並將它們鏈接到代碼以將包含線條的圖像隨著時間的推移旋轉超過 360 度——您在本文開頭看到的移動圖像。
import SwiftUI
import UIKit
let whiteHandTimer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
let redHandTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
struct Clock: View {
@State var whiteHand:Double = 0
@State var redHand:Double = 0
var body: some View {
VStack(spacing: 0) {
Text("12")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.black)
ZStack() {
Text("9")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.black)
.offset(x: -124, y: 0)
Text("3")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.black)
.offset(x: 124, y: 0)
Canvas { context, size in
// Code already posted...
}
.frame(width: 200, height: 200)
.onReceive(redHandTimer) { _ in
redHand += 1
}
.onReceive(whiteHandTimer) { _ in
whiteHand += 1
}
Text("6")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.black)
}
}
}
這篇教學到此為止,希望你喜歡這篇文章。