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

news2024/11/27 12:52:50

在这里插入图片描述

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: String
    var 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_items
    
    private 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 in
    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)
    }
}
.padding()

这样一段看起来“天衣无缝”的代码为什么会出现在更改 Item 名称时键盘反复关闭、输入焦点丢失的问题呢?

2. 无效的尝试:用子视图包装

我们首先猜测是子视图中 Item 名称的更改导致了父视图的“冗余”刷新,从而引起键盘不正确被重置。


更多 SwiftUI 和 Swift 代码调试的例子,请观赏我专题专栏中的博文:

  • 『第十三章』雨燕的自我修养:Swift 调试技巧(上)
  • 『第十四章』雨燕的自我修养:Swift 调试技巧(下)

因为键盘所属的视图发生重建所以键盘本身也会被重置,那么如何验证我们的猜测呢?一种方式是使用如下的调试技术:

  • SwiftUI 如何快速识别视图(View)界面的刷新是由哪个状态的改变导致的?

在这里我们假设病根果真如此。那么一种常用的解决办法立即浮现于脑海:我们可以将引起刷新的子视图片段包装在新的 View 结构中,这样做到原因是 SwiftUI 渲染器足够智能可以只刷新子视图而不是父视图中大段内容的更改。


更详细的原理请参考如下链接:

  • SwiftUI 中为什么应该经常用子视图替换父视图中的大段内容?

So,让我撸起袖子开动起来!

首先,将 ForEach 循环中编辑单个 Item 的 View 包装为一个新的视图 ItemEditView:

struct ItemEditView: View {
    @Binding var item: Item
    
    var 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 in
            ItemEditView(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: String
    var count: Int
}

如上代码所示,我们在 Item 创建时为 id 生成一个唯一的 UUID 对象,这可以保证两点:

  • 任意时刻 Item 的唯一性;
  • 任意 Item 在其生命周期中的稳定性;

有了如上修改之后,我们再来运行代码看看结果:

在这里插入图片描述

可以看到,现在我们可以毫无问题的连续输入 Item 的名字了,焦点不会再丢失,一切回归正常,棒棒哒!!!💯

总结

在本篇博文中,我们讨论了 SwiftUI 开发中一个非常常见的问题,并借助一步步溯本回原的推理找到症结根本之所在,最后一发入魂将其完美解决!相信小伙伴们都能由此受益匪浅。

感谢观赏,再会!😎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1172938.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023-11-05 LeetCode每日一题(重复的DNA序列)

2023-11-05每日一题 一、题目编号 187. 重复的DNA序列二、题目链接 点击跳转到题目位置 三、题目描述 DNA序列 由一系列核苷酸组成&#xff0c;缩写为 ‘A’, ‘C’, ‘G’ 和 ‘T’.。 例如&#xff0c;“ACGAATTCCG” 是一个 **DNA序列 **。 在研究 DNA 时&#xff0c…

网络运维Day04

文章目录 实验环境grep命令使用vim文本编辑器命令模式基本操作光标跳转复制、粘贴、删除 末行模式基本操作存盘、退出、文件操作开关设置 Linux命令补充man帮助历史命令du命令date指令 归档及压缩制作压缩包释放压缩包zip压缩包制作zip格式压缩包(打包)释放zip格式压缩包(解包)…

从Spring说起

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

高中信息技术学业水平考试模拟题库

单选 水仙花数的定义&#xff1a;指一个 3 位数&#xff0c;它的每个位上的数字的 3次幂之和等于它本身 ①数据&#xff1a;数据是信息和知识的来源。 ②信息&#xff1a;信息是经过加工的数据。 ③知识&#xff1a;知识是人们在改造世界的实践活动中所获得的可用于指导实践的…

docker---dockerfile相关知识

第 3 章 Docker 高级实践 在这一部分我们主要来介绍一些Docker的高级内容&#xff1a; Dockerfile 和 Docker compose 3.1 Dockerfile Dockerfile我们从下面的几个方面来介绍&#xff1a; Dockerfile简介 Dockerfile快速入门 Dockerfile详解 Dockerfile简单 实践 3.1.1 Docke…

利用Path工具在Plant Simulation快速进行agv路径规划

之前小伙伴在问B站上有个AGV遇到障碍动态调整路线的视频是如何实现的。波哥花了点时间实现了一下&#xff0c;说一下思路&#xff1a; 1. 在Plant Simulation里面实现任意一个路径规划算法(A*、D*、Dijkstra)。 2. 监控agv移动过程中道路情况的变化 3. 判断是否需要重新规划路线…

【Python语言】序列(列表,元组,字符串)切片操作

目录 序列切片操作 1.1 对list进行切片&#xff0c;从1开始&#xff0c;到5结束&#xff0c;步长为1 [ 1 : 5 ] 1.2 对tuple进行切片&#xff0c;从头开始&#xff0c;到最后结束&#xff0c;步长为1 [ : ] 1.3 对str进行切片&#xff0c;从头开始&#xff0c;到最…

什么是DITA?从百度的回答说起

▲ 搜索“大龙谈智能内容”关注GongZongHao▲ 什么是DITA? 把这个问题输入百度&#xff0c;获得以下回答&#xff1a; DITA 是“Darwin Information Typing Architecture”&#xff08;达尔文信息类型化体系结构&#xff09;的缩写&#xff0c;它是IBM 公司为OASIS 所支持…

一看就懂,把“百度”搬回家

引言 生活中&#xff0c;我们经常使用“百度”查询资料&#xff0c;访问“购物网站”购买商品&#xff0c;下面&#xff0c;我们搭建实验环境&#xff0c;将“百度”和“京东”搬回家。 前提 了解什么是计算机网络&#xff0c;参考&#xff1a;一看就懂&#xff0c;原来这就…

新版onenet平台安全鉴权的确定与使用

根据onenet官方更新的文档&#xff1a;平台提供开放的API接口&#xff0c;用户可以通过HTTP/HTTPS调用&#xff0c;进行设备管理&#xff0c;数据查询&#xff0c;设备命令交互等操作&#xff0c;在API的基础上&#xff0c;根据自己的个性化需求搭建上层应用。 为提高API访问安…

vue3后台管理系统之数据大屏适配解决方案

1&#xff1a;scale 方式 我们整个大屏的尺寸设置和设计图一样&#xff0c;只是通过css的scale放大缩小属性&#xff0c;来控制实际展示的大小。 通过监听浏览器窗口的大小&#xff0c;来改变scale的比例&#xff0c;从而实现数据大屏适配。&#xff08;百度、网易等大数据适配…

运维知识点-MySQL从小白到入土

MySQL从小白到入土 mysql 服务器安装windows mysql 服务漏洞复现-mysql jdbc反序列化-权限绕过 mysql 服务器安装 https://dev.mysql.com/downloads/mysql/https://www.cnblogs.com/xiaostudy/p/12262804.html 点餐小程序腾讯云服务器安装mysql8 windows mysql 服务 net sta…

YOLOv8改进:IOU创新篇 | 引入MPDIou、WIoU、SIoU、EIoU、α-IoU,在不同场景实现涨点

🚀🚀🚀本文改进:引入MPDIou、WIoU、SIoU、EIoU、α-IoU,适配各个YOLO 🚀🚀🚀MPDIou、WIoU、SIoU、EIoU、α-IoU在各个场景都能够有效涨点 🚀🚀🚀YOLOv8改进专栏:http://t.csdnimg.cn/hGhVK 学姐带你学习YOLOv8,从入门到创新,轻轻松松搞定科研; 1. …

(附源码)基于SSM 车险事故自助理赔小程序-计算机毕设 84607

车险事故自助理赔小程序 摘要 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;车险事故自助理赔小程序被用户普遍…

基于YOLOv8的烟雾检测:自研模块 BSAM注意力 PK CBAM注意力,提升一个多点

&#x1f4a1;&#x1f4a1;&#x1f4a1;本文全网首发独家改进&#xff1a;提出新颖的注意力BSAM&#xff08;BiLevel Spatial Attention Module&#xff09;&#xff0c;创新度极佳&#xff0c;适合科研创新&#xff0c;效果秒杀CBAM&#xff0c;Channel AttentionSpartial …

linux网络服务综合项目

前期环境配置 #主要写了192.168.146.130的代码&#xff0c;131的配置代码和其一样 [rootserver ~]# nmtui #通过图形化界面修改ens160的ip 192.168.146.130 [rootserver ~]# hostnamectl set-hostname Server-Web #修改130主机名…

程序员为啥要做副业(04)-新技术落地!

点击下方“JavaEdge”&#xff0c;选择“设为星标” 第一时间关注技术干货&#xff01; 免责声明~ 任何文章不要过度深思&#xff01; 万事万物都经不起审视&#xff0c;因为世上没有同样的成长环境&#xff0c;也没有同样的认知水平&#xff0c;更「没有适用于所有人的解决方案…

Python机器学习算法入门教程(第二部分)

接着Python机器学习算法入门教程&#xff08;第一部分&#xff09;&#xff0c;继续展开描述。 七、梯度下降求极值 在Python机器学习算法入门教程&#xff08;第一部分&#xff09;中的第六部分&#xff1a;线性回归&#xff1a;损失函数和假设函数一节&#xff0c;从数学的…

2023年【高处安装、维护、拆除】免费试题及高处安装、维护、拆除找解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 高处安装、维护、拆除免费试题根据新高处安装、维护、拆除考试大纲要求&#xff0c;安全生产模拟考试一点通将高处安装、维护、拆除模拟考试试题进行汇编&#xff0c;组成一套高处安装、维护、拆除全真模拟考试试题&a…

节点配置(添加黑名单)

1.首先配置一个单群组4节点的链 1.1创建操作目录 cd ~ && mkdir -p fisco && cd fisco 1.2下载国内脚本 curl -#LO https://osp-1257653870.cos.ap-guangzhou.myqcloud.com/FISCO-BCOS/FISCO-BCOS/releases/v2.9.1/build_chain.sh && chmod ux bu…