SwiftUI學習100天(Day70 - 項目 14,第三部分)

原創(chuàng)鏈接:https://www.hackingwithswift.com/100/swiftui
以下內(nèi)容僅供學習參考:

是時候開始將我們所有的技術付諸實踐了,這意味著構建一個地圖視圖,我們可以在其中添加注釋并與之交互。隨著我們的進步,我希望你能反思一下我們的應用程序如何從 iOS 附帶的所有標準設計功能中獲益,以及對我們的用戶意味著什么——他們已經(jīng)知道如何使用地圖,以及如何點擊標記激活功能,
多年前,史蒂夫·喬布斯說:“設計不僅僅是它看起來和感覺起來的樣子;設計就是它的工作原理?!?用戶知道我們的地圖是如何工作的,因為它的工作方式與 iOS 上的所有其他地圖一樣。這意味著他們可以快速上手我們的應用程序,也意味著我們可以專注于將他們引導到我們應用程序中獨特而有趣的部分。
今天,你需要完成三個主題,在這些主題中,我們將深入探討 MapKit 與 SwiftUI 的集成。

將用戶位置添加到地圖
該項目將基于地圖視圖,要求用戶將他們想要訪問的地點添加到地圖中。為此,我們需要放置 一個Map
使其占據(jù)我們的整個視圖,跟蹤其中心坐標,然后還要確定用戶是否正在查看地點詳細信息、他們有哪些注釋等等。
我們將從全屏Map
視圖開始,然后在頂部放置一個半透明圓圈來代表中心點。盡管此視圖將具有跟蹤中心坐標的綁定,但我們不需要使用它來放置圓圈——一個簡單的方法ZStack
將確保圓圈始終位于地圖的中心。
首先,添加import
一行,以便我們訪問 MapKit 的數(shù)據(jù)類型:
其次,在ContentView
其中添加一個屬性,用于存儲地圖的當前狀態(tài)。稍后我們將使用它來添加地標:
這將啟動地圖,以便可以看到大部分西歐和北非。
現(xiàn)在我們可以填寫body
屬性
如果你現(xiàn)在運行該應用程序,你會看到你可以自由移動地圖,但始終有一個藍色圓圈準確顯示中心位置。
所有這些工作本身并不是很有趣,所以下一步是在右下角添加一個按鈕,讓我們向地圖添加地點標記。我們已經(jīng)在一個ZStack
內(nèi)
,所以對齊此按鈕的最簡單方法是將它放在一個VStack
內(nèi)
,并且HStack
每次都在它前面加上間隔符。這兩個墊片最終占據(jù)了剩下的整個垂直和水平空間,使得最后出現(xiàn)的任何東西都舒適地位于右下角。
我們將很快為該按鈕添加一些功能,但首先讓我們將其放置到位并添加一些基本樣式以使其看起來不錯。
請在VStack
下面添加Circle
:
請注意我是如何在那里添加padding()
修飾符兩次的——一次是為了在我們添加背景顏色之前確保按鈕更大,第二次是為了將它推離后緣。
有趣的是我們?nèi)绾卧诘貓D上放置位置。我們已將地圖的位置綁定到 ContentView
中的屬性
,但現(xiàn)在我們需要發(fā)送要顯示的位置數(shù)組。
這需要幾個步驟,從我們在應用程序中創(chuàng)建的位置類型的基本定義開始。這需要符合一些協(xié)議:
Identifiable
,因此我們可以在地圖中創(chuàng)建許多位置標記。Codable
,這樣我們就可以輕松加載和保存地圖數(shù)據(jù)。Equatable
,所以我們可以在一組位置中找到一個特定的位置。
就它將包含的數(shù)據(jù)而言,我們將為每個位置提供名稱和描述,以及緯度和經(jīng)度。我們還需要添加一個唯一標識符,以便 SwiftUI 樂于從動態(tài)數(shù)據(jù)創(chuàng)建它們。
因此,創(chuàng)建一個名為 Location.swift 的新 Swift 文件,并為其提供以下代碼:
單獨存儲緯度和經(jīng)度讓我們Codable
開箱即用,這總是很好的。我們很快就會添加更多內(nèi)容,但這足以讓我們繼續(xù)前進。
現(xiàn)在我們有了可以存儲單個位置的數(shù)據(jù)類型,我們需要一個數(shù)組來存儲用戶想要訪問的所有位置。我們暫時把它放到ContentView
我們可以開始的地方,但我們很快會再次回到它來添加更多內(nèi)容。
因此,首先將此屬性添加到ContentView
:
接下來,我們希望在點擊 + 按鈕時為其添加一個位置,因此將// create a new location
注釋替換為:
最后,更新ContentView
以便它發(fā)送locations
要轉換為注釋的數(shù)組:
現(xiàn)在地圖工作已經(jīng)足夠了,所以繼續(xù)并再次運行你的應用程序——你應該能夠根據(jù)需要四處移動,然后按 + 按鈕添加位置。
我知道設置需要大量工作,但至少你可以看到應用程序的基礎知識整合在一起!



