SwiftUI:AnyView视图替代视图介绍

525 阅读5分钟

SwiftUI 配备了一个特殊的视图,称为AnyView ,它可以作为一个类型清除的包装器,使多个视图类型能够从一个函数或计算属性中返回,或者让我们引用一个视图而不必知道其底层类型。

然而,虽然在某些情况下我们可能需要使用AnyView ,但通常最好是尽可能地避免使用它。这是因为 SwiftUI 使用基于类型的算法来确定何时应该在屏幕上重绘一个给定的视图,而且由于两个AnyView 包裹的视图从类型系统的角度看总是完全相同的(即使它们的底层、包裹的类型不同),执行这种类型清除会大大降低 SwiftUI 有效更新我们视图的能力。

因此,在这篇文章中,让我们来看看两个核心技术,它们可以帮助我们避免AnyView ,同时还可以让我们以非常动态的方式处理多个视图类型。

处理多种返回类型

在使用SwiftUI构建视图时,我们经常使用some View 不透明的返回类型,以避免明确定义我们实际返回的确切类型。这一点特别有用,因为(几乎)每次我们对一个给定的视图应用一个修改器,或者改变一个容器的内容,我们实际上都在改变我们将返回的视图的类型。

然而,只有当一个给定的函数或计算属性中的所有代码分支都返回完全相同的类型时,编译器才能够推断出底层的返回类型。textView 因此,像下面这样的代码不会被编译,因为我们的ifelse 属性中的分支会返回不同类型的视图:

struct FolderInfoView: View {
    @Binding var folder: Folder
    var isEditable: Bool

    var body: some View {
        HStack {
            Image(systemName: "folder")
            textView
        }
    }

    private var textView: some View {
        // Error: Function declares an opaque return type, but
        // the return statements in its body do not have matching
        // underlying types.
        if isEditable {
            return TextField("Name", text: $folder.name)
        } else {
            return Text(folder.name)
        }
    }
}

初步看来,上述情况似乎是必须使用 AnyView ,以使我们所有的代码分支具有相同的返回类型。不过,非常有趣的是,如果我们改成将上述条件表达式放在我们的body 属性内联,编译器错误就会消失。

struct FolderInfoView: View {
    @Binding var folder: Folder
    var isEditable: Bool

    var body: some View {
        HStack {
            Image(systemName: "folder")

            if isEditable {
                TextField("Name", text: $folder.name)
            } else {
                Text(folder.name)
            }
        }
    }
}

这是因为SwiftUI使用了一个函数/结果生成器,将所有在给定范围内定义的视图(比如上面的HStack )合并为一个单一的返回类型,好消息是,我们也可以在自己的属性和函数中使用相同的生成器类型。

就像我们在 "将SwiftUI的ViewBuilder属性添加到函数中 "中看到的那样, 我们要利用同样强大的视图构建功能所要做的就是使用@ViewBuilder 属性--这又让我们在同一范围内表达多种类型的视图,就像这样:

struct FolderInfoView: View {
    @Binding var folder: Folder
    var isEditable: Bool

    var body: some View {
        HStack {
            Image(systemName: "folder")
            textView
        }
    }

    @ViewBuilder
    private var textView: some View {
        if isEditable {
            TextField("Name", text: $folder.name)
        } else {
            Text(folder.name)
        }
    }
}

请注意,在我们新的textView 属性实现中,我们不再使用任何return 语句,因为每个表达式现在都会被 SwiftUI 的ViewBuilder 解析,而不是被单独返回。

因此,经常可以避免AnyView 的第一种方式是,只要我们希望某个属性或函数能够返回多种视图类型,就使用ViewBuilder 属性。

通用的视图属性

另一种经常使用AnyView 的情况是当我们想在一个属性中存储一个给定的视图而不需要知道它的确切类型。例如,假设我们正在研究下面的ItemRow ,它目前使用AnyView ,使我们能够注入任何我们想在尾部边缘显示的accessoryView

struct ItemRow: View {
    var title: String
    var description: String
    var accessoryView: AnyView

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(title).bold()
                Text(description)
            }
            Spacer()
            accessoryView
        }
    }
}

由于我们不能对存储的属性使用some View 不透明的返回类型(毕竟它们没有返回任何东西),并且由于我们不再处理可以使用ViewBuilder 组合的预定义数量的视图,如果我们想在这种情况下取消对AnyView 的使用,我们就必须探索另一种策略。

就像我们之前在使用ViewBuilder 时从SwiftUI本身获得的灵感一样,让我们在这里做同样的事情。SwiftUI解决使任何视图都能被注入的问题的方法是使宿主视图在它将包含的视图类型上具有通用性。例如,内置的HStack 容器被定义为具有Content 类型的泛型,而该类型又被要求符合View 协议。

struct HStack<Content>: View where Content: View {
    ...
}

使用同样的泛型类型约束,我们可以让我们的ItemRow 采用完全相同的模式--这将让我们直接注入任何符合View 的类型作为我们的accessoryView

struct ItemRow<Accessory: View>: View {
    var title: String
    var description: String
    var accessoryView: Accessory

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(title).bold()
                Text(description)
            }
            Spacer()
            accessoryView
        }
    }
}

上述做法不仅在视图更新时给我们带来了更好的性能(因为所有涉及的类型现在都被很好地定义了,并且对类型系统是透明的),它还使我们的调用站点变得更简单,因为每个accessoryView 不再需要被手动包装在一个AnyView

// Before:
ItemRow(
    title: title,
    description: description,
    accessoryView: AnyView(Image(
        systemName: "checkmark.circle"
    ))
)

// After:
ItemRow(
    title: title,
    description: description,
    accessoryView: Image(
        systemName: "checkmark.circle"
    )
)

结论

虽然 SwiftUI 让 UI 开发的许多方面变得更简单,但不可否认的是,它是一个非常复杂的框架,大量使用了 Swift 的一些最强大的功能。因此,虽然使用它开始构建视图可能很容易,但我们往往必须使用相当高级的技术(如泛型编程),以便最好地利用SwiftUI所提供的东西。

当然,尽可能避免使用AnyView 可能是个好主意,但这并不意味着永远不应该使用它。它是SwiftUI公共API的一部分是有原因的,而且上述两种技术不会在每一种情况下都起作用--但当它们起作用时,往往会产生更优雅和高效的代码。

谢谢你的阅读!