大师学SwiftUI第6章 – 声明式用户界面

Coding Alan 4个月前 (12-18) 4912次浏览 0个评论 扫描二维码

其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记

状态

在上一章,我们介绍了SwiftUI的主要特性,声明式语法。借助SwiftUI,我们可以按希望在屏幕上显示的方式声明视图,余下交由系统来创建所需的代码。但声明式语法不只用于组织视图,还可在应用状态发生变化时更新视图。例如,我们可以有下面图6-1中的界面,显示标题的Text视图,用户输入新标题的输入字段以及将旧标题替换成新标题的按钮。原标题的Text视图表示我们界面的初始状态。用户在输入框中输入每个字符时状态都会发生更新(图6-1左图),点击按钮时,界面进入一个新状态,用户插入的标题会替换原标题,文本的颜色也发生变化(图6-1右图)。

大师学SwiftUI第6章 - 声明式用户界面

图6-1:用户界面

每次状态发生改变时,必须更新视图来进行反馈。在之前的系统中,这要求代码保持数据及界面同步,但在声明式语法中我们只需要声明每个状态的视图配置,系统会负责生成在屏幕上显示这些改变所需的代码。

界面可能经历的状态由存储在应用中的信息决定。例如,用户在输入框架中插入的字符以及示例中使用的颜色都是存储在应用中的值。每当这些值发生改变时,应用都会进入新状态,因此界面会发生更新进行反馈。建立应用数据与界面之间的依赖需要大量的代码,但SwiftUI通过属性包装器让其保持简单。

@State

在第3章中讨论过,属性包装器让我们可以定义用赋给它们的值定义可执行任务的属性。SwiftUI实现了大量的属性包装器来存储值并向视图上报修改。设计用于存储单个视图状态的名为@State。这个属性包装器将值存储在类型为State的结构体中,并在值发生改变时通知系统,这样视图会自动更新来在屏幕中进行反映。

属性包装器@State是设计用于存储单个视图的状态的。因此,我们应将这个类型的属性声明为视图结构体的一部分,并使用private,这样访问就可以限定在所声明的结构体内了。

示例6-1:定义一个状态

示例6-1中的代码声明了一个String类型名为title@State属性。该属性使用"Default Title"值进行初始化。在视图内容中,我们在垂直堆叠中以Text视图显示这个值,并在其下放了一个Button视图来修改其值。稍后我们会学习Button视图,但现在读者只需要知道Button视图显示一个标签并在用户点击按钮时操作一个操作。为展示标签,我们使用带"Change Title"文本的Text视图来让用户知道按钮的作用,并定义好操作,我们提供一个闭包修改title属性的值为"My New Title",这样在点击按钮时标题就会发生修改。

使用@State包装器创建的title属性在两个地方用到了,第一个是向用户显示当前值的Text视图,第二是Button视图中修改其值的操作。因此,每交点击按钮时,title属性的值会发生改变化,@State属性包装器通知系统应用的状态发生的变化,body属性的内容自动刷新在屏幕上显示新值。

大师学SwiftUI第6章 - 声明式用户界面

图6-2:初始状态(左)和点击按钮后的状态(右)

✍️跟我一起做:创建一个多平台项目。使用示例6-1中的代码更新ContentView视图。确保对画布启用了实时预览(图5-18,1号图)。点击Change Title按钮将字符串赋值给Text视图。会看到像图6-2右图中的效果。

整个过程是自动完成的。我们不用对Text视图赋新值或是告诉该视图新的值,这一切都由@State属性包装器处理。我们可以包含多个存储界面状态的@State属性。例如,下例中我们对视图添加了一个Bool类型的@State属性,在每次点击按钮时为title属性赋不同的文本。

示例6-2:定义多个状态

isValid属性存储表示当前校验状态的布尔值,这样可以在屏幕上显示相应的文本。赋值给title属性的字符串通过三元表达式来进行选取。使用三元运算符来设置视图是一种推荐的实践,因为它让系统可以获取视图能响应的所有可能的状态,并在状态间产生平滑的过渡。如isValid的值为true,将单词”Valid”赋值给title属性,否则赋值”Invalid”。每次点击按钮时,isValid属性的值都会发变化,屏幕上会显示不同的文本(参见示例3-55了解更多有关toggle()方法的信息)。

注意:示例6-2中有两种状态,同时发生改变,但系统会接管这一情况,保障界面仅在需要时发生更新。

@State属性创建自身和视图之间的依赖,因此在每次值发生改变时视图更新。说法是视图与属性发生了绑定。到目前为止我们使用的都是单向绑定。属性发生修改时视图更新。但也存在视图中值被用户修改,必须要在没有代码介入的情况下将值存回属性的情况。为此,SwiftUI支持我们定义双向绑定。双向绑定声明的方式是在属性名前添加$符号。

需要双向绑定的视图通常是控制视图,比如创建用户可打开和关闭的开关,或可插入文本的输入框。下例实现了一个TextField视图来演示这一功能。TextField视图创建一个输入框。其初始化方法需要的值是字符串及占位符文本,我们用绑定属性来存储用户插入的值。(稍后我们会学习TextField视图及其它控制视图。)

示例6-3:定义双向绑定

本例中,我们向视图添加了一个存储用户插入文本的@State属性,然后在标题和按钮之间定义了一个TextField视图。TextField视图使用占位文本”Insert Title”进行初始化,将新的titleInput属性喂给视图的绑定属性($titleInput)。这在TextField视图和属性之间创建了一个永久的连接,每当用户在输入框中输入或删除字符时,新值都会赋值给该属性。

Button视图的操作中,我们做了两个修改。首先将titleInput属性的值赋值给title属性。这样就会用户插入的文本更新视图标题。然后将空字符会串赋值给titleInput属性,来清除输入框以便用户再次输入。

✍️跟我一起做:使用示例6-3中的代码更新ContentView视图。点击输入框,输入文本。按下Change Title按钮。标题就会修改为该段文本。

@Binding

@State属性属于声明它的结构体,应仅在结构体内部的代码访问(因此我们将其声明为private),但在第5章中我们学到,在视图急剧增长时,建议将它们分别整合到独立的结构体中。这样整理视图的问题是其它结构体就无法再引用这些@State属性了,也就无法再读取或修改它们的值了。在其它视图中定义新的@State属性也不是解决方案,因为这创建的是新状态。我们需要的是建立一个视图中@State属性与其它视图中代码的双向绑定。为此,SwiftUI内置了Binding结构体和@Binding属性包装器。

以下示例和前例相同,但这次我们将TextTextField视图放到一个单独的HeaderView中。这个视图中包含两个@Binding属性,用于访问ContentView视图中的@State属性,这样处理就是同样的状态了。

示例6-4:使用@Binding属性

@Binding属性总是会从@State属性中接收值,因此不用为其赋默认值,但建立的连接是双向的,因此要记住在@State属性的前面添加$符号来与@Binding属性进行连接(HeaderView(title: title, titleInput: $titleInput))。因@Binding属性和@State属性之间是双向绑定,用户输入的值存储在同一个地方,每当系统识别到按钮点击变化时,HeaderView结构体的body属性会再次进行处理,新的值就会显示到屏幕上了。

