SwiftUI框架詳細(xì)解析 (六) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(一)

版本記錄

版本號(hào) 時(shí)間
V1.0 2019.11.21 星期四

前言

今天翻閱蘋果的API文檔,發(fā)現(xiàn)多了一個(gè)框架SwiftUI,這里我們就一起來看一下這個(gè)框架。感興趣的看下面幾篇文章。
1. SwiftUI框架詳細(xì)解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細(xì)解析 (二) —— 基于SwiftUI的閃屏頁(yè)的創(chuàng)建(一)
3. SwiftUI框架詳細(xì)解析 (三) —— 基于SwiftUI的閃屏頁(yè)的創(chuàng)建(二)
4. SwiftUI框架詳細(xì)解析 (四) —— 使用SwiftUI進(jìn)行蘋果登錄(一)
5. SwiftUI框架詳細(xì)解析 (五) —— 使用SwiftUI進(jìn)行蘋果登錄(二)

開始

首先看下主要內(nèi)容

在本教程中,您將使用SwiftUI實(shí)現(xiàn)主從應(yīng)用程序的導(dǎo)航。 您將學(xué)習(xí)如何實(shí)現(xiàn)導(dǎo)航堆棧,導(dǎo)航欄按鈕,上下文菜單和模式表(modal sheet)

下面看下寫作環(huán)境

Swift 5, iOS 13, Xcode 11

注意:本教程假定您熟悉使用Xcode開發(fā)iOS應(yīng)用。 您需要Xcode11。要查看SwiftUI預(yù)覽,您需要macOS 10.15。 熟悉UIKitSwiftUI將有所幫助。

PublicArt-Starter文件夾中打開PublicArt項(xiàng)目。 您將使用此項(xiàng)目中已經(jīng)包含的Artwork.swiftMapView.swift文件構(gòu)建主從應(yīng)用程序。


SwiftUI Basics in a Nutshell

SwiftUI允許您忽略Interface Builderstoryboards,而無需編寫詳細(xì)的分步說明來布局UI。 您可以將SwiftUI視圖及其代碼并排預(yù)覽-更改一側(cè)會(huì)更新另一側(cè),因此它們始終保持同步。 沒有任何標(biāo)識(shí)符字符串會(huì)出錯(cuò)。 它是代碼,但比您為UIKit編寫的要少得多,因此更易于理解,編輯和調(diào)試。 這不是很好嗎?

畫布預(yù)覽意味著您不需要storyboard。 子視圖會(huì)保持更新,因此您也不需要視圖控制器。 實(shí)時(shí)預(yù)覽意味著您幾乎不需要啟動(dòng)模擬器。

SwiftUI不會(huì)取代UIKit,就像SwiftObjective-C一樣,您可以在同一應(yīng)用程序中同時(shí)使用兩者。 在本教程的最后,您將看到在SwiftUI應(yīng)用程序中使用UIKit視圖有多么容易。

1. Declarative App Development

SwiftUI使您可以進(jìn)行聲明式(declarative)應(yīng)用程序開發(fā):您可以聲明希望UI中的視圖的外觀以及它們所依賴的數(shù)據(jù)。 SwiftUI框架負(fù)責(zé)在視圖應(yīng)出現(xiàn)時(shí)創(chuàng)建視圖,并在它們依賴的數(shù)據(jù)發(fā)生更改時(shí)對(duì)其進(jìn)行更新。它重新計(jì)算視圖及其所有子級(jí),然后呈現(xiàn)已更改的內(nèi)容。

視圖的狀態(tài)取決于其數(shù)據(jù),因此您可以為視圖聲明可能的狀態(tài),以及每種狀態(tài)下視圖的外觀-視圖如何對(duì)數(shù)據(jù)更改做出反應(yīng)或數(shù)據(jù)如何影響視圖。是的,SwiftUI絕對(duì)具有反應(yīng)性!因此,如果您已經(jīng)在使用一種反應(yīng)式編程框架,那么使用SwiftUI可能會(huì)更輕松。

2. Declaring Views

SwiftUI視圖是您的UI的一部分:您可以合并較小的視圖以構(gòu)建較大的視圖。有許多原始視圖,例如TextColor,您可以將其用作自定義視圖的基本構(gòu)建塊。

打開ContentView.swift,并確保其畫布處于打開狀態(tài)(Option-Command-Return)。然后單擊+按鈕或按Command-Shift-L打開庫(kù):

第一個(gè)選項(xiàng)卡列出了用于布局和控制的基本視圖,以及“其他視圖”和“繪畫”。 其中許多工具(尤其是控件視圖)作為UIKit元素是您熟悉的,但其中一些是SwiftUI特有的。

第二個(gè)選項(xiàng)卡列出了用于布局,效果,文本,事件和其他用途(例如演示,環(huán)境和可訪問性)的修飾符。 修飾符是一種從現(xiàn)有視圖創(chuàng)建新視圖的方法。 您可以像管道一樣鏈接修飾符以自定義任何視圖。

SwiftUI鼓勵(lì)您創(chuàng)建小的可重用視圖,然后使用修飾符針對(duì)使用它們的特定上下文自定義它們。 不用擔(dān)心,SwiftUI將修改后的視圖折疊為有效的數(shù)據(jù)結(jié)構(gòu),因此您將獲得所有便利,而不會(huì)產(chǎn)生明顯的性能損失。


Creating a Basic List

首先為您的主從應(yīng)用程序的主視圖創(chuàng)建一個(gè)基本列表。 在UIKit應(yīng)用中,這將是UITableViewController

編輯ContentView看起來像這樣:

struct ContentView: View {
  let disciplines = ["statue", "mural", "plaque"]
  var body: some View {
    List(disciplines, id: \.self) { discipline in
      Text(discipline)
    }
  }
}

您創(chuàng)建一個(gè)字符串的靜態(tài)數(shù)組,并在列表List視圖中顯示它們,該視圖在數(shù)組上進(jìn)行迭代,顯示為每個(gè)項(xiàng)目指定的內(nèi)容。 結(jié)果看起來就像一個(gè)UITableView

