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().ignoresSafeArea14. 儲存/加載圖像
在 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。
 
