NavigationView
是SwiftUI應用的一個重要組件预伺,它允許我們輕松地push
和pop
屏幕,以清晰脏嚷、分層的方式向用戶呈現(xiàn)信息瞒御。在本文中,我想演示在應用程序中使用NavigationView
的所有方法趾唱,包括設置標題和添加按鈕等簡單的事情蜻懦,但也包括編程導航、創(chuàng)建分割視圖悠咱,甚至處理其他蘋果平臺征炼,如macOS和watchOS。
有標題的基礎NavigationView
要開始使用NavigationView,你應該把你想要顯示的內容包裹在里面逗宜,像這樣:
struct ContentView: View {
var body: some View {
NavigationView {
Text("Hello, World!")
}
}
}
對于簡單的導航布局應該在我們視圖的頂層纺讲,但如果你在TabView
中使用它們那么導航視圖應該在標簽視圖中。
在學習SwiftUI時逢渔,有一件事讓人感到困惑乡括,那就是我們如何給導航視圖添加標題:
NavigationView {
Text("Hello, World!")
.navigationBarTitle("Navigation")
}
你可能注意到,為何navigationBarTitle()
修飾符附屬到text的視圖上盲赊,而不是導航視圖敷扫?這是有意為之的,并且是在這里添加標題的正確方法绘迁。
您可以看到,導航視圖讓我們通過從右邊緣滑動內容來顯示新屏幕棠赛。每個屏幕都可以有自己的標題将硝,而SwiftUI的工作就是確保標題一直顯示在導航視圖中——你會看到舊的標題會動畫消失,而新的標題出現(xiàn)了痰腮。
現(xiàn)在想想這個律罢,如果我們把標題直接附加到導航視圖,這被說是"這是固定的標題"沧踏。通過將標題附加到導航視圖內的內容巾钉,SwiftUI可以隨著內容的改變而改變標題。
提示:你可以在導航視圖中的任何視圖上使用navigationBarTitle()
,它不需要是最外層的潦匈。
通過添加displayMode
參數(shù)赚导,可以自定義標題的顯示方式。有三種選項:
-
.large
選項顯示大標題凰锡,這對于導航堆棧的頂級視圖很有用圈暗。 -
.inline
選項顯示小標題,這對于導航堆棧中的次要或后續(xù)視圖很有用菩掏。 -
.automatic
選項是默認選項昵济,并使用前一個視圖使用的任何內容。
對于大多數(shù)應用程序瞧栗,你應該依賴.automatic
選項來創(chuàng)建你的初始視圖,你可以完全跳過displayMode
參數(shù):
.navigationBarTitle("Navigation")
對于所有被推到導航堆棧上的視圖挣惰,你通常會使用.inline
選項殴边,像這樣:
.navigationBarTitle("Navigation", displayMode: .inline)
跳轉新的視圖
導航視圖使用NavigationLink
顯示新的屏幕锤岸,用戶可以通過點擊它們的內容或通過編程啟用它們來觸發(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")
記住持钉,這將禁用藍色調篱昔,這意味著圖像將不再具有交互性州刽。
視圖之間傳遞數(shù)據
當您使用NavigationLink
將一個新視圖推入導航堆棧時,您可以傳遞新視圖工作所需的任何參數(shù)辨绊。
例如匹表,如果我們拋硬幣宣鄙,并希望用戶選擇正面或反面默蚌,我們可能會有這樣的結果視圖:
struct ResultView: View {
var choice: String
var body: some View {
Text("You chose \(choice)")
}
}
然后在內容視圖中,我們可以顯示兩個不同的導航鏈接:一個以“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ù)挠铲,允許我們讀取或寫入當前導航鏈接是否處于活動狀態(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ā)生的原因窘面,而不是用戶與導航鏈接本身內的任何東西進行交互财边。
顯然点骑,使用多個布爾值來跟蹤不同的導航目的地是很困難的谍夭,所以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ù)據褂删。關鍵是要確保使用附加到導航視圖本身的environmentObject()
修飾符,而不是導航視圖內部的東西缅帘。
為了演示這一點,我們可以首先定義一個簡單的觀察對象逗栽,它將承載我們的數(shù)據:
class User: ObservableObject {
@Published var score = 0
}
然后我們可以創(chuàng)建一個細節(jié)視圖來顯示使用環(huán)境對象的數(shù)據失暂,同時也提供了一種增加分數(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
往枷。
提示:在生產應用程序中,您應該注意為視圖本地創(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
}
)
}
}
}
如果你想在左邊和右邊都有一個按鈕拾弃,只需要傳遞leading
和trailing
參數(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ū)域,所以在它們周圍添加一些內邊距是一個好主意携栋,使它們更容易點擊搭盾。
自定義導航欄
我們有很多方法可以自定義導航條,比如控制它的字體font婉支、顏色color或可見性visibility鸯隅。然而向挖,現(xiàn)在SwiftUI內部對這一功能的支持有點不足蝌以,事實上只有兩個修飾符你可以不添加到UIKit中:
-
navigationBarHidden()
修飾符讓我們可以控制整個欄是可見還是隱藏。 -
navigationBarBackButtonHidden()
修飾符讓我們可以控制返回按鈕是隱藏還是可見何之,這對于你想讓用戶在返回之前主動做出選擇很有幫助跟畅。
與navigationBarTitle()
類似,這兩個修飾符都附加在導航視圖內部的視圖上溶推,而不是導航視圖本身徊件。有些令人困惑的是,這與需要放在導航視圖上的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旋轉到橫屏赊淑,你會看到文本視圖消失。
SwiftUI會自動考慮橫向導航視圖來形成一個主細節(jié)拆分視圖仅讽,兩個屏幕可以并排顯示陶缺。同樣,只有在有足夠空間的情況下洁灵,這種情況才會發(fā)生在較大的iPhones和iPads上饱岸,但它仍然經常會讓人感到困惑。
首先徽千,你可以按照SwiftUI所期望的方式解決這個問題伶贰,在你的NavigationView
中提供兩個視圖,像這樣:
struct ContentView: View {
var body: some View {
NavigationView {
Text("Primary")
Text("Secondary")
}
}
}
當它在大型iPhone上橫屏運行時罐栈,你會看到“Secondary”占據了所有屏幕黍衙,導航欄按鈕在滑動時顯示主視圖。
在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
闷哆,簡單地將其內容包裝在一個平凡的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)建跨平臺應用時避免不必要的麻煩。