✍️跟我一起做:使用示例6-4中的代码更新ContentView视图。记住保留底部的#Preview宏以便在画布中进行预览。在输入框中插入值,点击Change Title按钮。效果和之前相同。

绑定结构体

前面讨论过,属性包装器以结构体进行定义,因此包含自己的属性。SwiftUI允许通过在属性名前加下划线(如_title)来访问属性的底层结构体。访问到结构体后,就可以处理其属性了。定义@State属性包装器的结构体为State。这是一个泛型结构体,可以处理任意类型的值。以下是该结构体定义用于存储状态值的属性。

  • wrappedValue:此属性返回由@State属性管理的值。
  • projectedValue:此属性返回Binding类型的结构体,创建与视图间的双向绑定。

wrappedValue属性存储赋给@State属性的值,就像是上例中赋值给title属性的”Default Title”字符串。projectedValue属性存储Binding类型的结构体,创建将值存回到属性的双向绑定。如果直接读取@State属性(如title),返回的值存储在wrappedValue属性中,如果在属性名前加上$符号(如$title),我们访问的是存储在projectedValue属性中的Binding结构体。这是SwiftUI推荐的使用@State属性的方式,但在理论上我们也可以直接访问这些属性,如下例所示。

示例6-5:访问State结构体的属性

它和前面的示例效果一样,但没有使用SwiftUI的快捷方式,而是直接读取了State结构体中的wrappedValueprojectedValue属性。当前不必这么做,但有时可用于克服SwiftUI自身的缺陷。比如,SwiftUI不允许我们在赋值给body的闭包外处理@State属性,但我们可以用一个State结构体来替换另一个。为此,可以使用以下由State结构体所提供的初始化方法。

  • State(initialValue: Value):该初始化方法使用initialValue所提供的值创建一个State属性。
  • State(wrappedValue: Value):该初始化方法使用wrappedValue所提供的包装值创建一个State属性。

例如,我们希望对前例的输入框赋一个初始值,可以对ContentView结构体添加一个初始化方法,并用它对该属性赋一个新的State结构体。

示例6-6:初始化@State属性

✍️跟我一起做:使用示例6-5中的代码更新ContentView视图。在ContentView结构体中添加示例6-6的初始化方法(放在@State属性下面)。这时会看到输入框初始化为了”Hello World”。

注意:这样访问绑定属性的内容仅在没有其它选择时才推荐使用。只要有可能,就应使用SwiftUI所提供的属性包装器或在第5章(示例5-58)中介绍过的onAppear()修饰符中安装始化@State属性,或者是在可观测对象是中存储状态,这个稍后会学到。

我们可以按访问@State属性同样的方式访问@Binding属性。如果只像示例6-5那样读取该属性,返回值就是其中存储的值,如果在名称前面加$,返回值是属性包装器用于建立与视图双向绑定的Binding结构体。但如果在@Binding属性的名称前添加下划线(如_title),返回值就不是State结构体而不是Binding结构体。这是因为@Binding属性包装器在类型为Binding的结构体中定义。当然,结构体中还包含访问这些值的属性。

  • wrappedValue:该属性返回由@Binding属性管理的值。
  • projectedValue:该属性返回创建与视图间双向绑定的类型为Binding的结构体。

State属性一样,我们可以访问及处理Binding结构体中存储的值。比如,下例中又实现了一个单独的视图,和示例6-4一样管理标题和输入框。在初始化了HeaderView之后,我们通过wrappedValue属性获取到Binding结构体中存储的值,对字符串的字符数计数,然后将结果与标题共同显示出来。

示例6-7:访问@Binding属性的值

示例6-7HeaderView中,我们定义了一个属性counter,并使用wrappedValue属性返回的字符串的字符数进行初始化。因为@Binding属性没有初始值,必须使用ContentView视图接收到的值进行初始化(_title = title)。注意HeaderView结构体接收到值是可管理String类型值的Binding结构体,因此参数的类型必须用Binding<String>来声明。

初始化完值之后,我们可以在视图中进行显示。标题现在显示 为用户插入的文本以及字符串的字符数。

大师学SwiftUI第6章 - 声明式用户界面

图6-3:@Binding属性的值定义的标题

✍️跟我一起做:使用示例6-7中的代码更新ContentView.swift文件。插入标题。会看到如图6-3所示的标题及其右侧的字符数。

HeaderView视图的@Binding属性与ContentView视图的@State属性相连接,因此可接收到该属性的值,但有时这种结构体实例是单独创建的,因而需要一个绑定值。要定义这种值,可以自己创建一个Binding结构体。结构体中包含如下初始化方法和类型方法。

  • Binding(get: Closure, set: Closure):这一初始化方法创建一个Binding结构体。get参数是一个会返回当前值的闭包,set参数是一个接收存储或处理新值的闭包。
  • constant(Value):这个类型方法创建一个带不可变值的Binding结构体。该参数是我们希望赋值给该结构体的值。

很多场景下需要用到Binding值。例如,我们希望创建一个HeaderView的预览,必须为titletitleInput属性提供值。以下示例描绘了如何创建一个新的Binding结构体提供这些值,以及如何定义这一视图的预览。

示例6-8:创建一个Binding结构体

Binding结构体包含一个getter和一个setter。getter返回当前值,setter接收赋值给结构体的值。本例中,返回的是字符串,因为没对结构体赋新值,只是在控制台中打印出了这个值。实例赋值给常量constantTitleconstantInput,然后发送给HeaderView结构体,因此在画布该视图有值可以显示。

本例中的Binding结构体没有多大用处,只是提供了Binding结构体所需要的值。在这种场景,可以使用constant()方法来简化代码。这一类型方法使用不可变值创建并返回一个Binding结构体,所以我们不用自己创建结构体。

示例6-9:通过不可变值创建一个Binding结构体

✍️跟我一起做:在ContentView.swift文件中添加示例6-9中的结构体。此时会在画布顶部多出现一个按钮,显示HeaderView视图的预览。

控制视图

控件是交互工具,用户通过交互修改界面状态、选取选项或插入、修改或删除信息。我们实现过其中的一部分,如前例中的Button视图以及TextField视图。要定义一个有用的接口,需要学习有关视图的更多知识以及其它由SwiftUI所提供的控制视图。

按钮视图

我们已经学到,Button视图创建一个简单的控件,在点击时执行操作。以下是该结构体部分初始化方法。

  • Button(String, action: Closure):此初始化方法创建一个Button视图。第一个参数是定义按钮标签的字符串,action参数是在点击按钮时执行的代码的闭包。
  • Button(action: Closure, label: Closure):此初始化方法创建一个Button视图。action参数是在点击按钮时执行的代码的闭包,label参数是返回用于创建标签的视图的闭包。
  • Button(String, role: ButtonRole?, action: Closure):此初始化方法创建一个Button视图。第一个参数是定义按钮标签的字符串。role参数一个结构体,包含描述按钮目的的类型属性。有两个属性:canceldestructiveaction参数是在点击按钮时执行的代码的闭包。