確保畫布是打開的,然后刷新預(yù)覽(單擊Resume或按Option-Command-P):

就像您期望看到的一樣,這里有您的清單。 那有多容易? 在tableView(_:cellForRowAt :)中,沒有實(shí)現(xiàn)UITableViewDataSource的方法,沒有要配置的UITableViewCell,也沒有要拼寫錯(cuò)誤的UITableViewCell標(biāo)識(shí)符!

1. The List id Parameter

List的參數(shù)是數(shù)組(很明顯)和id(不太明顯)。 List希望每個(gè)項(xiàng)目都有一個(gè)標(biāo)識(shí)符,因此它知道有多少個(gè)唯一項(xiàng)目(而不是tableView(_:numberOfRowsInSection :))。 參數(shù)\ .self告訴List每個(gè)項(xiàng)目都是由其自身標(biāo)識(shí)的。 只要該項(xiàng)的類型符合所有內(nèi)置類型都遵循的Hashable協(xié)議,就可以這樣做。

現(xiàn)在,仔細(xì)研究id的工作原理:向disciplines添加另一個(gè)statue

let disciplines = ["statue", "mural", "plaque", "statue"]

刷新預(yù)覽:將顯示所有四個(gè)項(xiàng)目。 但是,根據(jù)id:\ .self,只有三個(gè)唯一項(xiàng)。 斷點(diǎn)可能會(huì)有所啟發(fā)。

Text(discipline)處添加一個(gè)斷點(diǎn)。

2. Starting Debug Preview

實(shí)時(shí)預(yù)覽(Live Preview)按鈕是畫布設(shè)備右下角附近的“播放”按鈕。 它在畫布上運(yùn)行視圖,但是普通的實(shí)時(shí)預(yù)覽不會(huì)在斷點(diǎn)處停止。 右鍵單擊或按住Control鍵單擊“實(shí)時(shí)預(yù)覽”按鈕,然后從菜單中選擇“調(diào)試預(yù)覽”(Debug Preview)

第一次運(yùn)行Debug Preview時(shí),將花費(fèi)一些時(shí)間來加載所有內(nèi)容。 最終,執(zhí)行將在您的斷點(diǎn)處停止,并且Variables View顯示discipline

單擊Continue program execution按鈕:現(xiàn)在discipline = "mural"

再次單擊Continue以查看discipline = "plaque"

現(xiàn)在,下次您單擊Continue按鈕時(shí),您認(rèn)為會(huì)發(fā)生什么?又是statue!這是第四個(gè)清單項(xiàng)目嗎?

好吧,再單擊兩次Continue以再次看到“mural”“plaque”。然后,最后一個(gè)繼續(xù)顯示四個(gè)項(xiàng)目的列表。因此,不,第四個(gè)列表項(xiàng)不會(huì)停止執(zhí)行。

您剛剛看到的是:執(zhí)行兩次訪問了三個(gè)唯一項(xiàng); “statue”在每次運(yùn)行中僅出現(xiàn)一次。因此List只會(huì)看到三個(gè)獨(dú)特的項(xiàng)目。對(duì)于這個(gè)簡(jiǎn)單的字符串列表來說,這不是問題,但是您很快就會(huì)看到一個(gè)非唯一id問題的示例。

您還將學(xué)習(xí)處理id參數(shù)的更好方法。但是首先,您將看到導(dǎo)航到詳細(xì)視圖的簡(jiǎn)便性。

單擊Live Preview按鈕將其停止,然后刪除斷點(diǎn)。


Navigating to the Detail View

您剛剛看到了顯示主視圖有多么容易。導(dǎo)航到詳細(xì)視圖幾乎一樣容易。

首先,將List嵌入到NavigationView中,如下所示:

NavigationView {
  List(disciplines, id: \.self) { discipline in
    Text(discipline)
  }
  .navigationBarTitle("Disciplines")
}

這就像將視圖控制器嵌入導(dǎo)航控制器中:現(xiàn)在,您可以訪問所有導(dǎo)航內(nèi)容,例如導(dǎo)航欄標(biāo)題。 請(qǐng)注意,.navigationBarTitle修改List,而不是NavigationView。 您可以在NavigationView中聲明多個(gè)視圖,每個(gè)視圖可以具有自己的.navigationBarTitle

刷新預(yù)覽以查看外觀:

真好! 默認(rèn)情況下,您會(huì)得到一個(gè)大標(biāo)題。 這對(duì)于主列表很好,但是您將對(duì)詳細(xì)視圖的標(biāo)題進(jìn)行其他操作。

1. Creating a Navigation Link

NavigationView還啟用了NavigationLink,它需要一個(gè)destination視圖和一個(gè)label-就像在storyboard中創(chuàng)建segue,但沒有那些煩人的segue標(biāo)識(shí)符。

因此,首先,創(chuàng)建您的DetailView。 現(xiàn)在,只需在ContentView結(jié)構(gòu)體下面的ContentView.swift中聲明它:

struct DetailView: View {
  let discipline: String
  var body: some View {
    Text(discipline)
  }
}

它具有單個(gè)屬性,并且像任何Swift結(jié)構(gòu)一樣,具有默認(rèn)的初始化程序-在這種情況下為DetailView(discipline:String)。 該視圖只是String本身,以Text視圖顯示。

現(xiàn)在,在ContentViewList閉包內(nèi)部,將行視圖Text(discipline)放入NavigationLink按鈕中:

List(disciplines, id: \.self) { discipline in
  NavigationLink(
    destination: DetailView(discipline: discipline)) {
      Text(discipline)
  }
}

沒有prepare(for:sender :)-您只需將當(dāng)前列表項(xiàng)傳遞給DetailView即可初始化其discipline屬性。

刷新預(yù)覽以在每行的后沿看到disclosure箭頭:

