美文网首页有些文章不一定是为了上首页投稿
SwiftUI 导航栏 NavigationView 完全指南

SwiftUI 导航栏 NavigationView 完全指南

作者: 韦弦Zhy | 来源:发表于2020-05-28 19:17 被阅读0次
    SwiftUI

    NavigationView是SwiftUI应用程序最重要的组件之一,它使我们能够轻松推入和弹出屏幕,以清晰,分层的方式为用户呈现信息。在本文中,我想演示在应用程序中使用NavigationView的所有方式,包括诸如设置标题和添加按钮之类的简单操作,还包括程序化导航,创建拆分视图,甚至处理其他Apple平台,例如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的工作就是确保标题始终显示在导航视图中——您会看到旧标题动画消失,而新标题动画显示。

    现在考虑一下:如果我们将标题直接附加到导航视图,那么我们所说的是“这是给所有时间的固定标题”。通过将标题附加到导航视图内的任何内容,SwiftUI可以随着内容的更改来更改标题。

    提示:您可以在导航视图内的任何视图上使用navigationBarTitle();它不必是最外层的。

    您可以通过添加displayMode参数来自定义标题的显示方式。共有三个选项:

      1. .large选项显示大标题,这对于导航堆栈中的顶级视图很有用。
      1. .inline选项显示小标题,这些小标题对于导航堆栈中的辅助视图,第三视图或后续视图很有用。
      1. .automatic选项是默认选项(初始默认 .large),它使用前面视图使用的选项。

    对于大多数应用程序,您应该在初始视图中使用.automatic选项,只需完全跳过displayMode参数即可获取,此时为大标题:

    .navigationBarTitle("Navigation")
    

    对于所有推送到导航堆栈的视图,通常将使用.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中设置displayMode: .inline所以界面可能会有一点异常:

    示例1

    只有将界面上滑后才会和设置了displayMode: .inline一样出现我们预期的界面:

    示例2

    呈现新视图

    导航视图使用NavigationLink呈现新屏幕,可以通过用户点击其内容或以编程方式启用它们来触发。

    NavigationLink我最喜欢的功能之一是您可以推送到任何视图——它可以是您选择的自定义视图,但是如果您只是进行原型制作,它也可以是SwiftUI的原始视图之一。

    例如,这直接推送到文本视图:

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

    由于我在导航链接中使用了文本视图,因此SwiftUI会自动将文本显示为蓝色,以向用户表示它是交互式的。这是一个非常有用的功能,但可能会带来不利的副作用:如果在导航链接中使用图像,您可能会发现图像变成蓝色!

    要尝试此操作,请尝试将两张图像添加到项目的资产目录中——一张是照片,一张是具有一定透明度的形状。我添加了头像和Hacking with Swift 的logo,并像这样使用它们:

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

    我添加的图片是红色的,但是当我运行该应用程序时,SwiftUI会将其渲染成蓝色——试图提供帮助,向用户显示该图片是交互式的。但是,图像具有透明度,SwiftUI保持透明部分不变,因此您仍然可以清晰地看到一个蓝色的Logo。

    如果我改用我的头像,结果会更糟:

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

    由于这是一张照片,它没有任何透明度,因此SwiftUI将整个事物都染成蓝色——现在看起来就像一个蓝色正方形。

    如果您希望SwiftUI使用图片的原始颜色,则应为其附加一个renderingMode()修饰符,如下所示:

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

    请记住,这只是禁用蓝色,这意味着图像将看起来不具有交互性,但实际上它仍然可以点击。

    在视图之间传递数据

    使用NavigationLink将新视图推送到导航堆栈时,可以传递新视图需要工作的所有参数。

    例如,如果我们掷硬币并希望用户选择正面或反面,则可能会有类似以下结果视图:

    struct ResultView: View {
        var choice: String
    
        var body: some View {
            Text("你选择的是: \(choice)")
        }
    }
    

    然后,在内容视图中,我们可以显示两个不同的导航链接:一个以“正面”作为选择来创建ResultView,另一个是“反面”。在创建结果视图时,必须传递这些值,如下所示:

    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将始终确保您提供正确的值来初始化详细视图。

    以编程方式导航

    SwiftUI 的 NavigationLink具有第二个初始化器,该初始化器具有isActive参数,可让我们读取或写入导航链接当前是否处于活动状态。实际上,这意味着我们可以通过将状态设置为true来以编程方式触发导航链接的激活。

    例如,这将创建一个空的导航链接并将其绑定到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")
            }
        }
    }
    

    请注意,触发后,导航链接下方的按钮如何将isShowingDetailView设置为true ——这是使导航动作发生的原因,而不是用户与导航链接本身内部的任何内容进行交互的原因。

    显然,使用多个布尔值来跟踪不同的可能的导航目标将很困难,因此SwiftUI为我们提供了另一种选择:我们可以向每个导航链接添加一个标记,然后使用单个属性控制触发哪个标记。例如,这将显示两个详细视图之一,具体取决于所按下的按钮:

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

    值得补充的是,您可以使用状态属性隐藏视图就像显示视图一样。例如,我们可以编写代码来创建显示详细信息屏幕的可点击导航链接,还可以在两秒钟后将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
                }
            }
        }
    }
    

    使用环境传递值

    NavigationView会自动与其显示的任何子视图共享其环境,即使在非常深的导航堆栈中也可以轻松共享数据。关键是要确保您使用附加到导航视图本身的environmentObject()修饰符,而不是其中的某些东西。

    为了证明这一点,我们可以先定义一个简单的观察对象,该对象将承载我们的数据:

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

    然后,我们可以创建一个详细视图以使用环境对象显示该数据,同时还提供一种方法来增加那里的得分:

    struct ChangeView: View {
        @EnvironmentObject var user: User
    
        var body: some View {
            VStack {
                Text("Score: \(user.score)")
                Button("Increase") {
                    self.user.score += 1
                }
            }
        }
    }
    

    最后,我们可以让ContentView创建一个新的User实例,该实例将被注入到导航视图环境中,以便在任何地方共享:

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

    请记住,该环境对象将由导航视图提供的所有视图共享,这意味着如果ChangeView显示自己的详细信息屏幕,该屏幕也将继承该环境。

    提示:在生产应用程序中,您应该谨慎地在视图本地创建引用类型,并且应该为它们创建一个单独的模型层。

    添加导航栏按钮

    我们可以在导航视图中同时添加Leading导航按钮和Trailing导航按钮,可以在一侧或两侧使用一个或多个按钮。如果需要,这些按钮可以是标准按钮视图,但也可以使用导航链接。

    注:Leading 和 Trailing 是按书写顺序区分的,即从左往右书写习惯的地方,Leading在为左侧,从右往左则相反比如阿拉伯语。

    例如,这将创建一个右侧的导航栏按钮,在点击该按钮时会修改得分值:

    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参数,如下所示:

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

    提示:添加到导航栏中的按钮的可点击区域很小,因此最好在其周围添加一些填充(padding())以使其更易于点击。

    自定义导航栏

    我们可以通过多种方式自定义导航栏,例如控制其字体,颜色或可见性。但是,目前在SwiftUI中对此功能的支持还有些不足,实际上,只有两个修饰符可以使用,而无需使用UIKit:

    • navigationBarHidden()修饰符使我们可以控制整个导航栏是可见还是隐藏。
    • navigationBarBackButtonHidden()修饰符使我们可以控制后退按钮是隐藏还是可见,这对于希望用户在向后移动之前主动做出选择的时候很有用。

    navigationBarTitle()一样,这两个修饰符都附加到导航视图内部的视图上,而不是导航视图本身。令人困惑的是,这与statusBar(hidden:)修饰符不同,后者需要放置在导航视图中。

    为了说明这一点,下面的一些代码可以在点击按钮时显示和隐藏导航栏和状态栏:

    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之后,这会给系统带来一些改动。

    自定义栏本身意味着向 AppDelegate.swift 中的 didFinishLaunchingWithOptions 方法添加一些代码。例如,这将创建一个UINavigationBarAppearance的新实例,使用自定义背景色,前景色和字体对其进行配置,然后将其分配给导航栏代理:

    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 创建拆分视图

    NavigationView最有趣的行为之一是它还可以在较大的设备(通常是大尺寸的iPhone和iPad)上充当拆分视图的方式。

    默认情况下,此行为有些混乱,因为它可能导致看似空白的屏幕。例如,这在导航视图中显示一个单词标签:

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

    纵向显示效果不错,但是如果您使用iPhone 11 Pro Max将其旋转至横向,您会看到文本视图消失。

    发生的情况是,SwiftUI自动考虑横向导航视图以形成主要细节拆分视图,在该视图中可以并排显示两个屏幕。同样,只有在有足够空间的情况下,这才在大型iPhone和iPad上发生,但仍然经常令人困惑。

    首先,您可以通过在NavigationView中提供两个视图来解决SwiftUI期望的问题,如下所示:

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

    当它在横向的大型iPhone上运行时,您会看到“Secondary屏幕占据了整个屏幕,并带有导航栏按钮以将主视图作为幻灯片显示出来。在iPad上,大多数时候您会同时看到两种视图,但是如果空间有限,您将获得与竖屏iPhone相同的推/弹出行为。

    当使用两个这样的视图时,主视图中的任何NavigationLink都会自动显示其目的地,而不是辅助视图。

    另一种解决方案是要求SwiftUI一次只显示一个视图,而不管使用哪种设备或方向。这是通过将新的StackNavigationViewStyle()实例传递给navigationViewStyle()修饰符来完成的,如下所示:

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

    该解决方案在iPhone上运行得很好,但会在iPad上触发全屏导航,这在您看来并不令人愉快。

    在 macOS 和 watchOS 上工作

    尽管SwiftUI是跨平台框架,但它的目的是让您将技能应用到所有地方,而不是让您在所有平台上复制和粘贴相同的代码。区别是细微的,但对于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,我们可以创建自己的navigationBarTitle()修饰符,该修饰符根本不执行任何操作,如下所示:

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

    同样,这几乎没有增加UI工作量,Swift编译器甚至可以完全对其进行优化,从而完全不增加负担。

    这些更改可能看起来很小,但是它们在帮助我们避免使用SwiftUI创建跨平台应用程序时避免不必要的麻烦大有帮助。

    总结

    在本文中,我们研究了可以在SwiftUI中使用导航视图的多种方法,但是还有更多尝试的方法!

    如果您想学习所有SwiftUI,请查看我的100 Days of SwiftUI课程,该课程完全免费。

    译自 The Complete Guide to NavigationView in SwiftUI

    相关文章

      网友评论

        本文标题:SwiftUI 导航栏 NavigationView 完全指南

        本文链接:https://www.haomeiwen.com/subject/puyaahtx.html