剖析:如何用 SwiftUI 5天組裝一個微信 —— 通訊錄發現我篇


前置資源

GitHub: SwiftUI-WeChatDemo

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

第一章:剖析:如何用 SwiftUI 5天組裝一個微信 —— 聊天界面篇

效果圖


Tab 2:通訊錄

通訊錄的數據主體是聯系人 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 部分獨立出來。


Tab 3:發現

發現頁中列表每一項都是固定內容和固定順序,因此不使用 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 渲染后

Tab 4:我

這一頁的各項 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 下的配置方案

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM