SwiftUI 更自然地向自定义视图传递参数的“另类”方式

2024-02-21 05:04

本文主要是介绍SwiftUI 更自然地向自定义视图传递参数的“另类”方式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

概览

在 SwiftUI 中,正是自定义视图让我们的 App 变得与众不同!然而,除了传统的视图接口定义方式以外,我们其实还可以有更“银杏化”的选择。

在这里插入图片描述

如上图所示:对于 SubView 子视图所需的参数我们一开始并没有操之过急,而是随后再以独立、灵活的方式将其传入到了 SubView 中,这是怎么做到的呢?

在本篇博文中,您将学到如下内容:

  • 概览
  • 1. 一个简单的视图需求!
  • 2. “传统”的调用方式
  • 3. 灵动的方式:按需且独立!
  • 4. 再次验证 SwiftUI 视图状态的稳定性
  • 总结

闲言少叙,Let‘s go!!!😉


1. 一个简单的视图需求!

我们需要创建一个子视图,它用来显示 Model 可观察对象的内容,同时包括一个界面是否展开的状态,并且可以自定义用户点击的行为:

@Observable
class Model {var name = "hopy"var power = 5
}struct SubView: View {let model = Model()@Binding var isExpanding: Boolvar tapHandler: (()->Void)?//...
}

从上面代码中可以清楚的看到:SubView 子视图包含一个 Model 可观察对象,并且还有 isExpanding 和 tapHandler 属性来分别表示自身展开的状态和用户点击时执行的代码。

我们可以这样实现 SubView 的 body:

var body: some View {VStack {Text(model.name).font(.largeTitle.weight(.bold))if isExpanding {Divider()HStack {Text("POW: \(model.power)").foregroundStyle(.red).font(.headline.weight(.heavy))Spacer()Button("Add POW!") {model.power += 1}.buttonStyle(.borderedProminent)}}}.onTapGesture {tapHandler?()}.padding().background(Color.black.opacity(0.2), in: RoundedRectangle(cornerRadius: 15.0)).overlay {RoundedRectangle(cornerRadius: 15).stroke(.black, lineWidth: 5.0)}.shadow(radius: 5)
}

2. “传统”的调用方式

现在已经定义好了 SubView 视图,我们可以这样在主视图中创建并使用它:

@State var isExpanding = falseSubView(isExpanding: $isExpanding) {print("OK!")
}

SubView 的运行界面如下图所示:

在这里插入图片描述

如上代码,我们在创建 SubView 子视图时,就需要将它所有必要的传入参数都考虑周全。

当然,这样本身并没有什么不妥。只不过,假若视图包含海量传入参数可能会出现一些不“银杏化”的地方:

  1. 在视图创建时就需要考虑到它所有的传入参数,即使有些可以暂时“忽略不计”;
  2. 在视图创建时就需要绞尽脑汁让这一坨冗长的传入参数在代码缩进和排版上看起来不那么“毛骨悚然”;
  3. 无法清晰的隔离视图自身创建和其状态创建的不同逻辑;

那么,除了视图“传统”的接口设计方式之外,我们是否还有其它的解决方案呢?

答案是肯定的!

3. 灵动的方式:按需且独立!

回忆一下 SwiftUI 中视图的本质:它其实只是状态的函数,它本身很“廉价”,更重要的是它是一个值对象。

这意味着,我们可以随时创建它们的拷贝,并改变拷贝所包含的属性,然后再用修改后的拷贝替换原有的视图。

首先,我们将 SubView 定义修改为如下形式:

struct SubView: View {let model = Model()private var isExpanding = falseprivate var tapHandler: (()->Void)?
}

这样做的好处是:在 SubView 创建时无需传入任何参数,我们完全将 SubView 自身和其状态分开了。

注意在上面代码中我们用 private 关键字修饰了它的各个属性,那么我们必须找到随后改变它们的方法,这该如何是好呢?

因为私有属性只能在类型内部读写,但类型扩展显然属于“内部”这一范畴,所以我们可以在 SubView 的扩展中大展拳脚:

extension SubView {func isExpanding(_ expanding: Bool) -> Self {var view = selfview.isExpanding = expandingreturn view}func tapHandler(_ handler: @escaping ()->()) -> Self {var view = selfview.tapHandler = handlerreturn view}
}

可以看到:在上面 SubView 视图的扩展方法中我们像讨论过的那样显式拷贝了 SubView 对象的实例,然后更改它的属性,最后返回了更改后的视图。

现在,我们可以这样创建 SubView 视图了:

struct ContentView: View {@State var isExpanding = falsevar body: some View {NavigationStack {VStack {SubView().isExpanding(isExpanding).tapHandler {withAnimation(.snappy) {isExpanding.toggle()}}Button("Expanding!") {withAnimation(.bouncy) {isExpanding.toggle()}}.padding(.top, 100)}.padding()}}
}