我们已经实现过第二个初始化方法创建按钮,但如果仅需对标签使用字符中,可以简化代码使用第一个初始方法加后置的用于操作的闭包。

示例6-10:实现Button视图

上例在VStack中包仿一个Text视图和一个Button视图。Text视图展示固定的文本,背景色用colorActive属性定义。如果属性值是true,我们将green色赋值给背景,否则颜色为clear(透明)。在按下按钮时,会切换这一属性的值,再次运算body属性的值,文本的背景修改为下一个颜色。

大师学SwiftUI第6章 - 声明式用户界面

图6-4:按钮视图

✍️跟我一起做:创建一个多平台项目。使用示例6-10的代码更新ContentView视图。点击Change Color按钮。会看到文本背景色的变化(参见图6-4右图)。

如果希望将视图与控件执行的操作进行分离,可以将相关语句移到函数中。例如,可以上在ContentView结构体中添加一个函数,用于切换colorActive属性的值,然后在按钮的操作中调用这个函数。应用的功能的相同,但代码更有条理。

示例6-11:使用函数来组织代码

如果按钮唯一的操作就是调用方法,可以简化视图的定义为声明action参数并指定所要执行操作的方法名。如下所示。

示例6-12:引用方法

声明方法名称带括号会马上执行方法,但仅声明名称会提供一个方法的引用供系统稍后执行。

✍️跟我一起做:使用示例6-11中的代码更新ContentView视图。应用功能和之前相同。使用示例6-12中的Button视图更新Button视图。点击按钮确定所执行的操作。

上例中,我们使用了三元运算符来根据colorActive属性的值选取background()修饰符的值。这是推荐的做法,这样SwiftUI可以识别视图并有效管理状态的转换,但我们也可以使用if else语句来响应修改。例如,有时会用按钮这类控件在界面中显示或隐藏视图。

示例6-13:在界面中添加及删除视图

本例中的按钮切换@State属性showInfo的值。在按钮下方,可查看到该属性的当前值。若其值为true,显示 Text视图,否则什么也不显示。因此,在按下按钮时,showInfo的值发生改变,body属性的内容会重新绘制,Text视图根据showInfo的当前值出现或消失。

大师学SwiftUI第6章 - 声明式用户界面

图6-5:动态界面

if else语句可用于选择是否执行按钮的操作,但SwiftUI提供了如下修饰符来在要做禁用操作时禁用按钮。

  • disabled(Bool):这一修饰符决定该控件是否响应用户的交互。

下例使用了该修饰符在点击后禁用按钮,因此用户只能执行一次操作。

示例6-14:禁用按钮

这个视图包含两个@State属性,一个用于追踪颜色,另一个表示按钮是否处于禁用状态。在点击按钮时,操作中将true赋值给buttonDisabled属性,按钮就停止运作了,这样用户只能点一次。

大师学SwiftUI第6章 - 声明式用户界面

图6-6:按钮禁用

和之前一样,Button视图的初始化方法可以包含一个label参数来定义所需视图的标签。这个参数非常灵活,可以包含像Text视图和Image视图的视图。按钮中的图片以原始渲染模式显示 ,也就是说以原始颜色显示,但还有一种模式可以创建带图片的蒙版,以应用的着重色或赋值给控件的前景色显示。为选取渲染模式,Image视图包含如下修饰符。

  • renderingMode(TemplateRenderingMode):这个修饰符对Image视图定义了渲染械。参数是包含originaltemplate值的枚举。

下例定义了一个带图片和文本的按钮。将renderingMode()修饰符应用于Image视图来以模板显示图片。

示例6-15:定义带图按钮的标签

示例6-15中的视图包含一个@State属性expanded,用于控制Text视图的宽度。如该属性的值为true,我们使用infinity值让宽度为最宽,否则,宽度为150点。每当用户点击按钮时,expanded属性的值通过toggle()方法进行切换,Text视图的宽度随之发生变化。

大师学SwiftUI第6章 - 声明式用户界面

图6-7:带模板图片的按钮

✍️跟我一起做:下载expand.png和contract.png并添加至资源目录。使用示例6-15中的代码更新ContentView视图,点击Expand按钮。此时会看到图6-7中的界面。删除renderingMode()修饰符。我们应当会看到原色图。

可以通过如下修饰符对按钮赋标准样式。

  • buttonStyle(ButtonStyle):此修饰符定义了按钮的样式。参数是遵循ButtonStyle协议的一个结构体。
  • controlSize(ControlSize):此修饰符定义了按钮的样式。参数是一个枚举,值有largeminiregularsmall

SwiftUI框架自带有PrimitiveButtonStyle协议提供标准样式。为此,该协议定义了类型属性automaticborderedborderedProminentborderlessplain。这些样式满足不同目的。例如,bordered样式创建一个灰色背景的按钮,表示二级操作,borderedProminent样式创建一个应用着重色的按钮,表示主操作,比如用于保存或提交数据。例如以下视图包含两个按钮,一个取消处理,另一个将信息发送给服务端。

示例6-16:按钮样式

突出按钮应仅用于表示主操作。本例中,Cancel按钮加了边框,告诉用户这是一个二级操作,重要级为次级,但Send按钮为突出的,表示在点击该按钮时执行重要操作。

大师学SwiftUI第6章 - 声明式用户界面

图6-8:标准样式按钮

按钮用于取消处理(如上例)或删除某一项时,我们可以通过Button的初始化方法为其赋一个特定的角色。这样系统可以根据角色在应用运行的设备上对按钮添加样式。例如,在移动设备上,destructive角色的按钮以红色显示。

示例6-17:赋予角色

大师学SwiftUI第6章 - 声明式用户界面

图6-9:销毁按钮

✍️跟我一起做:使用示例6-16的代码更新ContentView视图。会看到如图6-8中所示的按钮。将Cancel按钮替换为示例6-17中的Button视图。会看到如图6-9中所示的删除按钮。

这些样式对SF图标进行了美化。SF图标替换普通图片的优势是它们会按对按钮添加的字体大小进行缩放。这配合对按钮自身进行缩放的controlSize()修饰符,使得我们可以创建不同大小的按钮。

示例6-18:缩放按钮

本例中,我们应用了imageScale()修饰符来缩放SF图标,font()修饰符对按钮添加了大字体,controlSize()修饰符对按钮进行缩放。结果如下。

大师学SwiftUI第6章 - 声明式用户界面

图6-10:自定义大小的按钮

如果希望定义一个样式与系统自带的进行区分,则需要创建自己的ButtonStyle结构体。该协议只要求实现如下方法。

  • makeBody(configuration: Configuration):该方法定义并返回一个替换按钮体的视图。configuration参数为包含按钮信息的Configuration类型的值。

该方法接收一个Configuration类型的值,是ButtonStyleConfiguration的类型别名,包含返回按钮相关信息的属性。以下是其中的属性。

  • isPressed:该属性返回表示按钮是否按下的布尔值。
  • label:该属性返回定义按钮当前标签的一个或多个视图。

