iPad 与 Mac

NavigationSplitView 是 SwiftUI 里用来做“分栏导航”的组件,专门对付 iPad、macOS 这种大屏幕布局的。简单说:左边列表,中间详情,右边再细分详情(可选),像个“三明治”。写代码时基本直接替换 NavigationStack 使用。

基本结构

NavigationSplitView {
    // Sidebar(左侧栏)
} content: {
    // 主内容区(中间)
} detail: {
    // 详情区(右侧)
}

最小可运行示例

import SwiftUI

struct ElapsedTime: View {
    let items = ["Apple", "Banana", "Orange"]

    @State private var selectedItem: String?

    var body: some View {
        NavigationSplitView {
            List(items, id: \.self, selection: $selectedItem) { item in
                Text(item)
            }
        } content: {
            Text(selectedItem ?? "请选择一个水果")
        } detail: {
            Text("这里是更详细的信息")
        }
    }
}

#Preview {
    ElapsedTime()
}

是 SwiftUI 给你准备的“分栏导航长相皮肤包”。SwiftUI 的 NavigationSplitView,然后系统会根据平台(iPad / Mac / iPhone)帮你决定是三栏还是两栏。但如果你不满意它默认那张“脸”,就可以用 .navigationSplitViewStyle(...) 改它的表现方式。

用来控制:

.navigationSplitViewStyle 你可以建议 SwiftUI 怎么分栏,但最终它听不听,取决于它今天心情和设备大小。

// .automatic 默认
.navigationSplitViewStyle(.automatic)
// iPad / Mac 三栏
// iPhone 变成 NavigationStack

// .balanced 三栏更“对称”,不会特别偏向某一栏
.navigationSplitViewStyle(.balanced)
// 比较适合 macOS / iPad 大屏布局
// 不会疯狂挤压 detail

// .prominentDetail 重点永远在 detail(右侧主内容)。
.navigationSplitViewStyle(.prominentDetail)
// detail 最大化
// sidebar 比较“背景板”
// 适合阅读类 / 内容展示类 app
import SwiftUI

struct ElapsedTime: View {
    let items = ["Apple", "Banana", "Orange"]

    @State private var selectedItem: String?

    var body: some View {
        NavigationSplitView {
            Text("Sidebar")
        } content: {
            Text("Content")
        } detail: {
            Text("Detail")
        }
        .navigationSplitViewStyle(.prominentDetail)
    }
}

#Preview {
    ElapsedTime()
}

columnVisibility

NavigationSplitView 的 columnVisibility 是用来控制“三栏结构里哪些列显示/隐藏”的状态变量,本质上就是 SwiftUI 给你留的一根遥控器线。

NavigationSplitView 通常有三列:

而 columnVisibility 控制的是:当前到底显示几列,以及隐藏哪一列。

@State private var columnVisibility: NavigationSplitViewVisibility = .all
// .all 三列全显示(sidebar + content + detail)
// .automatic 系统决定显示策略(iPhone 通常会折叠成单列或双列)
// .doubleColumn 显示两列 通常是 sidebar + detail,或 content + detail
// .detailOnly 只显示 detail sidebar 和 content 被折叠/隐藏

使用示例

import SwiftUI

struct ElapsedTime: View {
    @State private var columnVisibility: NavigationSplitViewVisibility = .detailOnly

    var body: some View {
        NavigationSplitView(columnVisibility: $columnVisibility) {
            Text("Sidebar")
        } content: {
            Text("Content")
        } detail: {
            Text("Detail")
        }
    }
}

#Preview {
    ElapsedTime()
}
NavigationSplitView(columnVisibility: .constant(.all)) {
    List {
        ForEach(...) ... in {
            ...
            NavigationLink(value: aA) {
                ...
            }
            NavigationLink(value: aB) {
                ...
            }
        }
    }
    .navigationDestination(for: A.self) { a in 
        ...
    }
    .navigationDestination(for: B.self) { b in 
        ...
    }
}

maxHeight-Width minHeight-Width

设置 .frame 不要设置死的大小,最多设置下 max 和 min 就好了

import SwiftUI

struct ElapsedTime: View {
    let items = [
        "Short",
        "A bit longer text",
        "This is a very very very long text to test maxWidth behavior"
    ]

    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                ForEach(items, id: \.self) { text in
                    CardView(text: text)
                }
            }
            .padding()
        }
    }
}

struct CardView: View {
    let text: String