改進我們的地圖注釋
現(xiàn)在我們正在使用MapMarker
在我們的Map
視圖中放置位置,但 SwiftUI 允許我們將任何類型的視圖放置在我們的地圖之上,這樣我們就可以擁有完全的可定制性。因此,我們將使用它來顯示一個自定義 SwiftUI 視圖,其中包含一個圖標和一些文本來顯示位置的名稱,然后查看底層數(shù)據(jù)類型以了解可以在那里進行哪些改進。
多虧了 SwiftUI 的出色表現(xiàn),這幾乎不需要任何代碼——MapMarker
用這個替換你現(xiàn)有的代碼:
這已經(jīng)是一個立竿見影的改進,因為現(xiàn)在每個標記代表什么都一目了然了——位置名稱直接出現(xiàn)在下方。但是,我想超越 SwiftUI 視圖:我想看看Location
結構本身,并應用一些改進使其變得更好。
首先,我不是特別喜歡必須CLLocationCoordinate2D
在我們的 SwiftUI 視圖中創(chuàng)建一個,我更愿意將這種邏輯移動到我們的Location
結構中。因此,我們可以將其移至計算屬性中以清理我們的代碼。首先,將 MapKit 的導入添加到 Location.swift 中,然后將其添加到Location
:
現(xiàn)在我們的ContentView
代碼更簡單了:
我想做的第二個改變是我鼓勵每個人在構建用于 SwiftUI 的自定義數(shù)據(jù)類型時做的改變:添加一個示例!這使得預覽變得非常容易,因此我鼓勵你在可能的情況下向你的類型添加一個example
靜態(tài)屬性,其中包含一些可以很好預覽的示例數(shù)據(jù)。
因此,將第二個屬性添加到Location
現(xiàn)在:
我想在這里做的最后一個更改是向==
結構添加自定義函數(shù)。我們已經(jīng)讓Location
符合Equatable
,這意味著我們已經(jīng)可以使用==
將一個位置與另一個位置進行比較
。在幕后,Swift 會通過將每個屬性與其他屬性進行比較來為我們編寫此函數(shù),這是相當浪費的——我們所有的位置都已經(jīng)有一個唯一的標識符,因此如果兩個位置具有相同的標識符,那么我們可以確定它們是同樣沒有檢查其他屬性。
因此,我們可以通過為Location
編寫自己的
,該函數(shù)只比較兩個標識符:==
函數(shù)來節(jié)省大量工作
我非常喜歡讓結構符合Equatable
標準,即使你不能像上面那樣使用優(yōu)化的比較函數(shù)——結構是簡單的值,比如字符串和整數(shù),我認為我們應該將相同的狀態(tài)擴展到我們自己的自定義結構也是。
有了它,我們項目的下一步就完成了,所以請現(xiàn)在運行它——你應該可以放下一個標記并看到我們的自定義注釋,但現(xiàn)在在幕后知道我們的代碼也更整潔了!



