
NavigationView是SwiftUI應(yīng)用程序最重要的組件之一,它使我們能夠輕松推入和彈出屏幕,以清晰,分層的方式為用戶呈現(xiàn)信息。在本文中,我想演示在應(yīng)用程序中使用NavigationView的所有方式,包括諸如設(shè)置標(biāo)題和添加按鈕之類的簡單操作,還包括程序化導(dǎo)航,創(chuàng)建拆分視圖,甚至處理其他Apple平臺,例如macOS和watchOS。
獲取帶有標(biāo)題的基本 NavigationView
要開始使用NavigationView,您應(yīng)該在要顯示的內(nèi)容周圍加上一個,例如:
struct ContentView: View {
var body: some View {
NavigationView {
Text("Hello, World!")
}
}
}
對于更簡單的布局,導(dǎo)航視圖應(yīng)該是視圖中的頂級內(nèi)容,但是如果您在TabView中使用它們,則導(dǎo)航視圖應(yīng)該在選項卡視圖中。
在學(xué)習(xí)SwiftUI時,人們會感到困惑的一件事是我們?nèi)绾螌?biāo)題附加到導(dǎo)航視圖:
NavigationView {
Text("Hello, World!")
.navigationBarTitle("Navigation")
}
請注意為什么navigationBarTitle()修飾符屬于Text視圖,而不屬于導(dǎo)航視圖?這是有意的,也是在此處添加標(biāo)題的正確方法。
您會看到,導(dǎo)航視圖使我們可以通過從右邊緣向內(nèi)滑動來顯示新的內(nèi)容屏幕。每個屏幕都可以有自己的標(biāo)題,SwiftUI的工作就是確保標(biāo)題始終顯示在導(dǎo)航視圖中——您會看到舊標(biāo)題動畫消失,而新標(biāo)題動畫顯示。
現(xiàn)在考慮一下:如果我們將標(biāo)題直接附加到導(dǎo)航視圖,那么我們所說的是“這是給所有時間的固定標(biāo)題”。通過將標(biāo)題附加到導(dǎo)航視圖內(nèi)的任何內(nèi)容,SwiftUI可以隨著內(nèi)容的更改來更改標(biāo)題。
提示:您可以在導(dǎo)航視圖內(nèi)的任何視圖上使用navigationBarTitle();它不必是最外層的。
您可以通過添加displayMode參數(shù)來自定義標(biāo)題的顯示方式。共有三個選項:
-
.large選項顯示大標(biāo)題,這對于導(dǎo)航堆棧中的頂級視圖很有用。
-
-
.inline選項顯示小標(biāo)題,這些小標(biāo)題對于導(dǎo)航堆棧中的輔助視圖,第三視圖或后續(xù)視圖很有用。
-
-
.automatic選項是默認選項(初始默認.large),它使用前面視圖使用的選項。
-
對于大多數(shù)應(yīng)用程序,您應(yīng)該在初始視圖中使用.automatic選項,只需完全跳過displayMode參數(shù)即可獲取,此時為大標(biāo)題:
.navigationBarTitle("Navigation")
對于所有推送到導(dǎo)航堆棧的視圖,通常將使用.inline選項,如下所示:
.navigationBarTitle("Navigation", displayMode: .inline)
可以嘗試一下代碼:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: NewTestView()) {
Text("Hello, World!")
}
.navigationBarTitle("Navigation")
}
}
}
struct NewTestView: View {
var body: some View {
List(0..<100) { item in
Text("\(item)")
}
.navigationBarTitle("NavigationNew")
}
}
因為沒有在NewTestView中設(shè)置displayMode: .inline所以界面可能會有一點異常:

只有將界面上滑后才會和設(shè)置了displayMode: .inline一樣出現(xiàn)我們預(yù)期的界面:

