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 比较“背景板”
// 适合阅读类 / 内容展示类 appimport 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()
}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
...
}
}设置 .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()
}.navigationTitle.navigationBarTitleDisplayMode可以设置 导航标题 以及 显示模式。
让 List 支持“选中行”这件事
.tag 用来告诉 SwiftUI:这个 View 对应的“选中值”是什么,
它通常配合这些一起用:
List(selection:)Picker(selection:)TabView(selection:)单选 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)
}
}
}iPhone、iPad 背后的 SDK 都是 iOS、而 Mac SDK 是 macOS。
mac 是可以支持运行 iOS 应用的。(Designed for iPad)
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 是 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)
}
}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()
}基本上 SwiftUI 控件都能塞:
Navigation 里常见用法
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
Form {
Section("账户") {
NavigationLink("修改密码") {
Text("Password View")
}
}
}
.navigationTitle("设置")
}
}
}
#Preview {
ContentView()
}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()
}