前置资源

GitHub: SwiftUI-WeChatDemo

第零章:用 SwiftUI 五天组装一个微信 - wavky - 博客园

整体结构

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 层级示意图:

  1. WeChatDemoApp
  2. └── TabView
  3. ├── ChatsView
  4. └── ChatDetailView
  5. ├── ContactsView
  6. ├── DiscoverView
  7. └── MeView

第一级容器:TabView

App 之下的第一级容器,使用的是 TabView,并通过自定义结构体 TabContainer 将其包裹封装,实现代码分离。

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

该变量需要声明为 @State 类型,以实现 UI 视图感应数据变化,自动刷新的功能(数据绑定)。(同样,语法上需要以 $ 的前缀方式将变量注入到 selection 参数)

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

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

  1. ChatsView().tabItem {
  2. Image(systemName: currentStateIconName(selecting: selectingTab))
  3. Text("Chats")
  4. }.tag(Tabs.Chats)
  5. func currentStateIconName(selecting: Tabs) -> String {
  6. selecting == self ? iconNameOn : iconNameOff
  7. }
  • 示例中自定义 ChatsView 为第一个 Tab 的视图 View
  • .tabItem { ... } 用于描述 Tabbar 上该 Tab 按钮的 UI 部分,仅可接受标准的 Image、Text 以及 Label 类型
  • .tag(...) 表示该 Tab 的识别符号,类型上需要与上面的 selection 变量一致,用户点击 Tab 按钮时,该处的 tag 变量会被赋值到 selection 中
  • 为了实现 Tab 按钮在激活・非激活状态下展示不同的图标(实心与空心),此处将一个根据 selecting 参数返回对应图片名字的函数传递到 Image 中,当 @State var selectingTab 变量发生变化时,相关联的 UI 将会重绘,并重新调用函数获取新的图片名字

最后通过对 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 容易控制)

  1. ForEach([Chat], id: \.self) { chat in
  2. NavigationLink(destination: createChatDetailView(with: chat)) {
  3. ChatItemView(chat: chat)
  4. }
  5. }

※ 不能使用普通的 for 语法

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

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

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

  1. NavigationLink(destination: createChatDetailView(with: chat)) {
  2. ChatItemView(chat: chat)
  3. }

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

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

  1. .navigationBarTitleDisplayMode(.inline)
  2. .navigationTitle("微信")
  3. .toolbar {
  4. ToolbarItem(placement: .navigationBarLeading){
  5. Button(action: {}) {
  6. Image(systemName: "ellipsis")
  7. }
  8. }
  9. ...
  10. }

这些部分描述当前界面的顶部的 Title 展示形式、字符串资源、Toolbar构成等。

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

聊天记录 View

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

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

头像部分,显示一个图片素材,指定其可缩放至指定 frame 大小,并追加圆角角度,最后的 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() 实现。


聊天窗口

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

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

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

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

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

  1. ChatMessageView(message: message).onAppear() {
  2. scrollView.scrollTo(message)
  3. }

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

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

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