呈現(xiàn)新視圖
導(dǎo)航視圖使用NavigationLink呈現(xiàn)新屏幕,可以通過用戶點擊其內(nèi)容或以編程方式啟用它們來觸發(fā)。
NavigationLink我最喜歡的功能之一是您可以推送到任何視圖——它可以是您選擇的自定義視圖,但是如果您只是進行原型制作,它也可以是SwiftUI的原始視圖之一。
例如,這直接推送到文本視圖:
NavigationView {
NavigationLink(destination: Text("Second View")) {
Text("Hello, World!")
}
.navigationBarTitle("Navigation")
}
由于我在導(dǎo)航鏈接中使用了文本視圖,因此SwiftUI會自動將文本顯示為藍色,以向用戶表示它是交互式的。這是一個非常有用的功能,但可能會帶來不利的副作用:如果在導(dǎo)航鏈接中使用圖像,您可能會發(fā)現(xiàn)圖像變成藍色!
要嘗試此操作,請嘗試將兩張圖像添加到項目的資產(chǎn)目錄中——一張是照片,一張是具有一定透明度的形狀。我添加了頭像和Hacking with Swift 的logo,并像這樣使用它們:
NavigationLink(destination: Text("Second View")) {
Image("hws")
}
.navigationBarTitle("Navigation")
我添加的圖片是紅色的,但是當(dāng)我運行該應(yīng)用程序時,SwiftUI會將其渲染成藍色——試圖提供幫助,向用戶顯示該圖片是交互式的。但是,圖像具有透明度,SwiftUI保持透明部分不變,因此您仍然可以清晰地看到一個藍色的Logo。
如果我改用我的頭像,結(jié)果會更糟:
NavigationLink(destination: Text("Second View")) {
Image("zhy")
}
.navigationBarTitle("Navigation")
由于這是一張照片,它沒有任何透明度,因此SwiftUI將整個事物都染成藍色——現(xiàn)在看起來就像一個藍色正方形。
如果您希望SwiftUI使用圖片的原始顏色,則應(yīng)為其附加一個renderingMode()修飾符,如下所示:
NavigationLink(destination: Text("Second View")) {
Image("zhy")
.renderingMode(.original)
}
.navigationBarTitle("Navigation")
請記住,這只是禁用藍色,這意味著圖像將看起來不具有交互性,但實際上它仍然可以點擊。
在視圖之間傳遞數(shù)據(jù)
使用NavigationLink將新視圖推送到導(dǎo)航堆棧時,可以傳遞新視圖需要工作的所有參數(shù)。
例如,如果我們擲硬幣并希望用戶選擇正面或反面,則可能會有類似以下結(jié)果視圖:
struct ResultView: View {
var choice: String
var body: some View {
Text("你選擇的是: \(choice)")
}
}
然后,在內(nèi)容視圖中,我們可以顯示兩個不同的導(dǎo)航鏈接:一個以“正面”作為選擇來創(chuàng)建ResultView,另一個是“反面”。在創(chuàng)建結(jié)果視圖時,必須傳遞這些值,如下所示:
struct ContentView: View {
var body: some View {
NavigationView {
VStack(spacing: 30) {
Text("您將擲硬幣–您要選擇正面還是反面?")
NavigationLink(destination: ResultView(choice: "正面")) {
Text("選擇 正面")
}
NavigationLink(destination: ResultView(choice: "反面")) {
Text("選擇 反面")
}
}
.navigationBarTitle("Navigation")
}
}
}
SwiftUI將始終確保您提供正確的值來初始化詳細視圖。
以編程方式導(dǎo)航
SwiftUI 的 NavigationLink具有第二個初始化器,該初始化器具有isActive參數(shù),可讓我們讀取或?qū)懭雽?dǎo)航鏈接當(dāng)前是否處于活動狀態(tài)。實際上,這意味著我們可以通過將狀態(tài)設(shè)置為true來以編程方式觸發(fā)導(dǎo)航鏈接的激活。
例如,這將創(chuàng)建一個空的導(dǎo)航鏈接并將其綁定到isShowingDetailView屬性:
struct ContentView: View {
@State private var isShowingDetailView = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View"), isActive: $isShowingDetailView) { EmptyView() }
Button("Tap to show detail") {
self.isShowingDetailView = true
}
}
.navigationBarTitle("Navigation")
}
}
}
請注意,觸發(fā)后,導(dǎo)航鏈接下方的按鈕如何將isShowingDetailView設(shè)置為true ——這是使導(dǎo)航動作發(fā)生的原因,而不是用戶與導(dǎo)航鏈接本身內(nèi)部的任何內(nèi)容進行交互的原因。
顯然,使用多個布爾值來跟蹤不同的可能的導(dǎo)航目標(biāo)將很困難,因此SwiftUI為我們提供了另一種選擇:我們可以向每個導(dǎo)航鏈接添加一個標(biāo)記,然后使用單個屬性控制觸發(fā)哪個標(biāo)記。例如,這將顯示兩個詳細視圖之一,具體取決于所按下的按鈕:
struct ContentView: View {
@State private var selection: String? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View"), tag: "Second", selection: $selection) { EmptyView() }
NavigationLink(destination: Text("Third View"), tag: "Third", selection: $selection) { EmptyView() }
Button("Tap to show second") {
self.selection = "Second"
}
Button("Tap to show third") {
self.selection = "Third"
}
}
.navigationBarTitle("Navigation")
}
}
}
值得補充的是,您可以使用狀態(tài)屬性隱藏視圖就像顯示視圖一樣。例如,我們可以編寫代碼來創(chuàng)建顯示詳細信息屏幕的可點擊導(dǎo)航鏈接,還可以在兩秒鐘后將isShowingDetailView設(shè)置為false。實際上,這意味著您可以啟動該應(yīng)用程序,用手點擊該鏈接以顯示第二個視圖,然后短暫停留后,您將自動回到上一個屏幕。
例如:
struct ContentView: View {
@State private var isShowingDetailView = false
var body: some View {
NavigationView {
NavigationLink(destination: Text("Second View"), isActive: $isShowingDetailView) {
Text("Show Detail")
}
.navigationBarTitle("Navigation")
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.isShowingDetailView = false
}
}
}
}
使用環(huán)境傳遞值
NavigationView會自動與其顯示的任何子視圖共享其環(huán)境,即使在非常深的導(dǎo)航堆棧中也可以輕松共享數(shù)據(jù)。關(guān)鍵是要確保您使用附加到導(dǎo)航視圖本身的environmentObject()修飾符,而不是其中的某些東西。
為了證明這一點,我們可以先定義一個簡單的觀察對象,該對象將承載我們的數(shù)據(jù):
class User: ObservableObject {
@Published var score = 0
}
然后,我們可以創(chuàng)建一個詳細視圖以使用環(huán)境對象顯示該數(shù)據(jù),同時還提供一種方法來增加那里的得分:
struct ChangeView: View {
@EnvironmentObject var user: User
var body: some View {
VStack {
Text("Score: \(user.score)")
Button("Increase") {
self.user.score += 1
}
}
}
}
最后,我們可以讓ContentView創(chuàng)建一個新的User實例,該實例將被注入到導(dǎo)航視圖環(huán)境中,以便在任何地方共享:
struct ContentView: View {
@ObservedObject var user = User()
var body: some View {
NavigationView {
VStack(spacing: 30) {
Text("Score: \(user.score)")
NavigationLink(destination: ChangeView()) {
Text("Show Detail View")
}
}
.navigationBarTitle("Navigation")
}
.environmentObject(user)
}
}
請記住,該環(huán)境對象將由導(dǎo)航視圖提供的所有視圖共享,這意味著如果ChangeView顯示自己的詳細信息屏幕,該屏幕也將繼承該環(huán)境。
提示:在生產(chǎn)應(yīng)用程序中,您應(yīng)該謹慎地在視圖本地創(chuàng)建引用類型,并且應(yīng)該為它們創(chuàng)建一個單獨的模型層。
添加導(dǎo)航欄按鈕
我們可以在導(dǎo)航視圖中同時添加Leading導(dǎo)航按鈕和Trailing導(dǎo)航按鈕,可以在一側(cè)或兩側(cè)使用一個或多個按鈕。如果需要,這些按鈕可以是標(biāo)準(zhǔn)按鈕視圖,但也可以使用導(dǎo)航鏈接。
注:Leading 和 Trailing 是按書寫順序區(qū)分的,即從左往右書寫習(xí)慣的地方,Leading在為左側(cè),從右往左則相反比如阿拉伯語。
例如,這將創(chuàng)建一個右側(cè)的導(dǎo)航欄按鈕,在點擊該按鈕時會修改得分值:
struct ContentView: View {
@State private var score = 0
var body: some View {
NavigationView {
Text("Score: \(score)")
.navigationBarTitle("Navigation")
.navigationBarItems(
trailing:
Button("Add 1") {
self.score += 1
}
)
}
}
}
如果您想要左邊和右邊按鈕,只需傳遞leading和trailing參數(shù),如下所示:
Text("Score: \(score)")
.navigationBarTitle("Navigation")
.navigationBarItems(
leading:
Button("Subtract 1") {
self.score -= 1
},
trailing:
Button("Add 1") {
self.score += 1
}
)
如果要將兩個按鈕都放在導(dǎo)航欄的同一側(cè),則應(yīng)將它們放在HStack中,如下所示:
Text("Score: \(score)")
.navigationBarTitle("Navigation")
.navigationBarItems(
trailing:
HStack {
Button("Subtract 1") {
self.score -= 1
}
Button("Add 1") {
self.score += 1
}
}
)
提示:添加到導(dǎo)航欄中的按鈕的可點擊區(qū)域很小,因此最好在其周圍添加一些填充(padding())以使其更易于點擊。
自定義導(dǎo)航欄
我們可以通過多種方式自定義導(dǎo)航欄,例如控制其字體,顏色或可見性。但是,目前在SwiftUI中對此功能的支持還有些不足,實際上,只有兩個修飾符可以使用,而無需使用UIKit:
-
navigationBarHidden()修飾符使我們可以控制整個導(dǎo)航欄是可見還是隱藏。 -
navigationBarBackButtonHidden()修飾符使我們可以控制后退按鈕是隱藏還是可見,這對于希望用戶在向后移動之前主動做出選擇的時候很有用。
像navigationBarTitle()一樣,這兩個修飾符都附加到導(dǎo)航視圖內(nèi)部的視圖上,而不是導(dǎo)航視圖本身。令人困惑的是,這與statusBar(hidden:)修飾符不同,后者需要放置在導(dǎo)航視圖中。
為了說明這一點,下面的一些代碼可以在點擊按鈕時顯示和隱藏導(dǎo)航欄和狀態(tài)欄:
struct ContentView: View {
@State private var fullScreen = false
var body: some View {
NavigationView {
Button("Toggle Full Screen") {
self.fullScreen.toggle()
}
.navigationBarTitle("Full Screen")
.navigationBarHidden(fullScreen)
}
.statusBar(hidden: fullScreen)
}
}
現(xiàn)在到了自定義導(dǎo)航欄本身——顏色,字體等——我們需要回到 UIKit。這并不難,尤其是如果您以前使用過 UIKit,但是在SwiftUI之后,這會給系統(tǒng)帶來一些改動。
自定義欄本身意味著向 AppDelegate.swift 中的 didFinishLaunchingWithOptions 方法添加一些代碼。例如,這將創(chuàng)建一個UINavigationBarAppearance的新實例,使用自定義背景色,前景色和字體對其進行配置,然后將其分配給導(dǎo)航欄代理:
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = .red
let attrs: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.font: UIFont.monospacedSystemFont(ofSize: 36, weight: .black)
]
appearance.largeTitleTextAttributes = attrs
UINavigationBar.appearance().standardAppearance = appearance
我不會在SwiftUI世界中聲稱這很好,但這就是事實。
使用 NavigationViewStyle 創(chuàng)建拆分視圖
NavigationView最有趣的行為之一是它還可以在較大的設(shè)備(通常是大尺寸的iPhone和iPad)上充當(dāng)拆分視圖的方式。
默認情況下,此行為有些混亂,因為它可能導(dǎo)致看似空白的屏幕。例如,這在導(dǎo)航視圖中顯示一個單詞標(biāo)簽:
struct ContentView: View {
var body: some View {
NavigationView {
Text("Primary")
}
}
}
縱向顯示效果不錯,但是如果您使用iPhone 11 Pro Max將其旋轉(zhuǎn)至橫向,您會看到文本視圖消失。
發(fā)生的情況是,SwiftUI自動考慮橫向?qū)Ш揭晥D以形成主要細節(jié)拆分視圖,在該視圖中可以并排顯示兩個屏幕。同樣,只有在有足夠空間的情況下,這才在大型iPhone和iPad上發(fā)生,但仍然經(jīng)常令人困惑。
首先,您可以通過在NavigationView中提供兩個視圖來解決SwiftUI期望的問題,如下所示:
struct ContentView: View {
var body: some View {
NavigationView {
Text("Primary")
Text("Secondary")
}
}
}
當(dāng)它在橫向的大型iPhone上運行時,您會看到“Secondary屏幕占據(jù)了整個屏幕,并帶有導(dǎo)航欄按鈕以將主視圖作為幻燈片顯示出來。在iPad上,大多數(shù)時候您會同時看到兩種視圖,但是如果空間有限,您將獲得與豎屏iPhone相同的推/彈出行為。
當(dāng)使用兩個這樣的視圖時,主視圖中的任何NavigationLink都會自動顯示其目的地,而不是輔助視圖。
另一種解決方案是要求SwiftUI一次只顯示一個視圖,而不管使用哪種設(shè)備或方向。這是通過將新的StackNavigationViewStyle()實例傳遞給navigationViewStyle()修飾符來完成的,如下所示:
NavigationView {
Text("Primary")
Text("Secondary")
}
.navigationViewStyle(StackNavigationViewStyle())
該解決方案在iPhone上運行得很好,但會在iPad上觸發(fā)全屏導(dǎo)航,這在您看來并不令人愉快。
在 macOS 和 watchOS 上工作
盡管SwiftUI是跨平臺框架,但它的目的是讓您將技能應(yīng)用到所有地方,而不是讓您在所有平臺上復(fù)制和粘貼相同的代碼。區(qū)別是細微的,但對于NavigationView來說很重要:
- 在macOS上,
navigationBarTitle()修飾符不存在。 - 在watchOS上,
NavigationView本身不存在。
其中任何一種都會阻止您共享代碼,因為您的代碼無法編譯。但是,我們可以通過一些小的修改輕松地解決它們。
例如,在watchOS上,我們可以添加自己的空NavigationView,將其內(nèi)容簡單地包裝在瑣碎的VStack中:
#if os(watchOS)
struct NavigationView<Content: View>: View {
let content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(spacing: 0) {
content()
}
}
}
#endif
使用#if os(watchOS)會限制其可見性,以便其他平臺能夠按預(yù)期運行,僅添加簡單的VStack不會使您的UI復(fù)雜化,因此很容易。
對于macOS,我們可以創(chuàng)建自己的navigationBarTitle()修飾符,該修飾符根本不執(zhí)行任何操作,如下所示:
#if os(macOS)
extension View {
func navigationBarTitle(_ title: String) -> some View {
self
}
}
#endif
同樣,這幾乎沒有增加UI工作量,Swift編譯器甚至可以完全對其進行優(yōu)化,從而完全不增加負擔(dān)。
這些更改可能看起來很小,但是它們在幫助我們避免使用SwiftUI創(chuàng)建跨平臺應(yīng)用程序時避免不必要的麻煩大有幫助。
總結(jié)
在本文中,我們研究了可以在SwiftUI中使用導(dǎo)航視圖的多種方法,但是還有更多嘗試的方法!
如果您想學(xué)習(xí)所有SwiftUI,請查看我的100 Days of SwiftUI課程,該課程完全免費。