零是必须发明的吗?对于许多古代文明来说,数字仅指有形的事物。根本没必要数任何东西。最终,学者们认识到,“零”不仅仅是缺席——它是一种价值。它是占位符,是起点,是正面与负面之间的关键桥梁。

在 SwiftUI 中, 空状态就是零点。总会有没有数据可显示、没有修改器可应用或内容可渲染的情况。然而,软件仍然必须向用户——或编译器——传达这种“无”的状态。SwiftUI 为我们提供了三种不同的工具来处理空虚,各自有不同的用途:

  1. EmptyView:用于视图层级(布局)
  2. EmptyModifier:用于类型系统(编译器)
  3. ContentUnavailableView:面向用户(体验)

EmptyView

EmptyView 是 SwiftUI 中看似最简单的类型之一。表面上看,它像是你不想渲染时随便插入的占位符。但它的作用更深层:

EmptyView 是一个零尺寸、非渲染视图。它不绘制任何东西,不占用空间,没有布局影响,且对视图树的参与极少。如果 SwiftUI 是一种标准编程语言,EmptyView 就是虚无—— 意思是“这里有一个视图的概念,但它是空的。”

和其他隐藏修饰符的区别:

EmptyView 的主要使用场景包括:

1. 处理可选内容

当你需要在某些条件下不显示任何内容时:

var body: some View {
    VStack {
        if showContent {
            Text("有内容")
        } else {
            EmptyView() // 什么都不显示
        }
    }
}

2. 统一 ViewBuilder 的类型

在使用 @ViewBuilder 时,不能返回“零”。它必须返回一个视图,所有分支需要返回相同的视图类型。

在复杂的通用上下文或手动 ViewBuilder 实现中,EmptyView 是你需要退出渲染时的正确返回类型。

@ViewBuilder
var conditionalView: some View {
    if isLoggedIn {
        ProfileView()
    } else {
        EmptyView() // 未登录时不显示任何内容
    }
}

3. 作为可选视图的默认值

struct CustomView<Content: View>: View {
    let content: Content
    var body: some View {
        VStack {
            Text("标题")
            content // 可能是实际内容,也可能是 EmptyView
        }
    }
}

// 使用
CustomView(content: EmptyView()) // 只显示标题

4. NavigationLink 的占位目标

NavigationLink(destination: EmptyView()) {
    Text("暂未开放")
}
.disabled(true)

5. 泛型视图的默认参数

struct CardView<Header: View, Footer: View>: View {
    let header: Header
    let footer: Footer

    var body: some View {
        VStack {
            header
            Text("主要内容")
            footer
        }
    }
}

// 只需要显示内容,不需要 header 和 footer
CardView(
    header: EmptyView(),
    footer: EmptyView()
)

EmptyModifier

如果 EmptyView 代表“无视图”,那么 EmptyModifier 代表 “无变换”。

官方文档指出,该修饰符在“编译时”非常有用。这是一个关键的区别。EmptyModifier 通常不用于运行时逻辑(比如开关不透明度);它是修饰符世界的身份元素。它的存在是为了满足类型检查器的需求,当需要提供修饰符,但你实际上并不想做任何操作时使用。

与条件编译结合

EmptyModifier 最强大的用例是将其与条件编译结合。

想象你想要一个特定的调试覆盖层:一个红色边框,但你希望那段代码完全从发布版本中剔除。你不想把每个调用点都包裹在 #if Debug 中。相反,你可以定义一个根据构建配置变化的修正值:

#if DEBUG
struct DebugBorder: ViewModifier {
    func body(content: Content) -> some View {
        content.overlay(
            RoundedRectangle(cornerRadius: 4)
                .stroke(.red, lineWidth: 1)
        )
    }
}
#else
// 否则在 Release 构建中, 这个修饰结构会变成 "Identity" modifier
typealias DebugBorder = EmptyModifier
#endif

现在,您的代码可以保持干净且一致:

Text("Hello World")
    .modifier(DebugBorder())

为什么不用在运行逻辑上?

你可能会想写这样的代码:view.modifier(isEnabled ? MyEffect() : EmptyModifier())

因为在编译过程中,通常会生成 _ConditionalContent 封装器。对于简单的运行时切换,通常将逻辑放入自定义修改器或使用标准视图扩展更为简洁。

EmptyModifier 在需要为通用系统提供默认值时发挥出色。如果你构建了一个可重用组件,并且接受泛型的修改器 ViewModifier,就可以将默认设置为 EmptyModifier

struct MyContainer<VM: ViewModifier>: View {
    var modifier: VM = EmptyModifier()   // Default is "do nothing"
    // ...
}

EmptyModifier 是你 UI 中隐形的绑定,在发布版本中看不到,但在调校时至关重要。用它大胆调试,干净部署。


ContentUnavailableView

iOS 17 中的新功能。

EmptyView 解决了渲染不存在的技术问题,但它在用户体验问题上却没有明确的作用。空白的屏幕需要意图、清晰度,且通常带有明显的行动邀请。

当您的应用程序没有任何内容可显示时,SwiftUI 的 ContentUnavailableView 可以显示标准的用户界面。它非常适合您的应用依赖尚未提供的用户信息的情况,例如当您的用户尚未创建任何数据时,或者他们正在搜索某些内容但没有结果时。

举个例子,如果您正在制作一个应用程序,让用户写下他们想要记住的 Swift 代码片段,那么默认情况下它可能会以没有代码片段的方式开始。因此,您可以像这样使用:


// 这将显示 SF Symbols 中的一个大 Swift 图标,以及下面的标题文本“无片段”
ContentUnavailableView("No snippets", systemImage: "swift")

// 这将显示 SF Symbols 中的一个大 signature 图标,以及下面的标题文本“无片段”
ContentUnavailableView("No snippets", systemImage: "signature.th")

// 还可以在下面添加额外的描述文本行,指定为 Text 视图,以便您可以添加额外的样式,例如自定义字体或自定义颜色:
ContentUnavailableView(
	"No snippets", 
	systemImage: "swift", 
	description: Text("You don't have any saved snippets yet.")
)
// 如果想要完全控制,可以提供标题和描述的单独视图,以及一些要显示的按钮以帮助用户开始使用:
ContentUnavailableView {
    Label("No snippets", systemImage: "swift")
} description: {
    Text("You don't have any saved snippets yet.")
} actions: {
    Button("Create Snippet") {
        // create a snippet
    }
    .buttonStyle(.borderedProminent)
}

SwiftUI 还内置了一些 ContentUnavailableView 的实例,方便使用。

// 例如:显示失败的搜索结果屏幕
ContentUnavailableView.search

// 可以自定义其内容
ContentUnavailableView.search(text: "Life, the Universe, and Everything")