    var body: some View {
        Text(text)
            .font(.body)
            .padding()
            .frame(
                minWidth: 120,
                idealWidth: 200,
                maxWidth: 300,
                minHeight: 50,
                alignment: .leading
            )
            .background(
                RoundedRectangle(cornerRadius: 12)
                    .fill(Color.blue.opacity(0.15))
            )
            .overlay(
                RoundedRectangle(cornerRadius: 12)
                    .stroke(Color.blue.opacity(0.4))
            )
    }
}


#Preview {
    ElapsedTime()
}

可以设置 导航标题 以及 显示模式。

List selection

让 List 支持“选中行”这件事

.tag 用来告诉 SwiftUI:这个 View 对应的“选中值”是什么, 它通常配合这些一起用:

单选 selection(macOS / iPadOS / visionOS)

import SwiftUI

struct ElapsedTime: View {
    @State private var selection: String?

    var body: some View {
        List(selection: $selection) {
            Text("Apple").tag("Apple")
            Text("Banana").tag("Banana")
            Text("Orange").tag("Orange")
        }
        .frame(minWidth: 200, minHeight: 200)

        Text("Selected: \(selection ?? "None")")
    }
}

#Preview {
    ElapsedTime()
}

多选 selection Set

struct ContentView: View {
    @State private var selection = Set<String>()

    var body: some View {
        List(selection: $selection) {
            Text("A").tag("A")
            Text("B").tag("B")
            Text("C").tag("C")
        }

        Text("Selected: \(selection.sorted().joined(separator: ", "))")
    }
}

iOS 上强制启用 selection EditMode,SwiftUI:不让你点选 你:偏要, SwiftUI:行吧,那你进入编辑模式

struct ContentView: View {
    @State private var selection = Set<String>()
    @State private var editMode: EditMode = .active

    var body: some View {
        List(selection: $selection) {
            Text("A").tag("A")
            Text("B").tag("B")
            Text("C").tag("C")
        }
        .environment(\.editMode, $editMode)
    }
}

用 Model 做 selection

struct Item: Identifiable, Hashable {
    let id = UUID()
    let name: String
}

struct ContentView: View {
    let items = [
        Item(name: "One"),
        Item(name: "Two"),
        Item(name: "Three")
    ]

    @State private var selection: Item.ID?

    var body: some View {
        List(items, selection: $selection) { item in
            Text(item.name)
                .tag(item.id)
        }
    }
}

三种设备的 SDK

iPhone、iPad 背后的 SDK 都是 iOS、而 Mac SDK 是 macOS。

mac 是可以支持运行 iOS 应用的。(Designed for iPad)

.contextMenu 与 .swipeActions

SwiftUI 的 .contextMenu 就是那个“长按/右键弹菜单”的玩意儿,本质是给 View 挂一个隐藏操作入口。你可以理解成:界面上不想占位置的按钮,全塞这里了。

Text("长按我")
    .contextMenu {
        Button("复制") {
            print("copy")
        }

        Button("删除") {
            print("delete")
        }
    }

效果:

import SwiftUI

struct Item: Identifiable {
    let id = UUID()
    var name: String
    var isFavorite: Bool
}

struct ContentView: View {
    @State private var items: [Item] = [
        Item(name: "Apple", isFavorite: false),
        Item(name: "Banana", isFavorite: true),
        Item(name: "Orange", isFavorite: false)
    ]

    var body: some View {
        NavigationStack {
            List {
                ForEach($items) { $item in
                    HStack {
                        Image(systemName: item.isFavorite ? "heart.fill" : "heart")
                            .foregroundStyle(item.isFavorite ? .red : .gray)

                        Text(item.name)

                        Spacer()

                        if item.isFavorite {
                            Text("⭐️")
                        }
                    }
                    .contentShape(Rectangle()) // 不然空白处点不到
                    .contextMenu {
                        Button {
                            item.isFavorite.toggle()
                        } label: {
                            Label(
                                item.isFavorite ? "取消收藏" : "收藏",
                                systemImage: item.isFavorite ? "heart.slash" : "heart"
                            )
                        }

                        Button {
                            print("复制 \(item.name)")
                        } label: {
                            Label("复制", systemImage: "doc.on.doc")
                        }

                        Button(role: .destructive) {
                            delete(item)
                        } label: {
                            Label("删除", systemImage: "trash")
                        }
                    }
                    .swipeActions(edge: .trailing) {
                        Button(role: .destructive) {
                            delete(item)
                        } label: {
                            Label("删除", systemImage: "trash")
                        }

                        Button {
                            item.isFavorite.toggle()
                        } label: {
                            Label("收藏", systemImage: "heart")
                        }
                        .tint(.yellow)
                    }
                }
            }
            .navigationTitle("水果列表")
        }
    }