于是乎,我们可以“赤裸裸的” 让 SubView 先诞生,然后根据需要再以视图扩展的方式为其“注入”必要的参数。这样我们就可以有的放矢的将重点放在视图的某些属性上,创建逻辑会更加清晰明了。

4. 再次验证 SwiftUI 视图状态的稳定性

如果小伙伴们观察的足够仔细就会发现,上述代码每次子视图的展开属性(isExpanding)发生改变时,其 Model 的 power 值就会被重置:

在这里插入图片描述

这是因为,每次 isExpanding 属性改变时 SubView 自身的重建也会导致其 Model 对象的重建。

在 Swift 5.9 新 @Observable 对象在 SwiftUI 使用中的陷阱与解决 这篇博文中,我们进行过 SwiftUI 视图 @Observable 对象稳定性的讨论。我们得出的一个重要结论是:如果想要 @Observable 对象保持稳定,必须将它用状态来承载!

在本案例中为了达到这一目的,我们可以有两种方法:

  1. 在主视图中将 Model 实例传递到 SubView 中;
  2. 或者在 SubView 中用 @State 修饰 Model 属性;

这里,我们采用第二种方法,将 SubView 中的 model 对象用 @State 属性包装器修饰:

struct SubView: View {@State var model = Model()//...
}

最后运行看一下结果:

在这里插入图片描述

看到了吗?现在无论 SubView 自身如何变化,我们的 Model 状态都不会“始乱终弃”,它的内容始终保持一致!棒棒哒!💯

总结

在本篇博文中,我们讨论了 SwiftUI “传统”的视图接口定义在具有海量传入参数时的一些不便之处,并且用更加“低耦合”的“环保”方法改善了这一情况。相信现在小伙伴们对于 SwiftUI 中视图的构建会有更写意、更灵活的方式啦!

感谢观赏,再会!😎

这篇关于SwiftUI 更自然地向自定义视图传递参数的“另类”方式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/730645

相关文章

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

使用Sentinel自定义返回和实现区分来源方式

《使用Sentinel自定义返回和实现区分来源方式》:本文主要介绍使用Sentinel自定义返回和实现区分来源方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Sentinel自定义返回和实现区分来源1. 自定义错误返回2. 实现区分来源总结Sentinel自定

Springboot处理跨域的实现方式(附Demo)

《Springboot处理跨域的实现方式(附Demo)》:本文主要介绍Springboot处理跨域的实现方式(附Demo),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录Springboot处理跨域的方式1. 基本知识2. @CrossOrigin3. 全局跨域设置4.

springboot security使用jwt认证方式

《springbootsecurity使用jwt认证方式》:本文主要介绍springbootsecurity使用jwt认证方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录前言代码示例依赖定义mapper定义用户信息的实体beansecurity相关的类提供登录接口测试提供一

springboot security之前后端分离配置方式

《springbootsecurity之前后端分离配置方式》:本文主要介绍springbootsecurity之前后端分离配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的... 目录前言自定义配置认证失败自定义处理登录相关接口匿名访问前置文章总结前言spring boot secu

SpringBoot中封装Cors自动配置方式

《SpringBoot中封装Cors自动配置方式》:本文主要介绍SpringBoot中封装Cors自动配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录SpringBoot封装Cors自动配置背景实现步骤1. 创建 GlobalCorsProperties

Flutter打包APK的几种方式小结

《Flutter打包APK的几种方式小结》Flutter打包不同于RN,Flutter可以在AndroidStudio里编写Flutter代码并最终打包为APK,本篇主要阐述涉及到的几种打包方式,通... 目录前言1. android原生打包APK方式2. Flutter通过原生工程打包方式3. Futte

在C#中调用Python代码的两种实现方式

《在C#中调用Python代码的两种实现方式》:本文主要介绍在C#中调用Python代码的两种实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#调用python代码的方式1. 使用 Python.NET2. 使用外部进程调用 Python 脚本总结C#调

Vue中组件之间传值的六种方式(完整版)

《Vue中组件之间传值的六种方式(完整版)》组件是vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用,针对不同的使用场景,如何选择行之有效的通信方式... 目录前言方法一、props/$emit1.父组件向子组件传值2.子组件向父组件传值(通过事件形式)方

如何自定义Nginx JSON日志格式配置

《如何自定义NginxJSON日志格式配置》Nginx作为最流行的Web服务器之一,其灵活的日志配置能力允许我们根据需求定制日志格式,本文将详细介绍如何配置Nginx以JSON格式记录访问日志,这种... 目录前言为什么选择jsON格式日志?配置步骤详解1. 安装Nginx服务2. 自定义JSON日志格式各