前置資源
GitHub: SwiftUI-WeChatDemo
第一章:剖析:如何用 SwiftUI 5天組裝一個微信 —— 聊天介面篇
通訊錄
通訊錄的資料主體是聯絡人 Contact,包含姓名和頭像,通過自定義函式,將聯絡人 Contact 陣列整理為按名字首字元為分組名的 聯絡人分組(ContactGroup) 陣列:
即:[{"A", [Contact]}, {"B", ...}, ...]
private var contactGroups: [ContactGroup] {
getDefaultContactList()
.group(withLocale: locale)
.sortedList()
}
private struct ContactGroup: Hashable {
let groupKey: Character
let groupValue: [Contact]
}
※ 函式實現參考 GitHub原始碼
由於該介面沒有實現子頁面跳轉功能,因此其實可以不使用 NavigationView,這裡使用它只是為了獲得 Toolbar 和頂部 Title 顯示。
列表檢視使用了 List,因為 List 自帶了 Section 分組功能,在這裡使用上比較方便。
List {
ForEach(contactGroups, id: \.self) { group in
// Completions will crash if ForEach nest together
ContactSection(contactGroup: group)
}
}
.listStyle(PlainListStyle())
通過 ForEach 讀取每一個聯絡人分組,生成自定義的 Section 檢視:
檢視結構相對簡單,但是在生成聯絡人(List item)檢視上,使用了兩個 ForEach 進行巢狀(第一層為根據分組 group 陣列生成若干 Section,第二層為在每個 Section 中根據該 group 分組中每一條聯絡人記錄生成列表的 Item 檢視),當前在 Xcode 12.5.1 上使用多個 ForEach 巢狀時,會發生程式碼自動提示功能崩壞問題,因此這裡將 Section 部分獨立出來。
發現
發現頁中列表每一項都是固定內容和固定順序,因此不使用 ForEach 而是手動排布各項元素檢視,包括用於灰色背景間隔的 Spacer 在內,總數量超過 10 個 View,因此需要引入 Group 來打包相鄰的幾個檢視項。
資料部分:
private let items = getDiscoverItems()
items 是一個 [DiscoverItemName : ItemBarInfo]
字典,其中 DiscoverItemName 只是一個用作字典 Key 標記便於索引呼叫的簡單列舉,檢視所用資料都打包在 ItemBarInfo 中:
enum DiscoverItemName {
case Moments, Channels, Live, Scan,
Shake, TopStories, Search, Nearby,
MiniPrograms
}
struct ItemBarInfo {
let icon: String
let iconPattern: IconPattern?
let title: LocalizedStringKey
let name: LocalizedString?
let profileImage: String?
}
- icon 表示圖示使用的素材名稱
- iconPattern 為一個自定義列舉型別,用於封裝圖示染色所用的顏色或漸變色物件(在下文中詳述)
- title 表示 Item 左側的名字
- name 與 profileImage 僅用於在該 Item 需要展示某個聯絡人的場合(參考上圖)
然後通過自定義函式,將每一條 Item 資料轉換為對應的 ItemView:
items[.Moments]?.toItemBarView()
extension ItemBarInfo {
func toItemBarView(withDivider: Bool = false,
withBudge: Bool = true) -> ItemBarView {
ItemBarView(itemBarInfo: self,
withDivider: withDivider,
withBudge: withBudge)
}
}
在這個 ItemView 上,需要重點關注的是 Item 左側的圖示,即 Icon:
Icon(image: itemBarInfo.icon,
pattern: itemBarInfo.iconPattern)
這個自定義 Icon 接受兩個引數:圖示的素材名稱,和對圖示進行色彩渲染所用的 IconPattern:
在發現頁上,朋友圈一欄所用圖示為多種色彩,而其他專案的圖示則都使用了單一顏色,在實現上,圖片素材採用了 SVG 向量圖,再針對性後期進行顏色渲染加工的思路。(朋友圈圖示使用漸變色 AngularGradient,而其他圖示則使用 Color)
由於顏色 Color 與 一眾漸變色型別缺乏公共父類、協議,因此使用自定義封裝來實現統一:
對圖示素材進行渲染加工,使用 mask 遮罩處理:
color.mask(Image(image))
gradient.mask(Image(image))
該函式工作原理是,對於 Image 中畫素所在位置,都使用前者(color 或者 gradient)進行畫素對等替換。
Image 原圖 | mask 渲染後 |
---|---|
![]() |
![]() |
我
這一頁的各項 Item,沿用發現頁的資源,只需針對性提供各項 Icon 資源、Title 指定的資料即可。
頂部的一大片資料區,只需要通過 HStack、VStack 對圖片 Image、文字 Text 進行堆砌擺放,區域性使用 offset 偏移修正位置即可。
重點關注中間的語言切換按鈕。
切換按鈕使用 Picker 實現,通過 pickerStyle 調整至該樣式。
Picker 初始化中傳入到形參 selection 的變數 appLanguage 使用了 @AppStorage 宣告,型別為 String,表示該變數會被持久化儲存在 UserDefaults 中,並且在未獲取到相應欄位值時,使用預設值 Language.English.code 進行初始化:
@AppStorage("AppLanguage")
private var appLanguage: String = Language.English.code
※ 與 @State 一樣,該變數會繫結到 UI,當資料發生變化時,相關聯的 UI 會進行自動重新整理重繪
而每個按鈕則是通過 ForEach 遍歷 Language 列舉實現,當用戶點選其中某個按鈕時,在 ForEach 中傳遞至按鈕中的 id 將會被賦值到 selection 的變數(即 appLanguage),並被持久化到 UserDefaults 內:
ForEach(languages, id: \.code) { lang in
Text(lang.rawValue)
}
※ ForEach 與 id 的使用參考第一章:剖析:如何用 SwiftUI 5天寫一個微信 —— 聊天介面篇
Language 列舉實現中,關鍵部分為 code,該變數所取用的語言程式碼需要與專案內的國際化資原始檔夾 .lproj 的名稱相同,可用語言程式碼可通過搜尋 iOS + Language code 等關鍵詞獲得,如:iOS Supported Language Codes (ISO-639):
最後被標記為使用者所選擇的語言,儲存到 UserDefaults 中的 AppLanguage,需要回到 App 應用的主入口,同樣通過 @AppStorage 獲取出來,並通過 .environment()
寫入到整個應用的根 View 的環境變數中,沿 View 結構樹傳遞至每一個 SwiftUI 部件,選擇性獲取對應語言版本的文字資源:
至於在各個 Text 中使用國際化文字資源的方法,則是通過 SwiftGen 輔助完成的,SwiftGen 的一鍵配置、使用方法可參考:SwiftGen 在 Swift5 + SwiftUI 下的配置方案
至此,應用內語言切換功能實裝完畢。