剖析:如何用 SwitchUI 5天写一个微信 —— 聊天界面篇的更多相关文章

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

    前置资源 GitHub: SwiftUI-WeChatDemo 第零章:用 SwiftUI 5天组装一个微信 第一章:剖析:如何用 SwiftUI 5天组装一个微信 -- 聊天界面篇 通讯录 通讯录的 ...

  2. 用java写一个用户登陆界面

    一.课堂测试源代码及其结果截图 用java的swing写一个用户登录界面,采用网格布局.源代码如下: /** * */package LiuLijia; import java.awt.CardLay ...

  3. QT学习日记篇-03-仿写一个智能家居界面

    课程大纲: <1>让界面漂亮起来,仿写一个智能家居界面 ->第一:给QT工程添加图片 进入下一步: <注意路径和名称一定不能有中文>                   ...

  4. Java Web 开发利用Struts2+Spring+mybatis写一个用户登录界面以及简单的数据交互

    框架的东西太复杂也难以讲通,直接上代码: 一.首先得配置环境 和导入必要的jar包 有一些重要的如下: Filter文件夹下的SafetyFilter.java   model文件夹下的 Global ...

  5. 使用Boostrap框架写一个登录\注册界面

    Bootstrap是一个Web前端开发框架,使用它提供的css.js文件可以简单.方便地美化HTML控件.一般情况下,对控件的美化需要我们自己编写css代码,并通过标签选择器.类选择器.ID选择器为指 ...

  6. 再也不怕和老外交流了!我用python实现一个微信聊天翻译助手!

    前言 在前面的一篇文章如何用python“优雅的”调用有道翻译中咱们清楚的写过如何一层一层的解开有道翻译的面纱,并且笔者说过那只是脑洞的开始.现在笔者又回来了.当你遇到一些外国小哥哥小姐姐很心动.想结 ...

  7. nodeJS+express+Jade写一个局域网聊天应用(node基础)

    为了复习一下nodeJS, 而且socketIO这东西听起来就好高端有木有, 而且有人写过了open, 也可以作为自己的参考有木有, 点击下载源代码: express是4.x的版本, 跟以前的配置有些 ...

  8. 微信小程序——手把手教你写一个微信小程序

    前言 微信小程序年前的跳一跳确实是火了一把,然后呢一直没有时间去实践项目,一直想搞但是工作上不需要所以,嗯嗯嗯嗯嗯emmmmm..... 需求 小程序语音识别,全景图片观看,登录授权,获取个人基本信息 ...

  9. WeUI基础样式库——写一个移动端界面

    WeUI是一套基础样式库,同微信原生视觉体验一致,由微信官方设计团队为微信内网页和微信小程序量身设计的.我们来看看这个基础库样式到底长什么样. 这些密密麻麻的就是压缩后的样式库.密密麻麻地看起来简直要 ...

随机推荐

  1. 换硬盘,装win10系统小记

    国庆在家给女朋友的电脑换了1T的固态,重装了系统,特此记录一下,方便后续有需要时查看. win10 激活问题 由于以前的系统就是正版 win10,即使重新装机也会自动激活,不需要做什么额外的步骤. 微 ...

  2. IPv6 与 IPv4现状

    IPv6 与 IPv4现状 一.概述 (1) IPv4可提供bai4,294,967,296个地址,IPv6将原来的32位地址空间增大du到128位,数目是zhi2的128次方.能够对地球上每平方米d ...

  3. 使用TensorRT集成推理inference

    使用TensorRT集成推理inference 使用TensorRT集成进行推理测试. 使用ResNet50模型对每个GPU进行推理,并对其它模型进行性能比较,最后与其它服务器进行比较测试. ResN ...

  4. JUC 并发编程--05, Volatile关键字特性: 可见性, 不保证原子性,禁止指令重排, 代码证明过程. CAS了解么 , ABA怎么解决, 手写自旋锁和死锁

    问: 了解volatile关键字么? 答: 他是java 的关键字, 保证可见性, 不保证原子性, 禁止指令重排 问: 你说的这三个特性, 能写代码证明么? 答: .... 问: 听说过 CAS么 他 ...

  5. Spring Cloud Alibaba(15)---Sleuth+Zipkin

    SpringCloudAlibaba整合Sleuth+Zipkin 有关Sleuth之前有写过两篇文章 Spring Cloud Alibaba(13)---Sleuth概述 Spring Cloud ...

  6. DHCP原理与配置

    一.DHCP应用场景 DHCP服务器能够为大量主机分配lp地址,并能够集中管理 二.DHCP报文类型 微软操作系统的DHCP服务是四个广播报文 三.地址池 主机-------------------- ...

  7. IDEA拷贝类路径

    1.方法一 1.1.鼠标右击需要复制的类 1.2.点击 Copy Reference 2.方法二 快捷键:Ctrl + Alt + Shift + C

  8. 『无为则无心』Python基础 — 11、Python中的数据类型转换

    目录 1.为什么要进行数据类型转换 2.数据类型转换本质 3.数据类型转换用到的函数 4.常用数据类型转换的函数 (1)int()函数 (2)float()函数 (3)str()函数 (4)bool( ...

  9. token & refresh token 机制总结

    token & refresh token 机制总结 废话 我在项目上写了个配置页面,之前很简单直接登录,毕竟配置页面自己人用就没有做token机制,后来公司的安全审核不过,现在要加上toke ...

  10. Ubuntu配置apt安装源为清华源[含自动配置脚本]

    Ubuntu配置apt安装源为清华源[含自动配置脚本] 一.备份原配置文件 Ubuntu 的软件源配置文件是/etc/apt/sources.list.将系统自带的该文件做个备份,以防万一. sudo ...