13 的話
如果你是第一次讀這個專欄,建議先從頭照順序閱讀。
SwiftUI 專欄是一個付費專欄,只是我沒有設立付費牆,而是採用「誠實訂閱」制度。
我的目標是累積到 100 位 Patreon 支持者,目前進度來到 23%,感謝新舊朋友們的支持。請注意 Patreon 會在初次訂閱和每個月 1 號扣款。
前陣子用讀者捐款添購了一些設備以及軟體,對我的寫作很有幫助。能夠做到取之於讀者用之於讀者的正向循環,還滿感動的。
上一期結尾我說到接下來要聊
Color
。不過我在推特上辦的小投票有 3 倍的人想先聽我講「那些你不該碰的 SwiftUI API」,所以我就先來寫這個題目。不過這題目比較麻煩,多寫了一週。以後還是盡量按照自己的步調好了😂
🙅不要相信任何人
在開始本期週報之前我想講一個非常重要的觀念:
SwiftUI 還很新,不要輕信網路上寫的東西。
你在網路上看到關於 SwiftUI 的資訊,不管是 blog、教學課程、StackOverflow 的答案、GitHub 上的程式碼、Twitter 上的討論,甚至 Apple 論壇跟官方範例程式碼,都有可能是錯的。
當然也包括我寫的東西。雖然我都會盡可能查證才寫出來,但我也是會錯的。
所謂的錯,不一定代表那樣寫程式會動不了,但很有可能會在某些狀況下炸掉或是超出你的預期。尤其是越早寫的出來的 StackOverflow 答案🌚。因為 SwiftUI 剛出來的前兩年,許多人還在摸索。光是要讓某些東西能夠「像以前 app 的行為」,就搞半天。
前一期我就有提到,覺得不適合 SwiftUI 用來複製 UIKit 的行為。如果硬要這麼做的話,有非常高的機率是在寫所謂的 workaround。
既然是 workaround,就表示它很有可能會在未來的某一天壞掉。有機會我再教怎麼管理 SwiftUI 的 workaround,最好不要派上用場。
總之網路上有不少東西是在教 workaround,「抄答案」的時候要小心。
說個題外話,我自己非常喜歡的 SwiftUI 文件網站叫作 SwiftOnTap。它集結了 Apple 的文件,再進行補充。最大的優點是除了範例程式碼以外,還會放一些畫面截圖。而且內容完全都是 SwiftUI,所以搜尋起來比 Apple 自己的文件好找東西。
不誇張地說,以前我每次寫 SwiftUI 都會用到這個網站來查資料,而且長久以來都大力推薦。
但是它其實在 iOS 15 推出以後並沒有做什麼更新,所以少了很多新東西。如果你的專案只從 iOS 14 開始支援起,是沒有什麼問題的。寫 iOS 15 以上的話,就會漏掉一些新功能了。
這也算是另一個不要輕信網路上東西的例子吧。
有比 SwiftOnTap 更好的替代方案嗎?有的,等需要時我們再來介紹。
🙅不要使用 Introspect
SwiftUI-Introspect 是一個非常熱門的 SwiftUI 套件。它的功能是利用 SwiftUI 自己的 API,讓開發者在使用 SwiftUI 的元件時,可以找到對應的 UIKit 或 AppKit 物件,進行修改。
舉例來說,List
在 iOS 13-15 使用 UITableView
來實作。在 iOS 15 以前 List
沒有管理分隔線的 API。
不過,透過該套件,你可以寫:
List {
// list 內容
}
.introspectTableView { tableView in
tableView.separatorStyle = .none // 使用 UIKit 的方法把分隔線藏起來
}
從某種角度來說,Introspect 是非常優秀橋接 SwiftUI 與 UIKit/AppKit 的機制。它能幫你找到 UINavigationController
、UIScrollView
、UITableView
、UITextField
等等 UIKit 使用者十分熟悉的元件。
我在 iOS 13 時代寫 SwiftUI 大量使用 Introspect。但是到了 iOS 14 之後,漸漸地就不再使用該套件了。原因有三個。
第一:SwiftUI 是很抽象的框架,底層實作會改變。
以我上面舉的例子來說,iOS 16 的 List
不再使用 UITableView
實作了。寫在 Introspect 裡面的程式碼,因為找不到對應元件,不會被執行。那這個 app 跑在 iOS 16 上會變成什麼樣子呢?就只能跑起來才知道炸成怎樣了。
第二:使用 Introspect 是 workaround。
早期 SwiftUI API 缺東缺西的時候,拿它來暫時繞過非常好用。但是如果有正式的 API,就不該繼續使用 workaround。
第三:過度依賴 Introspect 會導致你學不好 SwiftUI。
這才是我反對依賴 Introspect 的主要原因。它會讓你學習 SwiftUI 的時候,還一直想著畫面上的元件是什麼 UIKit 元件、要達成某個需求怎麼用 UIKit 的方法實現。
這樣很容易會變成只是用 SwiftUI 來刻一部分畫面,實際上還是用 UIKit 的心智模型在思考。
本專欄是強烈建議從 iOS 14 SwiftUI 開始支援起的,所以就別用 Introspect 了吧。
不過,如果你有興趣了解它找到對應 UIKit 元件的原理,其專案原始碼非常值得閱讀。
🙅不推薦使用 List
有沒有聽過一句玩笑話形容 iOS 工程師的主要工作是「把 JSON 變成 table view」。
這個嘛,我既不同意也不反對🤣
由此可見列表形式的畫面,在 iOS app 中十分常見。
但我不是很建議使用 List
來做列表畫面。幾個原因:
直到 iOS 15 才有比較好的自訂性,特別是分隔線的 style
它的選取效果是一整個「cell」。比如說設計的要求是整個 cell 可以點按,同時裡面有比較小的按鈕。那不管按哪邊,都會有整個 cell 被選起來的效果。這絕對不是你想要的
它有一堆 ListStyle 可以套用。但畫面比較複雜時,可能沒有符合你需要的樣式
你一開始還是可以用 List
試試看(問題會出在哪,總是要自己試過嘛):
List {
ForEach(data) {
// cell
}
}
一旦發現不夠用,替代方案可以考慮 ScrollView 跟 LazyVStack 的組合。反正 SwiftUI 在改變畫面結構的成本超低。
ScrollView {
LazyVStack {
ForEach(data) {
// cell
}
}
}
🙅盡可能不用 GeometryReader
在 StackOverflow 上尋找 SwiftUI 怎樣排版時,GeometryReader
會大量出現!通常是想要知道尺寸,然後要「傳遞」給另一個 View
作為排版依據。
但是其實很多時候,用其他的方法可以得到一樣的排版效果。
我今天不打算介紹它的原理或使用方式。用另一種說法勸退好了:我現在有一個很喜歡的腦力活動,就是每次看到有人分享 SwiftUI 的程式碼時,如果裡面用了 GeometryReader
,我就會想試試看能否不使用它而達到一樣的排版效果。
結果,大部分情況可以不需要 GeometryReader
。
這位 Apple 工程師的推文也是挺有趣的:
太多 SO 答案用了 GeometryReader
很難不抄到。等你跟 SwiftUI 比較熟以後,可以回頭看看以前寫的 GeometryReader
是不是可以改成更簡單的方法。
🙅不要使用 AnyView
AnyView
是個 type-erasure 的 View
。換言之,它會抹除 View
本身的型別資訊,對於 SwiftUI 識別是否需要更新畫面會造成困難,且浪費效能。
絕大多數情況你都不需要用到。你可能會在某些 StackOverflow 答案看到它,通常是錯的。
不做解釋只寫關鍵字的替代方案: @ViewBuilder
+ some View
,或用 Group
把 View 包起來。
🙅太急著使用 ObservableObject
這點純屬我個人意見。
大部分 SwiftUI 教學,很早就把 @State
、@Binding
、@ObservedObject
、@StateObject
、@EnvironmentObject
等 property wrapper 教給你。
結果就是,許多人開新畫面要塞資料時的起手式變成一定要來個 ObservableObject
:
class FooViewModel: ObservableObject { // 真的需要 class 嗎?
@Published var flag: Bool
@Published var items: [Item]
// init...
}
// 放在 FooView 裡:
// @StateObject var viewModel: FooViewModel
但是,我們通常都是先用 struct 或是其他 value type 來放資料,所以 @State
就夠了。換個角度來說,如果是要學習 SwiftUI 排版,那你只需要 @State
。
struct FooViewModel {
var flag: Bool
var items: [Item]
}
// 放在 FooView 裡:
// @State var viewModel: FooViewModel
當專案變大時,資料的確有可能是 reference type,就會用上 ObservableObject
,但根據我的經驗,初期先把 @State
跟 @Binding
這兩個用熟就好了。
🙅避免使用 @EnvironmentObject
@ObservedObject
、@StateObject
、@EnvironmentObject
這幾個 property wrapper 都使用 ObservableObject
,需求與功能也略有不同。
其中,我強烈建議避免使用 @EnvironmentObject
。即便你很清楚自己在幹嘛,以後還是很有可能會害到自己。
先講個觀念:
Environment 在 SwiftUI 裡面指的是從上層
View
往下層View
傳遞的東西。
比如說:
VStack { // 上層
HStack { // 第二層
Text("Hi") // 最下層
}
}
.font(.largeTitle) // 此 modifier 的設定會由上往下每一層都發生效果,最下層的 Text 會受影響
很多 modifier 會把資訊往下層「傳遞」,像是這邊的 .font
。在概念上我都稱之為 environment。
你可以想見,像 .font
這種 modifier 如果不小心加在很上層,那麼整個 app 有很多 Text
都會被影響到了。除非再寫一次 .font
把設定給「蓋掉」。
究竟哪些 modifier 有這種特性,其實是 SwiftUI 一個很大的初學者障礙。你只能一個一個嘗試。
Environment 這樣的東西,最適合去設定某範圍 app 畫面一致的行為。比如說,用 .preferredColorScheme 來設定某一部分的頁面強制 Dark Mode。
有一組 API 叫做 .environment() 與 @Environment var。上層用 .environment() 設定,下層用 @Environment var 讀取。
如果你不小心的話,就會把設定由上往下整個蓋掉。當然,除非這就是你想做的事情。
扯了這麼多,回到我們這一節講的 @EnvironmentObject
。它是上層用 .environmentObject()塞入物件,下層使用 @EnvironmentObject var 來讀取與寫入。
請注意,是讀取與寫入。
@Environment 的 init 參數用是 KeyPath
,所以是唯讀。
@EnvironmentObject 放的是 ObservableObject
,可讀可寫。
親愛的讀者啊,發揮你的想像力吧。有什麼事情比上層無條件影響所有下層還要糟糕?就是下層可以反過來直接修改上層的物件啊。
物件資料被誰改了,誰知道啊。
如果這樣你還不覺得坑(聽起來很像 singleton,沒問題啊!?),還有一件事我沒說。
如果你在下層宣告了 @EnvironmentObject var
,但是上層沒有任何地方透過 .environmentObject()
塞入物件,那這個 View
也會直接 crash 給你看。
我實在是想不到,除了把自己炸死或是要故意坑隊友以外,為什麼要用到這種行為的 API 哪。
如果你真的很想跨層傳遞物件,並且希望不同層都可以寫入,寧可用 @Binding 一層一層地寫出來,至少你很清楚有哪些層 View
會用到。
SwiftUI 雖然很好寫,但是不能亂寫。Environment 屬於隱含行為,在團隊開發中要特別小心使用。
結論
還有很多東西我覺得不適合 SwiftUI 新手使用,不過這期的字數又爆掉了,哈哈。有機會再補充。
以前我的觀念是:盡量告訴大家哪裡有坑、建議避開。不過後來我發現,有些時候還是要親身經歷,知道痛才學得起來😅。因此,我不反對你去玩玩看這期講到的這些 API。
也歡迎跟我分享你的踩坑經驗。
喜歡這篇文章的讀者,請到 Patreon 訂閱支持我🙏 我的目標是累積到 100 位支持者,目前 23%。也請按下愛心❤️、留言💬、回信✉️與我交流。這些回饋都會直接影響到我繼續寫作的動力與頻率喔。
希望今天的內容讓你可以少走一些彎路。我們下一期見!
Swiftontap 貼出公告 8/31 要關站了 QQ,原因更新很花時間跟燒錢:https://swiftontap.com/