05 让列表动态生成

相对于亲自指定列表的元素,我们可以直接从集合中生成行元素。

我们可以通过传入集合的数据并使用闭包为集合中的每一个元素提供一个视图来创建一个列表显示集合中的元素。这个列表通过提供的闭包将集合中的每个元素转换成一个子视图。

swiftui 打开新页面方式(构建列表和导航)(1)

第一步

移除现有的两个静态地标行,并将landmarkData传入List的初始化器。

List需要配合可辨识数据使用。可以通过两种方法让数据变得可辨识:通过随数据传入一个可以区分每个元素的属性字段,或者是让数据遵从Identifiable协议。

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

第二步

通过在闭包中返回一个LandmarkRow实例动态地生成列表。

这会为landmarkData数组中的每个元素创建一个LandmarkRow实例。

var body: some View { List(landmarkData, id: \.id) { landmark in LandmarkRow(landmark: landmark) } }

接下来,我们通过在Landmark类型中添加Identifiable协议简化List中的代码。

第三步

切换到Landmark.swift文件并为Landmark类型声明Identifiable协议。

因为Landmark类型已经有了Identifiable协议要求的id属性,我们无需再做修改。

import CoreLocation struct Landmark: Hashable, Codable, Identifiable { var id: Int var name: String

第四步

切换回LandmarkList.swift文件并移除id参数。

从这时起,我们可以直接使用Landmark集合中的元素。

struct LandmarkList: View { var body: some View { List(landmarkData) { landmark in LandmarkRow(landmark: landmark) }

06 设置列表和详情之间的导航

列表已经可以正确的渲染了,但是我们还不能点击每个地标来查看地标的详情页。

我们会将列表嵌套在一个NavigationView视图中添加导航的能力,然后将每个行视图嵌套在一个NavigationLink中设置到目标视图的转换。

swiftui 打开新页面方式(构建列表和导航)(2)

第一步

将动态生成的地标列表嵌套进NavigationView。

struct LandmarkList: View { var body: some View { NavigationView { List(landmarkData) { landmark in LandmarkRow(landmark: landmark) } } }

第二步

调用navigationBarTitle(_:)修饰器方法设置列表显示时导航条的标题。

LandmarkRow(landmark: landmark) } .navigationBarTitle(Text("Landmarks")) } }

第三步

在列表的闭包中,将返回的行视图嵌套在NavigationLink中,并指定LandmarkDetail作为目标。

NavigationView { List(landmarkData) { landmark in NavigationLink(destination: LandmarkDetail()) { LandmarkRow(landmark: landmark) } } .navigationBarTitle(Text("Landmarks"))

第四步

我们可以通过将预览切换到实时模式直接尝试导航的效果。点击Live Preview按钮并点击一个地标访问详情页。

swiftui 打开新页面方式(构建列表和导航)(3)

07 传递数据到子视图中

LandmarkDetail视图依然还在用硬编码的数据显示地标。如同LandmarkRow一样,LandmarkDetail类型及组成它的视图需要一个landmark属性作为数据源。

我们从子视图开始,转换CircleImage、MapView然后是LandmarkDetail来展示传入的数据而不是硬编码的信息。

swiftui 打开新页面方式(构建列表和导航)(4)

第一步

在CircleImage.swift文件中,添加一个存储图片的属性到CircleImage类中。

struct CircleImage: View { var image: Image var body: some View { image

第二步

更新预览提供器,传入Turtle Rock的图片。

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

第三步

在MapView.swift文件中,为MapView类添加一个坐标属性,并使用这个属性取代硬编码的经度和纬度。

struct MapView: UIViewRepresentable { var coordinate: CLLocationCoordinate2D func makeUIView(context: Context) -> MKMapView { MKMapView(frame: .zero)

第四步

更新预览提供器并传入数据数组中的第一个地标的坐标。

struct MapView_Previews: PreviewProvider { static var previews: some View { MapView(coordinate: landmarkData[0].locationCoordinate) } }

第五步

在LandmarkDetail.swift文件中,为LandmarkDetail类添加一个Landmark属性。

struct LandmarkDetail: View { var landmark: Landmark var body: some View { VStack {

第六步

更新预览提供器,使用landmarkData数组的第一个地标。

struct LandmarkDetail_Previews: PreviewProvider { static var previews: some View { LandmarkDetail(landmark: landmarkData[0]) } }

第七步

将需要的数据传入那些自定义的类型中。

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) } }

第八步

最后,在调用navigationBarTitle(_:displayMode:)修饰器在展示详情页的时候添加导航栏标题。

Spacer() } .navigationBarTitle(Text(landmark.name), displayMode: .inline) } }

第九步

在SceneDelegate.swift文件中,将App顶层的视图换成LandmarkList。

当App在模拟器中而不是预览器中运行的时候会从定义的SceneDelegate中的顶层视图开始。

if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: LandmarkList()) self.window = window window.makeKeyAndVisible()

第十步

在LandmarkList.swift文件中,将当前地标传入LandmarkDetail目标。

NavigationView { List(landmarkData) { landmark in NavigationLink(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark: landmark) }

第十一步

切换到实时预览模式查看从列表导航到详情页的时候是否显示了正确的地标。

swiftui 打开新页面方式(构建列表和导航)(5)

08 动态生成预览

接下来,我们会在LandmarkList_Previews预览提供器中添加代码在不同的设备中渲染列表视图。默认情况下,预览器会渲染当前选择的scheme的设备,可以通过调用previewDevice(_:)修饰器方法改变预览的设备。

swiftui 打开新页面方式(构建列表和导航)(6)

第一步

我们从改变LandmarkList.swift文件当前列表渲染图到iPhone SE设备尺寸开始。

我们可以提供出现在Xcode scheme菜单中的任何设备名称。

static var previews: some View { LandmarkList() .previewDevice(PreviewDevice(rawValue: "iPhone SE")) } }

第二步

在列表预览中,将LandmarkList嵌套在ForEach实例中,并使用设备名数组作为数据源。

ForEach实例操作集合的方式和列表一样,意味着我们可以在任何使用子视图的地方使用它,比如在stack、list、group中等等。当数据的元素是简单数据类型-如这里使用的字符串-我们可以使用\.self作为id的键索引。

struct LandmarkList_Previews: PreviewProvider { static var previews: some View { ForEach(["iPhone SE", "iPhone XS Max"], id: \.self) { deviceName in LandmarkList() .previewDevice(PreviewDevice(rawValue: deviceName)) } } }

第三步

使用previewDisplayName(_:)修饰器为预览器添加设备名标签。

LandmarkList() .previewDevice(PreviewDevice(rawValue: deviceName)) .previewDisplayName(deviceName) } }

第四步

我们可以完全从画布中就比较在不同设备中渲染的视图效果。

swiftui 打开新页面方式(构建列表和导航)(7)

,