SwiftUI 代码调试之都是“变心”惹的祸

0. 概览
这是一段非常简单的 SwiftUI 代码,我们将 Item 数组传递到子视图并在子视图中对其进行修改,修改的结果会立即在主视图中反映出来。

不幸的是,当我们修改 Item 名称时却发现不能连续输入:每次敲一个字符键盘都会立即收起并且原输入焦点会马上丢失,这是怎么回事呢?
在本篇博文中您将学到以下内容
- 0. 概览
- 1. 不该发生的错误
- 2. 无效的尝试:用子视图包装
- 3. 寻根究底
- 4. 解决之道
- 总结
该问题这是初学者在 SwiftUI 开发中常常会犯的一个错误,不过看完本篇之后相信大家都会对此自胸有成竹!
废话不再,Let‘s fix it!!!😉
1. 不该发生的错误
照例我们先看一下源代码。
例子中我们创建了 Item 结构用来作为 Model 中的“真相之源”。
想要了解更多 SwiftUI 编程和“真相之源”奥秘的小伙伴们,请观赏我专题专栏中的如下文章:
- 『第十章』仪态万千的雨燕:UIKit 和 SwiftUI
注意,我们让 Item 遵守了 Identifiable 协议,这样可以更好的适配 SwiftUI 列表中的显示:
struct Item: Identifiable {var id: String {name}var name: Stringvar count: Int
}let g_items: [Item] = [.init(name: "宇宙魔方", count: 11),.init(name: "宝石手套", count: 1),.init(name: "大黄蜂", count: 1)
]
接下来是主视图 ItemListView,可以看到我们将 items 状态传递到子视图的 ForEach 循环中去了:
struct ItemListView: View {@State var items = g_itemsprivate var total: Int {items.reduce(0) { $0 + $1.count}}private var desc: [String] {items.reduce([String]()) { $0 + [$1.name]}}var body: some View {NavigationStack {// 子视图 ForEach 循环...ForEach($items) { $item in// 代码马上来...}VStack {Text(desc.joined(separator: ",")).font(.title3).foregroundStyle(.pink)HStack {Text("宝贝总数量:\(total)").font(.headline)Spacer().frame(width: 20)Button("所有 +1"){for idx in items.indices {guard items[idx].count < 100 else { continue}items[idx].count += 1}}.font(.headline).buttonStyle(.borderedProminent)}}.offset(y: 200)}}
}
最后是 ForEach 循环中的内容,如下所示我们用单个 item 的值绑定来实现修改其内容的目的:
ForEach($items) { $item inHStack {TextField("输入项目名称", text: $item.name).font(.title2.weight(.heavy))Text("数量:\(item.count)").foregroundStyle(.gray)Slider(value: .init(get: {Double(item.count)}, set: {item.count = Int($0)}), in: 0.0...100.0)}
}
.padding()
这样一段看起来“天衣无缝”的代码为什么会出现在更改 Item 名称时键盘反复关闭、输入焦点丢失的问题呢?
2. 无效的尝试:用子视图包装
我们首先猜测是子视图中 Item 名称的更改导致了父视图的“冗余”刷新,从而引起键盘不正确被重置。
更多 SwiftUI 和 Swift 代码调试的例子,请观赏我专题专栏中的博文:
- 『第十三章』雨燕的自我修养:Swift 调试技巧(上)
- 『第十四章』雨燕的自我修养:Swift 调试技巧(下)
因为键盘所属的视图发生重建所以键盘本身也会被重置,那么如何验证我们的猜测呢?一种方式是使用如下的调试技术:
- SwiftUI 如何快速识别视图(View)界面的刷新是由哪个状态的改变导致的?
在这里我们假设病根果真如此。那么一种常用的解决办法立即浮现于脑海:我们可以将引起刷新的子视图片段包装在新的 View 结构中,这样做到原因是 SwiftUI 渲染器足够智能可以只刷新子视图而不是父视图中大段内容的更改。
更详细的原理请参考如下链接:
- SwiftUI 中为什么应该经常用子视图替换父视图中的大段内容?
So,让我撸起袖子开动起来!
首先,将 ForEach 循环中编辑单个 Item 的 View 包装为一个新的视图 ItemEditView:
struct ItemEditView: View {@Binding var item: Itemvar body: some View {HStack {TextField("输入项目名称", text: $item.name).font(.title2.weight(.heavy))Text("数量:\(item.count)").foregroundStyle(.gray)Slider(value: .init(get: {Double(item.count)}, set: {item.count = Int($0)}), in: 0.0...100.0)}}
}
接着,我们将 ForEach 循环本身用一个新视图取代:
struct EditView: View {@Binding var items: [Item]var body: some View {ForEach($items) { $item inItemEditView(item: $item)}.padding()}
}
最后,我们所要做的就是将父视图 ItemListView 中的 ForEach 循环变为 EditView 视图:
NavigationStack {EditView(items: $items)// 其它代码不变...
}
再次运行代码…不幸的是问题依旧:

看来这并不是简单父视图“过度”刷新的问题,一定是有什么不应有的行为触发了父视图的刷新,到底是什么呢?
3. 寻根究底
问题一定出在 ForEach 循环里!
回顾之前 Item 的定义,我们用 Identifiable 协议满足 ForEach 对子项目唯一性的挑剔,我们用 Item.name 构建了 id 属性。
当 Model 元素遵守 Identifiable 协议时,应该确保在任意时刻所有 Item 的 id 属性值都是唯一的!从目前来看,上述代码在修改 Item 名称时并没有发生重名的情况(虽然可能发生),所以对于唯一性是没有问题的。
当然在实际代码中用户很可能会输入重复的 Item 名称,所以还是不可接收的。
不过,这段代码在这里只是作为例子来向大家展示解决问题的推理过程,所以不必深究 😉
但是 id 还有另一个重要的特征:稳定性!
一般的,当 Identifiable 实体对象的 id 属性改变时,SwiftUI 会认为其不再是同一个对象,而立即刷新其所对应的视图界面。
所以,正如大家所看到的那样:每次用户输入 name 中的新字符时,键盘会被立即关闭焦点也随即丢失!
4. 解决之道
知道了问题原因,解决起来就很容易了。
我们只需要在 Item 生命周期中保证 id 的稳定性就可以了,这意味着不能再用 name 值作为 id 的“关联”值:
struct Item: Identifiable {let id = UUID()var name: Stringvar count: Int
}
如上代码所示,我们在 Item 创建时为 id 生成一个唯一的 UUID 对象,这可以保证两点:
- 任意时刻 Item 的唯一性;
- 任意 Item 在其生命周期中的稳定性;
有了如上修改之后,我们再来运行代码看看结果:

可以看到,现在我们可以毫无问题的连续输入 Item 的名字了,焦点不会再丢失,一切回归正常,棒棒哒!!!💯
总结
在本篇博文中,我们讨论了 SwiftUI 开发中一个非常常见的问题,并借助一步步溯本回原的推理找到症结根本之所在,最后一发入魂将其完美解决!相信小伙伴们都能由此受益匪浅。
感谢观赏,再会!😎
相关文章:
SwiftUI 代码调试之都是“变心”惹的祸
0. 概览 这是一段非常简单的 SwiftUI 代码,我们将 Item 数组传递到子视图并在子视图中对其进行修改,修改的结果会立即在主视图中反映出来。 不幸的是,当我们修改 Item 名称时却发现不能连续输入:每次敲一个字符键盘都会立即收起并…...
u20.04安装slam库
git clone https://github.com/strasdat/Sophus.git // 下载的最新版是模板类的 git checkout a621ff // 切换为非模板类的历史版本 模板类Sophus的依赖库是Eigen(版本为3.3.X)和fmt,需提前安装好Eigen库和fmt库 git clone https://github.c…...
齐纳二极管,肖特基二极管,瞬态电压抑制二极管
普通二极管,齐纳二极管,肖特基二极管的符号: 瞬态电压抑制(TVS)二极管是一种特殊的齐纳二极管,其符号如下: 普通二极管 普通二极管由n类型 的半导体和p类型的半导体结合而成。 硅材料制成的二…...
axios 全局错误处理和请求取消
这两个功能都是用拦截器实现。 前景提要: ts 简易封装 axios,统一 API 实现在 config 中配置开关拦截器 全局错误处理 在构造函数中,添加一个响应拦截器即可。在构造函数中注册拦截器的好处是,无论怎么实例化封装类,…...
无法加载文件 C:\Program Files\nodejs\cnpm.ps1,因为在此系统上禁止运行脚本。有
cnpm : 无法加载文件 C:\Program Files\nodejs\cnpm.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Poli cies。 所在位置 行:1 字符: 1 cnpm run debug ~~~~ Categ…...
学电脑编程零基础,计算机编程入门先学什么
学电脑编程零基础,计算机编程入门先学什么,建议先从容易学习的语言入手,比如中文编程。 给大家分享一款中文编程工具,零基础轻松学编程,不需英语基础,编程工具可下载。 这款工具不但可以连接部分硬件&…...
SQL左连接实战案例
要求:用表df1和表df2的数据,得到df3 一、创建表 CREATE TABLE df1 (姓名 varchar(255) DEFAULT NULL,年龄 int DEFAULT NULL,部门 varchar(255) DEFAULT NULL,id int DEFAULT NULL );CREATE TABLE df2 (部门 varchar(255) DEFAULT NULL,年龄 int DEFAU…...
2、Sentinel基本应用限流规则(2)
2.2.1 是什么 Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。 2.2.2 基本概念 • 资源 (需要被保护的东西…...
Qt的事件
2023年11月5日,周日上午 还没写完,不定期更新 目录 事件处理函数的字体特点Qt事件处理的工作原理一些常用的事件处理函数Qt中的事件类型QEvent类的type成员函数可以用来判断事件的类型事件的类型有哪些?有多少种事件类 事件处理函数的字体特…...
MTK联发科天玑9000旗舰5G移动平台处理器_MT6983芯片定制开发
MT6983天玑9000采用台积电4纳米工艺制程,CPU采用“134”三丛集Armv9架构,APU性能提升,ISP处理速度提升,最高支持3.2亿像素摄像头,采用Mali-G710十核GPU,搭载R16 5G调制解调器。 MT6983天玑9000芯片基本概…...
InnoDB中Buffer Pool详解
1. 概念及特点 Buffer Pool 是 MySQL 中 InnoDB 存储引擎用来缓存表数据和索引数据的内存区域。这个内存区域被用来存储磁盘上的数据页的副本,这样常用的数据可以在内存中快速被访问,而不必每次都从磁盘中读取。 以下是 Buffer Pool 的一些重要特点&a…...
3D视觉引导工业机器人上下料,助力汽车制造业实现智能化生产
在工业制造领域,机器人技术一直是推动生产效率和质量提升的重要力量。近年来,随着3D视觉技术的快速发展,工业机器人在处理复杂任务方面迈出了重要的一步。特别是在汽车制造行业,3D视觉引导工业机器人的应用已经取得了令人瞩目的成…...
从Spring说起
一. Spring是什么 在前面的博文中,我们学会了SpringMVC的使用,可以完成一些基本功能的开发了,但是铁子们肯定有很多问题,下面来从Spring开始介绍,第一个问题,什么是Spring? Spring是包含了众多工具方法的IOC容器. Spring有两个核心思想--IOC和AOP,本章先来讲解IOC...... 1.1…...
JavaScript从入门到精通系列第二十九篇:正则表达式初体验
大神链接:作者有幸结识技术大神孙哥为好友,获益匪浅。现在把孙哥视频分享给大家。 孙哥链接:孙哥个人主页 作者简介:一个颜值99分,只比孙哥差一点的程序员 本专栏简介:话不多说,让我们一起干翻J…...
Go语言并发控制:原理与实践
摘要: 本文将深入探讨Go语言的并发控制机制,包括goroutine、channel和sync等关键概念。我们将通过理论阐述和案例分析,揭示Go语言在并发编程中的优势和挑战,并介绍几种常见的并发控制策略。通过本文的学习,你将掌握Go…...
3、Sentinel 动态限流规则
Sentinel 的理念是开发者只需要关注资源的定义,当资源定义成功后可以动态增加各种流控降级规则。Sentinel 提供两种方式修改规则: • 通过 API 直接修改 (loadRules) • 通过 DataSource 适配不同数据源修改 通过 API 修改比较直观,可以通…...
HDU 2648:Shopping ← STL map
【题目来源】http://acm.hdu.edu.cn/showproblem.php?pid2648【题目描述】 Every girl likes shopping,so does dandelion.Now she finds the shop is increasing the price every day because the Spring Festival is coming .She is fond of a shop which is called "m…...
自己动手实现一个深度学习算法——三、神经网络的学习
文章目录 1.从数据中学习1)数据驱动2)训练数据和测试数据 2.损失函数1)均方误差2)交叉熵误差3)mini-batch学习 3.数值微分1)概念2)数值微分实现 4.梯度1)实现2)梯度法3)梯度法实现4)…...
C++中使用复制构造函数确保深复制
C中使用复制构造函数确保深复制 复制构造函数是一个重载的构造函数,由编写类的程序员提供。每当对象被复制时,编译器都将调用复制构造函数。 为 MyString 类声明复制构造函数的语法如下: class MyString {MyString(const MyString& cop…...
【Mysql】Mysql中表连接的原理
连接简介 在实际工作中,我们需要查询的数据很可能不是放在一张表中,而是需要同时从多张表中获取。下面我们以简单的两张表为例来进行说明。 连接的本质 为方便测试说明,,先创建两个简单的表并给它们填充一点数据: …...
网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...
C++中string流知识详解和示例
一、概览与类体系 C 提供三种基于内存字符串的流,定义在 <sstream> 中: std::istringstream:输入流,从已有字符串中读取并解析。std::ostringstream:输出流,向内部缓冲区写入内容,最终取…...
PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...
Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信
文章目录 Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket(服务端和客户端都要)2. 绑定本地地址和端口&#x…...
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...
算法岗面试经验分享-大模型篇
文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer (1)资源 论文&a…...
基于TurtleBot3在Gazebo地图实现机器人远程控制
1. TurtleBot3环境配置 # 下载TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…...