啟動(dòng)實(shí)時(shí)預(yù)覽(Live Preview),然后點(diǎn)擊一行以顯示其詳細(xì)信息視圖:

而且,可以正常工作! 注意,您也獲得了正常的后退按鈕。

但是視圖看起來很普通-甚至沒有標(biāo)題。

因此,添加標(biāo)題,如下所示:

var body: some View {
  Text(discipline)
    .navigationBarTitle(Text(discipline), displayMode: .inline)
}

該視圖由NavigationLink呈現(xiàn),因此不需要它自己的NavigationView即可顯示navigationBarTitle。 但是,此版本的navigationBarTitle的標(biāo)題參數(shù)需要使用Text視圖-如果僅使用discipline字符串進(jìn)行嘗試,則會(huì)收到毫無意義的錯(cuò)誤消息。 按住Option鍵單擊兩個(gè)NavigationBarTitle修飾符,以查看titletitleKey參數(shù)類型的不同。

displayMode:.inline參數(shù)顯示常規(guī)尺寸的標(biāo)題。

再次啟動(dòng)Live-preview,然后點(diǎn)擊一行以查看標(biāo)題:

現(xiàn)在,您知道了如何創(chuàng)建基本的主從應(yīng)用程序。 您使用了String對(duì)象,以避免任何可能使列表和導(dǎo)航的工作變得混亂的混亂情況。 但是列表項(xiàng)通常是您定義的模型類型的實(shí)例。 現(xiàn)在該使用一些實(shí)際數(shù)據(jù)了。


Revisiting Honolulu Public Artworks

入門項(xiàng)目包含Artwork.swift文件。 Artwork是具有八個(gè)屬性的結(jié)構(gòu),除最后一個(gè)屬性外,所有常量都可以由用戶設(shè)置:

struct Artwork {
  let artist: String
  let description: String
  let locationName: String
  let discipline: String
  let title: String
  let imageName: String
  let coordinate: CLLocationCoordinate2D
  var reaction: String
}

結(jié)構(gòu)下面是artDataArtwork對(duì)象的數(shù)組。

一些artData項(xiàng)的reaction屬性是??,??或??,但是對(duì)于大多數(shù)項(xiàng)目而言,它只是一個(gè)空字符串。 這個(gè)想法是當(dāng)用戶訪問藝術(shù)品時(shí),他們?cè)趹?yīng)用程序中對(duì)其做出反應(yīng)。 因此,如果出現(xiàn)空字符串reaction,則表示用戶尚未訪問過該藝術(shù)品。

現(xiàn)在開始更新項(xiàng)目以使用ArtworkartData:在ContentView中,添加以下屬性:

let artworks = artData

刪除disciplines數(shù)組。

artworks替換disciplines

List(artworks, id: \.self) { artwork in
  NavigationLink(
    destination: DetailView(artwork: artwork)) {
      Text(artwork.title)
  }
}
.navigationBarTitle("Artworks")

并編輯DetailView以使用Artwork

struct DetailView: View {
  let artwork: Artwork

  var body: some View {
    Text(artwork.title)
      .navigationBarTitle(Text(artwork.title), displayMode: .inline)
  }
}

啊,Artwork不可Hashable! 因此,將\ .self更改為\ .title

List(artworks, id: \.title) { artwork in

您很快就會(huì)為DetailView創(chuàng)建一個(gè)單獨(dú)的文件,但是現(xiàn)在就可以了。

現(xiàn)在,再來看一下List視圖中的id參數(shù)。

1. Creating Unique id Values With UUID()

id參數(shù)的參數(shù)可以使用列表項(xiàng)的Hashable屬性的任意組合。 但是,就像為數(shù)據(jù)庫(kù)選擇主鍵一樣,很容易弄錯(cuò)它,然后找出使標(biāo)識(shí)符不像您想象的那樣唯一的困難方法。

Artwork title是唯一的,但是要查看id值不是唯一的情況,請(qǐng)?jiān)?code>List中用\ .discipline替換\ .title

List(artworks, id: \.discipline) { artwork in

刷新預(yù)覽(Option-Command-P)

artData中的標(biāo)題各不相同,但列表認(rèn)為所有statues均為“ Jonah Kuhio Kalanianaole”,所有壁畫均為“The Makahiki Festival Mauka Mural”,所有匾額均為“Amelia Earhart Memorial Plaque”。 這些都是artData中出現(xiàn)的該discipline的第一項(xiàng)。 如果您的列表項(xiàng)沒有唯一的id值,就會(huì)發(fā)生這種情況。

幸運(yùn)的是,該解決方案很容易-它幾乎可以完成許多數(shù)據(jù)庫(kù)的工作:在模型類型中添加id屬性,并使用UUID()為每個(gè)新對(duì)象生成唯一的標(biāo)識(shí)符。

Artwork.swift中,將此屬性添加到Artwork屬性列表的頂部:

let id = UUID()

您可以使用UUID()來讓系統(tǒng)生成唯一的ID值,因?yàn)槟鸁o需擔(dān)心ID的實(shí)際值。 這個(gè)唯一的ID以后將非常有用!

然后,在ContentView.swift中,將List中的id參數(shù)更改為\ .id

