前置資源

GitHub: SwiftUI-WeChatDemo

第零章:用 SwiftUI 5天組裝一個微信

第一章:剖析:如何用 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 下的配置方案

至此,應用內語言切換功能實裝完畢。