    private func delete(_ item: Item) {
        items.removeAll { $0.id == item.id }
    }
}

#Preview {
    ContentView()
}

@Bindable

@Bindable 是 SwiftUI(尤其是配合 Observation / @Observable 新模型)里用来“把可观察对象拆出来变成绑定”的工具。

import SwiftUI
import Observation

@Observable
class UserModel {
    var name: String = ""
}

struct ContentView: View {
    @State var user = UserModel()

    var body: some View {
        UserView(user: user)
    }
}

struct UserView: View {
    @Bindable var user: UserModel

    var body: some View {
        TextField("name", text: $user.name)
    }
}

Form

SwiftUI Form 本质上就是一个“系统风格的表单容器”,专门用来做设置页、输入页、配置页这种——不是让你搞 UI 创意的,是让你老老实实填数据的。

系统已经帮你把“丑但统一”的样式都定好了,你只管塞内容。

基本结构

import SwiftUI

struct ContentView: View {
    @State private var username = ""
    @State private var enablePush = true

    var body: some View {
        Form {
            Section(header: Text("账号")) {
                TextField("用户名", text: $username)
            }

            Section(header: Text("设置")) {
                Toggle("推送通知", isOn: $enablePush)
            }
        }
    }
}

#Preview {
    ContentView()
}

Form 能放什么

基本上 SwiftUI 控件都能塞:

Navigation 里常见用法

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationStack {
            Form {
                Section("账户") {
                    NavigationLink("修改密码") {
                        Text("Password View")
                    }
                }
            }
            .navigationTitle("设置")
        }
    }
}

#Preview {
    ContentView()
}

Form 综合样例 Demo

import SwiftUI

struct ContentView: View {

    // MARK: - Account
    @State private var username: String = ""
    @State private var email: String = ""
    @State private var password: String = ""

    // MARK: - Preferences
    @State private var enableNotification: Bool = true
    @State private var darkMode: Bool = false
    @State private var selectedLanguage: String = "中文"

    // MARK: - Profile
    @State private var age: Int = 18
    @State private var birthday: Date = Date()

    // MARK: - Danger zone
    @State private var showResetAlert = false

    let languages = ["中文", "English", "日本語"]

    var body: some View {
        NavigationStack {
            Form {

                // =========================
                // 账号信息
                // =========================
                Section("账号信息") {
                    TextField("用户名", text: $username)
                        .textInputAutocapitalization(.never)

                    TextField("邮箱", text: $email)
                        .keyboardType(.emailAddress)
                        .textInputAutocapitalization(.never)

                    SecureField("密码", text: $password)
                }

                // =========================
                // 偏好设置
                // =========================
                Section("偏好设置") {

                    Toggle("推送通知", isOn: $enableNotification)

                    Toggle("深色模式", isOn: $darkMode)

                    Picker("语言", selection: $selectedLanguage) {
                        ForEach(languages, id: \.self) { lang in
                            Text(lang)
                        }
                    }
                }

                // =========================
                // 个人信息
                // =========================
                Section("个人信息") {

                    Stepper(value: $age, in: 1...120) {
                        Text("年龄:\(age)")
                    }

                    DatePicker(
                        "生日",
                        selection: $birthday,
                        displayedComponents: .date
                    )
                }

                // =========================
                // 操作区
                // =========================
                Section {

                    Button("保存设置") {
                        print("保存数据")
                    }

                }

                // =========================
                // 危险操作区
                // =========================
                Section {
                    Button(role: .destructive) {
                        showResetAlert = true
                    } label: {
                        Text("重置所有数据")
                    }
                } footer: {
                    Text("此操作不可恢复,请谨慎")
                }
            }
            .navigationTitle("设置")
            .alert("确认重置?", isPresented: $showResetAlert) {
                Button("取消", role: .cancel) { }
                Button("重置", role: .destructive) {
                    resetAll()
                }
            }
        }
    }

    private func resetAll() {
        username = ""
        email = ""
        password = ""
        enableNotification = true
        darkMode = false
        selectedLanguage = "中文"
        age = 18
        birthday = Date()
    }
}

#Preview {
    ContentView()
}