Swift协议在SwiftUI中的使用
原文链接: Swift protocols in SwiftUI
像所有其他的swift框架一样,SwiftUI把协议作为它的一个核心部分的定义:在之前的文章中,我们提到了SwiftUI的自带协议的示例,比如View或者LabelStyle。
在这篇新文章中,让我们一起看下Swift标准库中的协议在SwiftUI中的使用:Hashable,Identifiable,和Equatable。
Equatable
SwiftUI的特性就是尽可能的懒惰:
除非是必要的需求,否者没有任何东西会被重绘或者被计算。
Equatable是SwiftUI性能的无名英雄之一。计算布局,描绘组件,这些都是非常昂贵的开销:SwiftUI做的越少,性能越高。
Equatable是这样帮助SwiftUI去做决定的:即使这个视图没有声明Equatable一致性,但是Swift会通过Swift反射去遍历视图的定义,然后检查每个属性的相等性,决定是否需要基于这个去进行重绘。
这仅仅是屏幕幕后发生的事情:进一步的阅读,请移步下面这篇文章:Javier Nigro’s The Mystery Behind View Equality
Identifiable
当相等性被用来检测视图状态的变化(因此触发一次重绘),Identifiable用来将视图身份和状态分开。
这个特性在SwiftUI中被用来在List和OutlineGroup中做元素顺序的跟踪:
-
想象一组元素,只有他们的顺序将要被重新排序。如果这次重排没有牵涉到任何改变,只有列表将要更新网格的顺序,但是每个网格将不需要被重绘。
-
另一方面,如果只有网格的状态将被改变,顺序没有变化,仅仅一个特殊的网格将需要重绘,而这个列表不需要做任何事情。
在Picker中使用Identifiable的例子:
在这个情况下,无论元素类型定义中其他可能的状态,都使用Identifiable来判断所有可能候选者中的那个元素是选中元素(如果有的话)。
最后,Identifiable在所有的alerts/sheets中被使用:
- sheet(item:onDismiss:content:)
- fullScreenCover(item:onDismiss:content:)
- popover(item:attachmentAnchor:arrowEdge:content:)
- alert(item:content:)
- actionSheet(item:content:)
我相信在这种情况下,使用Identifiable给我们提供了一个机会,不单单可以清楚的定义可能不同的alerts/sheets,如果我们需要的话,也可以用来传递更多的数据。
想象一下我们两张不同的工作表:
struct ContentView: View {
@State private var showingSheet: ContentViewSheet?
var body: some View {
VStack {
Button("go to sheet 1") {
showingSheet = .one
}
Button("go to sheet 2") {
showingSheet = .two
}
}
.sheet(item: $showingSheet, content: presentSheet)
}
@ViewBuilder
private func presentSheet(for sheet: ContentViewSheet) -> some View {
switch sheet {
case .one:
Text("One")
case .two:
Text("Two")
}
}
}
这个视图展示了两个按钮,当我们点击的时候,就会触发相关工作表的展示:
)
我们说,我们想要给第二个界面传递数据,一个可行的办法是我们去扩展ContentViewSheet的例子,让他可以绑定关联的数据:
enum ContentViewSheet: Identifiable {
case one
case two(someData: Int) // new associated value
/// The identity ignores the `someData` value.
var id: Int {
switch self {
case .one:
return 1
case .two:
return 2
}
}
}
感谢someData,当我们创建一个新的界面的时候,可以传递Int数值。
struct ContentView: View {
...
var body: some View {
VStack {
...
Button("go to sheet 2") {
showingSheet = .two(someData: Int.random(in: 1...5)) // Pass data here
}
}
.sheet(item: $showingSheet, content: presentSheet)
}
@ViewBuilder
private func presentSheet(for sheet: ContentViewSheet) -> some View {
switch sheet {
...
case .two(let value): // read and use the data here
Text("Sheet two with value \(value)")
}
}
}

Hashable
Hashable主要在两个视图中使用:TabView和NavigationLink。
这些视图和标签一起使用,这是将视图声明连接到SwiftUI需要监听的关联Hashable类型的值的方式。
在TabView的例子中,我们会如下声明几个可能的标签:
enum Tab: Hashable {
case home
case view2
case view3
case view4
}
除了类型声明之外,我们现在需要一个视图关联一个标签的方法,这个可以通过tag(_:)方法实现:
struct ContentView: View {
@State private var tabBarState: Tab = .home
var body: some View {
TabView(selection: $tabBarState) {
Text("Home")
.tabItem { Text("Home") }
.tag(Tab.home)
Text("View 2")
.tabItem { Text("View 2") }
.tag(Tab.view2)
Text("View 3")
.tabItem { Text("View 3") }
.tag(Tab.view3)
Text("View 4")
.tabItem { Text("View 4") }
.tag(Tab.view4)
}
}
}
按照之前的惯例,我们需要声明一个为所有可能目的地的NavigationLink。
enum ContentViewNavigation: Hashable {
case a // destination a
case b // destination b
case c // destination a
}
struct ContentView: View {
@State var showingContent: ContentViewNavigation?
var body: some View {
NavigationView {
VStack {
NavigationLink("Go to A", destination: Text("A"), tag: .a, selection: $showingContent)
NavigationLink("Go to B", destination: Text("B"), tag: .b, selection: $showingContent)
NavigationLink("Go to C", destination: Text("C"), tag: .c, selection: $showingContent)
}
}
}
}
虽然标签到达对TabView来说是有意义的,但是到了今天,在app中使用控制导航依然是一种更好的方式,感谢NavigationLink。
当我们想要传递数据到达指定的视图的时候,这将变得麻烦,尤其是当我们想要程序化的时候。
这或许是从UIKit转移到SwiftUI中最尴尬的点之一,我敢肯定,SwiftUI团队有理由选择该路线仅用于导航,而不是其他所有视图演示文稿所使用的“可识别”方法(如上所示),但是我希望我们将来能看到一些改进以简化此路线 东西。
总结
与怎样真正掌握UIKit相似的是,我们需要掌握并且理解Objective-C, 学习并且理解SwiftUI需要去学习Swift:我们越了解一个,就越了解另外一个。
SwiftUI在今天可能并不完善,也不完美,但是在提到的这篇文中Build content-friendly layouts,当前的状态给我们提供了一个很好的机会去深入学习理解框架本身以及Swift语言。