Navigation

  • Navigation概述
  • 如安在Navigation中嵌入View
  • Navigation的可修正状况
  • Navigation增加bar-Items
  • 怎么将新视图push到NavigationStack上
  • 点击List-cell-push新的视图
  • 关于Navigation programmatic
  • 怎么运用Codable协议保存和加载NavigationStach Paths
  • 创立一个两列或三列布局用NavigatinSplitView
  • Sidebar的显现和躲藏
  • 在NavigationSplitView中自定义一个宽度的视图
  • 自定义NavigationSplitView的显现方式
  • NavigationSplitView自动挑选视图优先级
  • 在View上创立Inspector

概述

文章首要分享SwiftUI Modifier的学习进程,将运用事例的办法进行说明。内容深入浅出,Navigation展现部分调试成果,不过测验代码是完全的。假如想要运转成果,能够移步Github下载code -> github事例链接

1、Navigation概述

导航是许多应用程序的中心,SwiftUI的简略、易用的方面在Navigation上做的十分好,这意味着运用NavigationLink、运用NavigationPath完全操控导航,运用NavigationSplitcView支持第二视图和第三视图布局等。 这意味着许多时候能够疏忽Navigation相关的场景,由于十分简略,专心于需求更多思考的部分,例如:

  • 制作具有合理默许值的可自定义的toolbar
  • 经过状况康复保存和加载用户的导航
  • 决定怎么显现侧边栏,或在需求时增加额定的Content视图
  • 结合NavigationSplitView和NavigationStack取得想要的结构
struct FFNavigation: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("最简略的Navigation") {
                    Text("So easy")
                }
            }
        }
    }
}

调试成果

SwiftUI基础篇Navigation
SwiftUI基础篇Navigation

2、如安在Navigation中嵌入View

SwiftUI的NavigationStack或多或少的映射到UIKit的UINavigationController,由于它显现内容,它能够处理视图之间的导航。

2.1、根底款式

以最简略的办法,将Text放入NavigationStack中

struct FFNavigationEmbedView: View {
    var body: some View {
        NavigationStack {
            Text("This is a great app")
        }
    }
}

2.2、.navigationTitle(“Welcome”)

可是,顶部导航栏是空的。因而,一般会在嵌入的任何内容上运用navigationTitle()修饰符,因而能够在屏幕顶部增加标题。

struct FFNavigationEmbedView: View {
    var body: some View {
        NavigationStack {
            Text("Swift")
                .navigationTitle("Welcome")
        }
    }
}

2.3、navigationBarTitleDisplayMode

还有第二个修饰符,navigationBarTitleDisplayMode(),能够操控是否运用大标题仍是小标题的内联标题。例如,默许情况下,任何视图以继承其大标题的显现方式呈现的,或许假如是初始视图,则运用大标题。可是,假如更愿意手动启用或禁用大标题,应该运用.navigationBarTitleDisoplayMode()

struct FFNavigationEmbedView: View {
    var body: some View {
        NavigationStack {
            Text("SwiftUI")
                .navigationTitle("Meta BBlv")
                .navigationBarTitleDisplayMode(.inline)
        }
    }
}

这将产生小的导航标题,但能够运用.large来设置一个大标题

调试成果

SwiftUI基础篇Navigation
SwiftUI基础篇Navigation
SwiftUI基础篇Navigation

3、Navigation的可修正状况

SwiftUI的NavigationStack能够运用navigationTitle()显现一个简略的字符串,但同一修饰符也可接受字符串绑定,以便能够经过点击来修正标题

struct FFNavigationEdit: View {
    @State private var title = "Wecome"
    var body: some View {
        //例如,显现“欢迎”的默许标题,能够点击该标题进行更改。
        NavigationStack {
            Text("Hello, World!")
                .navigationTitle($title)
                .navigationBarTitleDisplayMode(.inline)
            //正在修正某些内容的名称,我主张增加.toolvarRole(.editor)让SwiftUI知道你即将要做的的事情,以便动态更改的标题更精确。
                .toolbarRole(.editor)
        }
    }
}

