剖析:如何用 SwiftUI 5天组装一个微信 —— 通讯录发现
前置资源
GitHub: SwiftUI-WeChatDemo
第一章:剖析:如何用 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 下的配置方案
至此,应用内语言切换功能实装完毕。