SwiftUI:NavigationView

NavigationView是SwiftUI應用的一個重要組件预伺,它允許我們輕松地pushpop屏幕,以清晰脏嚷、分層的方式向用戶呈現(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ù)赚导,可以自定義標題的顯示方式。有三種選項:

  1. .large選項顯示大標題凰锡,這對于導航堆棧的頂級視圖很有用圈暗。
  2. .inline選項顯示小標題,這對于導航堆棧中的次要或后續(xù)視圖很有用菩掏。
  3. .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
                        }
                )
        }
    }
}

如果你想在左邊和右邊都有一個按鈕拾弃,只需要傳遞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ū)域,所以在它們周圍添加一些內邊距是一個好主意携栋,使它們更容易點擊搭盾。

自定義導航欄

我們有很多方法可以自定義導航條,比如控制它的字體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)建跨平臺應用時避免不必要的麻煩。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末沫勿,一起剝皮案震驚了整個濱河市挨约,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌产雹,老刑警劉巖诫惭,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蔓挖,居然都是意外死亡夕土,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門瘟判,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怨绣,“玉大人,你說我怎么就攤上這事拷获±嫖酰” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵刀诬,是天一觀的道長咽扇。 經常有香客問我邪财,道長,這世上最難降的妖魔是什么质欲? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任树埠,我火速辦了婚禮,結果婚禮上嘶伟,老公的妹妹穿的比我還像新娘怎憋。我一直安慰自己,他們只是感情好九昧,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布绊袋。 她就那樣靜靜地躺著,像睡著了一般铸鹰。 火紅的嫁衣襯著肌膚如雪癌别。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天蹋笼,我揣著相機與錄音展姐,去河邊找鬼。 笑死剖毯,一個胖子當著我的面吹牛圾笨,可吹牛的內容都是我干的。 我是一名探鬼主播逊谋,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼擂达,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胶滋?” 一聲冷哼從身側響起板鬓,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎镀钓,沒想到半個月后穗熬,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡丁溅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年唤蔗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窟赏。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡妓柜,死狀恐怖,靈堂內的尸體忽然破棺而出涯穷,到底是詐尸還是另有隱情棍掐,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布拷况,位于F島的核電站作煌,受9級特大地震影響掘殴,放射性物質發(fā)生泄漏。R本人自食惡果不足惜粟誓,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一奏寨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鹰服,春花似錦病瞳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至设易,卻和暖如春逗柴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背亡嫌。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工嚎于, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掘而,地道東北人挟冠。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像袍睡,于是被迫代替她去往敵國和親知染。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內容