地标详情页视图已经创建完成,我们需要提供一种方式让用户可以查看完整的地标列表,并且可以查看每一个地标的详情
下面会创建一个可以展示任何地标信息的视图,并动态生成一个可滚动列表,用户可以点击列表项去查看地标的详细信息。优化视图显示时,可以使用Xcode画布来渲染多个不同设备大小下的预览视图。
下载下面的工程文件,并跟着教程一步步学习构建列表和视图间导航
前面的教程中,自定义视图所展示的信息都直接被写死在代码中,这篇教程中会学习给自定义视图传入样本数据进行展示

步骤1 打开项目导航器,选择Models->Landmark.swift文件,这个文件中声明了需要在应用中展示一个地标所需要信息的结构化名称,并通过导入landmarkData.json文件中的数据,生成一个地标信息数组。

步骤2 在项目导航器中选择Resources->landmarkData.json,在后面的教程中我们都会使用这个样本数据文件

步骤3 注意,之前的ContentView视图,已经被改名为LandmarkDetail了,在本教程和后面的教程中,还会创建一些其它的视图

本教程中创建的第一个视图就是用来显示每个地标的行视图,行视图把地标的相关信息存储在一个属性中,一行就可以代表一个地标,稍后就会把这些行组合成为一个列表。

步骤1 创建一个名为LandmarkRow.swift的SwiftUI视图

步骤2 如果预览视图没有出现,可以选择菜单编辑器->画布,打开画布,并点击Resume进行预览,或者使用Command+Option+Enter快捷键调出画面,再使用Command+Option+P快捷键开始预览模式
步骤3 添加landmark属性做为LandmarkRow视图的一个存储属性。当添加landmark属性后,预览视图可能会停止工作,因为LandmarkRow视图初始化时需要有一个landmark实例。要想修复预览视图,需要修改Preview Provider
步骤4 在LandmarkRow_Previews的静态属性previews中给LandmarkRow初始化器中传入landmark参数,这个参数使用landmarkData数组的第一个元素。预览视图当前显示Hello, World

步骤5 在一个HStack中嵌入一个Text
步骤6 修改这个Text,让它使用landmark属性的name字段
步骤7 在Text视图前面添加一个图片视图,在Text视图后面添加Spacer视图

Xcode的画布会自动识别当前代码编辑器中遵循PreviewProvider协议的类型,并将它们渲染并展示在画面上。一个视图预览提供者(preview provider)返回一个或多个视图,这些视图可以配置不同的大小和设备型号。
可以定制从preview provider中返回的视图被渲染在何种场景下。

步骤1 在LandmarkRow_Previews中,把landmark参数更新为landmarkData数组的第二个元素,预览视图会立即刷新反映第二个元素的渲染情况

步骤2 使用previewLayout(_:)修改器设置一个行视图在列表中显示的尺寸大小。可以使用Group的方式,返回多个不同场景下的预览视图

步骤3 把预览的行视图包裹在Group中,把之前的第一个行视图也加进去。Group是一个容器,它可以把视图内容组织起来,Xcode会把Group内的每个子视图当作画布内一个单独的预览视图处理

步骤4 为了简化代码,可以把previewLayout(_:)这个修改器应用到外层的Group上,Group的每一个子视图会继承自己所处环境的配置。对preivew provider的修改只会影响预览画布的表现,对实际的应用不会产生影响。

使用SwiftUI列表类型可以展示平台相关的列表视图。列表的元素可以是静态的,类似于栈内部的子视图,也可以是动态生成的视图,也可以混合动态和静态的视图。

步骤1 创建SwiftUI视图,命名为LandmarkList.swift
步骤2 用List替换默认创建的Text,并将前两个LandmarkRow实例做为列表的子元素,预览视图中会以列表的形式展示出两个地标


除了单独列出列表中的每个元素外,列表还可以从一个集合中动态的生成。

创建列表时可以传入一个集合数据和一个闭包,闭包会针对每一个数据元素返回一个视图,这个视图就是列表的行视图。
步骤1 从列表中移除两个静态指定的行视图,给列表初始化器传入landmarkData数据,列表要配合可辨别的数据类型使用。想让数据变成可辨别的数据类型有两种方法:
传入一个keypath指定数据中哪一个字段用来唯一标识这个数据元素。
让数据遵循Identifiable协议
步骤2 在闭包中返回一个LandmarkRow视图,List初始化器中指定数据集合landmarkData和唯一标识符keypath:\.id,这样列表就会动态生成,如下图所示

