SwiftUI:NavigationView

NavigationView是SwiftUI應用的一個重要組件,它允許我們輕松地pushpop屏幕,以清晰、分層的方式向用戶呈現(xiàn)信息。在本文中,我想演示在應用程序中使用NavigationView的所有方法,包括設置標題和添加按鈕等簡單的事情,但也包括編程導航、創(chuàng)建分割視圖,甚至處理其他蘋果平臺,如macOS和watchOS。

有標題的基礎NavigationView

要開始使用NavigationView,你應該把你想要顯示的內(nèi)容包裹在里面,像這樣:

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("Hello, World!")
        }
    }
}

對于簡單的導航布局應該在我們視圖的頂層,但如果你在TabView中使用它們那么導航視圖應該在標簽視圖中。
在學習SwiftUI時,有一件事讓人感到困惑,那就是我們?nèi)绾谓o導航視圖添加標題:

NavigationView {
    Text("Hello, World!")
        .navigationBarTitle("Navigation")
}

你可能注意到,為何navigationBarTitle()修飾符附屬到text的視圖上,而不是導航視圖?這是有意為之的,并且是在這里添加標題的正確方法。

您可以看到,導航視圖讓我們通過從右邊緣滑動內(nèi)容來顯示新屏幕。每個屏幕都可以有自己的標題,而SwiftUI的工作就是確保標題一直顯示在導航視圖中——你會看到舊的標題會動畫消失,而新的標題出現(xiàn)了。

現(xiàn)在想想這個,如果我們把標題直接附加到導航視圖,這被說是"這是固定的標題"。通過將標題附加到導航視圖內(nèi)的內(nèi)容,SwiftUI可以隨著內(nèi)容的改變而改變標題。

提示:你可以在導航視圖中的任何視圖上使用navigationBarTitle(),它不需要是最外層的。

通過添加displayMode參數(shù),可以自定義標題的顯示方式。有三種選項:

  1. .large選項顯示大標題,這對于導航堆棧的頂級視圖很有用。
  2. .inline選項顯示小標題,這對于導航堆棧中的次要或后續(xù)視圖很有用。
  3. .automatic選項是默認選項,并使用前一個視圖使用的任何內(nèi)容。

對于大多數(shù)應用程序,你應該依賴.automatic選項來創(chuàng)建你的初始視圖,你可以完全跳過displayMode參數(shù):

.navigationBarTitle("Navigation")

對于所有被推到導航堆棧上的視圖,你通常會使用.inline選項,像這樣:

.navigationBarTitle("Navigation", displayMode: .inline)

跳轉(zhuǎn)新的視圖

導航視圖使用NavigationLink顯示新的屏幕,用戶可以通過點擊它們的內(nèi)容或通過編程啟用它們來觸發(fā)導航視圖。

NavigationLink功能之一是你可以push到任何視圖——可以是你選擇的自定義視圖,也可以是SwiftUI的原始視圖之一(如果你只是在創(chuàng)建原型的話)。
例如,它直接push到一個文本視圖:

NavigationView {
    NavigationLink(destination: Text("Second View")) {
        Text("Hello, World!")
    }
    .navigationBarTitle("Navigation")
}

因為我在我的導航鏈接中使用了文本視圖,SwiftUI會自動將文本設置為藍色,以向用戶表明它是交互式的。這是一個非常有用的功能,但它也會帶來一個無用的副作用:如果你在導航鏈接中使用一個image圖像,你可能會發(fā)現(xiàn)image圖像變成藍色!

要嘗試一下,可以在項目的asset目錄中添加兩張圖片——一張是照片,另一張是帶有一些透明度的形狀。我添加我的頭像和Swift的logo,并像這樣使用它們:

NavigationLink(destination: Text("Second View")) {
    Image("hws")
}
.navigationBarTitle("Navigation")

我添加的圖像是紅色的,但當我運行應用程序時,SwiftUI將把它涂成藍色——這是為了幫助用戶,顯示圖像是交互式的。然而,這張圖片是不透明的,SwiftUI讓透明部分保持原樣,這樣你仍然可以清楚地看到logo。

如果我用我的照片代替,結果會更糟:

NavigationLink(destination: Text("Second View")) {
    Image("Paul")
}
.navigationBarTitle("Navigation")

由于這是一張沒有任何透明度的照片,所以SwiftUI把整個物體涂成了藍色——現(xiàn)在它看起來就像一個藍色的正方形。
如果你想讓SwiftUI使用你的圖像的原始顏色,你應該附加一個renderingMode()修飾符,像這樣:

NavigationLink(destination: Text("Second View")) {
    Image("hws")
        .renderingMode(.original)
}
.navigationBarTitle("Navigation")