List(artworks, id: \.id) { artwork in

刷新預(yù)覽

現(xiàn)在,每個(gè)artwork都有一個(gè)唯一的id值,因此列表可以正確顯示所有內(nèi)容。

注意:如果僅刷新預(yù)覽不能解決該列表,請(qǐng)構(gòu)建項(xiàng)目(Command-B),然后刷新預(yù)覽。

2. Conforming to Identifiable

但是有一種更好的方法:返回Artwork.swift,并在Artwork結(jié)構(gòu)之外添加此擴(kuò)展名:

extension Artwork: Identifiable { }

id屬性是使Artwork符合Identifiable所需的全部,并且您已經(jīng)添加了該屬性。

現(xiàn)在,您可以完全刪除id參數(shù):

List(artworks) { artwork in

現(xiàn)在看起來更整潔了! 由于Artwork符合Identifiable,因此List知道它具有id屬性,并自動(dòng)將此屬性用作其id參數(shù)。

刷新預(yù)覽(Option-Command-P)

而且仍然可以正常工作。


Showing More Detail

Artwork對(duì)象具有很多可以顯示的信息,因此請(qǐng)更新DetailView以顯示更多詳細(xì)信息。

首先,創(chuàng)建一個(gè)新的SwiftUI View文件:Command-N ? iOS ? User Interface ? SwiftUI View。 將其命名為DetailView.swift

ContentView.swift中的DetailView替換新文件中的DetailView。 確保從ContentView.swift中將其刪除。

預(yù)覽需artwork參數(shù),因此添加它:

struct DetailView_Previews: PreviewProvider {
  static var previews: some View {
    DetailView(artwork: artData[0])
  }
}

然后,向視圖添加許多新內(nèi)容:

struct DetailView: View {
  let artwork: Artwork

  var body: some View {
    VStack {
      Image(artwork.imageName)
        .resizable()
        .frame(maxWidth: 300, maxHeight: 600)
        .aspectRatio(contentMode: .fit)
      Text("\(artwork.reaction)  \(artwork.title)")
        .font(.headline)
        .multilineTextAlignment(.center)
        .lineLimit(3)
      Text(artwork.locationName)
        .font(.subheadline)
      Text("Artist: \(artwork.artist)")
        .font(.subheadline)
      Divider()
      Text(artwork.description)
        .multilineTextAlignment(.leading)
        .lineLimit(20)
    }
    .padding()
    .navigationBarTitle(Text(artwork.title), displayMode: .inline)
  }
}

您正在以垂直布局顯示多個(gè)視圖,因此所有內(nèi)容都在VStack中。

首先是圖像ImageartData圖像的大小和寬高比都不同,因此您可以指定寬高比適合,并將frame限制為最多300點(diǎn)寬,600點(diǎn)高。 但是,除非您首先將Image修改為可調(diào)整resizable大小,否則這些修改器不會(huì)生效。

您修改Text視圖以指定字體大小和multilineTextAlignment,因?yàn)槟承?biāo)題和描述對(duì)于一行來說太長(zhǎng)了。

最后,在堆棧周圍添加一些填充。

刷新預(yù)覽:

還有Prince Jonah! 以防萬(wàn)一,Kalanianaole中有七個(gè)音節(jié),最后六個(gè)字母中有四個(gè)。

當(dāng)您預(yù)覽甚至實(shí)時(shí)預(yù)覽DetailView時(shí),導(dǎo)航欄不會(huì)出現(xiàn),因?yàn)樗恢浪趯?dǎo)航堆棧中。

返回ContentView.swift并啟動(dòng)Live Preview,然后點(diǎn)擊一行以查看完整的詳細(xì)信息視圖:


Handling Split View

到目前為止,我一直在向您展示iPhone 8 scheme的預(yù)覽。 但是,當(dāng)然,您可以在iPad上(甚至在Mac上,作為Mac Catalyst應(yīng)用程序)查看此內(nèi)容。

要查看在iPad上的外觀,請(qǐng)選擇一個(gè)iPad scheme,然后重新啟動(dòng)Live Preview

這是iPad,因此SwiftUI會(huì)顯示分割視圖(split view)。 當(dāng)iPad處于縱向時(shí),您必須從前端滑動(dòng)以打開主列表視圖,然后選擇一個(gè)項(xiàng)目:

為避免在啟動(dòng)時(shí)顯示空白詳細(xì)視圖,只需在ContentView中的List之后添加特定的DetailView。 在.navigationBarTitle(“ Artworks”)之后添加以下內(nèi)容:

DetailView(artwork: artworks[0])

刷新預(yù)覽(不必實(shí)時(shí)預(yù)覽):

現(xiàn)在,split view將使用默認(rèn)的詳細(xì)視圖加載。

將方案改回iPhone,可以看到這個(gè)DetailView不會(huì)弄亂您的主列表視圖!

注意:Xcode的Master-Detail模板通過使用.navigationViewStyle(DoubleColumnNavigationViewStyle())修改NavigationView來明確顯示這一點(diǎn)。 如果您根本不想split view,請(qǐng)指定StackNavigationViewStyle()強(qiáng)制執(zhí)行iPhone樣式的導(dǎo)航堆棧行為。


Declaring Data Dependencies

您已經(jīng)了解了聲明UI的簡(jiǎn)便性。現(xiàn)在是時(shí)候了解SwiftUI的另一個(gè)重要功能:聲明性數(shù)據(jù)依賴項(xiàng)(declarative data dependencies)

1. Guiding Principles

SwiftUI有兩個(gè)指導(dǎo)原則來管理數(shù)據(jù)如何通過您的應(yīng)用程序流動(dòng):

  • Data access = dependency:讀取視圖中的一條數(shù)據(jù)會(huì)為該視圖中的數(shù)據(jù)創(chuàng)建依賴關(guān)系。每個(gè)視圖都是其數(shù)據(jù)依賴關(guān)系的函數(shù)-輸入或狀態(tài)。
  • Single source of truth:視圖讀取的每條數(shù)據(jù)都有一個(gè)事實(shí)來源,該來源要么由視圖擁有,要么位于視圖外部。無論事實(shí)的來源在哪里,您都應(yīng)該始終有一個(gè)事實(shí)的來源。您可以通過傳遞對(duì)事實(shí)源的綁定來對(duì)其進(jìn)行讀寫訪問。

UIKit中,視圖控制器使模型和視圖保持同步。在SwiftUI中,聲明性視圖層次結(jié)構(gòu)加上事實(shí)的單一來源意味著您不再需要視圖控制器。

2. Tools for Data Flow

SwiftUI提供了多種工具來幫助您管理應(yīng)用程序中的數(shù)據(jù)流。

