上一节我们介绍了 SwiftUI 的列表(List)视图,ForEach和Identifiable协议。
这一节我们将详细介绍在 SwiftUI 中如何使用 NavigationView(iOS 16 前)和 NavigationStack(iOS 16 及更高版本)
来构建导航界面,同时探讨如何自定义导航栏的外观、返回按钮,以及使用 SwiftUI 提供的 toolbar 等高级用法。
在 iOS 应用开发中,导航界面是非常常见且重要的部分。无论是显示一系列数据还是管理不同的功能模块,
都少不了使用导航栏和导航视图来为用户提供分层次的浏览体验。
本文将详细介绍在 SwiftUI 中如何使用 NavigationView(iOS 16 前)和 NavigationStack(iOS 16 及更高版本)
来构建导航界面,同时探讨如何自定义导航栏的外观、返回按钮,以及使用 SwiftUI 提供的 toolbar 等高级用法。
目录
- 导航界面简介
- 使用 NavigationStack 搭建导航视图
- 为导航视图添加标题
- 使用 NavigationLink 传递数据到详情视图
- 自定义导航栏外观
- 隐藏系统返回按钮并自定义返回按钮
- 实战案例:餐馆列表与详情视图
- 实战案例:文章列表与详情视图
- 总结
- 完整示例代码
导航界面简介
在绝大多数 iOS 应用中,用户都会和一个带有导航栏和列表的界面进行交互,通过点击列表项进入详情视图,再通过返回按钮回到上一级页面。在 SwiftUI 中,这种交互可以通过以下组件来完成:
- NavigationView(在 iOS 16 之前使用)
- NavigationStack(在 iOS 16 及更高版本中建议使用)
- NavigationLink(用于在列表或其他视图中触发导航行为)
- NavigationTitle / navigationBarTitle / toolbar 等修饰器,用于定制导航栏外观和功能
使用 NavigationStack 搭建导航视图
在 iOS 16(或更新版本)中,Apple 引入了新的 NavigationStack 组件来取代原有的 NavigationView。虽然 NavigationView 目前仍然可用,但建议使用 NavigationStack 以获得更好的未来兼容性。使用方式非常类似:
NavigationStack {
List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
}
}
.listStyle(.plain)
}
如果你在较早版本的 iOS 中使用 SwiftUI,可以使用:
NavigationView {
List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
}
}
.listStyle(.plain)
}
为导航视图添加标题
在 SwiftUI 中,可以使用修饰器 navigationTitle(或 navigationBarTitle)为导航栏添加标题,比如:
NavigationStack {
List {
ForEach(restaurants) { restaurant in
BasicImageRow(restaurant: restaurant)
}
}
.listStyle(.plain)
.navigationTitle("Restaurants")
}
这样就可以为导航界面设置一个大标题(Large Title)。如果你希望在界面中展示小标题,可以使用 设置大标题还是小标题 章节所述的方法来自定义显示模式。
使用 NavigationLink 传递数据到详情视图
在实际项目中,我们通常需要在点击列表项后跳转到详情视图,展示更详细的信息。SwiftUI 提供了 NavigationLink 来实现这一功能。
举例来说,如果我们有一个 RestaurantDetailView 视图需要接收 restaurant 参数,可以这样做:
List {
ForEach(restaurants) { restaurant in
NavigationLink(destination: RestaurantDetailView(restaurant: restaurant)) {
BasicImageRow(restaurant: restaurant)
}
}
}
.listStyle(.plain)
当用户点击列表中的某一行时,应用就会通过 NavigationLink 把对应的 restaurant 对象传递给详情视图进行渲染。
自定义导航栏外观
设置大标题还是小标题
在 SwiftUI 中,如果你想让导航栏以大标题的形式显示,可以使用 .navigationBarTitleDisplayMode(.automatic) 或者在 iOS 较早版本使用 .largeNavigationBarTitle。如果你想要小标题,则可以使用:
.navigationBarTitleDisplayMode(.inline)
.inline:小标题.large:大标题.automatic:系统自动决定
示例:
.navigationTitle("Restaurants")
.navigationBarTitleDisplayMode(.inline)
使用 UINavigationBarAppearance 修改字体和颜色
虽然 SwiftUI 的修饰器非常便捷,但它目前还不直接提供修改导航栏文字颜色和字体的原生 API。我们可以借助 UIKit 的 UINavigationBarAppearance 做到这一点。
在 SwiftUI 的 init() 方法中配置:
init() {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.largeTitleTextAttributes = [
.foregroundColor: UIColor.red,
.font: UIFont(name: "ArialRoundedMTBold", size: 35)!
]
navBarAppearance.titleTextAttributes = [
.foregroundColor: UIColor.red,
.font: UIFont(name: "ArialRoundedMTBold", size: 20)!
]
UINavigationBar.appearance().standardAppearance = navBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
UINavigationBar.appearance().compactAppearance = navBarAppearance
}
其中:
largeTitleTextAttributes:大标题的样式titleTextAttributes:普通标题的样式
通过为这三个属性(standardAppearance,scrollEdgeAppearance,compactAppearance)都赋值,可以保证导航栏在不同滚动状态下的样式一致。
修改返回按钮的图片及颜色
默认的返回按钮是一个蓝色的 chevron 图标。如果需要自定义返回按钮的图标,可以在 UINavigationBarAppearance 上调用 setBackIndicatorImage 方法。例如:
navBarAppearance.setBackIndicatorImage(
UIImage(systemName: "arrow.turn.up.left"),
transitionMaskImage: UIImage(systemName: "arrow.turn.up.left")
)
要修改返回按钮的颜色,可以在对应的 NavigationStack 或 NavigationView 中使用:
.tint(.black)
这样就可以将返回按钮的颜色改为黑色等你需要的颜色。
隐藏系统返回按钮并自定义返回按钮
除了通过 UIKit 接口修改返回按钮的图标,你还可以在 SwiftUI 层面隐藏系统返回按钮,然后使用一个自定义按钮来替代它,实现更多灵活的交互或视觉效果。
可以使用以下修饰器隐藏默认返回按钮:
.navigationBarBackButtonHidden(true)
然后在 .toolbar 中加入自己的按钮:
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
dismiss()
} label: {
Text("\(Image(systemName: "chevron.left")) 自定义返回")
.foregroundColor(.black)
}
}
}
这里使用了 SwiftUI 提供的 @Environment(\.dismiss) 来获取一个可以关闭当前视图的函数 dismiss(),只要在 iOS 15 或更高版本中就能使用。如果需要支持更早的版本,可以改用 @Environment(\.presentationMode) 并调用 presentationMode.wrappedValue.dismiss()。
实战案例:餐馆列表与详情视图
除了文章列表,我们也可以用同样的方式构建餐馆列表和详情视图。以下是一个餐馆列表的示例,它在 init() 中使用了 UINavigationBarAppearance 来自定义导航栏标题字体及颜色:
餐馆列表及自定义导航栏外观
struct ContentView: View {
init() {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.largeTitleTextAttributes = [
.foregroundColor: UIColor.red,
.font: UIFont(name: "ArialRoundedMTBold", size: 35)!
]
navBarAppearance.titleTextAttributes = [
.foregroundColor: UIColor.red,
.font: UIFont(name: "ArialRoundedMTBold", size: 20)!
]
UINavigationBar.appearance().standardAppearance = navBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
UINavigationBar.appearance().compactAppearance = navBarAppearance
}
var body: some View {
NavigationStack {
List {
ForEach(restaurants) { restaurant in
NavigationLink(destination: RestaurantDetailView(restaurant: restaurant)) {
BasicImageRow(restaurant: restaurant)
}
}
}
.listStyle(.plain)
.navigationBarTitle("Restaurants")
.navigationBarTitleDisplayMode(.automatic)
}
}
}
如上所示,我们把大标题的字体、颜色、大小都改为红色、ArialRoundedMTBold,并应用到不同的 appearance 属性上,实现大标题和小标题同样的风格。
自定义返回按钮及更多设置
在详情页面,我们可以隐藏系统默认的返回按钮并自定义一个带图标和文字的返回按钮,借助 SwiftUI 的 toolbar 修饰器:
struct RestaurantDetailView: View {
@Environment(\.dismiss) var dismiss
var restaurant: Restaurant
var body: some View {
VStack {
Image(restaurant.image)
.resizable()
.aspectRatio(contentMode: .fit)
Text(restaurant.name)
.font(.system(.title, design: .rounded))
Spacer()
}
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
dismiss()
}) {
Text("\(Image(systemName: "chevron.left")) \(restaurant.name)")
.foregroundColor(.black)
}
}
}
}
}
这样就可以让返回按钮既包含自定义的图标,也能携带文字说明,用户体验更友好。
实战案例:文章列表与详情视图
接下来让我们看一个完整的示例。假设我们有一个应用用来展示一系列文章,主界面是文章列表,详情界面则显示文章的全文内容。下面是一个示例项目的思路(下载示例工程可参考文末链接):
创建详情视图
我们先在 ArticleDetailView.swift 文件中创建详情视图:
struct ArticleDetailView: View {
var article: Article
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Image(article.image)
.resizable()
.aspectRatio(contentMode: .fit)
Group {
Text(article.title)
.font(.system(.title, design: .rounded))
.fontWeight(.black)
.lineLimit(3)
Text("By \(article.author)")
.font(.subheadline)
.foregroundColor(.secondary)
.uppercased()
}
.padding(.bottom, 0)
.padding(.horizontal)
Text(article.content)
.font(.body)
.padding()
.lineLimit(1000)
.multilineTextAlignment(.leading)
}
}
}
}
注意:为了在预览时不报错,可以在
#Preview中传入一个示例的Article对象。
为列表添加导航功能
然后我们在 ContentView.swift 中,把列表嵌入到 NavigationStack 中,
并使用 NavigationLink 导向详情视图:
struct ContentView: View {
var body: some View {
NavigationStack {
List(articles) { article in
NavigationLink(destination: ArticleDetailView(article: article)) {
ArticleRow(article: article)
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
.navigationTitle("Your Reading")
}
}
}
这样就实现了从文章列表到文章详情的导航,并把相应的 article 传递给详情页。