只要当导航栏以内联方式运转时,导航标题修正才有用。仅适用于iOS和iPadOS

调试成果

SwiftUI基础篇Navigation
SwiftUI基础篇Navigation
SwiftUI基础篇Navigation

4、Navigation增加bar-Items

toolbar()修饰符能够将单个或多个item按钮增加到navigationStack的前面和后边,以及视图的其他部分。这些可能是可点击的按钮,但没有限制–能够增加任何类型的视图。

4.1、在导航条的后边增加了两个按钮

struct FFNavigationAddBehind: View {
    var body: some View {
        NavigationStack {
            Text("SwiftUI")
                .navigationTitle("Welcome")
                .toolbar(content: {
                    Button("About") {
                        print("About tapped!")
                    }
                    Button("Help") {
                        print("Help tapped!")
                    }
                })
        }
    }
}

4.2、在导航条的前面增加了一个按钮

假如想操控按钮的确切方位,能够经过将其包装在ToolBarItem并拟定所需的方位来做到这一点。例如,创立一个按钮,并强制将其放在导航栏前面。

struct FFNavigationAddFront: View {
    var body: some View {
        NavigationStack {
            Text("SwiftUI")
                .navigationTitle("Welcome")
                .toolbar(content: {
                    ToolbarItem(placement: .topBarLeading) {
                        Button("Help") {
                            print("Help tapped!")
                        }
                    }
                })
        }
    }
}

4.3、多个item放在相同的方位(ToolbarItenmGroup)

假如想在不同方位放置多个item-button,只需求依据屡次重复ToolBarItem,并每次指定不同的方位。要将多个item放在相同的方位,需求将他们包装在ToolbarItenmGroup

struct FFNavigationToolBarItem: View {
    var body: some View {
        NavigationStack {
            Text("SwiftUI")
                .navigationTitle("Welcome")
                .toolbar(content: {
                    ToolbarItemGroup(placement: .primaryAction) {
                        Button("About") {
                            print("About tapped!")
                        }
                        Button("Help") {
                            print("About tapped!")
                        }
                    }
                })
        }
    }
}

这运用.primaryAction,将依据平台(iOS等)以为最重要的按钮的方位来定位按钮。

4.4、primaryAction与secondaryAction优先级设置item

还有一个.secondaryAction放置,专为有用但不需求操作而规划,在iOS上,将导致该组中的按钮折叠为单个具体信息按钮

struct FFNavigationViewPlacement: View {
    var body: some View {
        Text("SwiftUI")
            .navigationTitle("Welcome")
            .toolbar(content: {
                ToolbarItemGroup(placement: .primaryAction) {
                    Button("About") {
                        print("About tapped!")
                    }
                    Button("Help") {
                        print("About tapped!")
                    }
                }
                ToolbarItemGroup(placement: .secondaryAction) {
                    Button("Settings") {
                        print("Credits tapped!")
                    }
                    Button("Email Me") {
                        print("Email tapped!")
                    }
                }
            })
    }
}

调试成果

SwiftUI基础篇Navigation
SwiftUI基础篇Navigation
SwiftUI基础篇Navigation

5、怎么将新视图push到NavigationStack上

SwiftUI能够运用NavigationLink将任何视图推送到navigationStack上,在其最简略的方式中,能够为其标题供给字符串和方针视图作为跟随闭包。

//创立一个简略的DetailView结构,然后经过NavigationStack显现它
struct DetailView_013: View {
    var body: some View {
        Text("This is the detail view")
    }
}
struct FFnavigationPushView: View {
    var body: some View {
        NavigationStack {
            VStack {
                NavigationLink("Show Detail View") {
                    DetailView_013()
                }
                //假如需求对Label进行更多的自定义,那么运用跟随闭包。例如,运用Label视图而不是简略的字符串
                NavigationLink {
                    DetailView_013()
                } label: {
                    Label("Show Detail View - Label", systemImage: "globe")
                }
                //SwiftUI将自动将链接款式设置为按钮,以便用户知道他们是交互式的。
                //能够经过将.buttonStule(.plain)应用在NavigationLink禁用此行为
            }
            .navigationTitle("navigation")
        }
    }
}