記住,這將禁用藍色調(diào),這意味著圖像將不再具有交互性。

視圖之間傳遞數(shù)據(jù)

當您使用NavigationLink將一個新視圖推入導航堆棧時,您可以傳遞新視圖工作所需的任何參數(shù)。

例如,如果我們拋硬幣,并希望用戶選擇正面或反面,我們可能會有這樣的結果視圖:

struct ResultView: View {
    var choice: String

    var body: some View {
        Text("You chose \(choice)")
    }
}

然后在內(nèi)容視圖中,我們可以顯示兩個不同的導航鏈接:一個以“Heads”作為選擇創(chuàng)建ResultView,另一個以“Tails”為選擇。這些值必須在創(chuàng)建結果視圖時傳入,如下所示:

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack(spacing: 30) {
                Text("You're going to flip a coin – do you want to choose heads or tails?")

                NavigationLink(destination: ResultView(choice: "Heads")) {
                    Text("Choose Heads")
                }

                NavigationLink(destination: ResultView(choice: "Tails")) {
                    Text("Choose Tails")
                }
            }
            .navigationBarTitle("Navigation")
        }
    }
}

SwiftUI總是會確保你提供正確的值來初始化你的詳細視圖。

程序化的導航

SwiftUI的NavigationLink有第二個初始化方法,它有一個isActive參數(shù),允許我們讀取或?qū)懭氘斍皩Ш芥溄邮欠裉幱诨顒訝顟B(tài)。實際上,這意味著我們可以通過編程方式觸發(fā)導航鏈接的激活,方法是將它所監(jiān)視的狀態(tài)設置為true。

例如,這會創(chuàng)建一個空的導航鏈接,并將其綁定到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ā)時將isShowingDetailView設置為true的——這是導航操作發(fā)生的原因,而不是用戶與導航鏈接本身內(nèi)的任何東西進行交互。

顯然,使用多個布爾值來跟蹤不同的導航目的地是很困難的,所以SwiftUI提供了另一種選擇:我們可以為每個導航鏈接添加一個標記,然后使用單個屬性控制哪個鏈接被觸發(fā)。
作為一個例子,這將顯示兩個細節(jié)視圖中的一個,這取決于哪個按鈕被按下:

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")
        }
    }
}

值得一提的是,你可以使用state屬性來dismiss視圖和present視圖。例如,我們可以編寫代碼來創(chuàng)建一個顯示detail屏幕的可點擊導航鏈接,但也可以在兩秒鐘后將isShowingDetailView設為false。實際上,這意味著你可以啟動應用程序,手動點擊鏈接來顯示第二個視圖,然后短暫暫停后,你會自動回到上一個屏幕。

例如:

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
            }
        }
    }
}

使用environment傳值

NavigationView自動與它所呈現(xiàn)的任何子視圖共享它的環(huán)境,這使得即使在很深的導航堆棧中也很容易共享數(shù)據(jù)。關鍵是要確保使用附加到導航視圖本身的environmentObject()修飾符,而不是導航視圖內(nèi)部的東西。

為了演示這一點,我們可以首先定義一個簡單的觀察對象,它將承載我們的數(shù)據(jù):

class User: ObservableObject {
    @Published var score = 0
}

然后我們可以創(chuàng)建一個細節(jié)視圖來顯示使用環(huán)境對象的數(shù)據(jù),同時也提供了一種增加分數(shù)的方法:

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實例,它被注入到導航視圖環(huán)境中,這樣它就可以在任何地方共享了:

struct ContentView: View {
    @StateObject 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)
    }
}

記住,environment對象將被導航視圖所呈現(xiàn)的所有視圖所共享,這意味著如果ChangeView顯示了它自己的詳情視圖,它也將會被注入environment。

提示:在生產(chǎn)應用程序中,您應該注意為視圖本地創(chuàng)建引用類型,并且應該為它們創(chuàng)建一個單獨的模型層。

添加導航欄按鈕

我們可以在導航視圖中同時添加leading按鈕和trailing按鈕,在任意一側或兩側使用一個或多個按鈕。如果你愿意,這些可以是標準的按鈕視圖,但是你也可以使用導航鏈接。

例如,這創(chuàng)建了一個trailing導航欄按鈕,當點擊時可以修改分數(shù)值:

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
            }
    )

如果你想把兩個按鈕放在導航欄的同一側,你應該把它們放在HStack中,像這樣:

Text("Score: \(score)")
    .navigationBarTitle("Navigation")
    .navigationBarItems(
        trailing:
            HStack {
                Button("Subtract 1") {
                    self.score -= 1
                }
                Button("Add 1") {
                    self.score += 1
                }
            }
    )

