Skip to content

1.2 SwiftUI Essentials(Landmarks)列表和导航

developer.apple.com/tutorials/swiftui/building-lists-and-navigation

数据源处理

1). 导入示例项目中的 landmarkData.json和其他图片素材导入到自己的项目,用于后面列表数据读取
2). 创建模型类 Landmark.swift

swift
import Foundation 
import SwiftUI
import CoreLocation

struct Landmark: Hashable, Codable { 
	var id: Int 
	var name: String 
	var park: String 
	var state: String 
	var description: String  
    // 这里额外增加了imageName, 从数据中读取图像的名称
    // 这个属性是私有的,因为Landmarks的构建只关心图像本身
	private var imageName: String 
	// 增加了计算属性的image
	var image: Image {
	 Image(imageName) 
	}

    // 增加了经纬度信息以及对应的结构体
	private var coordinates: Coordinates 
	var locationCoordinate: CLLocationCoordinate2D { 
		CLLocationCoordinate2D( 
		latitude: coordinates.latitude, 
		longitude: coordinates.longitude) 
	}
	
	struct Coordinates: Hashable, Codable { 
		var latitude: Double 
		var longitude: Double 
	}
}
import Foundation 
import SwiftUI
import CoreLocation

struct Landmark: Hashable, Codable { 
	var id: Int 
	var name: String 
	var park: String 
	var state: String 
	var description: String  
    // 这里额外增加了imageName, 从数据中读取图像的名称
    // 这个属性是私有的,因为Landmarks的构建只关心图像本身
	private var imageName: String 
	// 增加了计算属性的image
	var image: Image {
	 Image(imageName) 
	}

    // 增加了经纬度信息以及对应的结构体
	private var coordinates: Coordinates 
	var locationCoordinate: CLLocationCoordinate2D { 
		CLLocationCoordinate2D( 
		latitude: coordinates.latitude, 
		longitude: coordinates.longitude) 
	}
	
	struct Coordinates: Hashable, Codable { 
		var latitude: Double 
		var longitude: Double 
	}
}

3). 创建ModelData.swift文件,用于处理数据的读取和解析

swift
import Foundation 

// gloabl
var landmarks: [Landmark] = load("landmarkData.json")

func load<T: Decodable>(_ filename: String) -> T { 
												  
 let data: Data guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else { 
 fatalError("Couldn't find \(filename) in main bundle.") 
 } 
 
 do {
	data = try Data(contentsOf: file) 
  }catch { 
	fatalError("Couldn't load \(filename) from main bundle:\n\(error)") 
  } 
  
  do { 
	  let decoder = JSONDecoder() 
	  return try decoder.decode(T.self, from: data) 
  } catch {
      fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)") 
  }
}
import Foundation 

// gloabl
var landmarks: [Landmark] = load("landmarkData.json")

func load<T: Decodable>(_ filename: String) -> T { 
												  
 let data: Data guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else { 
 fatalError("Couldn't find \(filename) in main bundle.") 
 } 
 
 do {
	data = try Data(contentsOf: file) 
  }catch { 
	fatalError("Couldn't load \(filename) from main bundle:\n\(error)") 
  } 
  
  do { 
	  let decoder = JSONDecoder() 
	  return try decoder.decode(T.self, from: data) 
  } catch {
      fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)") 
  }
}

创建列表视图

  1. 新增一个 LandmarkRow.swift 的SwiftUI视图
swift
import SwiftUI

struct LandmarkRow: View {
    var landMark: Landmark
    
    var body: some View {
        Text(landMark.name)
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        // 读取第0个资源用于测试
        LandmarkRow(landMark: landmarks[0])
    }
}
import SwiftUI

struct LandmarkRow: View {
    var landMark: Landmark
    
    var body: some View {
        Text(landMark.name)
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        // 读取第0个资源用于测试
        LandmarkRow(landMark: landmarks[0])
    }
}

2). 进行页面布局

