[SwiftUI 100 天] Bookworm - part2 类型擦除

664 阅读5分钟
译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

使用 size 类和 AnyView 类型擦除

SwiftUI 为我们的视图提供了一个共享的信息池,这个池被称为 environment,我们之前曾经用它来关闭 sheet 。回忆一下,用环境包装器来创建属性的方式是像下面这样的:

@Environment(\.presentationMode) var presentationMode

然后,在我们有需要时,可以像下面这样使用:

Text("Hello World")
    .onTapGesture {
        self.presentationMode.wrappedValue.dismiss()
    }

这个方法使得 SwiftUI 能够确保视图被关闭时相关的状态正确更新 —— 假如我们让一个@State属性对应于 sheet 的呈现,那么当 sheet 被关闭时,它会被设置回 false。

这个“环境”实际上包罗了各种有趣的信息,借助这些信息我们可以让应用更好地工作。在这个项目中,我们将使用环境来协同 Core Data 一起工作,但在这之前我将向你展示另一个重要的用法:size classes。Size 类是 Apple 告知我们视图有多少可用空间的一种“比较隐晦”的方式。

当我强调“比较隐晦”,我的意思是:我们只有两个 size 类,包括水平的和竖直的,有 “compact” 和 “regular” 两种值,就这些 —— 它们覆盖了从最大的横向摆放的 iPad Pro 的水平方向到最小的竖向摆放的 iPhone。这并不意味着这个信息没用,相反,这个信息很有价值,它能让我们在最宽的频谱上考虑我们的用户界面。

为了演示 size 类的用法,我们可以创建一个视图,包含一个追踪当前 size 类的属性,并且在文本视图里显示:

struct ContentView: View {
    @Environment(\.horizontalSizeClass) var sizeClass

    var body: some View {
        if sizeClass == .compact {
            return HStack {
                Text("Active size class:")
                Text("COMPACT")
            }
            .font(.largeTitle)
        } else {
            return HStack {
                Text("Active size class:")
                Text("REGULAR")
            }
            .font(.largeTitle)
        }
    }
}

请尝试在 12.9 寸 iPad Pro 模拟器横向模式下运行代码,以便你观察完整的效果。首先,你应该会看到“REGULAR”,因为我们的应用是全屏显示。但是假如你从模拟器底部向上轻划,dock 会出现,然后你可以拖出某个其他应用,比如 Safari,然后放进 iPad 的右手边,以进入多任务模式。

尽管现在应用只有半屏可用,你仍然会看到“REGULAR”标签。但是一旦你把分隔条往左边拖 —— 比如,只给我们的应用四分之一的可用空间 —— 这个时候标签会变成 “COMPACT”。

因此,在全屏宽度下,我们是处于一个常规尺寸的类别下;在半屏宽度下,我们仍处于常规尺寸的类别下,但当我们继续缩小时,就变成了紧凑类别。

有趣的地方在于,假如我们希望根据环境改变布局,那么在紧凑类别下,使用VStack显然比HStack更合理。不过,这个过程比你想象的复杂。

首先,修改代码,返回一个VStack或者HStack

if sizeClass == .compact {
    return VStack {
        Text("Active size class:")
        Text("COMPACT")
    }
    .font(.largeTitle)
} else {
    return HStack {
        Text("Active size class:")
        Text("REGULAR")
    }
    .font(.largeTitle)
}

编译代码,你会看到一个错误:“Function declares an opaque return type, but the return statements in its body do not have matching underlying types.” 这个错误的含义是:body的返回类型some View要求代码的所有路径返回某个单一的类型 —— 我们不能一会返回一种视图,一会返回另一种视图。

你可能会想你得学聪明,把整个条件包进另一个视图,比如一个VStack,但这也行不通。相反,我们需要用到一个更高级的解决方案,叫类型擦除。之所以说 “更高级” ,因为概念上它是很聪明的做法,并且它的实现并不简单,但从我们的视角看,实际使用它 —— 类型擦除是非常简单的。

首先,让我们看一眼代码 —— 将当前的body代码替换为下面这样的:

if sizeClass == .compact {
    return AnyView(VStack {
        Text("Active size class:")
        Text("COMPACT")
    }
    .font(.largeTitle))
} else {
    return AnyView(HStack {
        Text("Active size class:")
        Text("REGULAR")
    }
    .font(.largeTitle))
}

让我简化呈现改变的地方:

return AnyView(HStack {
    // ...
}
.font(.largeTitle))

你会发现代码可以编译通过了,不仅如此,应用运行时可以根据尺寸类别在HStackVStack间无缝切换。改变之处在于我们把两个栈都包进一个新的视图类型,AnyView,我们称之为类型擦除包装器

AnyViewTextColorVStack等类型一样,遵循View协议,并且它内部包含一个特定的视图类型。但是,对外AnyView并不暴露它的内容 —— Swift 看到的是,我们的条件化视图返回只是AnyView。这正是 “类型擦除” 这个说法的由来:AnyView有效地隐藏了,或者说擦除了,它所包含的视图的类型。

到此,合理的结尾是问一问为什么我们不一直使用AnyView,这样可以规避some View的限制。答案很简单:性能。当 SwiftUI 准确知道我们视图层级里的东西时,它可以按需添加或者移除视图中更小的部分,但是假如我们使用的是AnyView,我们是主动地对 SwiftUI 否认了这些信息。因此,它很有可能不得不做多得多的工作,以保证在常规变化发生时,用户界面保持更新,所以除非特别需要,通常最好是避免使用AnyView


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~