步骤3 切换到文件Landmark.swfit,声明Landmark类型遵循Identifiable协议,因为Landmark类型已经定义了id属性,正好满足Identifiable协议,所以不需要添加其它代码

步骤4 现在切换回文件LandmarkList.swift,移除keypath\.id,因为landmarkData数据集合的元素已经遵循了Identifiable协议,所以在列表初始化器中可以直接使用,不需要手动标明数据的唯一标识符了

地标列表可以正常渲染展示,但是列表的元素点击后没有反应,跳转不到地标详情页。现在就要给列表添加导航能力,把列表视图嵌套到NavigationView视图中,然后把列表的每一个行视图嵌套进NavigationLink视图中,就可以建立起从地标列表视图到地标详情页的跳转。

步骤1 把动态生成的列表视图嵌套进一个NavigationView视图中

步骤2 调用navigationBarTitle(_:)修改器设置地标列表显示时的导航条标题

步骤3 在列表的闭包中,将每一个行元素包裹在NavigationLink中返回,并指定LandmarkDetail视图为目标视图

步骤4 切换到实时预览模式下可以直接点击地标列表的任意一行,现在就可以跳转到地标详情页了。

LandmarkDetail视图目前还是使用写死的数据进行展示,与LandmarkRow视图一样,LandmarkDetail视图及它内部的子视图也需要传入landmark数据,并使用它来进行实际的展示
从LandmarkDetail的子视图(CircleImage、MapView)开始,需要把它们都改造成为使用传入的数据进行展示,而不是在布局代码中写死数据展示

步骤1 在CircleImage.swift文件中,添加一个存储属性,命名为image。这是一种在构建SwiftUI视图中很常用的模式,常常会包裹或封装一些属性修改器。

步骤2 更新CirleImage的预览结构体,并传入Turtle Rock这个图片进行预览

步骤3 在MapView.swift中添加一个coordinate属性,并使用这个属性来替换写死的经纬度坐标

步骤4 更新MapView的预览结构体,并传入每一个地标的经纬度数据

步骤5 在LandmarkDetail.swift中添加landmark属性。
步骤6 更新LandmarkDetail预览结构体,并传入第一个地标的数据
步骤7 把对应子视图的数据传入

步骤8 最后调用navigationBarTitle(_:displayMode:)修改器为地标详情页展示时在导航条上设置一个标题

步骤9 在SceneDelegate.swift中把应用的根视图替换为LandmarkList。应用在模拟器中独立启动时使用SceneDelegate的根视图做为第一个展示的视图

步骤10 在LandmarkList.swift中,传入当前行的地标数据到地标详情页LandmarkDetail

步骤11 切换到实时预览模式下去查看从地标列表页对应的行跳转到对应地标详情页是否正常


接下来要在不同尺寸设备上展示不同的预览视图,默认情况下,预览视图会选择当前Scheme选中的设备尺寸进行渲染,可以使用previewDevice(_:)修改器来改变预览视图的设备
步骤1 改变当前预览列表,让它渲染在iPhone SE设备上。可以使用Xcode Scheme菜单上的设备名称来指定渲染设备。

步骤2 在列表的预览视图中,还可以把LandmarkList嵌套进入ForEach实例中,使用设备数组名作为数据。ForEach运算作用在集合类型的数据上,就和列表使用集合类型数据一样,可以在子视图使用的任何场景下使用ForEach,例如:stack、list、group等。当元素数据是简单值类型时(例如字符串类型),可以使用\.self作为keypath去标识

步骤3 使用previewDisplayName(_:)修改器可以给预览视图添加设备标签
步骤4 可以在画布上多设置几个设备进行预览,比较不同设备下视图的展示情况

问题1 除了List外,下面哪种类型可以从集合数据中展示动态列表视图
GroupForEachUITableView问题2 可以从遵循了Identifiable协议的集合数据创建列表视图。但如果集合数据不遵循Identifiable协议,还有什么办法可以创建列表视图?
map(_:)方法sorted(by:)方法List(_:id:)类型传入集合数据的同时,使用keypath指定一个唯一标识符字段问题3 使用什么类型才能让列表的行实现点击跳转到其它视图页面?
NavigationLinkUITableViewDelegateNavigationView问题4 下面哪种方式不是用来设置预览设备的?
scheme中选中的模拟器previewDevice(_:)指定一个或多个预览设备