当前位置: 首页 > news >正文

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 代码&#xff0c;我们将 Item 数组传递到子视图并在子视图中对其进行修改&#xff0c;修改的结果会立即在主视图中反映出来。 不幸的是&#xff0c;当我们修改 Item 名称时却发现不能连续输入&#xff1a;每次敲一个字符键盘都会立即收起并…...

u20.04安装slam库

git clone https://github.com/strasdat/Sophus.git // 下载的最新版是模板类的 git checkout a621ff // 切换为非模板类的历史版本 模板类Sophus的依赖库是Eigen(版本为3.3.X)和fmt&#xff0c;需提前安装好Eigen库和fmt库 git clone https://github.c…...

齐纳二极管,肖特基二极管,瞬态电压抑制二极管

普通二极管&#xff0c;齐纳二极管&#xff0c;肖特基二极管的符号&#xff1a; 瞬态电压抑制&#xff08;TVS&#xff09;二极管是一种特殊的齐纳二极管&#xff0c;其符号如下&#xff1a; 普通二极管 普通二极管由n类型 的半导体和p类型的半导体结合而成。 硅材料制成的二…...

axios 全局错误处理和请求取消

这两个功能都是用拦截器实现。 前景提要&#xff1a; ts 简易封装 axios&#xff0c;统一 API 实现在 config 中配置开关拦截器 全局错误处理 在构造函数中&#xff0c;添加一个响应拦截器即可。在构造函数中注册拦截器的好处是&#xff0c;无论怎么实例化封装类&#xff0c…...

无法加载文件 C:\Program Files\nodejs\cnpm.ps1,因为在此系统上禁止运行脚本。有

cnpm : 无法加载文件 C:\Program Files\nodejs\cnpm.ps1&#xff0c;因为在此系统上禁止运行脚本。有关详细信息&#xff0c;请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Poli cies。 所在位置 行:1 字符: 1 cnpm run debug ~~~~ Categ…...

学电脑编程零基础,计算机编程入门先学什么

学电脑编程零基础&#xff0c;计算机编程入门先学什么&#xff0c;建议先从容易学习的语言入手&#xff0c;比如中文编程。 给大家分享一款中文编程工具&#xff0c;零基础轻松学编程&#xff0c;不需英语基础&#xff0c;编程工具可下载。 这款工具不但可以连接部分硬件&…...

SQL左连接实战案例

要求&#xff1a;用表df1和表df2的数据&#xff0c;得到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 是阿里中间件团队开源的&#xff0c;面向分布式服务架构的轻量级高可用流量控制组件&#xff0c;主要以流量为切入点&#xff0c;从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。 2.2.2 基本概念 • 资源 (需要被保护的东西…...

Qt的事件

2023年11月5日&#xff0c;周日上午 还没写完&#xff0c;不定期更新 目录 事件处理函数的字体特点Qt事件处理的工作原理一些常用的事件处理函数Qt中的事件类型QEvent类的type成员函数可以用来判断事件的类型事件的类型有哪些&#xff1f;有多少种事件类 事件处理函数的字体特…...

MTK联发科天玑9000旗舰5G移动平台处理器_MT6983芯片定制开发

MT6983天玑9000采用台积电4纳米工艺制程&#xff0c;CPU采用“134”三丛集Armv9架构&#xff0c;APU性能提升&#xff0c;ISP处理速度提升&#xff0c;最高支持3.2亿像素摄像头&#xff0c;采用Mali-G710十核GPU&#xff0c;搭载R16 5G调制解调器。 MT6983天玑9000芯片基本概…...

InnoDB中Buffer Pool详解

1. 概念及特点 Buffer Pool 是 MySQL 中 InnoDB 存储引擎用来缓存表数据和索引数据的内存区域。这个内存区域被用来存储磁盘上的数据页的副本&#xff0c;这样常用的数据可以在内存中快速被访问&#xff0c;而不必每次都从磁盘中读取。 以下是 Buffer Pool 的一些重要特点&a…...

3D视觉引导工业机器人上下料,助力汽车制造业实现智能化生产

在工业制造领域&#xff0c;机器人技术一直是推动生产效率和质量提升的重要力量。近年来&#xff0c;随着3D视觉技术的快速发展&#xff0c;工业机器人在处理复杂任务方面迈出了重要的一步。特别是在汽车制造行业&#xff0c;3D视觉引导工业机器人的应用已经取得了令人瞩目的成…...

从Spring说起