以下示例定义在点击时会放大的示例。样式包含一个内边距和绿色边框。要应用这些样式,必须创建一个符合ButtonStyle协议的结构体,即实现makeBody()方法并通过该方法返回希望赋值给按钮体的视图。

组成按钮体的视图由Configuration结构体的label属性提供,因此我们可以读取并修改这一属性值来应用新的样式,如下所示。

示例6-19:为按钮添加自定义样式

示例6-19中,我们定义了一个结构体MyStyle并实现了所要求的makeBody()方法。该方法通过类型属性获取到了按钮的当前配置,进而修改并返回标签。首先,我们读取isPressed属性的值来了解按钮是否被按下,然后对label属性应用新的样式。这个属性返回创建按钮当前标签的一个视图拷贝,然后通过修改其值我们也就修改了标签。本例中,我们应用了一个内边距、一个边框,然后根据isPressed属性的值赋了一个缩放比例。如果该值为true,也就是按钮被按下了,我们将比例设为1.2进行放大,但在值为false时,比例又回到了1。

在这一视图中,我们创建了该结构体的实例并通过buttonStyle()修饰符将其值赋值给Button视图。如果如下所示。

大师学SwiftUI第6章 - 声明式用户界面

图6-11:带自定义样式的按钮

✍️跟我一起做:使用示例6-19中的代码更新ContentView.swift文件。点击按钮。会看到按钮如图6-11右图那样放大了。SwiftUI自动对按钮添加了动画。我们会在第11章中学习自定义动画以及如何创建。

文本框视图

TextField又是一个我们之前介绍过的控件。该视图创建一个输入框,用户可进行交互并插入值(单行文本)。以下是结构体中所包含的一个初始化方法。

  • TextField(String, text: Binding, axis: Axis):此初始化方法创建一个输入框。第一个参数定义该字段的占位符,text参数是用于存储由用户所插入值的绑定属性,axis参数定义在文本超出视图边界时沿哪条轴进行滚动。这是一个枚举,值有horizontalvertical

框架为TextField视图定义了几个修饰符。以下是最常用的一些。

  • textFieldStyle(TextFieldStyle):此修饰符定义文本框的样式。参数是一个符合TextFieldStyle协议的结构体。框架自带了几个提供标准样式的结构体。这些结构体定义了类型属性automaticplainroundedBordersquareBorder
  • autocorrectionDisabled(Bool):此修饰符启用或禁用系统的自动修正特性。默认,该值为true(禁用状态)。
  • textInputAutocapitalization(TextInputAutocapitalization?):此修饰符定义用于格式化文本的大写样式。该参数是一个结构体,包含类型属性charactersnever, sentences (默认值)words
  • keyboardType(UIKeyboardType):此修饰符定义待定输入框后系统打开的键盘类型。其参数是一个枚举,值有defaultasciiCapablenumbersAndPunctuationURLnumberPadphonePadnamePhonePademailAddressdecimalPadtwitterwebSearchasciiCapableNumberPadalphabet

我们已经学习如何包含一个简单的TextField视图来获取用户的输入,但只使用了少数几个修饰符。下例展示了如何对视图添加样式让单词变成大写。

示例6-20:配置文本框

示例6-20中对TextField视图应用的样式为roundedBorder。它为输入框添加一个边框,让视图占据的区域变得可见,如下所示。

大师学SwiftUI第6章 - 声明式用户界面

图6-12:带圆角边框的文本框

✍️跟我一起做:使用示例6-20中的代码更新ContentView视图。在输入框中插入文本并按下Save按钮。会看到如图6-12所示的效果。

除了按钮,用户通过期望能够通过点击键盘上的Done按钮保存数据。为此框架提供了如下的修饰符。

  • onSubmit(of: SubmitTriggers, Closure):在发生触发条件(比如在键盘上按下Done/Return按钮时)时该修饰符执行一个操作。of参数是指定修饰符所响应的触发条件类型的结构体。结构体中包含searchtext(默认)属性。第二个参数是希望执行的闭包。
  • submitLabel(SubmitLabel):该修饰符指定虚拟键盘中Done按钮所使用的标签。参数结构体包含的类型属性有continuedonegojoinnextreturnroutesearchsend
  • submitScope(Bool):该修饰符指定在发生触发条件时是否提交视图。

赋值给onSubmit()修饰符的闭包在聚焦于视图(例如用户编辑输入框)时执行。如果应用于TextField视图,可省略of参数,如下例如下。

示例6-21:响应Done按钮

示例6-21中的代码实现了submitLabel()修饰符来修改Done按钮的标题为Continue,然后向结构体添加一个名为assignTitle()的方法,执行和之前同样的操作。该方法在两处有调用,赋值给onSubmit()修饰符的闭包和Button视图操作,因此在按下界面的按钮或点击键盘上的Done/Return按钮时执行该操作。不管用户决定执行什么操作,插入文本框的值总是存储于title属性中。

✍️跟我一起做:使用示例6-21中的代码更新ContentView结构体,并在iPhone模拟器上运行应用。点击输入框,插入文本并在键盘上点击Continue按钮。(若要在模拟器上启用虚拟键盘,打开I/O菜单,点击Keyword,选择Toggle Software Keyboard选项。)文本会像此前一样赋值给标题。

在视图可接收输入或处理用户选定的反馈时,我们就说视图聚焦了。SwiftUI包含了一些处理这种状态的工具。可以在视图获得焦点时处理某一任务、知道视图是否获得焦点或是从视图移除焦点。为此有两个属性包装器:@FocusState@FocusedBinding@FocusState存储表明焦点当前存储在哪里的值,@FocusedBinding用于将状态传递给其它视图。为管理状态,框架内置了如下 修饰符。

  • focused(Binding, equals: Hashable):此修饰符将视图当前状态存储于绑定属性中。第一个参数是对@FocusState属性的引用,equals参数是用于标识视图的可哈希值。
  • focusable(Bool):此标识符表示是否可将焦点放在视图上。

为追踪视图的状态,我们需要一个可哈希数据类型的@FocusState属性,提供用于标识视图的值。下例中,属性通过枚举值进行创建。定义了两个值namesurname,用于追踪两个输入框的聚焦状态,并在用户输入时修改背景色。

示例6-22:响应焦点中的变化

@FocusState属性的初始值是nil,表示未聚焦于任何视图。在用户点击文本框时,焦点移至该视图,标识视图的值会被赋值给该属性。通过将该值与枚举中的值进行比较,我们就知道是哪个TextField视图于聚焦状态,相应地修改背景色。注意roundedBorder样式对文本框添加了一个边框和白色背景,所以本例中只有边距的背景可见。

大师学SwiftUI第6章 - 声明式用户界面

图6-13:聚焦

在移动设备中,在可处理输入的视图(如TextField视图)获取到焦点时会打开虚拟键盘。只要焦点还在该视图上键盘就保持打开状态。也就是说要关闭键盘,我们必须移除该视图的焦点。在SwiftUI中通过对@FocusState属性的赋值nil来实现,如下所示。

示例6-23:关闭键盘

示例6-22中的Button视图换成了示例6-23中的Button视图。现在,每当点击Save按钮时,会对值进行处理并关闭键盘。