提示:添加到導航欄的按鈕有一個非常小的可點擊區(qū)域,所以在它們周圍添加一些內(nèi)邊距是一個好主意,使它們更容易點擊。

自定義導航欄

我們有很多方法可以自定義導航條,比如控制它的字體font、顏色color或可見性visibility。然而,現(xiàn)在SwiftUI內(nèi)部對這一功能的支持有點不足,事實上只有兩個修飾符你可以不添加到UIKit中:

  • navigationBarHidden()修飾符讓我們可以控制整個欄是可見還是隱藏。
  • navigationBarBackButtonHidden()修飾符讓我們可以控制返回按鈕是隱藏還是可見,這對于你想讓用戶在返回之前主動做出選擇很有幫助。

navigationBarTitle()類似,這兩個修飾符都附加在導航視圖內(nèi)部的視圖上,而不是導航視圖本身。有些令人困惑的是,這與需要放在導航視圖上的statusBar(hidden:)修飾符不同。

為了演示這一點,這里有一些代碼,當一個按鈕被點擊時,顯示和隱藏導航欄和狀態(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)
    }
}

當需要自定義工具條本身時——它的顏色、字體等等——我們需要下拉到UIKit。這并不難,特別是如果你以前使用過UIKit,但在SwiftUI之后,這對系統(tǒng)有點沖擊。

自定義導航欄意味著需要在AppDelegate.swift中的didFinishLaunchingWithOptions方法中添加一些代碼。例如創(chuàng)建一個新的UINavigationBarAppearance實例,為它配置自定義的背景色、前景色和字體,然后將其分配給導航欄的appearance proxy:

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().scrollEdgeAppearance = appearance

我不會說這在SwiftUI的世界里很好,但事實就是這樣。

使用NavigationViewStyle創(chuàng)建拆分視圖

NavigationView最有趣的行為之一是它在更大的設備上處理拆分視圖的方式——通常是大尺寸的iPhones和iPads。

默認情況下,這種行為有點令人困惑,因為它可能會導致看似空白的屏幕。例如,在導航視圖中顯示一個單字標簽:

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("Primary")
        }
    }
}

這在豎屏時看起來很棒,但如果你用iPhone11 Pro Max旋轉(zhuǎn)到橫屏,你會看到文本視圖消失。
SwiftUI會自動考慮橫向?qū)Ш揭晥D來形成一個主細節(jié)拆分視圖,兩個屏幕可以并排顯示。同樣,只有在有足夠空間的情況下,這種情況才會發(fā)生在較大的iPhones和iPads上,但它仍然經(jīng)常會讓人感到困惑。

首先,你可以按照SwiftUI所期望的方式解決這個問題,在你的NavigationView中提供兩個視圖,像這樣:

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("Primary")
            Text("Secondary")
        }
    }
}

當它在大型iPhone上橫屏運行時,你會看到“Secondary”占據(jù)了所有屏幕,導航欄按鈕在滑動時顯示主視圖。
在iPad上,大多數(shù)時候你會同時看到兩個視圖,但如果空間受到限制,你會得到與豎屏iPhones上相同的push/pop行為。
當使用像這樣的兩個視圖時,主視圖中的任何NavigationLink都會自動顯示它的目的地,而不是輔助視圖。

另一種解決方案是要求SwiftUI每次只顯示一個視圖,而不管使用的是什么設備或方向。這是通過將一個新的StackNavigationViewStyle()實例傳遞給navigationViewStyle()修飾符來實現(xiàn)的,像這樣:

NavigationView {
    Text("Primary")
    Text("Secondary")
}
.navigationViewStyle(StackNavigationViewStyle())

這個解決方案在iPhone上運行得很好,但在iPad上會觸發(fā)全屏push導航,這會讓你的眼睛不舒服。

工作在macOS和watchOS

盡管SwiftUI是一個跨平臺的框架,但它讓你可以在任何地方應用你的技能,而不是在所有平臺上復制粘貼相同的代碼。區(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)限制了它的可見性,以便其他平臺按照預期工作,而僅僅添加一個簡單的VStack不會讓你的UI復雜化,所以它做起來很容易。

對于macOS,我們可以創(chuàng)建自己的navigationBarTitle()修飾符,它什么也不做,就像這樣:

#if os(macOS)
extension View {
    func navigationBarTitle(_ title: String) -> some View {
        self
    }
}
#endif

同樣,這對我們的UI工作增加的很少,而且Swift編譯器甚至可以完全優(yōu)化它。

這些改變看似微不足道,但卻能幫助我們在使用SwiftUI創(chuàng)建跨平臺應用時避免不必要的麻煩。

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

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

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