屬性包裝器(Property wrappers)增強(qiáng)了變量的行為。特定于SwiftUI的包裝器-@ State,@ Binding,@ ObservedObject@EnvironmentObject-聲明了視圖對(duì)變量表示的數(shù)據(jù)的依賴關(guān)系。

每個(gè)包裝器指示不同的數(shù)據(jù)源:

  • 視圖擁有@State變量。@State var分配持久性存儲(chǔ),因此您必須初始化其值。 Apple建議您將這些標(biāo)記為private,以強(qiáng)調(diào)@State變量專門由該視圖擁有和管理。
  • @Binding聲明對(duì)另一個(gè)視圖擁有的@State var的依賴關(guān)系,該變量使用$前綴將對(duì)此狀態(tài)變量的綁定傳遞給另一個(gè)視圖。在接收視圖中,@ Binding var是對(duì)數(shù)據(jù)的引用,因此不需要初始化。該引用使視圖可以編輯依賴于此數(shù)據(jù)的任何視圖的狀態(tài)。
  • @ObservedObject聲明對(duì)符合ObservableObject協(xié)議的引用類型的依賴:它實(shí)現(xiàn)了objectWillChange屬性以發(fā)布對(duì)其數(shù)據(jù)的更改。
  • @EnvironmentObject聲明對(duì)某些共享數(shù)據(jù)的依賴-這些數(shù)據(jù)對(duì)于應(yīng)用程序中的所有視圖都是可見的。這是一種間接傳遞數(shù)據(jù)的簡(jiǎn)便方法,而不是將數(shù)據(jù)從父視圖傳遞到子視圖與孫子視圖,尤其是在子視圖不需要時(shí)。

現(xiàn)在繼續(xù)練習(xí)使用@State@Binding進(jìn)行導(dǎo)航。


Adding a Navigation Bar Button

如果Artworkreaction值為??,??或??,則表明用戶已經(jīng)訪問了該藝術(shù)品。 一個(gè)有用的功能是讓用戶隱藏他們?cè)L問過的藝術(shù)品,以便他們隨后可以選擇其他人之一進(jìn)行訪問。

在本部分中,您將在導(dǎo)航欄中添加一個(gè)按鈕,以僅顯示用戶尚未訪問的藝術(shù)品。

首先在藝術(shù)品標(biāo)題旁邊的列表行中顯示reaction值:將Text(artwork.title)更改為以下內(nèi)容:

Text("\(artwork.reaction)  \(artwork.title)")

刷新預(yù)覽以查看哪些項(xiàng)目有非空reaction

現(xiàn)在,將這些屬性添加到ContentView的頂部:

@State private var hideVisited = false
var showArt: [Artwork] {
  hideVisited ? artworks.filter { $0.reaction == "" } : artworks
}

@State屬性包裝器聲明了數(shù)據(jù)依賴關(guān)系:更改此hideVisited屬性的值將觸發(fā)對(duì)此視圖的更新。 在這種情況下,更改hideVisited的值將隱藏或顯示已訪問的藝術(shù)品。 您將其初始化為false,因此啟動(dòng)應(yīng)用程序時(shí),列表將顯示所有藝術(shù)品。

如果hideVisitedfalse,則計(jì)算的屬性showArt是所有artworks; 否則,它是artworks的子陣列,僅包含藝術(shù)品中具有空字符串reaction的那些物品。

現(xiàn)在,將List聲明的第一行替換為:

List(showArt) { artwork in

現(xiàn)在,在.navigationBarTitle(“ Artworks”)之后,在列表List中添加navigationBarItems修飾符:

.navigationBarItems(trailing:
  Toggle(isOn: $hideVisited, label: { Text("Hide Visited") }))

您要在導(dǎo)航欄的右側(cè)(后緣)添加導(dǎo)航欄項(xiàng)。 此項(xiàng)目是帶有標(biāo)簽“Hide Visited”的切換視圖。

您將綁定$ hideVisited傳遞給Toggle。 綁定允許讀寫訪問,因此Toggle能夠在用戶點(diǎn)擊時(shí)更改hideVisited的值。 此更改將通過更新列表視圖進(jìn)行。

啟動(dòng)實(shí)時(shí)預(yù)覽以查看此工作:

輕觸切換開關(guān),即可查看所訪問的artworks消失:僅保留具有空字符串reactions的藝術(shù)品。 再次點(diǎn)擊以查看再次出現(xiàn)的參觀藝術(shù)品。

您剛剛實(shí)現(xiàn)的Toggle的另一種選擇是:tab view! 當(dāng)我告訴您在SwiftUI中輕松實(shí)現(xiàn)標(biāo)簽視圖時(shí),您不會(huì)感到驚訝。 為用戶設(shè)置對(duì)藝術(shù)品的反應(yīng)方式后,您將立即執(zhí)行此操作,因?yàn)檫@將使未訪問的標(biāo)簽更加有趣。


Reacting to Artwork

該應(yīng)用程序缺少的一項(xiàng)功能是用戶對(duì)藝術(shù)品進(jìn)行反應(yīng)的一種方式。 在本部分中,您將在列表行中添加一個(gè)上下文菜單,以允許用戶設(shè)置對(duì)該作品的反應(yīng)。

1. Adding a Context Menu

仍在ContentView.swift中,將artworks設(shè)為@State變量:

@State var artworks = artData

ContentView結(jié)構(gòu)是不可變的,因此您需要此@State屬性包裝器才能將值分配給Artwork屬性。

接下來,將此輔助方法存根添加到ContentView

private func setReaction(_ reaction: String, for item: Artwork) { }

然后將contextMenu修飾符添加到列表行Text視圖中:

Text("\(artwork.reaction)  \(artwork.title)")
  .contextMenu {
    Button("Love it: ??") {
      self.setReaction("??", for: artwork)
    }
    Button("Thoughtful: ??") {
      self.setReaction("??", for: artwork)
    }
    Button("Wow!: ??") {
      self.setReaction("??", for: artwork)
    }
}