✍️跟我一起做:使用示例6-22中的代码更新ContentView.swift文件并在iPhone模拟器上运行应用。点击输入框。背景会像图6-13那样变成灰色。使用示例6-23中的视图替换原Button视图。再次运行应用。在两个文本框中插入值并点击Save按钮。此时标题会被赋上新值,虚拟键盘关闭。

上例中,我们没有检测用户是否插入了值,但通常应用必须防止用户保存无效值或空值。有几种控制方式。一种是在存储之前就检测值。我们允许用户输入任意值,但仅保存应用所接受的值。

示例6-24:在存储前检测值

本例中,我们首先对nameInputsurnameInput进行修剪去除其首尾的空格(参数第4章字符串一节),然后在将它们赋值给title属性之前检测结果值是否为空。Save按钮仍保持为激活状态,但仅在用户对两个字段都插入值时才执行保存。

✍️跟我一起做:使用示例6-24中的代码更新ContentView视图中的Button视图。此时必须同时对名和姓两个字段插入值才能修改标题。

另一种方式是在用户插入的为非应用预期值时通过disabled()修饰符禁用按钮。

示例6-25:禁用按钮

本例中,我们使用了前面介绍的disabled()修饰符来在用户在两个字段中输入文本前禁用按钮。如果其中一个或两个字段为空,按钮就无法使用。

✍️跟我一起做:使用示例6-25中的代码更新Button视图。只有同时插入名和姓时才能按下Save按钮。

除了可检测属性是否包含有效值,我们还能限定用户在字段中输入的内容。例如,我们可以只接受数字或指定数量的字符。这时,我们需要在每次视图状态发生改变时检测用户插入的值是否有效。框架为此内置了如下的修饰符。

  • onChange(of: State, initial: Bool, Closure):该修饰符在状态发生改变时执行闭包。of参数是存储待检测值的属性,initial参数为指定在视图出现时是否还执行检测的布尔值,最后一个参数是在系统报出值发生改变时执行的闭包。闭包可接收两个值,一个表示属性的老值,另一个表示新值。

该修饰符只能检测一个状态,因此我们应对所有希望进行控制的视图应用该修饰符。例如,我们可以在示例中对那两个TextField视图使用它来限定允许用户输入的字符数。如果超出,会移除掉多余的字符将结果赋回属性,如下所示。

示例6-26:控制用户的输入

示例6-26的代码中,我们检测存储文本框状态的属性的变化。在用户输入或删除字符时,相应的属性值发生改变,执行赋值给onChange()修饰符的闭包。闭包接收属性的值。使用该值,我们检测用户插入的文本是否有效并进行相应的响应。在示例中,我们计算字符串中的字符数,如果值超出上限,我们使用prefix()方法从文本的开头进行截取,并将结果赋回给属性,这会更新视图并删除文本框中多余的字符。结果 是在字符数超出上限时,用户就无法输入更多的字符了。

✍️跟我一起做:使用示例6-26中的代码更新项目中的TextField视图。在iPhone模拟器中运行应用。插入名和姓。在名超过10个字符、姓超过15个字符时都无法再添加更多的字符。

当然,我们可以指定字符数外的其它条件。下例创建了一个仅接收整数数字的小应用。

示例6-27:仅接收整数数字

和之前一样,视图中包含一个带有onChange()修饰符的TextField。不同之处于在于如何对输入有进行有效性检测。本例中,我们需要确保文本框不为空,然后查看是否可以将其转化为整数,这表示用户只输入了数字。如果不能,就将闭包接收到的旧值赋值给numberInput属性,文本框回复到之前的状态。

注意我们还实现了keyboardType()修饰符来显示适配我们预期用户输入内容(本例为数字)的键盘。

✍️跟我一起做:使用示例6-27中的代码更新ContentView.swift文件。在iPhone模拟器上运行应用。此时只能输入数字。

默认,TextField视图只显示一行文本,但我们可以使用lineLimit()修饰符来允许视图进行扩展来包含更多的文本。(此前展开Text视图实现的同一个修饰符)。除了应用修饰符来设置我们所需的行数,我们还要告诉视图在纵轴上滚动内容,如下所示。

示例6-28:定义多行文本框

本例中,TextView视图会进行扩展,直至到5行的高度时,然后会在垂直方向上滚动来允许用户持续输入。如若要对视图设置最小和最大尺寸,可以使用区间来声明修饰符,如lineLimit(3...5)

大师学SwiftUI第6章 - 声明式用户界面

图6-14:多行文本框

安全域视图

SwiftUI还内置了创建安全文本框的视图。这一视图会把用户输入的字符替换成点以及隐藏敏感信息,比如密码。

  • SecureField(String, text: Binding):该初始化方法创建一个安全输入框。第一个参数定义占位文本,text参数为存储用户插入值的绑定属性。

实现方式与TextField视图相同,我们也可以应用相同的修饰符,如下所示。

示例6-29:使用安全文本框

SecureField视图和TextField视图外观一样。唯一的差别是其中的字符是隐藏的。

大师学SwiftUI第6章 - 声明式用户界面

图6-15:安全文本框

✍️跟我一起做:创建一个多平台项目。使用示例6-29中的代码更新ContentView视图。在输入框中插入字符。会看到字符被替换成为小黑点,如图6-15所示。

文本编辑器视图

SwiftUI还自带一个称为TextEditor的视图让用户可以插入多行文本。以下是该视图的初始化方法。

  • TextEditor(text: Binding):此初始化方法创建一个文本编辑器。text参数是存储用户所插入文本的绑定属性。

该视图可以接收前面用于格式化文本的TextFieldText视图的一些修饰符。例如,我们可以设置视图中文本的对齐、行间距以及是否做错误检查。

示例6-30:实现一个文本编辑器

大师学SwiftUI第6章 - 声明式用户界面

图6-16:文本编辑器

开关视图

Toggle视图创建一个在两种状态间切换的控件。默认在移动设备上显示为对用户友好的开关,在Mac上显示为复选框。该视图包含如下初始化方法。

  • Toggle(String, isOn: Binding):该初始化方法创建一个Toggle视图。第一个参数定义标签,isOn参数为存储当前状态的绑定属性。本视图还自带有一个由闭包返回视图的标签(Toggle(isOn: Binding, label: Closure))。

该视图要求绑定属性存储当前值。在下例中,我们提供了一个@State属性并使用属性值来选取适当的标签。

示例6-31:实现一个开关

示例6-31中的代码使用三元运算符来检测currentState属性的值并显示相应的文本(On或Off)。默认我们设置属性值为true,因此开关处于打开状态并在屏幕上显示On标签,但如果点击开关,就会关闭,视图更新为显示Off标签。

大师学SwiftUI第6章 - 声明式用户界面

图6-17:打开和关闭开关

✍️跟我一起做:创建一个多平台项目。使用示例6-31中的代码更新ContentView视图。点击打开或关闭开关。使用这个项目测试下面的例子。

赋值给label参数的闭包可以包含另外一个定义副标题的视图,如下例所示。

示例6-32:添加副标题

