SwiftUIViews

View 基础

深入理解 SwiftUI View 协议及其核心概念,包括视图声明、修饰符、ViewBuilder 等关键主题。

概述

View 是 SwiftUI 应用用户界面的基本构建块。每个视图都包含对给定状态下显示内容的描述。应用中对用户可见的所有内容都源自视图的描述,任何遵循 View 协议的类型都可以作为应用中的视图。

通过在视图的 body 计算属性中组合 SwiftUI 提供的内置视图和自定义视图来构建自定义视图。使用 SwiftUI 提供的视图修饰符配置视图,或使用 ViewModifier 协议和 modifier(_:) 方法定义自己的视图修饰符。

View 协议

View 协议是 SwiftUI 中表示用户界面部分的类型,并提供用于配置视图的修饰符。

协议声明

@MainActor @preconcurrency protocol View

核心要求

遵循 View 协议的类型必须满足两个要求:

  1. body 属性: 必须实现 body 计算属性来提供视图的内容
  2. Body 关联类型: 必须指定表示视图主体的类型
struct MyView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

SwiftUI 在需要更新视图时会读取 body 属性的值,这在视图的生命周期中可能会重复发生,通常是响应用户输入或系统事件。

声明自定义视图

声明式方法

SwiftUI 采用声明式方法进行用户界面设计。与传统的命令式方法不同,你无需在控制器代码中实例化、布局和配置视图,也无需在条件变化时持续更新。相反,通过在层次结构中声明视图来创建用户界面的轻量级描述,该层次结构反映了界面的期望布局。

定义自定义视图

通过定义遵循 View 协议的结构来声明自定义视图类型:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("欢迎使用 SwiftUI")
                .font(.title)
            Text("声明式 UI 框架")
                .foregroundColor(.secondary)
        }
    }
}

添加输入属性

为视图添加属性以提供输入。如果输入值发生变化,SwiftUI 会注意到更改并仅重绘界面的受影响部分:

struct GreetingView: View {
    let name: String
    let font: Font
    
    var body: some View {
        Text("Hello, \(name)!")
            .font(font)
    }
}

由于系统可能随时重新初始化视图,因此避免在视图的初始化代码中执行任何重要工作非常重要。通常最好省略显式初始化器,允许 Swift 合成成员初始化器。

ViewBuilder

ViewBuilder 是一个自定义参数属性,用于从闭包构建视图。

声明

@resultBuilder struct ViewBuilder

使用场景

通常将 ViewBuilder 用作生成子视图的闭包参数的参数属性,允许这些闭包提供多个子视图:

func contextMenu<MenuItems: View>(
    @ViewBuilder menuItems: () -> MenuItems
) -> some View

多语句闭包

使用 ViewBuilder 的函数客户端可以使用多语句闭包来提供多个子视图:

myView.contextMenu {
    Text("剪切")
    Text("复制")
    Text("粘贴")
    if isSymbol {
        Text("跳转到定义")
    }
}

条件内容

ViewBuilder 支持条件语句和可选值:

struct ConditionalView: View {
    let isLoggedIn: Bool
    
    var body: some View {
        VStack {
            if isLoggedIn {
                Text("欢迎回来!")
                Button("退出登录") { }
            } else {
                Text("请登录")
                Button("登录") { }
            }
        }
    }
}

配置视图

视图修饰符

在 SwiftUI 中,通过应用视图修饰符来调整视图的特性。修饰符是具有默认实现的 View 协议方法,可以应用于任何遵循 View 协议的类型。

修饰符工作原理

修饰符通过将调用它们的视图实例包装在具有指定特性的另一个视图中来工作:

Text("Hello, World!")
    .foregroundColor(.red)

修饰符返回一个包装原始视图的视图,并在视图层次结构中替换它。

链式修饰符

通常通过一个接一个地调用修饰符来链接它们,每个修饰符都包装前一个修饰符的结果:

Text("Hello, World!")
    .frame(width: 200)
    .border(Color.blue, width: 2)

修饰符顺序

应用修饰符的顺序很重要。例如,边框会围绕应用它的视图绘制:

// 边框围绕 200 点宽的框
Text("Hello")
    .frame(width: 200)
    .border(Color.blue)

// 边框围绕文本,然后添加框
Text("Hello")
    .border(Color.blue)
    .frame(width: 200)

常用修饰符类别

struct ModifierExamples: View {
    var body: some View {
        VStack(spacing: 20) {
            // 外观修饰符
            Text("外观")
                .font(.headline)
                .foregroundColor(.blue)
                .background(Color.yellow)
            
            // 布局修饰符
            Text("布局")
                .frame(width: 200, height: 50)
                .padding()
                .border(Color.gray)
            
            // 图形修饰符
            Text("图形")
                .opacity(0.7)
                .shadow(radius: 5)
                .blur(radius: 0.5)
            
            // 交互修饰符
            Button("点击我") { }
                .buttonStyle(.borderedProminent)
                .disabled(false)
        }
    }
}

ViewModifier 协议

自定义修饰符

采用 ViewModifier 协议来创建可应用于任何视图的可重用修饰符:

struct BorderedCaption: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.caption2)
            .padding(10)
            .overlay(
                RoundedRectangle(cornerRadius: 15)
                    .stroke(lineWidth: 1)
            )
            .foregroundColor(Color.blue)
    }
}

扩展 View

更常见和惯用的方法是使用 modifier(_:) 定义 View 的扩展:

extension View {
    func borderedCaption() -> some View {
        modifier(BorderedCaption())
    }
}

使用自定义修饰符

Text("市区巴士")
    .borderedCaption()

带参数的修饰符

struct RoundedBorder: ViewModifier {
    let color: Color
    let width: CGFloat
    let cornerRadius: CGFloat
    
    func body(content: Content) -> some View {
        content
            .padding()
            .overlay(
                RoundedRectangle(cornerRadius: cornerRadius)
                    .stroke(color, lineWidth: width)
            )
    }
}

extension View {
    func roundedBorder(
        color: Color = .blue,
        width: CGFloat = 2,
        cornerRadius: CGFloat = 10
    ) -> some View {
        modifier(RoundedBorder(
            color: color,
            width: width,
            cornerRadius: cornerRadius
        ))
    }
}

// 使用
Text("自定义边框")
    .roundedBorder(color: .green, width: 3)

管理视图层次结构

id 修饰符

使用 id(_:) 修饰符为视图提供显式标识:

struct ItemList: View {
    let items: [Item]
    @State private var selectedID: UUID?
    
    var body: some View {
        ForEach(items) { item in
            ItemRow(item: item)
                .id(item.id)
        }
    }
}

当标识符改变时,SwiftUI 会将视图视为新视图,销毁旧层次结构并为新类型创建新层次结构。

tag 修饰符

使用 tag(_:includeOptional:) 为可选择的视图设置标签值:

struct SettingsView: View {
    @State private var selection = 0
    
    var body: some View {
        TabView(selection: $selection) {
            GeneralSettings()
                .tag(0)
                .tabItem {
                    Label("通用", systemImage: "gear")
                }
            
            PrivacySettings()
                .tag(1)
                .tabItem {
                    Label("隐私", systemImage: "hand.raised")
                }
        }
    }
}

equatable 修饰符

使用 equatable() 防止不必要的视图更新:

struct ExpensiveView: View, Equatable {
    let data: ComplexData
    
    var body: some View {
        // 复杂的视图层次结构
        ComplexContent(data: data)
    }
    
    static func == (lhs: ExpensiveView, rhs: ExpensiveView) -> Bool {
        lhs.data.id == rhs.data.id
    }
}

struct ParentView: View {
    @State private var data: ComplexData
    
    var body: some View {
        ExpensiveView(data: data)
            .equatable()
    }
}

异步任务

task 修饰符

使用 task(priority:_:) 在视图出现时执行异步工作:

struct UserProfileView: View {
    @State private var user: User?
    let userID: String
    
    var body: some View {
        Group {
            if let user {
                UserDetails(user: user)
            } else {
                ProgressView()
            }
        }
        .task {
            do {
                user = try await fetchUser(id: userID)
            } catch {
                print("获取用户失败: \(error)")
            }
        }
    }
}

带标识符的 task

使用 task(id:priority:_:) 在标识符改变时重新执行任务:

struct SearchView: View {
    @State private var searchText = ""
    @State private var results: [SearchResult] = []
    
    var body: some View {
        List(results) { result in
            SearchResultRow(result: result)
        }
        .searchable(text: $searchText)
        .task(id: searchText) {
            guard !searchText.isEmpty else {
                results = []
                return
            }
            
            do {
                // 当 searchText 改变时,之前的任务会被取消
                results = try await performSearch(query: searchText)
            } catch {
                results = []
            }
        }
    }
}

支持视图类型

EmptyView

EmptyView 是不包含任何内容的视图。很少需要直接创建 EmptyView,它表示视图的缺失:

struct ConditionalContent: View {
    let showContent: Bool
    
    var body: some View {
        if showContent {
            Text("内容可见")
        } else {
            EmptyView()
        }
    }
}

SwiftUI 在视图类型定义一个或多个具有泛型参数的子视图并允许子视图缺失的情况下使用 EmptyView:

let progressView = ProgressView()
// 类型为 ProgressView<EmptyView, EmptyView>

AnyView

AnyView 是类型擦除的视图,允许在给定视图层次结构中更改使用的视图类型:

struct DynamicView: View {
    let useImage: Bool
    
    var body: some View {
        Group {
            if useImage {
                AnyView(Image(systemName: "star"))
            } else {
                AnyView(Text("星标"))
            }
        }
    }
}

每当与 AnyView 一起使用的视图类型发生变化时,旧层次结构会被销毁,并为新类型创建新层次结构。

性能考虑

应谨慎使用 AnyView,因为类型变化会导致视图层次结构重建。在可能的情况下,优先使用泛型或 @ViewBuilder:

// 推荐: 使用泛型
struct BetterDynamicView<Content: View>: View {
    let content: Content
    
    var body: some View {
        content
    }
}

// 推荐: 使用 @ViewBuilder
struct BestDynamicView: View {
    let useImage: Bool
    
    @ViewBuilder
    var body: some View {
        if useImage {
            Image(systemName: "star")
        } else {
            Text("星标")
        }
    }
}

视图组合最佳实践

保持视图简单

将复杂视图分解为更小的可重用组件:

struct ProfileView: View {
    let user: User
    
    var body: some View {
        VStack {
            ProfileHeader(user: user)
            ProfileStats(user: user)
            ProfileBio(user: user)
        }
    }
}

struct ProfileHeader: View {
    let user: User
    
    var body: some View {
        HStack {
            AsyncImage(url: user.avatarURL) { image in
                image.resizable()
            } placeholder: {
                ProgressView()
            }
            .frame(width: 60, height: 60)
            .clipShape(Circle())
            
            VStack(alignment: .leading) {
                Text(user.name)
                    .font(.headline)
                Text("@\(user.username)")
                    .foregroundColor(.secondary)
            }
        }
    }
}

提取子视图

使用计算属性或方法提取子视图以提高可读性:

struct DashboardView: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                headerSection
                statsSection
                recentActivitySection
            }
        }
    }
    
    private var headerSection: some View {
        VStack {
            Text("仪表板")
                .font(.largeTitle)
            Text("欢迎回来")
                .foregroundColor(.secondary)
        }
    }
    
    private var statsSection: some View {
        HStack {
            StatCard(title: "总计", value: "1,234")
            StatCard(title: "活跃", value: "567")
            StatCard(title: "待处理", value: "89")
        }
    }
    
    private var recentActivitySection: some View {
        VStack(alignment: .leading) {
            Text("最近活动")
                .font(.headline)
            ActivityList()
        }
    }
}

避免过度嵌套

使用容器视图和组合来避免深度嵌套:

// 避免
VStack {
    HStack {
        VStack {
            HStack {
                // 深度嵌套
            }
        }
    }
}

// 推荐
struct ContentView: View {
    var body: some View {
        VStack {
            TopSection()
            MiddleSection()
            BottomSection()
        }
    }
}

总结

SwiftUI 的 View 协议和相关概念构成了声明式 UI 开发的基础:

  • View 协议定义了视图的基本要求,主要是 body 属性
  • ViewBuilder 允许使用声明式语法构建复杂的视图层次结构
  • 视图修饰符通过包装视图来配置外观和行为
  • ViewModifier 协议支持创建可重用的自定义修饰符
  • idtagequatable 修饰符帮助管理视图标识和更新
  • task 修饰符简化了异步操作的生命周期管理
  • EmptyViewAnyView 等支持类型处理特殊场景

理解这些基础概念对于构建高效、可维护的 SwiftUI 应用至关重要。通过遵循最佳实践,如保持视图简单、提取子视图和避免过度嵌套,可以创建清晰、高性能的用户界面。

在 GitHub 上编辑

上次更新于