注意:每當(dāng)在閉包內(nèi)部使用view屬性或方法時(shí),都必須使用self。 —不用擔(dān)心,如果您忘記了,Xcode會(huì)告訴您并提出修復(fù)它。

上下文菜單顯示三個(gè)按鈕,每個(gè)反應(yīng)一個(gè)。 每個(gè)按鈕都使用適當(dāng)?shù)谋砬榉?hào)調(diào)用setReaction(_:for :)

最后,實(shí)現(xiàn)setReaction(_:for :)幫助器方法:

private func setReaction(_ reaction: String, for item: Artwork) {
  if let index = self.artworks.firstIndex(
    where: { $0.id == item.id }) {
    artworks[index].reaction = reaction
  }
}

這就是唯一ID值的用途! 您可以比較id值,以在artworks數(shù)組中找到該項(xiàng)目的索引,然后設(shè)置該數(shù)組項(xiàng)目的reaction值。

注意:您可能會(huì)想,直接設(shè)置Artwork.reaction =“??”會(huì)更容易。 不幸的是,artwork列表迭代器是一個(gè)let常量。

刷新實(shí)時(shí)預(yù)覽(Option-Command-P),然后觸摸并按住一個(gè)項(xiàng)目以顯示上下文菜單。 點(diǎn)擊上下文菜單按鈕以選擇reaction,或點(diǎn)擊菜單外部以將其關(guān)閉。

那讓你感覺如何? ?? ?? ??!


Creating a Tab View App

現(xiàn)在,您可以構(gòu)建一個(gè)替代應(yīng)用,該應(yīng)用使用tab view列出所有藝術(shù)品或僅列出未訪問的藝術(shù)品。

首先創(chuàng)建一個(gè)新的SwiftUI View文件來創(chuàng)建您的備用主視圖。 將其命名為ArtTabView.swift

接下來,復(fù)制ContentView內(nèi)部的所有代碼-而不是結(jié)構(gòu)ContentView行或右括號(hào)-并將其粘貼到結(jié)構(gòu)體ArtTabView閉包內(nèi),替換樣板代碼。

現(xiàn)在,在畫布處于打開狀態(tài)(Option-Command-Return)的同時(shí),單擊Command-單擊List,然后從菜單中選擇Extract Subview

命名新的子視圖ArtList

接下來,刪除navigationBarItems開關(guān)。 第二個(gè)選項(xiàng)卡將替換此功能。

現(xiàn)在將這些屬性添加到ArtList中:

@Binding var artworks: [Artwork]
let tabTitle: String
let hideVisited: Bool

您將傳遞一個(gè)綁定到@State變量藝術(shù)品,從ArtTabViewArtList。 這樣,上下文菜單仍然可以使用。

每個(gè)標(biāo)簽都需要一個(gè)導(dǎo)航欄標(biāo)題。 您將使用hideVisited來控制顯示哪些項(xiàng)目,盡管它不再需要是@State變量。

接下來,將showArtsetReactionArtTabView移到ArtList,以處理ArtList中的這些工作。

然后將.navigationBarTitle(“ Artworks”)替換為:

.navigationBarTitle(tabTitle)

幾乎存在:在ArtTabViewbody中,向ArtList添加必要的參數(shù):

ArtList(artworks: $artworks, tabTitle: "All Artworks", hideVisited: false)

刷新預(yù)覽以檢查所有內(nèi)容是否仍然有效:

看起來不錯(cuò)! 現(xiàn)在,通過將ArtTabViewbody定義替換為如下部分,從而使TabView具有兩個(gè)tabs

TabView {
  NavigationView {
    ArtList(artworks: $artworks, tabTitle: "All Artworks", hideVisited: false)
    DetailView(artwork: artworks[0])
  }
  .tabItem({
    Text("Artworks ?? ?? ??")
  })
  
  NavigationView {
    ArtList(artworks: $artworks, tabTitle: "Unvisited Artworks", hideVisited: true)
    DetailView(artwork: artworks[0])
  }
  .tabItem({ Text("Unvisited Artworks") })
}

第一個(gè)標(biāo)簽是未過濾的列表,第二個(gè)標(biāo)簽是未訪問的藝術(shù)品的列表。tabItem修飾符指定每個(gè)選項(xiàng)卡上的標(biāo)簽。

啟動(dòng)實(shí)時(shí)預(yù)覽體驗(yàn)?zāi)奶娲鷳?yīng)用程序:

Unvisited Artworks標(biāo)簽中,使用快捷菜單向藝術(shù)品添加reaction:由于不再參觀,該藝術(shù)品從此列表中消失了!

注意:要使用此視圖啟動(dòng)應(yīng)用,請(qǐng)打開SceneDelegate.swift并將let contentView = ContentView()替換為let contentView = ArtTabView()


Displaying a Modal Sheet

此應(yīng)用程序缺少的另一個(gè)功能是地圖-您想訪問此藝術(shù)品,但是它在哪里,以及如何到達(dá)那里?

SwiftUI沒有地圖基元視圖,但是Apple的Interfacing With UIKit教程中有一個(gè)。我對(duì)其進(jìn)行了修改,添加了pin annotation,并將其包含在入門項(xiàng)目中。

1. UIViewRepresentable Protocol

打開MapView.swift:這是一個(gè)托管MKMapView的視圖。 makeUIViewupdateUIView中的所有代碼都是標(biāo)準(zhǔn)的MapKitSwiftUI的神奇之處在于UIViewRepresentable協(xié)議及其必需的方法中-您猜對(duì)了:makeUIViewupdateUIView。這顯示了在SwiftUI項(xiàng)目中顯示UIKit視圖有多么容易。它也適用于您的任何自定義UIKit視圖。

現(xiàn)在嘗試預(yù)覽MapView(Option-Command-P)。好吧,它正在嘗試顯示地圖,但它并不在那里。訣竅是:您必須啟動(dòng)Live Preview才能查看地圖:

預(yù)覽使用artData [5] .coordinate作為樣本數(shù)據(jù),因此地圖圖釘顯示了檀香山動(dòng)物園大象展覽的位置,您可以在其中參觀長(zhǎng)頸鹿雕塑。

2. Adding a Button

現(xiàn)在回到DetailView.swift,它需要一個(gè)按鈕來顯示地圖。 您可以將一個(gè)放置在導(dǎo)航欄中,但是在藝術(shù)品位置旁邊也是放置“顯示地圖”按鈕的合理位置。

要將Button放置在Text視圖旁邊,您需要一個(gè)HStack。 確保畫布處于打開狀態(tài)(Option-Command-Return),然后在此代碼行中按Command并單擊Text

Text(artwork.locationName)

然后從菜單中選擇Embed in HStack

現(xiàn)在,要將按鈕放置在位置文本的左側(cè),請(qǐng)將其添加到HStack中的Text之前:打開庫(kù)(Shift-Command-L),然后將Button拖到您的代碼中Text(artwork.locationName)的上方。

注意:拖動(dòng)Button時(shí),將鼠標(biāo)懸停在Text附近,直到在Text上方打開新行,然后釋放Button

您的代碼現(xiàn)在如下所示:

Button(action: {}) {
  Text("Button")
}
Text(artwork.locationName)
  .font(.subheadline)

Text("Button")是按鈕的標(biāo)簽。 更改為:

Image(systemName: "mappin.and.ellipse")

刷新預(yù)覽

注意:此系統(tǒng)圖像來自Apple的新SFSymbols系列。 要查看完整的套件,請(qǐng)從Apple下載并安裝SF Symbols應(yīng)用程序。 至少有兩個(gè)符號(hào)似乎已被棄用:我嘗試使用mappin.circle及其填充版本,但沒有出現(xiàn)。

因此標(biāo)簽看起來正確。 現(xiàn)在,按鈕的action應(yīng)該怎么做?

3. Showing a Modal Sheet

您將以模式表的形式顯示地圖。 它在SwiftUI中的工作方式是使用Bool值,該值是模式表的參數(shù)。 SwiftUI僅在該值為true時(shí)顯示模式表。

操作如下:在DetailView頂部,添加以下@State屬性:

@State private var showMap = false

同樣,您要聲明一個(gè)數(shù)據(jù)依賴性:更改showMap的值會(huì)觸發(fā)顯示和關(guān)閉模式表。 您將showMap初始化為false,這樣在加載DetailView時(shí)地圖不會(huì)出現(xiàn)。

接下來,在按鈕的action中,將showMap設(shè)置為true。 所以您的Button現(xiàn)在看起來像這樣:

Button(action: { self.showMap = true }) {
  Image(systemName: "mappin.and.ellipse")
}

好的,您的按鈕已準(zhǔn)備就緒。 現(xiàn)在,您在哪里聲明模態(tài)表? 好吧,您將其附加為修飾符。 任何看法! 您不必將其附加到按鈕上,但這是放置按鈕最明顯的地方。 因此,修改您的新按鈕:

Button(action: { self.showMap = true }) {
  Image(systemName: "mappin.and.ellipse")
}
.sheet(isPresented: $showMap) {
  MapView(coordinate: self.artwork.coordinate)
}

您將綁定傳遞給showMap作為工作表的isPresented參數(shù),因?yàn)楸仨殞⑵渲蹈臑?code>false才能關(guān)閉工作表。 系統(tǒng)或工作表視圖都會(huì)進(jìn)行此更改。

注意:修改器的isPresented參數(shù)是顯示或隱藏工作表的一種方法。 觸發(fā)器也可以是可選對(duì)象。 在這種情況下,修飾符的item參數(shù)將綁定到可選對(duì)象。 該對(duì)象變?yōu)榉?code>nil時(shí),該工作表出現(xiàn),而該對(duì)象變?yōu)?code>nil時(shí),該工作表消失。

您將MapView指定為要顯示的視圖,并將該artwork的位置坐標(biāo)作為coordinate參數(shù)傳遞。

要測(cè)試新按鈕,請(qǐng)切換到ContentView.swift,然后運(yùn)行實(shí)時(shí)預(yù)覽。 然后點(diǎn)擊一個(gè)項(xiàng)目以查看其DetailView,然后點(diǎn)擊地圖按鈕:

還有地圖釘(map pin)

注意:創(chuàng)建alertaction sheet or popover的過程與創(chuàng)建過程相同。您可以在修飾符-.alert,.actionSheet或.popover中聲明工作表。要顯示或隱藏工作表,您可以將綁定傳遞給Bool變量作為isPresented的參數(shù),或傳遞給可選對(duì)象作為item的參數(shù)。然后,創(chuàng)建帶有標(biāo)題,消息和按鈕的AlertActionSheet.popover修飾符僅需要顯示一個(gè)視圖。

4. Dismissing the Modal Sheet

現(xiàn)在,如何移除modal sheet?通常,在iPhone上,您只需向下滑動(dòng)模式視圖即可將其關(guān)閉。這個(gè)手勢(shì)告訴SwiftUI將Bool值設(shè)置為false,模態(tài)消失。

但是,當(dāng)您滑動(dòng)時(shí),此MapView會(huì)滾動(dòng)!公平地說,這可能就是您想要的,這正是您的用戶所期望的。因此,您必須提供一個(gè)按鈕來手動(dòng)關(guān)閉地圖。

為此,您需要將MapView包裹在另一個(gè)視圖中,您可以在其中添加Done按鈕。在使用時(shí),您將添加標(biāo)簽以顯示藝術(shù)品的locationName

首先,創(chuàng)建一個(gè)新的SwiftUI View文件,并將其命名為LocationMap.swift

接下來,將這些屬性添加到LocationMap中:

@Binding var showModal: Bool
var artwork: Artwork