6、点击List-cellpush新的视图

SwiftUI的NavigationLink可在List-cell中运用,以便在点击cell时显现新的视图。假如NavigationLink包括整个cell,体系会自动理解为整个cell可点击,以显现新视图。有两种办法能够指定导航目的地,经过明确链接,或许假如方针是iOS16以及更高版别,能够运用navigationDestination()

struct PlayerView_013: View {
    let name: String
    var body: some View {
        Text("Select player: \(name)")
            .font(.largeTitle)
    }
}
struct FFNavigationForList: View {
    let players = [
        "Roy Kent",
        "Richard Montlaur",
        "Dani Rojas",
        "Jamie Tartt"
    ]
    var body: some View {
        //创立一个包括List的NavigationStack,答应用户挑选Player
        NavigationStack {
            List(players, id: \.self) { player in
                NavigationLink(player, value: player)
            }
            .navigationDestination(for: String.self, destination: PlayerView_013.init)
            .navigationTitle("Select a Player")
            Divider()
            //能够经过附加navigationDestination()来监督多种不同的数据类型
            List {
                NavigationLink("Show an integer", value: 29)
                NavigationLink("show a String", value: "Meta BBLv")
                NavigationLink("Show a Double", value: Double.pi)
            }
            .navigationDestination(for: Int.self) { Text("Received Int: \($0)") }
            .navigationDestination(for: String.self) { Text("Received String \($0)") }
            .navigationDestination(for: Double.self) { Text("Receicved Double \($0)") }
        }
    }
}

假如不需求高度自定义的导航,并且能够在iOS16以及更高的版别运转,强烈主张运用navigationDestination(),由于SwiftUI会懒加载方针视图。

7、关于Navigation programmatic

能够运用SwiftUI的NavigationLink以编程的办法将新视图推送到NavigationStack,这意味着能够在准备好时触发导航,而不是仅当点击了button或cell时。从iOS16以及更高的版别,咱们能够将一系列的Hashable数据直接传递给NavigationStack,以操控那些数据当时在stack上。

7.1、绑定Path,跟踪显现

struct FFNavigationProgrammatic: View {
    @State private var presentedNumbers = [1, 4, 8]
    var body: some View {
        //跟踪正在显现的数字,并首先将1、4、8push到stack上。
        NavigationStack(path: $presentedNumbers) {
            List(1..<50) { i in
                NavigationLink(value: i) {
                    Label("Row \(i)", systemImage: "\(i).circle")
                }
            }
            .navigationDestination(for: Int.self) { i in
                Text("Detail \(i)")
            }
            //当代码运转时,将看到“Detail8”,点回来看到“Detail 4”,点回来看到“Detail 1”,点回来看到List
        }
        .navigationTitle("NavigationForPath")
    }
}

7.2、在detail页面直接跳转下一个detail(驾考答题需求)

这种办法很强壮,由于能够在任何时候修正NavigationStack来push一个自定义视图。他是一个简略的数组,所以能够add、insert、delete item或许依据显现需求做使命操作。途径数组开端为空,在点击的detail中增加了一个button,能够直接在当时detailpush(n+1)个detail

struct FFNavigationProgrammaticForNext: View {
    @State private var presentedNumbers = [Int]()
    var body: some View {
        NavigationStack(path: $presentedNumbers) {
            List(1..<50) { i in
                NavigationLink(value: i) {
                    Label("Row \(i)", systemImage: "\(i).circle")
                }
                .navigationDestination(for: Int.self) { i in
                    VStack {
                        Text("Detail \(i)")
                        Button("Go to Next") {
                            presentedNumbers.append(i + 1)
                        }
                    }
                }
                .navigationTitle("NavigationForNext")
            }
        }
    }
}

7.3、NavigationPath