選擇和編輯地圖注釋
用戶現(xiàn)在可以將標記放到我們的 SwiftUI Map
上
,但他們不能對它們做任何事情——他們不能附上自己的名字和描述。解決這個問題需要幾個步驟,并在此過程中學習一些東西,但它確實將整個應用程序整合在一起,正如你將看到的那樣。
首先,我們希望在用戶選擇地圖注釋時顯示某種工作表,讓他們有機會查看或編輯有關位置的詳細信息。
我們之前處理工作表的方式意味著創(chuàng)建一個布爾值來確定工作表是否可見,然后發(fā)送一些其他數(shù)據(jù)供工作表顯示或編輯。不過這一次,我們將采用不同的方法:我們將使用一個屬性來處理所有問題。
因此,將其添加到ContentView
現(xiàn)在:
我們要說的是,我們可能有一個選定的位置,也可能沒有——這就是 SwiftUI 需要知道的所有內(nèi)容,以便呈現(xiàn)工作表。一旦我們將一個值放入該可選值中,我們就會告訴 SwiftUI 顯示工作表,并且該值將自動設置回nil
,工作
表被關閉時的值。更好的是,SwiftUI 會自動為我們解包可選,因此當我們創(chuàng)建工作表的內(nèi)容時,我們可以確保我們有真正的價值可以使用。
要嘗試一下,請將此修飾符附加到ContentView
的
ZStack
:
如你所見,它需要一個可選綁定,還有一個函數(shù),當它有一個值集時,該函數(shù)將接收解包的可選。因此,在里面我們的工作表可以直接引用place.name
而不需要打開可選的包裝或使用 nil 合并。
現(xiàn)在要使整個事物栩栩如生,我們只需要通過向地圖注釋中selectedPlace
的 VStack
中添加點擊手勢來賦予一個值:
就是這樣!我們現(xiàn)在可以顯示一張顯示所選位置名稱的表格,而且只需要少量代碼。這種可選綁定并不總是可行的,但我認為在可能的情況下它會產(chǎn)生更自然的代碼——SwiftUI 自動解包可選的行為非常有用。
當然,僅僅顯示地名并沒有太大用處,因此這里的下一步是創(chuàng)建一個詳細視圖,用戶可以在其中查看和調(diào)整地名和描述。這需要接收一個要編輯的位置,允許用戶調(diào)整該位置的兩個值,然后將使用調(diào)整后的數(shù)據(jù)發(fā)回一個新位置——它會像一個函數(shù)一樣有效地工作,接收數(shù)據(jù)并發(fā)回轉換后的東西。
與往常一樣,我們將從小處著手,逐步推進,因此請創(chuàng)建一個名為“EditView”的新 SwiftUI 視圖,然后為其提供以下代碼:
該代碼無法編譯,因為我們遇到了一個難題:我們應該為name
和description
屬性使用什么初始值?以前我們使用@State
過初始值,但我們不能在這里這樣做——它們的初始值應該來自傳入的位置,以便用戶看到保存的數(shù)據(jù)。
解決方案是創(chuàng)建一個接受位置的新初始化程序,并使用它來創(chuàng)建State
使用位置數(shù)據(jù)的結構。這使用了我們在初始化器中創(chuàng)建獲取請求時使用的相同下劃線方法,它允許我們創(chuàng)建屬性包裝器的實例而不是包裝器內(nèi)的數(shù)據(jù)。
所以,為了解決我們的問題,我們需要將這個初始化器添加到EditView
:
你需要修改預覽代碼以使用該初始化程序:
這使得代碼可以編譯,但是我們有第二個問題:當我們完成位置編輯后,我們?nèi)绾螌⑿碌奈恢脭?shù)據(jù)傳回?我們可以使用類似@Binding
傳入遠程值的方法,但這會給我們的可選輸入帶來問題ContentView
——我們希望EditView
綁定到一個真實值而不是可選值,否則會造成混淆。
我們將采用最簡單的解決方案:我們將需要一個函數(shù)來調(diào)用我們可以傳回我們想要的任何新位置的地方。這意味著任何其他 SwiftUI 都可以向我們發(fā)送一些數(shù)據(jù),并取回一些新數(shù)據(jù)以進行我們想要的處理。
首先將此屬性添加到EditView
:
這需要一個接受單個位置且不返回任何內(nèi)容的函數(shù),這非常適合我們的使用。我們需要在初始化程序中接受它,如下所示:
請記住,@escaping
意味著該函數(shù)稍后會被隱藏起來供用戶使用,而不是立即被調(diào)用,這里需要它,因為onSave
只有當用戶按下保存時才會調(diào)用該函數(shù)。
說到這里,我們需要更新保存按鈕以使用修改后的詳細信息創(chuàng)建一個新位置,并將其發(fā)回onSave()
:
通過獲取原始位置的可變副本,我們可以訪問其現(xiàn)有數(shù)據(jù)——標識符、緯度和經(jīng)度。
也不要忘記更新你的預覽代碼——在這里只傳遞一個占位符閉包就可以了:
現(xiàn)在已經(jīng)完成EditView
,但還有一些工作要做,ContentView
因為我們需要在我們的工作表中顯示新的 UI,發(fā)送選定的位置,并處理更新更改。
好吧,由于我們構建代碼的方式,這只需要幾行代碼——將其放入ContentView
中的
:sheet()
修飾符中
因此,它將位置傳遞給EditView
,并且還傳遞了一個閉包以在按下“保存”按鈕時運行。它接受新位置,然后查找當前位置并在數(shù)組中替換它。這將使我們的地圖立即更新為新數(shù)據(jù)。
繼續(xù)嘗試該應用程序——看看你是否發(fā)現(xiàn)我們的代碼有問題。希望它相當明顯:重命名實際上不起作用!
這里的問題是,我們告訴 SwiftUI 如果兩個地方的 ID 相同,那么這兩個地方是相同的,現(xiàn)在不再是這樣了——當我們更新一個標記使其具有不同的名稱時,SwiftUI 將比較舊標記和新標記,看到他們的ID是一樣的,也就懶得去換地圖了。
這里的解決方法是使id
屬性可變,如下所示:
現(xiàn)在我們可以在創(chuàng)建新位置時進行調(diào)整:
對于什么時候最好從頭開始制作一個全新的對象,或者只是復制一個現(xiàn)有的對象并像我們在這里做的那樣更改你想要的比特,沒有硬性規(guī)定;我鼓勵你嘗試并找到你喜歡的方法。
無論如何,你現(xiàn)在可以再次運行你的代碼。當然,它還沒有保存任何數(shù)據(jù),但你現(xiàn)在可以根據(jù)需要添加任意數(shù)量的位置并為它們指定有意義的名稱。
不過,還有最后一件事,這完全有可能在未來的 SwiftUI 更新中不存在,所以你自己試試吧:現(xiàn)在我發(fā)現(xiàn)給一個位置一個簡短的名字,比如“家”,然后把它改成一個長名稱,例如“這是我的家”,將導致其標簽被剪裁,直到你與地圖進行交互。
我們可以用一個名為 fixedSize()
的新修飾符來解決這個問題
,它強制任何視圖都被賦予其自然大小,而不是試圖適應其父視圖提供的空間量。在這種情況下,MapAnnotation
并不能很好地處理調(diào)整子項的大小,這會導致剪裁,但fixedSize()
讓我們繞過它,以便文本自動增長到所需的空間。
因此,要完成此步驟,請將你的地圖注釋內(nèi)容修改為:
這是一個小改動,再次希望它能在未來的 SwiftUI 版本中得到解決,但它暫時解決了我們的問題。