大师学SwiftUI第6章 - 声明式用户界面

图6-18:带标题和副标题的开关

Toggle视图创建了一个包含标题和控件中间为弹性空间的横向布局,结果就是整个视图占满容器的横向空间,标签和控件位于两端。如果希望对视图的位置和尺寸做精确控制,可以应用此前介绍过的fixedSize修饰符来降低视图的尺寸,或使用如下修饰符隐藏标签。

  • labelsHidden():此修饰符隐藏赋值给控件的标签。

这一修饰符适用于多款控件,但对开关尤为有用。下例展示了如何实现它来为控件定义一个自定义标签。

示例6-33:为Toggle视图定义一个自定义标签

视图现在为控件的大小并在屏幕中心显示。标签不再显示 ,因此我们将其声明为空字符串,但在当前值的侧边包含一个Text视图。

大师学SwiftUI第6章 - 声明式用户界面

图6-19:自定义大小以及开关的标签

类似Button视图,Toggle视图也实现了修饰符用于定义控件的样式。

  • toggleStyle(ToggleStyle):这一修饰符定义开关的样式。参数是一个遵循ToggleStyle协议的结构体。为创建标准的结构体,框架包含了automaticbuttoncheckboxswitch这些属性。

默认值为automatic,表示控件的样式由系统选择。如果希望保持同一种样式,可以赋值switchcheckbox(仅能用于Mac)。这些值用于指定标准样式,但框架还内置了button值来创建完全不同类型的控件。在将这一样式赋值给视图时,系统显示一个开关按钮来表示开和关的状态。在按钮处于开的状态时,高亮显示,否则显示 为标准按钮。

示例6-34:实现一个开关按钮

大师学SwiftUI第6章 - 声明式用户界面

图6-20:切换按钮为开关状态

框架提供的样式是有限的,但我们可以自行创建。只需要定义一个遵循ToggleStyle协议的结构体。该协议要求结构体实现如下方法。

  • makeBody(configuration: Configuration):该方法定义并返回一个替换开关主体的视图。configuration参数是一个Configuration类型的值,包含控件相关信息。

这个方法接收一个类型为Configuration的值,是ToggleStyleConfiguration的类型别名,包含如下属性来返回控件相关的信息。

  • isOn:该属性返回一个表示开关处于开或关状态的布尔值。
  • label:该属性返回定义开关标签的视图。

isOn是一个绑定属性,创建与视图的双向绑定,因此我们可以读取并修改其值来激活或停用控件。在下例中,我们创建了一个类似复选框的Toggle视图。点击控件时,图形变换颜色来表示当前的状态(灰色为停用,绿色为激活)。

示例6-35:定义一个自定义Toggle视图

在自定义Toggle视图前必须要考虑几件事。首先,Configuration结构体的label属性包含一个控件当前标签的视图副本,因此如果想要保留这个标签,必须在新的内容中包含这个值。第二,Toggle视图使用HStack视图和标签与控件间的Spacer视图来设计。如果想要保留标准设计,必须保持这种布局。第三,我们负责响应用户的交互以及更新控件的状态,因此必须检测手势并在用户执行手势时通过修改isOn属性的值来变更控件状态。

示例6-35中,我们定义了一个结构体MyStyle并实现了所要求的makeBody()方法来为Toggle视图提供新设计。为保留标准样式,我们使用HStack视图来包装视图并使用Spacer视图来分隔标签与控件。首先我们读取label属性的值来添加当前标签,然后声明Spacer视图,最后声明一个Image视图来展示外观像复选框的SF图标。为将Image视图转换为控件,我们使用font()修饰符定义其大小,应用foregroundColor()修饰符来根据isOn属性的当前值来修改图标的颜色,最后,使用onTapGesture()修饰符监测用户何时点击Image视图。我们会在第12章中学习更多有关手势修饰符的知识。现在,只需要知道这一修饰符在每次用户点击视图时执行一个闭包。在这个闭包中,我们访问isOn属性的绑定值并通过对wrappedValue属性中的布尔值应用toggle()修饰符切换值。(本例中,绑定值的setter是私有的,因此通过wrappedValue属性访问它,本章前面做过讲解。)这会修改该属性的当前值,进而改变控件的状态,将其打开及关闭。

大师学SwiftUI第6章 - 声明式用户界面

图6-21Toggle视图的自定义样式

滑块视图

Slider视图创建一个控件,允许用户选择一个范围内的值。显示为一个带结点的横条,结点对应所选择的值。该结构体包含如下初始化方法。

  • Slider(value: Binding, in: Range, step: Float, onEditingChanged: Closure):这个初始化方法创建一个Slider视图。value参数是希望用于存储当前值的绑定属性,in参数是指定用户选择的最大、最小值范围,step参数表示当前值递增或递减的量,onEditingChanged参数是在用户开始或结束移动滑块时执行的闭包。

要创建滑动,必须至少提供一个@State属性来存储值以及一个决定允许最小和最值的范围。

示例6-36:创建滑块

示例6-36中的代码创建一个0到10的滑块,以Text视图显示当前值。Slider视图接收类型为FloatDouble类型的值,因此允许我们选择浮点值,但我们可以通过声明step参数值为1.0来指定希望用户选择的是整数,如本例所示。(注意我们需要使用第4章中介绍的formatted()方法将Text视图的值格式化为整数。)因为currentValue属性使用数字5进行了初始化,结点的初始位置就位于正中间。

大师学SwiftUI第6章 - 声明式用户界面

图6-22:整数值滑块

Slider初始化方法还内置了onEdittingChanged参数,接收一个闭包,闭包接收表示用户开始或结束移动滑块的布尔值。我们可以使用它来高亮显示编辑中的值,如下例所示。

示例6-37:响应滑块状态

示例6-37中的视图包含一个名为textActive的新@State属性。赋值给闭包的onEdittingChanged参数在用户开始移动滑块时对该属性赋值true,释放结点时赋值falseText视图的background()修饰符读取该值根据当前状态设置不同的背景色。因此,在用户移动滑块时显示当前值的文本为黄色背景,否则没有背景色。

进度视图

SwiftUi内置有ProgressView视图来用于创建进度条。该视图设计用于显示任务的进度。

  • ProgressView(String, value: Binding, total: Double):此初始化方法创建一个进度条。第一个参数指定标签,value参数表示当前进度,total参数指定表示任务完成度的值。(默认值为0.0到1.0)。

视图的实现非常直观。我们只需要一个表示当前进度的属性值。

示例6-38:显示进度

这一ProgressView视图值由0.0到10.0,初始值为5(通过currentValue赋了初始值),因此进度位于中心。

大师学SwiftUI第6章 - 声明式用户界面

图6-24:进度条

ProgressView视图设计用于显示任务即时的进度,比如从服务端下载的当前数据量,或是还差多少完成任务。稍后我们会学习如何执行其中一些任务,但现在我们通过Slider视图来做测试,如下所示。

示例6-39:模拟进度

本例中,我们对SliderProgressView设置了相同的值。范围为0到10,因此每当我们移动滑块时,进度条就显示相同值。

大师学SwiftUI第6章 - 声明式用户界面