假如将一种很数据类型push到Stack,则能够运用简略数组作为navigation path,但假如需求heterogeneous数据,则能够运用名为NavigationPath的特殊类型擦除包装器。这能够处理任何可hash的数据,所以能够增加一些Staing、Int和custom struct,只需契合hashable都能够

struct FFNavigationProgrammaticForHash: View {
    @State private var navPath = NavigationPath()
    var body: some View {
        NavigationStack(path: $navPath) {
            Button("Jump to random") {
                navPath.append(Int.random(in: 1..<50))
            }
            List(1..<50) { i in
                NavigationLink(value: "Row \(i)") {
                    Label("row \(i)", systemImage: "\(i).circle")
                }
                .navigationDestination(for: Int.self) { i in
                    Text("Int detail \(i)")
                }
                .navigationDestination(for: String.self) { o in
                    Text("String detail \(i)")
                }
                .navigationTitle("NavigationForHashable")
            }
        }
    }
}

能够随意的调整path–在哪里附加了一个value,但假如需求的话,能够一次附加多个值,像旧的UIKit的pop到根视图操控器这样的东西,就像从你的途径中清楚一切东西相同的简略-navPath.removeLast(navpath.count)这样就能够做到。

调试成果

SwiftUI基础篇Navigation
SwiftUI基础篇Navigation
SwiftUI基础篇Navigation

8、怎么运用Codable协议保存和加载NavigationStach Paths

当运用NavigationPath对象管理SwiftUI的NavigationStack途径时,运用Codable协议保存和加载整个途径–能够存储完好的NavigationStack并在需求的时刻康复它,这样用户就能精确的回到离开的当地。最好的处理办法是将存储封装在一个独自的ObservableObject类中,这个类能够负责从视图中加载和保存途径数据。

构建PathStore

例如,这个类在创立时加载一个保存的途径,并在他的NavigationPath属性改变时保存途径

class PathStore: ObservableObject {
    @Published var path = NavigationPath() {
        didSet {
            save()
        }
    }
    private let savePath = URL.documentsDirectory.appending(path: "SavedPathStore")
    init() {
        if let data = try? Data(contentsOf: savePath) {
            if let decoded = try? JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) {
                path = NavigationPath(decoded)
            }
        }
    }
    func save() {
        guard let representation = path.codable else { return }
        do {
            let data = try JSONEncoder().encode(representation)
            try data.write(to: savePath)
        } catch {
            print("Failed to save navigation data")
        }
    }
}

这是一个整齐的可重用类,只需写入NavigationPath的数据的Codable类型,就能够了。

构建DetailViewCodable_013

创立一个简略的Detail视图,能够显现用户挑选的数字,同时经过挑选另一个数字进行更深的导航,然后将其与PathStore类一同运用,以便自动加载和保存导航

struct DetailViewCodable_013: View {
    var id: Int
    var body: some View {
        VStack {
            Text("View \(id)")
                .font(.largeTitle)
            NavigationLink("Junp to random", value: Int.random(in: 1...100))
        }
    }
}

调试代码

假如运转哪个代码,会看到能够浏览任意多的DetailView级别,数据会自动被存储–能够退出App并回来,导航历史将坚持完好。

struct FFNavigationCodable: View {
    @StateObject private var pathStore = PathStore()
    var body: some View {
        NavigationStack(path: $pathStore.path) {
            DetailViewCodable_013(id: 0)
                .navigationDestination(for: Int.self, destination: DetailViewCodable_013.init)
                .navigationTitle("Navigation")
        }
    }
}

调试成果

SwiftUI基础篇Navigation
SwiftUI基础篇Navigation
SwiftUI基础篇Navigation

9、创立一个两列或三列布局用NavigatinSplitView

SwiftUI的NavigationSplitView答应咱们在更大的设备上创立多列布局(iPad macOS和大屏幕的iPhone上),可是当空间有限时,将自动折叠成NavigationStack风格的布局。在最简略的方式中,应该供给侧边栏作为它的第一个跟随闭包,的细节视图作为他的第二个闭包。

