版本記錄
版本號(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
。 熟悉UIKit
和SwiftUI
將有所幫助。
在PublicArt-Starter
文件夾中打開PublicArt
項(xiàng)目。 您將使用此項(xiàng)目中已經(jīng)包含的Artwork.swift
和MapView.swift
文件構(gòu)建主從應(yīng)用程序。
SwiftUI Basics in a Nutshell
SwiftUI
允許您忽略Interface Builder
和storyboards
,而無需編寫詳細(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
,就像Swift
和Objective-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)建較大的視圖。有許多原始視圖,例如Text
和 Color
,您可以將其用作自定義視圖的基本構(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)在,在ContentView
的List
閉包內(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
修飾符,以查看title
和titleKey
參數(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)下面是artData
,Artwork
對(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)目以使用Artwork
和artData
:在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
中。
首先是圖像Image
:artData
圖像的大小和寬高比都不同,因此您可以指定寬高比適合,并將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
如果Artwork
的reaction
值為??,??或??,則表明用戶已經(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ù)品。
如果hideVisited
為false
,則計(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ù)品,從ArtTabView
到ArtList
。 這樣,上下文菜單仍然可以使用。
每個(gè)標(biāo)簽都需要一個(gè)導(dǎo)航欄標(biāo)題。 您將使用hideVisited
來控制顯示哪些項(xiàng)目,盡管它不再需要是@State
變量。
接下來,將showArt
和setReaction
從ArtTabView
移到ArtList
,以處理ArtList
中的這些工作。
然后將.navigationBarTitle(“ Artworks”)
替換為:
.navigationBarTitle(tabTitle)
幾乎存在:在ArtTabView
的body
中,向ArtList
添加必要的參數(shù):
ArtList(artworks: $artworks, tabTitle: "All Artworks", hideVisited: false)
刷新預(yù)覽以檢查所有內(nèi)容是否仍然有效:
看起來不錯(cuò)! 現(xiàn)在,通過將ArtTabView
的body
定義替換為如下部分,從而使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
的視圖。 makeUIView
和updateUIView
中的所有代碼都是標(biāo)準(zhǔn)的MapKit
。 SwiftUI
的神奇之處在于UIViewRepresentable
協(xié)議及其必需的方法中-您猜對(duì)了:makeUIView
和updateUIView
。這顯示了在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)建
alert
,action sheet or popover
的過程與創(chuàng)建過程相同。您可以在修飾符-.alert,.actionSheet或.popover
中聲明工作表。要顯示或隱藏工作表,您可以將綁定傳遞給Bool
變量作為isPresented
的參數(shù),或傳遞給可選對(duì)象作為item
的參數(shù)。然后,創(chuàng)建帶有標(biāo)題,消息和按鈕的Alert
或ActionSheet
。.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
,使它可以訪問coordinate
和locationName
屬性。
現(xiàn)在,預(yù)覽需要showModal
和artwork
的值,因此添加以下參數(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è)視圖分開。
VStack
將MapView
放置在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
,并向showMap
和artwork
對(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)注~~~