前置资源

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

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

第一级容器:TabView

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

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

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

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

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

ChatsView().tabItem {
Image(systemName: currentStateIconName(selecting: selectingTab))
Text("Chats")
}.tag(Tabs.Chats) func currentStateIconName(selecting: Tabs) -> String {
selecting == self ? iconNameOn : iconNameOff
}
  • 示例中自定义 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 容易控制)

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(作为子页布局展示)。

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

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

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

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

聊天记录 View

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

let isShowBadge: Bool = Float.random(in: 0...1) > 0.45

Image("avatar01")
.resizable()
.scaledToFit()
.frame(width: 50, height: 50, alignment: .center)
.cornerRadius(4.0)
.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() 实现。


聊天窗口

@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,来实现布局排序方向变化。

剖析:如何用 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. 太方便了!利用Python对批量Pdf转Word

    在wps或者office里面可以将pdf转word,不过只能免费转前面5页,超过5页就需要会员.今天教大家一个Python办公小技巧:批量Pdf转Word ,这样可以自由想转多少页都可以. 思路:这里 ...

  2. 桥接PyTorch和TVM

    桥接PyTorch和TVM 人工智能最引人入胜的一些应用是自然语言处理.像BERT或GPT-2之类的模型及其变体,可以获住足够多的文本信息. 这些模型属于称为Transformers的神经网络类体系结 ...

  3. AI框架类FAQ

    AI框架类FAQ 数据处理 问题:如何在训练过程中高效读取数量很大的数据集? 答复:当训练时使用的数据集数据量较大或者预处理逻辑复杂时,如果串行地进行数据读取,数据读取往往会成为训练效率的瓶颈.这种情 ...

  4. NVIDIA A100 GPUs上硬件JPEG解码器和NVIDIA nvJPEG库

    NVIDIA A100 GPUs上硬件JPEG解码器和NVIDIA nvJPEG库 Leveraging the Hardware JPEG Decoder and NVIDIA nvJPEG Lib ...

  5. TVM vs TensorRT比较

    TVM vs TensorRT比较 如果理解正确的话,TensorRT和TVM会加快预测速度. TensorRT优化预测GPU和TVM优化预测几乎所有平台支持GPU,ARM,Mobile... 两者在 ...

  6. MEMS传感器作为变革的驱动力

    MEMS sensors as drivers for change 物联网(IoT)正在改变与周围世界互动的方式.每个人,每件事,都是相互联系的,很快就会相互联系.微机电系统(MEMS)设备和传感器 ...

  7. java后端知识点梳理——多线程与高并发

    进程与线程 进程是一个"执行中的程序",是系统进行资源分配和调度的一个独立单位 线程是进程的一个实体,一个进程中一般拥有多个线程. 线程和进程的区别 进程是操作系统分配资源的最小单 ...

  8. 【NX二次开发】Block UI 属性类型

     Block UI 属性类型的读写总结: 帮助文件 NXOpen::BlockStyler::UIBlock::GetProperties() String类型 //设置值 this->块ID- ...

  9. centos 7查看系统网络情况netstat

    查看系统网络情况 netstat ➢ 基本语法 netstat [选项] ➢ 选项说明 -an 按一定顺序排列输出 -p 显示哪个进程在调用 应用案例 请查看服务名为 sshd 的服务的信息. ➢ N ...

  10. 无规矩不成方圆,聊一聊 Spring Boot 中 RESTful 接口设计规范

    在设计接口时,有很多因素要考虑,如接口的业务定位,接口的安全性,接口的可扩展性.接口的稳定性.接口的跨域性.接口的协议规则.接口的路径规则.接口单一原则.接口过滤和接口组合等诸多因素,本篇文章将简要分 ...