WWDC 2022 即將開始了,而且有可能會推出 SwiftUI 4.0 版本。我從一開始就在使用 SwiftUI 框架,因此我希望寫一篇文章,整合一些我總是重覆使用的擴充功能 (extension)。希望部分功能在新的 SwiftUI 版本中都能夠使用吧!
1. 隱藏視圖
這是一個視圖修飾符,讓我們可以顯示或隱藏視圖。如果沒有這個修飾符,我們就無法這樣設置視圖。視圖修飾符是一種非常方便的模式,我們應該好好記住和善用它。
struct Show: ViewModifier {
let isVisible: Bool
@ViewBuilder
func body(content: Content) -> some View {
if isVisible {
content
} else {
content.hidden()
}
}
}
我們可以這樣在視圖中套用修飾符,而 condition 變數就是一個 bool。
.modifier(Show(isVisible: condition))
你會發現這個修飾符雖然好用,但其實它也釋放了 view
的空間並強制重繪,如此一來就會影響效能。要解決這個問題,我們可以使用 opacity
tab 來達致相似的效果,同時又會運行得更快,又不會釋放使用的空間。
2. Branch
這個修飾符可以完美地控制應包含或排除不同屬性 (attribute)。
extension View {
@ViewBuilder
func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
if condition { transform(self) }
else { self }
}
}
我們可以這樣使用這個修飾符,它也有一個變數,在這個範例中,我們就設置為 colored
。
.if(colored) { view in
view.background(Color.blue)
}
3. Print
如果你是剛剛才開始使用 SwiftUI,最容易出錯的步驟可能就是 print
。這雖然是除錯技巧中的史前文物,但課堂還是會教這個技巧的,而當我們無法在 SwiftUI 視圖中使用,就會十分痛苦。因此,這段程式碼就十分珍貴。
extension View {
func Print(_ vars: Any...) -> some View {
for v in vars { print(v) }
return EmptyView()
}
}
如此一來,我們就可以在程式碼中使用這個 Statement:
self.Print("Inside ForEach", varOne, varTwo ...)
4. 延遲
這在 iOS 15 剛剛更新了,雖然不是 SwiftUI 本身的擴充功能,但它絕對是我們會在程式碼中會做的事情。
extension Task where Success == Never, Failure == Never {
static func sleep(seconds: Double) async throws {
let duration = UInt64(seconds * 1000_000_000)
try await sleep(nanoseconds: duration)
}
}
當然,我們想要在延遲後執行的程式碼,就要放在本身造成延遲的 Task
後面。
Task { try! await Task.sleep(seconds: 0.5) }
5. PassThruSubjects
當我開始在 SwiftUI 使用 Combine
時,我發現了 PassThroughSubjects
這個十分好用的方法,可以讓我們連結舊的與新的東西。我之前寫過一篇文章,在 in-app purchases 中應用這個修飾符;但它並不是完美的,在傳送的時候,我經常會觸發到它們。以下的程式碼可以幫助我們發現這個問題:
let changeColor = PassthroughSubject<Int,Never>()
我們的程式碼會是這樣的:
.onReceive(signalButton
.eraseToAnyPublisher()
.throttle(for: .milliseconds(10), scheduler: RunLoop.main, latest: true))
{ value in
if value == 2 {
button2 = true
levelColor = Color.red
}
}
在這個情況下,有了 calling
routine,這裡的數字讓我們可以為一個 subject 命名,如此一來,我們就可以在同一段 SwiftUI 程式碼中觸發多個分支。
changeColor.send(3)
6. Subscription
這是另一個開始編寫程式碼的方法。雖然這也不是 SwiftUI 本身的擴充功能,但我在不同的 SwiftUI 程式碼中總是會重覆使用它。我們可以利用以下程式碼進行設置:
let cameraGesture = PassthroughSubject<cameraActions,Never>()
var cameraSubscription:AnyCancellable? = nil
然後,像之前一樣傳送一個 combine
訊息:
cameraGesture.send(._1orbitTurntable)
來使用兩個 combine
declaration:
cameraSubscription = cameraGesture
.eraseToAnyPublisher()
.throttle(for: .milliseconds(10), scheduler: RunLoop.main, latest: true)
.sink(receiveValue: { value in
// do something with the value
})
7. 計時器 (Timer)
要創建一個計時器,最好的方法就是 publisher
,幾乎每個專案我都會使用 publisher
。
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
以上就是程式碼。
你會注意到在 _unused
變數中有時間數值,這與 send
不一樣,它更加可靠,因為它只發送一個訊息,而不會發送一堆訊息。
.onReceive(timer) { _ in
// do something
}
8. Coordinates/Size
這個擴充功能非常非常重要,讓我們來看看範例。當中有一點要留意的,就是它回傳的 Size 是父級 (parent),而我們使用的是其子級。
struct returnSize: View {
var body: some View {
GeometryReader { geometry in
Color.clear
.onAppear(perform: {
print("geo \(geometry.size)")
})
}
}
}
因此,我們通常把它用作背景視圖。我們可以這樣使用這個 command,來回傳視圖的 Size:
.background(returnSize())
9. Attributed String
我在 Stack Overflow 找到這個非常有用的擴充功能,可以應用於 iOS 15 Text
物件的屬性上。
extension Text {
init(_ string: String, configure: ((inout AttributedString) -> Void)) {
var attributedString = AttributedString(string) /// create an `AttributedString`
configure(&attributedString)
self.init(attributedString)
}
}
我們可以這樣使用上面的程式碼:
Text("GAME OVER") { $0.kern = CGFloat(2) }
我們就可以在這篇文章中進一步應用這些參數 (parameter)。
10. AnyView
這個擴充功能可以用來修復 SwiftUI 上視圖不匹配的訊息。雖然我通常不鼓勵大家使用 AnyView
,但有時卻沒有其他解決方法。
extension View {
func eraseToAnyView() -> AnyView {
AnyView(self)
}
}
以下是是回傳文本物件或圖像的範例:
struct returnDifferentViews: View {
@State var means:Bool
var body: some View {
if means {
return Image("1528")
.eraseToAnyView()
} else {
return Text("1528")
.eraseToAnyView()
}
}
}
11. 下標 (SubScript)
這個擴充功能讓我們可以下標一個字串 (string),當中使用的是一個設立已久的標準:
extension String {
var length: Int {
return count
}
subscript (i: Int) -> String {
return self[i ..< i + 1]
}
func substring(fromIndex: Int) -> String {
return self[min(fromIndex, length) ..< length]
}
func substring(toIndex: Int) -> String {
return self[0 ..< max(0, toIndex)]
}
subscript (r: Range<Int>) -> String {
let range = Range(uncheckedBounds: (lower: max(0, min(length, r.lowerBound)),
upper: min(length, max(0, r.upperBound))))
let start = index(startIndex, offsetBy: range.lowerBound)
let end = index(start, offsetBy: range.upperBound - range.lowerBound)
return String(self[start ..< end])
}
}
像其他語言一樣,我們可以這樣使用程式碼:
let word = "Start"
for i in 0..<word.length {
print(word[i])
}
12. 偵測 Shake 動作
以下是我尋覓已久的一個 Google 的程式碼,有誰記得這個序列 (sequence) 嗎?
extension NSNotification.Name {
public static let deviceDidShakeNotification = NSNotification.Name("MyDeviceDidShakeNotification")
}
extension UIWindow {
open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
super.motionEnded(motion, with: event)
NotificationCenter.default.post(name: .deviceDidShakeNotification, object: event)
}
}
extension View {
func onShake(perform action: @escaping () -> Void) -> some View {
self.modifier(ShakeDetector(onShake: action))
}
}
struct ShakeDetector: ViewModifier {
let onShake: () -> Void
func body(content: Content) -> some View {
content
.onAppear() // this has to be here because of a SwiftUI bug
.onReceive(NotificationCenter.default.publisher(for:
.deviceDidShakeNotification)) { _ in
onShake()
}
}
}
我們可以這樣創建擴充功能:
.onShake { print("stop it shaking")}
13. 擷取視圖
HWS 可以說是 Swift 的資源庫,以下這個擴充功能就是來自 HWS 的。
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
我們可以這樣使用它:
let image = textView.snapshot().ignoresSafeArea
14. 儲存/加載圖像
在 Apple Developers 網站的這個擴充功能派上用場了。
extension URL {
func loadImage(_ image: inout UIImage) {
if let loaded = UIImage(contentsOfFile: self.path) {
image = loaded
}
}
func saveImage(_ image: UIImage) {
if let data = image.jpegData(compressionQuality: 1.0) {
try? data.write(to: self)
}
}
}
https://developer.apple.com/forums/thread/661144
在這個擴充功能中,我們可以這樣使用程式碼:
@State private var image = UIImage(systemName: "xmark")!
private var url: URL { let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return paths[0].appendingPathComponent("image.jpg")}
var body: some View {
Image(uiImage: image)
.onAppear { url.load(&image) }
.onTapGesture { url.save(image) }
}
15. List Fonts
已經來到第 15 個擴充功能了,這個功能也十分有用。
let fontFamilyNames = UIFont.familyNames
for familyName in fontFamilyNames {
print("Font Family Name = [\(familyName)]")
let names = UIFont.fontNames(forFamilyName: familyName)
print("Font Names = [\(names)]")
}
有了以上的 SwiftUI 程式碼,我們就可以這樣尋找字型:
struct Fonts {
static func avenirNextCondensedBold (size: CGFloat) -> Font {
return Font.custom("AvenirNextCondensed-Bold", size: size)
}
我們可以如此調用 construct 來使用程式碼:
.font(Fonts.avenirNextCondensedBold(size: 12))
16. Ternary Operator
最後要介紹的這個擴充功能,可以應用於 SwiftUI 的不同地方,但我永遠都記不住語法。
在這裡,Ternary Operator 會評估 condition
,如果:
- 如果
condition
是true
,就會執行expression1
。 - 如果
condition
是false
,就會執行expression2
。
因為 Ternary Operator 有三個操作數(condition
、 expression1
、和 expression2
),所以它的名稱是 Ternary Operator 。
因此,如果 flipColor
是 blue
,我就會收到 green
;如果 flipColor
是 green
,我就會收到 blue
。
總結
這篇文章到此為止,希望你在這裡找到有用的程式碼吧!
謝謝你的閱讀。
特別鳴謝 Anupam Chugh。