9.1、Sidebar + Detail

struct FFNavigationSplitView: View {
    var body: some View {
        NavigationSplitView {
            List(1..<50) { i in
                NavigationLink("Row \(i)", value: i)
            }
            .navigationDestination(for: Int.self) {
                Text("Select row \($0)")
            }
            .navigationTitle("Split View")
        } detail: {
            Text("Please select a row")
        }
    }
}

在该代码中,“Please select a row”文本仅在用户尚未在侧边栏中进行挑选时显现,可是当用户进行挑选时,将自动被替换。navigationDestination()修饰符将自动在具体信息区域中显现其目的地视图。更棒的是,当空间有限时,会看到整个东西被平展成一个常规的NavigationStack。

调试成果

iPad调试,这图上传一直failure,太大了,想看调试成果,移步GitHub吧,下面的NavigationSplitView都是相同。

9.2、Sidebar + Content + Detail

NavigationSplitView能够在布局中增加第三个视图,它能够经过点击Button来显现。

struct FFNavigationSplitViewContent: View {
    var body: some View {
        NavigationSplitView {
            Text("Sidebar")
        } content: {
            Text("Primary")
        } detail: {
            Text("Detail View")
        }
    }
}

SwiftUI将自动显现一个按钮滑动在你的栏从屏幕的一边。

10、Sidebar的显现和躲藏

当在macOS上和iPad上运用NavigationSplitView时,SwiftUI运用NavigationSplitViewVisibility的enum来切换显现侧边栏、内容视图和具体视图。有四种方式能够挑选:

  • 在.detailonly方式下,具体视图将占用应用程序的一切可用屏幕空间
  • 在.doublecolumn方式下,将同时看到内容视图和具体视图
  • 在.all方式下,假如存在,体系将测验显现一切三个视图。在没有内容视图(中间视图)的情况下,它将只显现两个。
  • 在.automatic,体系将自动依据当时设备和方向做最好的UI布局
struct FFNavigationSplitViewHideAndShow: View {
    @State private var columnVisibility = NavigationSplitViewVisibility.detailOnly
    var body: some View {
        NavigationSplitView(columnVisibility: $columnVisibility) {
            Text("Sidebar")
        } content: {
            Text("Content")
        } detail: {
            VStack {
                Button("Detail Only") {
                    columnVisibility = .detailOnly
                }
                Button("Content and Detail") {
                    columnVisibility = .doubleColumn
                }
                Button("Show All") {
                    columnVisibility = .all
                }
            }
        }
    }
}

供给colunmVisibility是运用绑定完结的,由于当用户与UI交互时,value将自动更新。 尽管SwiftUI对分屏视图界面的这三个部分运用了不同的名称,但他们与UIKit的对应部分直接匹配:在UIKit中,侧sidebar时”首要的“,content时“弥补的”,detail是“非必须的”

11、在NavigationSplitView中自定义一个宽度的视图

SwiftUI的NavigationSplitView为它呈现的视图运用体系标准宽度,可是经过navigationSPlitViewColumnWidth()修饰符,能够测验自定义它。 体系能够挑选疏忽指定的宽度。在编写文本时,此修饰符在iPhone上被疏忽,而在iPad上仅适用于小于默许巨细的值。

11.1、navigationSplitViewColumnWidth固定巨细

在其最简略的方式,传入一个固定的值给navigationSplitViewColumnWidth()导致它运用一个固定的巨细,未运用更小或更大。

struct FFNavigationSplitViewCustomize: View {
    var body: some View {
        NavigationSplitView {
            Text("Sidebar")
                .navigationSplitViewColumnWidth(100)
        } content: {
            Text("Content")
                .navigationSplitViewColumnWidth(200)
        } detail: {
            Text("Detail")
        }
    }
}

11.2、navigationSplitViewColumnWidth规模设定巨细

可是,假如想经过最大值、最小值来设定规模,在macOS上,能够供给最小,最理想和最大的size