图6-25:使用中的进度条

ProgressView结构体内置了如下修饰符用于定义进度条的样式。

  • progressViewStyle(ProgressViewStyle):这一修饰符指定ProgressView视图的颜色 。参数是一个遵循ProgressViewStyle协议的结构体。框架定义了automaticcircularlinear属性来创建标准视图。

默认样式为automatic,表示视图显示为一个线性进度条,但我们可以指定circular值来创建活动指示。这是一个表示任务在处理中的转盘,但不同于进度条,这类指示符没有隐性界限,所示无需指定任何值,如下所示。

示例6-40:显示活动指示器

大师学SwiftUI第6章 - 声明式用户界面

图6-26:活动指示器

步进器视图

Stepper视图创建一个带递增和递减按钮的控件。该结构体提供了多个初始化方法,包含不同的配置参数组合。以下是最常用的一部分。

  • Stepper(String, value: Binding, in: Range, step: Float, onEditingChanged: Closure):此初始化方法创建一个Stepper视图。第一个参数定义标签,value参数是希望用于存储当前值的绑定属性,in参数是允许的最大最小值范围,step参数是一个指定递增或递减值FloatDouble(取决于绑定属性。),onEditingChanged参数是在用户开始及结束编辑该值时执行的闭包。
  • Stepper(String, onIncrement: Closure?, onDecrement: Closure?, onEditingChanged: Closure):该初始化方法创建一个Stepper视图。第一个参数定义标签,onIncrement参数是在用户点击+按钮时执行的闭包,onDecrement参数是在用户点击-按钮时执行的闭包,onEditingChanged参数是在用户开始及结束编辑该值时执行的闭包。

要实现一个Stepper视图,我们需要一个存储当前值的@State属性,并定义希望用户选取的范围值。

示例6-41:创建一个步进器

Stepper视图使用FloatDouble类型的浮点值,因此我们将值格式化为显示整数。结果如下所示。

大师学SwiftUI第6章 - 声明式用户界面

图6-27:步进器

默认,值按1个单位递增或递减,但我们可以通过step参数来进行修改。下例定义了一个按5个单位递增或递减的Stepper视图。

示例6-42:定义步进器的步长

类似Toggle视图,Stepper视图通过水平堆叠以及标签与控件之间的弹性空间实现。如果希望提供自己的标签并对控件定义自定义位置,我们需要应用labelsHidden()修饰符,如示例6-33。下例定义了一个自定义标签并通过onIncrementonDecrement参数创建一个视图在屏幕上显示箭头在告知用户最终是递增或递减。

示例6-43:在值递增或增减时修改界面

本例中,我们定义了两个@State属性:currentValue用于存储步进器的当前值,布尔类型的属性goingUp用于表示最终值是做了递增还是递减。各视图位于HStack中并排显示。第一个是和之前一样的显示步进器当前值的Text视图。在它之后,有一个Image视图,检测goingUp属性来根据属性值朝向、朝下的SF图标。同一个属性用于决定箭头的颜色。最后,我们定义了带onIncrementonDecrement参数的Stepper视图。在赋值给这些参数的闭包中,我们按5进行递增和递减,并修改goingUp属性的值来表示最终是做了递增还是递减。结果就是,用记点击+按钮时看到绿色的向上箭头,点击-按钮时为红色的向下箭头。

大师学SwiftUI第6章 - 声明式用户界面

图6-28:自定义步进器

组合框视图

SwiftUi内置了一个GroupBox视图用于在一堆视图周边创建一个框。该视图定义有背景色以及视觉上对视图和控件分组的圆角。以下是该视图的一个初始化方法。

  • GroupBox(String, content: Closure):该初始化方法创建一个GroupBox视图。第一个参数定义在框顶显示的标签,content参数是一个闭包,定义组中所包含的视图。

该视图默认样式带有背景色,因此我们增压机实现它并添加一个包含希望放到框内视图的闭包。

示例6-44:定义一个视图分组

大师学SwiftUI第6章 - 声明式用户界面

图6-29:组合框

模型

专业应用中包含有多个视图,分别表示可以导航的不同界面。这些视图要访问同样的数据并对应用中状态的改变进行响应。因此 ,这些应用必须可供访问的独立数据源并可由所有视图修改。这一数据源通常称之为模型。模型是应用基本结构的一个部分。在这一范式中,一组长结构体或对象定义该模型(应用的数据及状态),而连接数据的视图在屏幕上展示数据并根据用户的输入更新模型。

大师学SwiftUI第6章 - 声明式用户界面

示例6-30:数据模型

这种组织方式无法通过@State属性创建。前面示例中使用的@State属性封装器只能存储控制单一视图状态的值。我们需要的是一个可以传递给其它视图并对系统上报变更的对象。SwiftUI内置了如下的宏来定义这一对象。

  • @Observable:这个宏对类添加允许属性存储和管理应用状态所需的代码。

借助这个宏,我们可以将任意的类转换为可观测对象,也就是说我们可以使用该对象的属性存储和管理应用的状态。下面可以看到这种类定义的示例。我们称之为ApplicationData,但可以使用任意其它的名称。注意@Observable宏在Observation框架中定义,因此需要导入该框架才能使用这个宏。

示例6-45:在可观测对象中存储数据

这一模型包含两个属性。名为title的属性用于存储书的标题,另一个名为titleInput的属性用于允许用户输入新标题。但因为我们是使用@Observable宏来修改该类,就无需再在视图中声明@State属性了。这个宏会处理该属性存储状态和上报变更到系统所需要代码的生成。

重要:定义用于存储数据的类型是应用的核心部分,可能会很大,因此将它们放到单独的Swift文件中比较合理。只需打开屏幕上方的File菜单,选择New/File,再在iOS面板中选择Swift File图标(见图5-107)。就会添加该文件,文件内部所定义的数据可在代码的任意位置访问。

有了模型之后,我们需要创建一个该类的实例并将其传递给视图。对于简单的应用,我们只需要创建实例并将其赋值给视图的属性即可,如下例所示。

示例6-46:初始化可观测对象

和之前极其相似,但不再读取和存储@State属性中的值,而是对ApplicationData对象的属性进行操作。视图中包含一个读取title属性的Text视图并在屏幕上显示其值,以及一个对属性赋值新字符串的按钮。在点击按钮时,新字符串New Title被赋值给了title属性,该属性向系统上报变更,系统更新视图来在界面中显示新的值。

✍️跟我一起做:创建一个多平台荐。打开屏幕顶部的File菜单,点击New/File选项创建一个新的Swift文件(见图5-107)。将文件命名为ApplicationData.swift,使用示例6-45中的代码替换其中的内容。使用示例6-46中的代码更新ContentView视图。此时会在屏幕上看到一个标题和一个按钮。点击按钮,标题会发生改变。

在本例是中,我们显示了title属性并在点击按钮时修改了其值,但前面我们已经学到,有控件可以让用户输入新值。此时,我们需要创建一个双向绑定以便每次用户与控件进行交互时,新值存储到相应的属性中,界面得到更新。为此,我们需要使用如下的属性包装器将可观测对象声明为可绑定。

  • @Bindable:这一属性包装器在属性和可观测对象之间创建了一个双向绑定。