swift
import SwiftUI

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 {
        // 读取第0个资源用于测试
        LandmarkRow(landMark: landmarks[0])
    }
}
import SwiftUI

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 {
        // 读取第0个资源用于测试
        LandmarkRow(landMark: landmarks[0])
    }
}

自定义预览(_Previews

  1. 在上面的例子中,我们预览了第0个数据。可以通过修改下标,改成自己想要预览的数据
  2. 可以通过调整previewLayout,设置canvas预览的大小
swift
struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landMark: landmarks[0])
            .previewLayout(PreviewLayout.fixed(width: 500, height: 80))
            .background(.red)
    }
}
struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkRow(landMark: landmarks[0])
            .previewLayout(PreviewLayout.fixed(width: 500, height: 80))
            .background(.red)
    }
}

列表展示

编写如下代码:

swift
import SwiftUI

struct LandmarkList: View {
    var body: some View {
        List {
            LandmarkRow(landmark: landmarks[0])
            LandmarkRow(landmark: landmarks[1])
            LandmarkRow(landmark: landmarks[2])
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
import SwiftUI

struct LandmarkList: View {
    var body: some View {
        List {
            LandmarkRow(landmark: landmarks[0])
            LandmarkRow(landmark: landmarks[1])
            LandmarkRow(landmark: landmarks[2])
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}

展示效果如图:

list|300

循环列表

swift
import SwiftUI

struct LandmarkList: View {
    var body: some View {
        List(landmarks, id: \.id) { item in
            LandmarkRow(landmark: item)
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
import SwiftUI

struct LandmarkList: View {
    var body: some View {
        List(landmarks, id: \.id) { item in
            LandmarkRow(landmark: item)
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}

接下来,通过向 Landmark 类型添加 Identifiable 的一致性来简化 List 的代码,额外新增Identifiable 协议(已经遵从了Identifiable协议,因此直接增加Identifiable)。

swift
struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    ....
struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    ....

简化完成后,可以删除 id: \.id:

swift
struct LandmarkList: View {
    var body: some View {
        List(landmarks) { item in
            LandmarkRow(landmark: item)
        }
    }
}
struct LandmarkList: View {
    var body: some View {
        List(landmarks) { item in
            LandmarkRow(landmark: item)
        }
    }
}

导航和页面跳转

1). 将列表页面放在ContentView的位置、单独提取详情页面

2). 增加导航页面并实现跳转,关键字NavigationViewNavigationLink

swift
import SwiftUI

struct LandmarkList: View {
    var body: some View {
        NavigationView {
            List(landmarks) { item in
                NavigationLink {
                    LandmarkDetail()
                } label: {
                    LandmarkRow(landmark: item)
                }

            }.navigationTitle("landmark")
        }
    }
}
import SwiftUI

struct LandmarkList: View {
    var body: some View {
        NavigationView {
            List(landmarks) { item in
                NavigationLink {
                    LandmarkDetail()
                } label: {
                    LandmarkRow(landmark: item)
                }

            }.navigationTitle("landmark")
        }
    }
}

3). 详情页面改成动态页面,依次修改页面即可:

CircleImage:

swift
import SwiftUI

struct CircleImage: View {
    var image: Image
    var body: some View {
        image
            .clipShape(Circle())
            .overlay {
                Circle().stroke(.red, lineWidth: 5)
            }
            .shadow(radius: 7)
    }
}

struct CircleImage_Previews: PreviewProvider {
    static var previews: some View {
        CircleImage(image: Image("turtlerock"))
    }
}
import SwiftUI

struct CircleImage: View {
    var image: Image
    var body: some View {
        image
            .clipShape(Circle())
            .overlay {
                Circle().stroke(.red, lineWidth: 5)
            }
            .shadow(radius: 7)
    }
}

struct CircleImage_Previews: PreviewProvider {
    static var previews: some View {
        CircleImage(image: Image("turtlerock"))
    }
}

MapView: 这个稍微复杂,需要进入页面的时候更新region

swift
import SwiftUI
import MapKit

struct MapView: View {
    
    var coordinates: CLLocationCoordinate2D
    
    // 创建保存地图区域信息的私有状态变量。
    // 你可以使用@State属性来修饰数据源,这样你可以从多个视图中修改数据。 当数据源改变SwiftUI会根据值的变化动态更新依赖该值的视图
    @State
    private var region = MKCoordinateRegion(
            center: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868),
            span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
        )
    
    var body: some View {
        // 通过给状态变量加上前缀$,传递了一个绑定,就像是对底层值的引用。当用户与地图交互时,地图更新区域值以匹配用户界面中当前可见的地图部分。
        // Binding<MKCoordinateRegion>
        Map(coordinateRegion: $region)
            .onAppear{
                setRegion(coordinates: coordinates)
            }
    }
    
    private func setRegion(coordinates: CLLocationCoordinate2D) {
        region = MKCoordinateRegion(center: coordinates,
                                    span: MKCoordinateSpan.init(latitudeDelta: 0.2, longitudeDelta: 0.2))
    }
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView(coordinates: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868))
    }
}
import SwiftUI
import MapKit

struct MapView: View {
    
    var coordinates: CLLocationCoordinate2D
    
    // 创建保存地图区域信息的私有状态变量。
    // 你可以使用@State属性来修饰数据源,这样你可以从多个视图中修改数据。 当数据源改变SwiftUI会根据值的变化动态更新依赖该值的视图
    @State
    private var region = MKCoordinateRegion(
            center: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868),
            span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
        )
    
    var body: some View {
        // 通过给状态变量加上前缀$,传递了一个绑定,就像是对底层值的引用。当用户与地图交互时,地图更新区域值以匹配用户界面中当前可见的地图部分。
        // Binding<MKCoordinateRegion>
        Map(coordinateRegion: $region)
            .onAppear{
                setRegion(coordinates: coordinates)
            }
    }
    
    private func setRegion(coordinates: CLLocationCoordinate2D) {
        region = MKCoordinateRegion(center: coordinates,
                                    span: MKCoordinateSpan.init(latitudeDelta: 0.2, longitudeDelta: 0.2))
    }
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView(coordinates: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868))
    }
}

详情页面跳转的优化

目前详情页面展示文字不全,需要将vstack改成scrollView,并且导航栏的样式也要调整一下,因为默认是无标题,large title。 关键字navigationTitlenavigationBarTitleDisplayMode

swift
import SwiftUI

struct LandmarkDetail: View {