一. Spring是什么 在前面的博文中,我们学会了SpringMVC的使用,可以完成一些基本功能的开发了,但是铁子们肯定有很多问题,下面来从Spring开始介绍,第一个问题,什么是Spring? Spring是包含了众多工具方法的IOC容器. Spring有两个核心思想--IOC和AOP,本章先来讲解IOC...... 1.1…...

JavaScript从入门到精通系列第二十九篇:正则表达式初体验

大神链接&#xff1a;作者有幸结识技术大神孙哥为好友&#xff0c;获益匪浅。现在把孙哥视频分享给大家。 孙哥链接&#xff1a;孙哥个人主页 作者简介&#xff1a;一个颜值99分&#xff0c;只比孙哥差一点的程序员 本专栏简介&#xff1a;话不多说&#xff0c;让我们一起干翻J…...

Go语言并发控制:原理与实践

摘要&#xff1a; 本文将深入探讨Go语言的并发控制机制&#xff0c;包括goroutine、channel和sync等关键概念。我们将通过理论阐述和案例分析&#xff0c;揭示Go语言在并发编程中的优势和挑战&#xff0c;并介绍几种常见的并发控制策略。通过本文的学习&#xff0c;你将掌握Go…...

3、Sentinel 动态限流规则

Sentinel 的理念是开发者只需要关注资源的定义&#xff0c;当资源定义成功后可以动态增加各种流控降级规则。Sentinel 提供两种方式修改规则&#xff1a; • 通过 API 直接修改 (loadRules) • 通过 DataSource 适配不同数据源修改 通过 API 修改比较直观&#xff0c;可以通…...

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&#xff09;数据驱动2&#xff09;训练数据和测试数据 2.损失函数1)均方误差2)交叉熵误差3)mini-batch学习 3.数值微分1&#xff09;概念2&#xff09;数值微分实现 4.梯度1&#xff09;实现2&#xff09;梯度法3&#xff09;梯度法实现4&#xff09;…...

C++中使用复制构造函数确保深复制

C中使用复制构造函数确保深复制 复制构造函数是一个重载的构造函数&#xff0c;由编写类的程序员提供。每当对象被复制时&#xff0c;编译器都将调用复制构造函数。 为 MyString 类声明复制构造函数的语法如下&#xff1a; class MyString {MyString(const MyString& cop…...

【Mysql】Mysql中表连接的原理

连接简介 在实际工作中&#xff0c;我们需要查询的数据很可能不是放在一张表中&#xff0c;而是需要同时从多张表中获取。下面我们以简单的两张表为例来进行说明。 连接的本质 为方便测试说明&#xff0c;&#xff0c;先创建两个简单的表并给它们填充一点数据&#xff1a; …...

label-studio的使用教程(导入本地路径)

文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

深入理解JavaScript设计模式之单例模式

目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式&#xff08;Singleton Pattern&#…...

Java - Mysql数据类型对应

Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...

如何在看板中有效管理突发紧急任务

在看板中有效管理突发紧急任务需要&#xff1a;设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP&#xff08;Work-in-Progress&#xff09;弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中&#xff0c;设立专门的紧急任务通道尤为重要&#xff0c;这能…...

ffmpeg(四):滤镜命令

FFmpeg 的滤镜命令是用于音视频处理中的强大工具&#xff0c;可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下&#xff1a; ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜&#xff1a; ffmpeg…...

ETLCloud可能遇到的问题有哪些?常见坑位解析

数据集成平台ETLCloud&#xff0c;主要用于支持数据的抽取&#xff08;Extract&#xff09;、转换&#xff08;Transform&#xff09;和加载&#xff08;Load&#xff09;过程。提供了一个简洁直观的界面&#xff0c;以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...

关于 WASM:1. WASM 基础原理

一、WASM 简介 1.1 WebAssembly 是什么&#xff1f; WebAssembly&#xff08;WASM&#xff09; 是一种能在现代浏览器中高效运行的二进制指令格式&#xff0c;它不是传统的编程语言&#xff0c;而是一种 低级字节码格式&#xff0c;可由高级语言&#xff08;如 C、C、Rust&am…...

[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...

uniapp手机号一键登录保姆级教程(包含前端和后端)

目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号&#xff08;第三种&#xff09;后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...

redis和redission的区别

Redis 和 Redisson 是两个密切相关但又本质不同的技术&#xff0c;它们扮演着完全不同的角色&#xff1a; Redis: 内存数据库/数据结构存储 本质&#xff1a; 它是一个开源的、高性能的、基于内存的 键值存储数据库。它也可以将数据持久化到磁盘。 核心功能&#xff1a; 提供丰…...