在下面的代码中,我们对前例添加了一个TextField视图来演示如何与可观测对象建立双向绑定。

示例6-47:与可观测对象建立双向绑定

和之前一样,我们需要在属性前添加一个美元符号来告知TextField视图要存储用户插入的值。因为使用@Bindable修改了appData属性,可观测对象接收到值并存入模型中。在点击按钮时,我们执行与之前一样的流程。用户插入的字符被赋值给了title属性,屏幕上的界面进行更新显示这些字符。

✍️跟我一起做:使用示例6-47中的代码更新ContentView视图。在界面中会看到标题、文本框以及一个按钮。在文本框中插入文本并按下按钮。输入的文本会替换掉原标题。

示例6-45的模型中,我们定义了两个属性,title用于存储实际信息,titleInput用于接收用户的输入。应用中稍后添加的其它视图可能需要访问title属性来向用户显示其值,但titleInput属性仅在包含TextField视图的视图中用到。也就意味着我们在应用的模型存储了视图的私有状态。虽然这种方法没什么错,但推荐做法是用模型存储应用数据,但在视图内管理视图的状态。可以实现不同的模式来组织应用。一种方法是为每个视图分别定义@State属性,类似前面那样,但另一种试是创建额外的可观测对象。例如,我们可以从模型中删除titleInput属性,并为视图定义一个可观测对象来管理用户的输入,如下所示。

示例6-48:定义一个视图级别的可观测对象

基本和之前一样,只是定义了一个针对 ContentView视图的可观测对象ViewData。现在用户插入的字符存储于这一对象的titleInput属性中,因此模型和视图状态做了隔离。在用户点击Save按钮时,我们将titleInput属性的值赋值给title属性,这样新标题就存储到了模型中。

✍️跟我一起做:删除ApplicationData类中的titleInput属性。使用示例6-48中的代码更新ContentView.swift文件。功能和此前一样,但现由视图自己取代模型来管理其状态。

使用可观测对象来代替@State属性管理视图状态的一个好处是,更易于动态初始化对象的属性。例如,我们可以在ContentView结构体初始化时将存储在模型中的当前标题赋值给titleInput属性,这样用户就可以在屏幕上看到之前的值。

示例6-49:初始化视图的可观测对象

这个初始化方法将模型的title属性值赋值给viewData对象的titleInput属性,因而TextField视图在整体视图出现时显示当前值。

大师学SwiftUI第6章 - 声明式用户界面

图6-31:使用模型中的值初始化文本字段

✍️跟我一起做:将示例6-49中的初始化方法添加到ContentView结构体中(body属性定义的上方)。赋值给模型中title属性的字符串会如图6-31那样出现在文本框内。

另一种使用可观测对象的属性或@State属性初始化的方法是onAppear()修饰符。如前所见,这一修饰符在视图出现于屏幕上时执行一个闭包。例如,我们可以将其赋值给ContentView视图的VStack视图来在屏幕上显示视图时初始化titleInput属性。

示例6-50:在视图出现时初始化视图的可观测对象

✍️跟我一起做:从ContentView结构体中删除示例6-49中添加的初始化方法。对VStack视图添加示例6-50中的onAppear()修饰符(位于padding()修饰符下方)。结果和前面一样。

虽然可观测对象对于构建应用的模型以及管理视图状态很好,但视图并不一定需要在值发生改变时进行更新。如果可观测对象中存在属性,不需要在每次发生值改变时更新视图,就可以实现如下的装饰宏。

  • @ObservationIgnored:这个宏生成取消指定属性观测的代码。

例如,我们可以对可观测对象添加一个属性,用于对本例视图中Save按钮点击次数进行计数。

示例6-51:取消可观测对象中属性的观测

每次按下Save按钮时,我们都对可观测对象中的counter属性累加1,但因为这个属性通过ObservationIgnored宏进行修饰,视图不会在每次值发生改变时更新。

访问模型

我们的应用可能一个展示菜单的视图、一个展示内容列表的视图以及另一个显示用户所做选择信息的视图。所有这些视图都必须访问相同的数据,因此都要包含对模型的引用。将指针从一个视图传到另一个视图直到需要使用值的视图比较笨重且容易出错。更好的选择是将模型的指针传入环境,然后在需要时从环境中读取引用。

大师学SwiftUI第6章 - 声明式用户界面

图6-32:通过环境访问模型

如前所述,环境是存储应用与视图相关信息的通用容易,但它也可以存储自定义数据,包含对可观测对象的引用。在图6-32中,可观测对象的一个实例添加到环境中,然后仅由需要使用它的视图访问。

可观测对象通过environment()修饰符添加,并通过@Environment属性所定义的属性访问。必须要考虑的是environment()修饰符将对象赋值给一个视图层级的对象,因此我们必须将其应用于界面上所有视图的初始视图才能访问它。下例展示了我们需要对App结构体所做的修改,创建了一个ApplicationData对象并将其添加到应用的初始视图的环境中(ContentView视图)。

示例 6-52:将可观测对象赋值给视图的环境

ApplicationData类的实例必须存储于@State属性中。有了这个对象后,我们对ContentView应用environment()修饰符将对象注入到环境中。在视图中访问对象很简单。只需要通过@Environment属性包装器创建一个属性,如下所示。

示例6-53:从环境中获取可观测对象的引用

使用@Environment属性包装器,我们可以在界面中的任意视图中访问模型。但这并不适用于不同层级的视图,比如由预览所创建的ContentView视图。这一视图有其自身的层级和环境,因此我们需要再创建一个模型实例,将其注入到预览自己的环境中才能使用。

✍️跟我一起做:使用示例6-52中的代码更新App结构体,并用示例6-53中的代码更新ContentView.swift文件。应用和之前功能相同,这现在是通过环境获取可观测对象,因此它们在ContentView相同层级的所有视图中都可使用。我们会在第8章中学习如何对相同层级添加更多视图。

如果希望环境属性能够处理双向绑定属性,需要使用@Bindable属性包装器将其转化为可绑定属性。例如,我们像示例6-45中那样将用户输入存储到模型的属性中,还需要像如下那样创建单独的视图来处理其值。

示例6-54:将环境属性转变成可绑定属性

上例中,我们像之前一样从环境中访问模型,但现在将值传递给另一个视图MyInputView。这个视图通过@Bindable属性包装器使得模型可绑定,此时就可以使用模型中的titleInput属性管理用户输入的值。

如果仅部分属性需要双向绑定,我们可以不使用属性包装器,而是通过Bindable结构体逐一转化属性。下例和之前功能一致,但我们通过仅在需要时将appData属性转化为可绑定属性简化了代码。

示例6-55:通过Bindable结构体创建一个可绑定属性

✍️跟我一起做:使用示例6-54中的代码更新ContentView.swift,再使用示例6-45中的ApplicationData.swift。应用功能和之前相同,但现在可以管理模型中的双向绑定属性。

代码请见:GitHub仓库

喜欢 (1)
[]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址