前言SwiftUI与Apple之前的UI框架的不同之处不仅在于视图和其他UI组件的定义方式,还在于视图级状态在整个使用它的应用程序中的管理方式。SwiftUI没有使用委托、数据源或UIKit和AppKit等命令式框架中常见的任何其他状态管理模式,而是配备了一些属性包装器[1],这些包装器允许我们准确地声明我们的数据是如何被我们的视图观察访问的,渲染和改变。本周,让我们仔细看看这些属性包装器中的每一个,它们如何相互关联,以及它们如何构成SwiftUI整体状态管理系统的不同部分。属性状态由于SwiftUI主要是一个UI框架(尽管它也开始获得用于定义应用程序和场景等更高级别结构的API),它的声明式设计不一定需要影响应用程序的整个模型和数据层——-而不是直接绑定到我们各种视图的状态。例如,假设我们正在开发一个SignupView,它使用户能够通过输入用户名和电子邮件地址在应用程序中注册一个新帐户。我们将使用这两个值来形成一个User模型并将其传递到一个闭包中:structSignupView:View{varhandler:(User)->Voidvarusername=""varemail=""varbody:someView{...}}由于这三个属性中只有两个-用户名和电子邮件-实际上会被我们的视图修改,并且这两个状态可以保密,我们将使用SwiftUI的State属性包装器对它们进行签名-如下所示:structSignupView:View{varhandler:(User)->Void@Stateprivatevarusername=""@Stateprivatevaremail=""varbody:someView{...}}这样做会自动创建两者之间的连接这两个值和我们的视图本身——意味着每次这两个值改变时我们的视图都会被重新渲染。在我们的主体中,我们将这两个属性中的每一个绑定到相应的TextField以使它们可由用户编辑:structSignupView:View{varhandler:(User)->Void@Stateprivatevarusername=""@Stateprivatevaremail=""varbody:someView{VStack{TextField("用户名",text:$username)TextField("Email",text:$email)Button(action:{self.handler(User(username:self.username,email:self.email))},label:{Text("Signup")})}.padding()}}因此,State用于表示SwiftUI视图的内部状态,并自动使视图当状态改变时更新。因此,最常见的做法是将State属性包装器保持私有,以确保它们仅在该视图的主体内更改(尝试在其他地方更改它们实际上会导致运行时崩溃)。双向绑定查看上面的代码示例,我们将每个属性传递到其TextField的方式是在这些属性名称前加上$。这是因为我们不只是将纯字符串值传递到这些文本字段中,而是与我们的State包装属性本身绑定。为了更详细地探讨这意味着什么,现在假设我们要创建一个视图,让我们的用户编辑他们最初在注册时输入的个人资料信息。由于我们现在正在修改外部状态值,而不仅仅是私有状态值,所以这次我们将用户名和电子邮件属性标记为Bingding:structProfileEditingView:View{@Bindingvarusername:String@Bindingvaremail:Stringvarbody:someView{VStack{TextField("Username",text:$username)TextField("Email",text:$email)}.padding()}}很酷的是绑定不限于单个内置值,像字符串或整数,但可用于将任何Swift值绑定到我们的视图之一。例如,我们可以将用户模型本身传递给ProfileEditingView,而不是传递两个单独的用户名和电子邮件:$user.username)TextField("Email",text:$user.email)}.padding()}}就像我们在传递State和State和Binding包裹的属性到各种TextFields实例中都以$为前缀,当将任何状态值连接到我们自己定义的绑定属性时,我们可以做同样的事情。例如,这是ProfileView的一个实现,它使用Stage包装器属性来跟踪用户模型,然后在将上述ProfileEditingView实例呈现为工作表时向该模型传递一个绑定——这将自动同步用户对原始状态属性值:structProfileView:View{@Stateprivatevaruser=User.load()@StateprivatevarisEditingViewShown=falsevarbody:someView{VStack(alignment:.leading,spacing:10){Text("用户名:").foregroundColor(.secondary)+Text(user.username)Text("Email:").foregroundColor(.secondary)+Text(user.email)Button(action:{self.isEditingViewShown=true},label:{Text("Edit")})}.padding().sheet(isPresented:$isEditingViewShown){VStack{ProfileEditingView(user:self.$user)Button(action:{self.isEditingViewShown=false},label:{文本(“完毕”)})请注意,我们还可以通过为其分配新值来更改Statewrapped属性-例如,我们在“完成”按钮的操作处理程序中将isEditingViewShown设置为false。因此,BindingTagged属性在给定视图和视图外部定义的状态属性之间提供双向连接,而Statr和Bindingwrapped属性可以通过在其属性名称前加上$materialdelivery来用作绑定。ObservablesState和Binding的共同点是它们处理在SwiftUI视图层次结构本身中管理的值。然而,虽然构建一个在其各种视图中保留其所有状态的应用程序当然是可能的,但从架构和关注点分离的角度来看,这通常不是一个好主意,并且很容易导致我们的视图变得非常庞大和复杂。值得庆幸的是,SwiftUI还提供了允许我们将外部模型对象连接到各种视图的机制。其中一种机制是ObservableObject协议,当它与ObservedObject属性包装器结合使用时,我们可以设置绑定到在我们的视图层之外管理的引用类型。例如,让我们更新上面定义的ProfileView-通过将管理User模型的责任从视图本身转移到一个新的专用对象中。现在,我们可以用许多不同的方式描述这样一个对象,但由于我们正在寻找一种类型来控制我们模型之一的实例-让我们让它成为一个符合SwiftUI的ObservableObject协议[2]:class的模型控制器UserModelController:ObservableObject{@Publishedvaruser:User...}Published属性包装器用于定义对象的哪些属性在修改时应该触发可观察通知。有了上述类型,现在让我们回到ProfileView并让它将新UserModelController的实例作为ObservedObject来观察,而不是使用State属性包装器跟踪我们的UserModel。最重要的是,我们仍然可以像以前一样轻松地将此模型绑定到我们的ProfileEditingView,因为ObservedObject属性包装器也可以转换为绑定:structProfileView:View{@ObservedObjectvaruserController:UserModelController@StateprivatevarisEditingViewShown=falsevarbody:someView{VStack(alignment:.leading,spacing:10){Text("Username:").foregroundColor(.secondary)+Text(userController.user.username)Text("Email:").foregroundColor(.secondary)+Text(userController.user.email)Button(action:{self.isEditingViewShown=true},label:{Text("Edit")})}.padding().sheet(isPresented:$isEditingViewShown){VStack{ProfileEditingView(user:self.$userController.user)Button(action:{self.isEditingViewShown=false},label:{Text("完成")})}}}}但是,我们的新实现与之前使用的基于状态的实现之间的一个重要区别是,我们的UserModelController现在需要作为初始化程序的一部分注入到ProfileView中,除了“强制”我们超越在我们的代码库中建立更明确的依赖关系图的原因是标记为ObservedObject的属性并不暗示对该属性指向的对象的任何形式的所有权。因此,虽然以下内容在技术上可能会编译,但最终会导致问题在运行时——因为当我们的视图在更新时被重新创建时,UserModelController实例可能会被删除(因为我们的视图现在是它的主要所有者):structProfileView:View{@ObservedObjectvaruserController=UserModelController.load()...}这很重要请记住:SwiftUI视图不是对正在屏幕上呈现的实际UI组件的引用,而是描述我们UI的轻量级值——因此它们没有生命周期e喜欢UIView实例。为了解决上述问题,Apple在iOS14和macOSBigSur中引入了一个名为StateObject的新属性包装器。标记为StateObject的属性的行为与ObservedObject完全相同——此外,SwiftUI将确保存储在此类属性中的任何对象不会意外释放,因为框架会在重新渲染视图时重新创建新实例:structProfileView:View{@StateObjectvaruserController=UserModelController.load()...}尽管从技术上讲从现在开始只使用StateObject是可行的——我仍然建议在观察外部对象时使用ObservedObject,并且只在处理对象拥有的对象时使用StateObject看自己。将StateObject与ObservedObject结合将其视为State和Binding的引用类型,或强属性和弱属性的SwiftUI版本。观察和修改环境变量最后,让我们看看如何使用SwiftUI的环境系统在两个没有直接连接的视图之间传递各种状态。虽然在父视图与其子视图之一之间创建绑定通常很容易,但在整个视图层次结构中传递对象或值可能相当麻烦——这就是环境变量旨在解决的问题类型。使用SwiftUI环境的主要方式有两种。一种是首先在要检索给定对象的视图中定义一个EnvironmentObjectwrapped属性——例如,ArticleView如何检索包含颜色信息的Theme对象:structArticleView:View{@EnvironmentObjectvartheme:Themevararticle:Articlevarbody:someView{VStack(alignment:.leading){Text(article.title).foregroundColor(theme.titleTextColor)Text(article.body).foregroundColor(theme.bodyTextColor)}}}然后我们必须确保在我们视图的父类之一中提供我们的环境对象(在本例中为Theme实例),SwiftUI将负责其余的工作。这是通过使用environmentalObject修饰符完成的,例如,像这样:)}}请注意,我们不需要将上述修饰符应用于将使用我们的环境对象的确切视图-我们可以将它应用于我们层次结构中位于它之上的任何视图。使用SwiftUI环境系统的第二种方法是定义一个自定义的EnvironmentKey——然后可以使用它从内置的EnvironmentValues类型中分配和检索值:structThemeEnvironmentKey:EnvironmentKey{staticvardefaultValue=Theme.default}extensionEnvironmentValues{vartheme:Theme{get{self[ThemeEnvironmentKey.self]}set{self[ThemeEnvironmentKey.self]=newValue}}}有了上面的内容,我们现在可以使用环境属性包装器(而不是EnvironmentObject)标记我们视图的主题属性,并传入我们希望检索的环境键的键值路径:structArticleView:View{@Environment(\.theme)vartheme:Themevararticle:Articlevarbody:someView{VStack(alignment:.leading){Text(article.title).foregroundColor(theme.titleTextColor)Text(article.body).foregroundColor(theme.bodyTextColor)}}}上述两种方法之间的一个明显区别是基于密钥的方法要求我们默认值是在编译时定义的,而基于EnvironmentObject的方法假定这样的值是在运行时提供的(否则会导致崩溃)。总结SwiftUI管理状态的方式绝对是该框架最有趣的方面之一,它可能需要我们稍微重新考虑数据在我们的应用程序中传递的方式——至少在涉及将直接使用和修改的数据时通过我们的用户界面。所以。我希望本指南是一个很好的方式来概述SwiftUI的各种状态处理机制,虽然省略了一些更具体的API,但本文中强调的概念应该涵盖所有基于SwiftUI的状态处理。大多数用例。谢谢阅读!参考资料[1]属性包装器:https://www.swiftbysundell.com/articles/property-wrappers-in-swift[2]模型控制器:https://www.swiftbysundell.com/articles/model-controllers-in-迅速/
