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)")
}
}
创建列表视图
- 新增一个
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
)
- 在上面的例子中,我们预览了第0个数据。可以通过修改下标,改成自己想要预览的数据
- 可以通过调整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()
}
}
展示效果如图:
循环列表
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). 增加导航页面并实现跳转,关键字NavigationView
和 NavigationLink
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。 关键字navigationTitle
、navigationBarTitleDisplayMode
。
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)
}
}