簡化大量 modifier 程式的兩個方法,自訂組合 modifier 的 function 和透過 ViewModifier 實作 custom modifier

開發 SwiftUI App 時,我們時常透過大量的 modifier 客製元件,將畫面變成賞心悅目的樣子。不過 App 裡很多元件都是類似的設計,比方以下 SongList & LyricsView 裡的文字都是橘色粗體加陰影,透過以下 3 個 modifier 實現。

  • .font(.system(size: 24, weight: .bold))
  • .foregroundColor(.white)
  • .shadow(radius: 20)
struct SongList: View {

let songs = ["街角的祝福", "光著我的腳丫子", "怎樣", "愛瘋了", "誤解"]

var body: some View {
NavigationStack {
List(songs, id: \.self) { song in
Text(song)
.font(.system(size: 24, weight: .bold))
.foregroundColor(.orange)
.shadow(radius: 20)
}
.navigationTitle("戴佩妮")
}
}
}
struct LyricsView: View {
let lyrics: String

var body: some View {
ZStack {
Rectangle()
.fill(.black.gradient)
.ignoresSafeArea()
ScrollView {
Text(lyrics)
.font(.system(size: 24, weight: .bold))
.foregroundColor(.orange)
.shadow(radius: 20)
}
.padding()
}
}
}

每次都要輸入 3 個 modifier 實在有點辛苦,有沒有偷懶的方法呢?

有的,以下兩個方法都可簡化大量的 modifier 程式。

方法1: 自訂組合 modifier 的 function

在 extension Text 定義 function songStyle。

extension Text {
func songStyle() -> some View {
self
.font(.system(size: 24, weight: .bold))
.foregroundColor(.orange)
.shadow(radius: 20)
}
}

之後從 Text 呼叫 function songStyle 即可讓文字有橘色粗體陰影。

ForEach(songs, id: \.self) { (song) in
Text(song)
.songStyle()
}

方法 2: 透過 ViewModifier 實作 custom modifier

定義 custom modifier

  • 定義遵從 protocol ViewModifier 的 SongTextViewModifier。
import SwiftUI

struct SongTextViewModifier: ViewModifier {
}
  • 定義 protocol ViewModifier 的 function body。

輸入 body 後從自動完成選單選擇 body。

  • 在 function body 裡呼叫想設定的 modifier。

body 的參數 content 將是我們想修改的 view,因此我們從 content 呼叫想設定的 modifier。

struct SongTextViewModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(.system(size: 24, weight: .bold))
.foregroundColor(.orange)
.shadow(radius: 20)
}
}

設定 custom modifier

呼叫 function modifier,傳入自訂的 modifier,即可一行套用多個 modifier。比方傳入 SongTextViewModifier(),套用橘色粗體加陰影的文青文字樣式。

  • SongList

Text(song) 將成為 SongTextViewModifier 的 function body 收到的參數 content。

ForEach(songs, id: \.self) { song in
Text(song)
.modifier(SongTextViewModifier())
}
  • LyricsView

Text(song) 將成為 SongTextViewModifier 的 function body 收到的參數 content。

ScrollView {
Text(lyrics)
.modifier(SongTextViewModifier())
}
.padding()

搭配參數生成 custom modifier

剛剛的 SongTextViewModifier 將產生大小 24 的文字,若我們需要不同大小的文字,難道要為了它另外定義一個 custom modifer ?

不用這麼麻煩,我們可以在 custom modifier 加上 property,比方代表文字大小的 size,然後在產生 SongTextViewModifier 時傳入文字的大小。

struct SongTextViewModifier: ViewModifier {
let size: CGFloat

func body(content: Content) -> some View {
content
.font(.system(size: size, weight: .bold))
.foregroundColor(.orange)
.shadow(radius: 20)
}
}

生成 SongTextViewModifier 時指定想要 size 24 的文字。

Text(lyrics)
.modifier(SongTextViewModifier(size: 24))

透過 extension 讓 custom modifier 更容易呼叫

.modifier(SongTextViewModifier(size: 24)) 設定 custom modifier 沒什麼問題,但在 SwiftUI 的程式裡跟其它的 modifier 放在一起,卻顯得格格不入的感覺,比方以下例子:

Text(lyrics)
.modifier(SongTextViewModifier(size: 24))
.padding()
.background(Color.yellow)
.blur(radius: 1)

能不能讓它看起來就像一般的 modifier function,就像 padding,blur 一樣,直接以功能的名字呼叫,而不用先呼叫 function modifier 呢 ?

可以的,我們可以 extension View,在裡面定義 function songTextStyle 代表自訂的 modifier function。

  • 寫法1

呼叫 function modifier。

extension View {
func songTextStyle(size: CGFloat) -> some View {
modifier(SongTextViewModifier(size: size))
}
}
  • 寫法2

function modifier 的宣告如下

func modifier<T>(_ modifier: T) -> ModifiedContent<Self, T>

它的回傳型別是 ModifiedContent,因此我們也可以直接生成 ModifiedContent,在參數 content 傳入 self,modifier 傳入自訂的 SongTextViewModifier。

extension View {
func songTextStyle(size: CGFloat) -> some View {
ModifiedContent(content: self, modifier: SongTextViewModifier(size: size))

}
}

透過剛剛定義的 extension,之後 SwiftUI 任何的 view 都可以呼叫 songTextStyle,把它當成一般的 modifier 來使用。

Text(lyrics)
.songTextStyle(size: 24)
.padding()
.background(Color.yellow)
.blur(radius: 1)

將自訂的 SwiftUI modifier 加到 Modifiers library

我們也可以將自訂的 modifier 加到 Modifiers library,定義以下程式讓 Song Text Style 出現在 Modifiers library。

struct Previews_AppleView_LibraryContent: LibraryContentProvider {
func modifiers(base: any View) -> [LibraryItem] {
LibraryItem(base.songTextStyle(size: 20))
}
}

其它參考資訊

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com