zoukankan      html  css  js  c++  java
  • 剖析:如何用 SwiftUI 5天组装一个微信 —— 聊天界面篇

    前置资源

    GitHub: SwiftUI-WeChatDemo

    第零章:用 SwiftUI 5天组装一个微信

    第二章:剖析:如何用 SwiftUI 5天组装一个微信 —— 通讯录发现我篇

    效果图

    整体结构

    UI 部分的代码分布如上图所示,App 的主入口类为 WeChatDemoApp。

    该类在创建新的 SwiftUI 项目时会自动生成,这里只需要将其展示内容的部分变更为第一级容器:TabContainer,该容器包括微信的四个页面,分别为 聊天界面部分、通讯录、发现、我,这四个部分的主 UI 视图分布如上图标示,由各自的分级文件夹装载,并统合到主目录 UI 下面。

    与 UI 目录同级目录有:

    • Data:相当于 MVVM 中的 Repository 层级的简单替代,因为该项目主要展示 SwiftUI 使用,因此该层最大限度精简,只提供装载必要数据的类
    • Resources:资源目录,包括 Image.xcassets、Color.xcassets、Localizable.strings 等
    • Generated:通过 SwiftGen 自动为 Resources 目录生成的资源索引类目录,主要是为了给代码编写提供便捷的自动提示功能,提高工作效率,适配 SwiftUI 的一键配置及使用方法可参考 SwiftGenConfigForSwiftUI

    UI 层级示意图谱:

    WeChatDemoApp
    └── TabView
        ├── ChatsView
        │   └── ChatDetailView
        ├── ContactsView
        ├── DiscoverView
        └── MeView
    

    第一级容器:TabContainer

    App 所引入的第一级视图容器,使用的是 TabView,通过自定义结构体 TabContainer 将其包裹封装,实现代码分离:

    TabView 需要提供一个参数 selection,类型可随意自定义,用于告诉 TabView 在初始化后应该展示哪个 Tab,以及当用户选择其他 Tab 时,将其反馈、存储到该变量中。

    此处用于 selection 的类型为自定义枚举:

    enum Tabs {
        case Chats, Contacts, Discover, Me
    }
    

    传递给 selection 的变量需要声明为 @State 类型,以实现 UI 视图感知数据变化、自动刷新重绘的功能(数据绑定)。(同样,将变量注入到 selection 参数时,语法上需要添加 $ 的前缀):

    @State var selectingTab: Tabs = Tabs.Chats
    
    TabView(selection: $selectingTab) { ...
    

    其他如 Picker 等可提供用户 选择 能力的 UI,也都需要与一个 selection 变量进行绑定。

    在 TabView 下面按序放置 4 个 View 视图,代表提供了 4 个 Tab 的界面,每个 Tab 视图的结构如下:

    ChatsView().tabItem {
        Image(systemName: currentStateIconName(selecting: selectingTab))
        Text("Chats")
    }.tag(Tabs.Chats)
    
    • 示例中自定义 ChatsView 为第一个 Tab 的视图 View
    • .tabItem { ... } 用于描述 Tabbar 上该 Tab 按钮的 UI 部分,仅可接受标准的 Image、Text 以及 Label 类型
    • .tag(...) 表示该 Tab 的识别符号,数据类型需要与上面的 selection 变量一致,用户点击 Tab 按钮时,该处的 tag 变量会被赋值到 selection 绑定的变量中

    为了实现 Tab 按钮在激活・非激活状态下展示不同的图标(实心与空心),此处通过一个函数动态返回对应图标素材名字,传递到 Image 中,该函数接受一个指向当前正在选择的 Tab 的 selecting 参数,当 @State var selectingTab 变量发生变化时,相关联的 UI 将会重绘,并重新调用函数获取新的图片素材名字:

    func currentStateIconName(selecting: Tabs) -> String {
        selecting == self ? iconNameOn : iconNameOff
    }
    

    最后通过对 TabView 整体添加 .edgesIgnoringSafeArea(.all),使其可用空间扩展到刘海顶部实现全屏。


    Tab 1:聊天列表

    整一个 Tab 视图的根容器是一个 NavigationView,该部件提供了标准的 Toolbar、子页面跳转、返回等功能。

    在 NavigationView 之中包裹的是真正的视图布局。

    视图布局三剑客

    • HStack:提供一个横向的自动布局容器,默认 UI 元素自左向右排布
    • VStack:提供一个纵向的自动布局容器,默认 UI 元素自上向下排布
    • ZStack:提供一个沿 Z 轴排布的布局容器,后面的 UI 元素将覆盖前面的 UI 元素(相当于 Android 中的 FrameLayout)

    (※ 这三个容器默认带有元素间距,可使用参数 spacing: 0 消除)

    在界面上,排除由 NavigationView 提供的 Toolbar 部分,视图由一个自定义的搜索栏 SearchBar,以及一个列表组成,通过 VStack 纵向排列布局。

    列表部分可使用 SwitchUI 中专用的循环体 ForEach,实现一个根据数据数组元素数量、内容,不断添加 UI 元素的循环结构。(也可以考虑使用 List,但 List 带有比较强烈的样式倾向,不如 ForEach 容易控制)

    ForEach([Chat], id: .self) { chat in
        NavigationLink(destination: createChatDetailView(with: chat)) {
            ChatItemView(chat: chat)
        }
    }
    

    ※ 不能使用普通的 for 语法

    ForEach 要求提供的数据类型遵循 Hashable 协议,同样 id 也要求遵循 Hashable,以便其识别、追踪每次循环所生成的 View,在数据发生变化时能够在正确位置插入、删除、修改对应的 UI。

    id: .self 使用的是一种名为 KeyPath 的类型及语法糖,该语法从 Swift5.2 开始提供,描述了一个从形式参数类型 KeyPath 所声明绑定的 Root 泛型类型对象中获取指定的某个属性的 路径,可类比于 Java 的反射或 JS 的 eval 功能等,提供了将某个属性访问的 动态操作 转换为另一种体现在某个属性值上的 静态描述 能力。

    此处 .self 表示,id 参数使用前面的数组中 Chat 元素自身,相当于 chat.self

    NavigationLink(destination: createChatDetailView(with: chat)) {
        ChatItemView(chat: chat)
    }
    

    上述代码中,ForEach 根据 Chat 数组,生成聊天列表中每一个聊天记录项的 View,该 View 由 NavigationLink 所包裹,当用户点击该 View 时,将自动通过外层的 NavigationView 进行页面导航,跳转至此处指定的 destination 指向的 View(作为子页布局展示)。

    同时,NavigationView 会为该子页面添加一个 Toolbar,以及一个用于返回的箭头按钮。

    NavigationLink { ... } 中的 UI 则是这个聊天记录项的布局(list item)。

    .navigationBarTitleDisplayMode(.inline)
    .navigationTitle("微信")
    .toolbar {
        ToolbarItem(placement: .navigationBarLeading){
            Button(action: {}) {
                Image(systemName: "ellipsis")
            }
        }
        ...
    }
    

    这些部分描述当前界面的顶部的 Title 展示形式、标题文本、Toolbar构成等。

    需要注意的是这部分描述需要在 NavigationView 内的 View 上书写,而不是附加在 NavigationView 自身上。

    聊天列表记录 ItemView

    聊天列表上的一条记录

    聊天列表上的记录 Item View:

    Spacer 是一个可以依据剩余可用空间自动填充扩展的结构,用于自动撑开两个 View 或将容器撑满整个屏幕等。

    头像部分,显示一个图片素材,指定其可缩放至指定 frame 大小,并追加圆角角度:

    let isShowBadge: Bool = Float.random(in: 0...1) > 0.45
    
    Image("avatar01")
        .resizable()
        .scaledToFit()
        .frame( 50, height: 50, alignment: .center)
        .cornerRadius(4.0)
        .withBadge(isShowBadge)
    

    最后的 withBadge(isShowBadge) 为自定义函数扩展:

    该函数通过为 View 添加一个圆形的 overlay,指定放置在右上角并偏移一半尺寸到 View 外侧,来实现信息红点功能。

    函数返回类型指定为 some View,属于 SwiftUI 中类型擦除的概念,在函数中使用 if、switch 等根据情况返回不同 View 的场合,需要使用 AnyView 进行包裹并返回,否则将出现代码检测错误:

    Function declares an opaque return type, but the return statements in its body do not have matching underlying types

    分割线可使用 Divider() 实现。


    聊天窗口

    数据部分

    @State var chat: Chat
    @StateObject var viewModel: ChatDetailViewModel
    @Environment(.presentationMode) var presentationMode
    
    • @State chat:一个 struct 类型,包含对方最后一条聊天消息和联系人(头像)信息
    • @StateObject viewModel:用于承载复杂的使用场景,在该界面上 ChatDetailViewModel 托管了一个发送消息记录的数组,以及通过 Combine 响应式框架模拟聊天对方在 1 秒后回信的功能
    • @Environment presentationMode:从环境变量中获取当前的界面的展开模式控制对象,用于在 Toolbar 中赋予自定义返回按钮的返回聊天列表能力

    界面主体由两大部分组成:

    • 聊天信息记录的 ChatFlowView
    • 底部文字输入部分的 ChatInputView

    ChatFlowView 根据由 ViewModel 所托管的消息记录 messageFlow 数组数据,使用 ForEach 生成每一条对话消息:

    ScrollViewReader 用于提供对 ScrollView 的滑动控制能力,在不需要程序自动控制 ScrollView 滑动时,则不需要使用该部件。

    ChatMessageView(message: message).onAppear() {
        scrollView.scrollTo(message)
    }
    

    ChatMessageView 表示头像+信息组成的一条聊天信息,使用一个 ChatMessage 类型的数据进行初始化,数据包含聊天信息内容的 String,以及发送方向、头像。

    其中的 MessageText 基本上是一个普通 Text,展示 ChatMessage 中的信息内容,并根据其中的发送方向(左侧或是右侧)决定 Text 的底色。

    在外层 HStack 上通过改写局部环境变量 layoutDirection,来实现布局排序方向变化。

  • 相关阅读:
    [论文阅读]VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION(VGGNet)
    VMware虚拟机 Ubuntu 实用技巧 (1) -- 安装VMware Tool
    python 开发技巧(4)-- 用PyCharm实用技巧(我自己常用的)
    python 开发技巧(3)-- 连接mysql 出现错误 ModuleNotFoundError: No module named 'MySQLdb'
    python 开发技巧(2)-- Django的安装与使用
    Linux netstat命令
    RabbitMQ(一):Windows下RabbitMQ安装
    python 开发技巧(1)-- 用PyCharm安装第三方库
    Swoole系列(三):建立TCP服务器并发送数据测试
    Swoole系列(二):安装
  • 原文地址:https://www.cnblogs.com/wavky/p/SwiftUI-WeChatDemo-Coding-Chat.html
Copyright © 2011-2022 走看看