    var landmark:Landmark
    
    var body: some View {
        ScrollView {
            MapView(coordinates: landmark.locationCoordinate)
                .ignoresSafeArea()
                .frame(height: 300)

            CircleImage(image: landmark.image)
                .offset(y: -180)
                .padding(.bottom, -180)
            
            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                .foregroundColor(.cyan)
                HStack {
                    Text(landmark.park)
                        .font(.subheadline)
                    Spacer()
                    Text(landmark.state)
                        .font(.subheadline)
                }
                Divider()
                Text("About \(landmark.name)").font(.title2)
                Text(landmark.description).font(.subheadline)
            }
        }.navigationTitle(landmark.name)
            .navigationBarTitleDisplayMode(.inline)
    }
}
import SwiftUI

struct LandmarkDetail: View {

    var landmark:Landmark
    
    var body: some View {
        ScrollView {
            MapView(coordinates: landmark.locationCoordinate)
                .ignoresSafeArea()
                .frame(height: 300)

            CircleImage(image: landmark.image)
                .offset(y: -180)
                .padding(.bottom, -180)
            
            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                .foregroundColor(.cyan)
                HStack {
                    Text(landmark.park)
                        .font(.subheadline)
                    Spacer()
                    Text(landmark.state)
                        .font(.subheadline)
                }
                Divider()
                Text("About \(landmark.name)").font(.title2)
                Text(landmark.description).font(.subheadline)
            }
        }.navigationTitle(landmark.name)
            .navigationBarTitleDisplayMode(.inline)
    }
}