SwiftUI導(dǎo)航欄完全指南

SwiftUI-NavigationView - 韋弦zhy

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)題的顯示方式。共有三個選項:

    1. .large選項顯示大標(biāo)題,這對于導(dǎo)航堆棧中的頂級視圖很有用。
    1. .inline選項顯示小標(biāo)題,這些小標(biāo)題對于導(dǎo)航堆棧中的輔助視圖,第三視圖或后續(xù)視圖很有用。
    1. .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所以界面可能會有一點異常:

示例1

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

示例2

呈現(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
                        }
                )
        }
    }
}

如果您想要左邊和右邊按鈕,只需傳遞leadingtrailing參數(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課程,該課程完全免費。

譯自 The Complete Guide to NavigationView in SwiftUI

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容