您需要將$ showMap作為showModal參數(shù)傳遞給LocationMap。 這是@Binding,因?yàn)?code>LocationMap會(huì)將showModal更改為false,并且此更改必須流回到DetailView才能關(guān)閉模式表。

然后,您將整個(gè)artwork對(duì)象傳遞給LocationMap,使它可以訪問coordinatelocationName屬性。

現(xiàn)在,預(yù)覽需要showModalartwork的值,因此添加以下參數(shù):

LocationMap(showModal: .constant(true), artwork: artData[0])

注意:showModal的參數(shù)必須是綁定,而不是純值。 您可以使用.constant()將任何普通值更改為綁定。

接下來,將body替換為以下內(nèi)容:

var body: some View {
  VStack {
    MapView(coordinate: artwork.coordinate)
    HStack {
      Text(self.artwork.locationName)
      Spacer()
      Button("Done") { self.showModal = false }
    }
    .padding()
  }
}

內(nèi)部HStack包含位置名稱和Done按鈕。 Spacer將兩個(gè)視圖分開。

VStackMapView放置在HStack上方,HStack周圍有一些填充。

啟動(dòng)實(shí)時(shí)預(yù)覽以查看其外觀:

正是您所期望的樣子!

現(xiàn)在,回到DetailView.swift:用以下這一行替換MapView(coordinate:self.artwork.coordinate)

LocationMap(showModal: self.$showMap, artwork: self.artwork)

您正在顯示LocationMap而不是MapView,并向showMapartwork對(duì)象傳遞了綁定。

現(xiàn)在再次實(shí)時(shí)預(yù)覽ContentView,點(diǎn)擊一個(gè)項(xiàng)目,然后點(diǎn)擊地圖按鈕。

然后點(diǎn)擊Done以關(guān)閉地圖。 做得好!


Bonus Section: Eager Evaluation

SwiftUI應(yīng)用程序啟動(dòng)時(shí)發(fā)生了一件奇怪的事情:它初始化出現(xiàn)在ContentView中的每個(gè)對(duì)象。 例如,它會(huì)在用戶點(diǎn)擊導(dǎo)航到該視圖的任何內(nèi)容之前初始化DetailView。 它將初始化List中的每個(gè)項(xiàng)目,無論該項(xiàng)目在窗口中是否可見。

這是一種eager evaluation方式,也是編程語(yǔ)言的常見策略。 這是個(gè)問題嗎? 好吧,如果您的應(yīng)用程序包含大量項(xiàng)目,并且每個(gè)項(xiàng)目都下載了一個(gè)大型媒體文件,則您可能不希望初始化程序開始下載。

要模擬正在發(fā)生的事情,請(qǐng)向Artwork添加一個(gè)init()方法,以便您可以包含一條打印語(yǔ)句:

init(
  artist: String, 
  description: String, 
  locationName: String, 
  discipline: String,
  title: String, 
  imageName: String, 
  coordinate: CLLocationCoordinate2D, 
  reaction: String
) {
  print(">>>>> Downloading \(imageName) <<<<<")
  self.artist = artist
  self.description = description
  self.locationName = locationName
  self.discipline = discipline
  self.title = title
  self.imageName = imageName
  self.coordinate = coordinate
  self.reaction = reaction
}

現(xiàn)在,在ContentView.swift中,啟動(dòng)Debug Preview (Control-click the Live Preview button,然后觀察調(diào)試控制臺(tái):

>>>>> Downloading 002_200105 <<<<<
>>>>> Downloading 19300102 <<<<<
>>>>> Downloading 193701 <<<<<
>>>>> Downloading 193901-5 <<<<<
>>>>> Downloading 195801 <<<<<
>>>>> Downloading 198912 <<<<<
>>>>> Downloading 196001 <<<<<
>>>>> Downloading 193301-2 <<<<<
>>>>> Downloading 193101 <<<<<
>>>>> Downloading 199909 <<<<<
>>>>> Downloading 199103-3 <<<<<
>>>>> Downloading 197613-5 <<<<<
>>>>> Downloading 199802 <<<<<
>>>>> Downloading 198803 <<<<<
>>>>> Downloading 199303-2 <<<<<
>>>>> Downloading 19350202a <<<<<
>>>>> Downloading 200304 <<<<<

毫不奇怪,它初始化了所有Artwork項(xiàng)目。 如果有1000個(gè)項(xiàng)目,并且每個(gè)項(xiàng)目都下載了較大的圖像或視頻文件,那么這對(duì)于移動(dòng)應(yīng)用程序可能是個(gè)問題。

這是一個(gè)可能的解決方案:將下載活動(dòng)移至幫助方法,并僅在該項(xiàng)目出現(xiàn)在屏幕上時(shí)調(diào)用此方法。

Artwork.swift中,注釋掉init()并添加此方法:

func load() {
  print(">>>>> Downloading \(self.imageName) <<<<<")
}

然后返回ContentView.swift,修改List行:

Text("\(artwork.reaction)  \(artwork.title)")
  .onAppear() { artwork.load() }

僅當(dāng)此Artwork的行在屏幕上時(shí),才調(diào)用load()

啟動(dòng)調(diào)試預(yù)覽:

<code>
>>>>> Downloading 002_200105 <<<<<
>>>>> Downloading 19300102 <<<<<
>>>>> Downloading 193701 <<<<<
>>>>> Downloading 193901-5 <<<<<
>>>>> Downloading 195801 <<<<<
>>>>> Downloading 198912 <<<<<
>>>>> Downloading 196001 <<<<<
>>>>> Downloading 193301-2 <<<<<
>>>>> Downloading 193101 <<<<<
>>>>> Downloading 199909 <<<<<
>>>>> Downloading 199103-3 <<<<<
>>>>> Downloading 197613-5 <<<<<
>>>>> Downloading 199802 <<<<<
</code>

后記

本篇主要講述了基于SwiftUI的導(dǎo)航的實(shí)現(xiàn),感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容