SwiftUI 框架

整合 16 個好用的 SwiftUI 擴充功能 來迎接 WWDC 2022

WWDC 2022 即將開始了,而且有可能會推出 SwiftUI 4.0 版本。在這篇文章中,Mark 會為大家介紹 16 個好用的 SwiftUI 擴充功能 (extension),希望部分功能在新的 SwftUI 版本中都能夠使用吧!
整合 16 個好用的 SwiftUI 擴充功能 來迎接 WWDC 2022
Photo by Khaleelah Ajibola on Unsplash
In: SwiftUI 框架, WWDC
本篇原文(標題:16 Useful Extensions for SwiftUI)刊登於作者 Medium,由 Mark Lucking 所著,並授權翻譯及轉載。

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 ,如果:

  • 如果 conditiontrue,就會執行 expression1
  • 如果 conditionfalse,就會執行 expression2

因為 Ternary Operator 有三個操作數(conditionexpression1、和 expression2),所以它的名稱是 Ternary Operator 。

因此,如果 flipColorblue,我就會收到 green;如果 flipColorgreen,我就會收到 blue

總結

這篇文章到此為止,希望你在這裡找到有用的程式碼吧!

謝謝你的閱讀。

特別鳴謝 Anupam Chugh。

本篇原文(標題:16 Useful Extensions for SwiftUI)刊登於作者 Medium,由 Mark Lucking 所著,並授權翻譯及轉載。
作者簡介:Mark Lucking,編程資歷超過 35 年,熱愛使用及學習 Swift/iOS 開發,定期在 Better ProgrammingThe StartUpMac O’ClockLevel Up Coding、及其它平台上發表文章。
譯者簡介:Kelly Chan-AppCoda 編輯小姐。
作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
更多來自 AppCoda 中文版
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。