一、前言
- 地標詳情頁視圖已經創建完成,我們需要提供一種方式讓用戶可以查看完整的地標串列,并且可以查看每一個地標的詳情,地標詳情頁視圖的創建,請參考我的博客:SwiftUI之深入決議如何創建和組合視圖,
- 本文將分析如何創建一個可以展示任何地標資訊的視圖,并動態生成一個可滾動串列,用戶可以點擊串列項去查看地標的詳細資訊;優化視圖顯示時,可以使用 Xcode 畫布來渲染多個不同設備大小下的預覽視圖,
二、樣本資料
- 自定義視圖所展示的資訊都直接被寫死在代碼中,那么如將何自定義視圖傳入樣本資料進行展示:

- 專案工程中的 Models->Landmark.swift 檔案,宣告了需要在應用中展示一個地標所需要資訊的結構化名稱,并通過匯入 landmarkData.json 檔案中的資料,生成一個地標資訊陣列:
import SwiftUI
import CoreLocation
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var name: String
fileprivate var imageName: String
fileprivate var coordinates: Coordinates
var state: String
var park: String
var category: Category
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
enum Category: String, CaseIterable, Codable, Hashable {
case featured = "Featured"
case lakes = "Lakes"
case rivers = "Rivers"
}
}
extension Landmark {
var image: Image {
ImageStore.shared.image(name: imageName)
}
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
- 選擇 Resources->landmarkData.json,這個樣本資料檔案如下所示:

三、創建行視圖
- 本文創建的第一個視圖就是用來顯示每個地標的行視圖,行視圖把地標的相關資訊存盤在一個屬性中,一行就可以代表一個地標,稍后就會把這些行組合成為一個串列:

- 創建一個名為 LandmarkRow.swift 的 SwiftUI 視圖;
- 如果預覽視圖沒有出現,可以選擇選單編輯器->畫布,打開畫布,并點擊 Resume 進行預覽,或者使用 Command+Option+Enter 快捷鍵調出畫面,再使用 Command+Option+P 快捷鍵開始預覽模式;
- 添加 landmark 屬性做為 LandmarkRow視圖的一個存盤屬性,當添加 landmark 屬性后,預覽視圖可能會停止作業,因為 LandmarkRow 視圖初始化時需要有一個 landmark 實體,要想修復預覽視圖,需要修改 Preview Provider;
- 在 LandmarkRow_Previews 的靜態屬性 previews 中給 LandmarkRow 初始化器中傳入 landmark 引數,這個引數使用 landmarkData 陣列的第一個元素,預覽視圖當前顯示 Hello, World;

- 在一個 HStack 中嵌入一個 Text,修改這個 Text,讓它使用 landmark 屬性的 name 欄位,在 Text 視圖前面添加一個圖片視圖,在 Text 視圖后面添加 Spacer 視圖:
struct LandmarkRow: View {
var landmark : Landmark
var body: some View {
HStack {
landmark.image
.resizable()
.frame(width: 50, height: 50)
Text(landmark.name)
Spacer()
}
}
}
struct LandmarkRow_Previews: PreviewProvider {
static var previews: some View {
LandmarkRow(landmark: landmarkData[0])
}
}
- 效果如下:

四、自定義行預覽
- Xcode 的畫布會自動識別當前代碼編輯器中遵循 PreviewProvider 協議的型別,并將它們渲染并展示在畫面上;一個視圖預覽提供者(preview provider)回傳一個或多個視圖,這些視圖可以配置不同的大小和設備型號,可以定制從 preview provider 中回傳的視圖被渲染在何種場景下:

- 在 LandmarkRow_Previews 中,把 landmark 引數更新為 landmarkData 陣列的第二個元素,預覽視圖會立即重繪反映第二個元素的渲染情況:
struct LandmarkRow_Previews: PreviewProvider {
static var previews: some View {
LandmarkRow(landmark: landmarkData[1])
.previewLayout(.fixed(width: 300, height: 70))
}
}

- 使用 previewLayout(_😃 修改器設定一個行視圖在串列中顯示的尺寸大小,可以使用 Group 的方式,回傳多個不同場景下的預覽視圖:

- 把預覽的行視圖包裹在 Group 中,把之前的第一個行視圖也加進去,Group 是一個容器,它可以把視圖內容組織起來,Xcode 會把 Group 內的每個子視圖當作畫布內一個單獨的預覽視圖處理:

- 為了簡化代碼,可以把 previewLayout(_😃 這個修改器應用到外層的 Group 上,Group 的每一個子視圖會繼承自己所處環境的配置,對 preivew provider 的修改只會影響預覽畫布的表現,對實際的應用不會產生影響:

五、創建地標串列
- 使用 SwiftUI 串列型別可以展示平臺相關的串列視圖,串列的元素可以是靜態的,類似于堆疊內部的子視圖,也可以是動態生成的視圖,也可以混合動態和靜態的視圖:

- 創建 SwiftUI 視圖,命名為 LandmarkList.swift,用 List 替換默認創建的 Text,并將前兩個 LandmarkRow 實體做為串列的子元素,預覽視圖中會以串列的形式展示出兩個地標:

六、創建動態串列
- 除了單獨列出串列中的每個元素外,串列還可以從一個集合中動態的生成:

- 創建串列時可以傳入一個集合資料和一個閉包,閉包會針對每一個資料元素回傳一個視圖,這個視圖就是串列的行視圖,
- 從串列中移除兩個靜態指定的行視圖,給串列初始化器傳入 landmarkData 資料,串列要配合可辨別的資料型別使用,想讓資料變成可辨別的資料型別有兩種方法:
-
- 傳入一個 keypath 指定資料中哪一個欄位用來唯一標識這個資料元素;
-
- 讓資料遵循 Identifiable 協議,
- 在閉包中回傳一個 LandmarkRow 視圖,List 初始化器中指定資料集合 landmarkData 和唯一識別符號keypath:.id,這樣串列就會動態生成,如下圖所示:

- 切換到檔案 Landmark.swfit,宣告 Landmark 型別遵循 Identifiable 協議,因為 Landmark 型別已經定義了 id 屬性,正好滿足 Identifiable 協議,所以不需要添加其它代碼:

- 現在切換回檔案 LandmarkList.swift,移除 keypath.id,因為 landmarkData 資料集合的元素已經遵循了 Identifiable 協議,所以在串列初始化器中可以直接使用,不需要手動標明資料的唯一識別符號了:

七、設定從串列頁到詳情頁的頁面導航
- 地標串列可以正常渲染展示,但是串列的元素點擊后沒有反應,跳轉不到地標詳情頁;現在就要給串列添加導航能力,把串列視圖嵌套到 NavigationView 視圖中,然后把串列的每一個行視圖嵌套進 NavigationLink 視圖中,就可以建立起從地標串列視圖到地標詳情頁的跳轉:

- 把動態生成的串列視圖嵌套進一個 NavigationView 視圖中:
struct LandmarkList: View {
var body: some View {
NavigationView {
List(landmarkData) { landmark in
LandmarkRow(landmark: landmark)
}
}
}
}
- 呼叫 navigationBarTitle( _: ) 修改器設定地標串列顯示時的導航條標題:

- 在串列的閉包中,將每一個行元素包裹在 NavigationLink 中回傳,并指定 ContentView 視圖為目標視圖:
struct LandmarkList: View {
var body: some View {
NavigationView {
List(landmarkData) { landmark in
NavigationLink(destination: ContentView()) {
LandmarkRow(landmark: landmark)
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
- 切換到實時預覽模式下可以直接點擊地標串列的任意一行,現在就可以跳轉到地標詳情頁:


八、子視圖傳入資料
- ContentView 視圖目前還是使用寫死的資料進行展示,與 LandmarkRow 視圖一樣,ContentView 視圖及它內部的子視圖也需要傳入 landmark 資料,并使用它來進行實際的展示,從 ContentView 的子視圖(CircleImage、MapView)開始,需要把它們都改造成為使用傳入的資料進行展示,而不是在布局代碼中寫死資料展示:

- 在 CircleImage.swift 檔案中,添加一個存盤屬性,命名為 image,這是一種在構建 SwiftUI 視圖中很常用的模式,常常會包裹或封裝一些屬性修改器:
struct CircleImage: View {
var image : Image
var body: some View {
image
.clipShape(Circle())
.overlay(
Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
}
}
- 更新 CirleImage 的預覽結構體,并傳入 Turtle Rock 這個圖片進行預覽:
struct CircleImage_Previews: PreviewProvider {
static var previews: some View {
CircleImage(image: Image("turtlerock"))
}
}
- 在 MapView.swift 中添加一個 coordinate 屬性,并使用這個屬性來替換寫死的經緯度坐標:
struct MapView: UIViewRepresentable {
var coordinate : CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
let region = MKCoordinateRegion(center: coordinate, span: span)
uiView.setRegion(region, animated: true)
}
}
- 更新 MapView 的預覽結構體,并傳入每一個地標的經緯度資料:
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView(coordinate: landmarkData[0].locationCoordinate)
}
}
- 在 ContentView.swift 中添加 landmark 屬性,更新 ContentView 預覽結構體,并傳入第一個地標的資料,把對應子視圖的資料傳入:
struct ContentView: View {
var landmark : Landmark
var body: some View {
VStack {
MapView(coordinate: landmark.locationCoordinate)
.edgesIgnoringSafeArea(.top)
.frame(height: 300)
CircleImage(image: landmark.image)
.offset(x: 0, y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
Text(landmark.name)
.font(.title)
HStack(alignment: .top) {
Text(landmark.park)
.font(.subheadline)
Spacer()
Text(landmark.state)
.font(.subheadline)
}
}
.padding()
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(landmark: landmarkData[0])
}
}
- 最后呼叫 navigationBarTitle(_:displayMode:) 修改器為地標詳情頁展示時在導航條上設定一個標題:

- 在 App 入口 BuildingListsApp.swift 類中的 Main 函式中修改根視圖為 LandmarkList:
@main
struct BuildingListsApp: App {
var body: some Scene {
WindowGroup {
LandmarkList()
}
}
}
- 在 LandmarkList.swift 中,傳入當前行的地標資料到地標詳情頁 ContentView:
struct LandmarkList: View {
var body: some View {
NavigationView {
List(landmarkData) { landmark in
NavigationLink(destination: ContentView(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList()
}
}
- 切換到實時預覽模式下去查看從地標串列頁對應的行跳轉到對應地標詳情頁是否正常:
九、動態生成預覽視圖
- 接下來要在不同尺寸設備上展示不同的預覽視圖,默認情況下,預覽視圖會選擇當前 Scheme 選中的設備尺寸進行渲染,可以使用 previewDevice(_😃 修改器來改變預覽視圖的設備:

- 改變當前預覽串列,讓它渲染在 iPhone 8 Plus 設備上,可以使用 Xcode Scheme 選單上的設備名稱來指定渲染設備:

- 在串列的預覽視圖中,還可以把 LandmarkList 嵌套進入 ForEach 實體中,使用設備陣列名作為資料,ForEach 運算作用在集合型別的資料上,就和串列使用集合型別資料一樣,可以在子視圖使用的任何場景下使用 ForEach,例如:stack、list、group 等,當元素資料是簡單值型別時(例如字串型別),可以使用 .self 作為 keypath 去標識:
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone 8 Plus", "iPhone 11 Pro Max"], id: \.self) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
}
}
}
- 使用 previewDisplayName( _: ) 修改器可以給預覽視圖添加設備標簽,可以在畫布上多設定幾個設備進行預覽,比較不同設備下視圖的展示情況,
- 完整示例:SwiftUI之創建串列展示頁和導航跳轉詳情頁,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/386985.html
標籤:其他
上一篇:如何使用cy.intercept()用不同的存根存根兩個請求?
下一篇:如何使此影像正確顯示?