struct FFNavigationSplitViewCustomizeMax: View {
    var body: some View {
        NavigationSplitView {
            Text("Sidebar")
                .navigationSplitViewColumnWidth(min:100, ideal: 200, max: 300)
        } content: {
            Text("Content")
                .navigationSplitViewColumnWidth(min:100, ideal: 200, max: 300)
        } detail: {
            Text("Detail")
        }
    }
}

12、自定义NavigationSplitView的显现方式

SwiftUI的NavigationSplitView有三个选项来操控旁边面栏的显现办法,每个选项都能够运用navigationSplitViewStyle()修饰符进行调整。

12.1、.prominentDetail

第一个是.prominentDetail,它告知SwiftUI你想让细节视图一直坚持完好的巨细–侧边栏和内容视图会滑动到Detail视图上,而不是把他推到一边或把它挤得更小。

struct FFNavigationSplitViewDisplayMode: View {
    var body: some View {
        NavigationSplitView {
            Text("Sidebar")
        } content: {
            Text("Content")
        } detail: {
            Text("Detail")
        }
        .navigationSplitViewStyle(.prominentDetail)
    }
}

12.2、.balanced

第二个选项是.balanced,这将在显现侧边栏或Content栏时减少Detail视图的巨细–只需将.prominentDetail切换为.balance

struct FFNavigationSplitViewDisplayModeBalance: View {
    var body: some View {
        NavigationSplitView {
            Text("Sidebar")
        } content: {
            Text("Content")
        } detail: {
            Text("Detail")
        }
        .navigationSplitViewStyle(.balanced)
    }
}

默许设置是.automatic,这将依据平台的不同而有所不同,在写Text时,在iPhone上变成了.prominentDetail,在iPad上变成.balance

13、NavigationSplitView自动挑选视图优先级

当有一个NavigationSplitView运转在一个紧凑的size class中。SwiftUI试图揣度分屏视图列中哪一个是最优先的显现,这种揣度一般是正确的,但能够经过分屏视图设置首选紧凑列来操控它。

struct FFNavigationSplitViewCompact: View {
    @State private var preferredColumn = NavigationSplitViewColumn.detail
    var body: some View {
        //例如:这段代码强制将detail视图列为首选,然后覆盖了SwiftUI的默许挑选
        NavigationSplitView(preferredCompactColumn: $preferredColumn) {
            Text("Sidebar View")
        } detail: {
            Text("Detail View")
        }
    }
}

假如供给了一个不存在的值,比方,当只要sidebar和detail,挑选content,那么SwiftUI就会挑选侧边栏。

14、在View上创立Inspector

SwiftUi的inspector()修饰符能够在任何需求的当地增加查看器视图。这就像Xcode相同,查看器增加到UI的后边,并且能够依据需求与NavigationSplitView和NavigationStack一同运用。

14.1、 基本款式

当按钮被按下时,将显现一个查看器视图。

struct FFInspector: View {
    @State private var isShowingInspector = false
    var body: some View {
        Button("Hello, world!") {
            isShowingInspector.toggle()
        }
        .font(.largeTitle)
        .inspector(isPresented: $isShowingInspector) {
            Text("Inspector View")
        }
    }
}

当空间很大时,比方运用全屏的iPad应用程序或macOS时,查看器就放在按钮旁边。可是,当空间有限时,例如在iPhone上,查看器作为一个页面向上滑动(present mode)。

14.1、动态调整width

在支持他的平台上,你能够经过供给一个 (.inspectorColumnWidth(500)) 固定的巨细来占用空间,或许经过供给一个.inspectorColumnWidth(min: 50, ideal: 150, max: 200)规模来调整查看器的占用空间。

struct FFInspectorIdeal: View {
    @State private var isShowingInspector1 = false
    var body: some View {
        Button("Hi, metaBBLv") {
            isShowingInspector1.toggle()
        }
        .font(.largeTitle)
        .inspector(isPresented: $isShowingInspector1) {
            Text("Insepctor View BBlv")
                .inspectorColumnWidth(min:50, ideal: 150, max: 200)
        }
    }
}