简介
此示例是记录学习SwiftUI的过程,原文出自
SwiftUI Essentials Building Lists and Navigation。
在SwiftUI 01-创建和组合视图 (Creating and Combining Views) 中创建了landmark
的详情页,本节我们做landmark
的列表页。
我们将创建可以显示任何landmark
信息的视图,并动态生成一个滚动列表,用户可以点击该列表查看landmark
的详细视图。要微调UI,我们将使用Xcode的画布以不同的设备大小呈现多个预览。
下载项目文件以开始构建此项目,并按照以下步骤操作。
第一节 了解示例中的数据
在第一个教程中, 我们直接把数据写死在视图的代码中。现在我们将学习把数据传递给自定义视图以供其显示。
首先下载入门项目并熟悉示例数据。
1.在Project导航器中,选择
Models> Landmark.swift
。
Landmark.swift
声明了一个Landmark
的结构体,用于存储应用程序需要显示的所有landmark
信息,根据landmarkData.json
中其中一组数据的所有字段构建的landmark
的model。2.在Project导航器中,选择
Resources> landmarkData.json
。
我们将在本教程的其余部分以及随后的所有内容中使用此示例数据。
- 3.请注意,创建和组合视图中的
ContentView
类现在被修改为LandmarkDetail
。
我们将在此以及以下每个教程中创建多个view的类。
第2节 创建行视图
我们将在本教程中构建的第一个视图是用于显示每个地标的详细信息的行。 此行视图将信息存储在其显示的地标的属性中,以便一个视图可以显示任何地标。 稍后,您将多个行组合成一个地标列表。
1.创建一个名为
LandmarkRow.swift
的新SwiftUI
视图。2.如果预览不可见,请通过选择
Editor > Editor and Canvas
来显示画布,然后单击Get Started
。3.添加
LandmarkRow
类中添加一个Landmark
类型的landmark
属性作为这个视图的模型。
import SwiftUI
struct LandmarkRow : View {
var landmark: Landmark
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/)
}
}
#if DEBUG
struct LandmarkRow_Previews : PreviewProvider {
static var previews: some View {
LandmarkRow()
}
}
#endif
添加landmark
属性时,预览将停止工作,因为LandmarkRow
类型在初始化期间需要传入一个Landmark
的模型。
要修复预览,我们需要修改预览中初始化LandmarkRow()
的代码。
- 4.在
LandmarkRow_Previews
的预览静态属性中,将landmark
参数添加到LandmarkRow()
初始化方法的参数中,指定landmarkData
数组的第一个元素。
预览显示文本Hello World。
修复后,您可以为行构建布局。
5.将现有文本视图嵌入HStack中。
6.修改文本视图以使用
landmark
属性的name
。7.通过在文本视图之前添加图像来完成行。
第3节 自定义行预览
Xcode的画布自动识别并显示当前编辑器中符合PreviewProvider协议的任何类型。 预览提供程序返回一个或多个视图,其中包含用于配置大小和设备的选项。
我们可以从预览提供程序自定义返回的内容,以准确呈现对我们最有帮助的预览。
- 1.在
LandmarkRow_Previews
中,将landmark
参数更新为landmarkData
数组中的第二个元素。
修改后预览立即更改以显示第二个model的数据而不是第一个。
- 2.使用
previewLayout(_ :)
方法设置列表中这一行的cell大小。
我们可以在previews
中使用group
返回多行cell预览。
- 3.将返回的行换放到一个
Group
中,然后在Group
中再添加一个LandmarkRow
。
Group
是用于对视图内容进行分组的容器。 Xcode将组的子视图渲染为画布中的单独预览。
- 4.要简化代码,请将
previewLayout(_ :)
调用移动到Group
的子声明外部。
import SwiftUI
struct LandmarkRow : View {
var landmark: Landmark
var body: some View {
HStack {
landmark.image(forSize: 50)
Text(landmark.name)
}
}
}
#if DEBUG
struct LandmarkRow_Previews : PreviewProvider {
static var previews: some View {
Group {
LandmarkRow(landmark: landmarkData[0])
LandmarkRow(landmark: landmarkData[1])
}
.previewLayout(.fixed(width: 300, height: 70))
}
}
#endif
视图的子项继承视图的上下文设置,例如预览配置。
我们在预览提供程序中编写的代码仅更改Xcode
在画布中显示的内容。 由于#if DEBUG
指令,编译器会删除代码,因此它在release下不会打包到应用程序中。
第4节 创建landmark
列表
使用SwiftUI
的List
类型时,可以显示特定于平台的视图列表。 列表的元素可以是静态的,就像我们目前创建的堆栈的子视图一样,或者是动态生成的。 您甚至可以混合静态和动态生成的视图。
1.创建一个名为
LandmarkList.swift
的新SwiftUI
视图。2.用
List
替换模板中默认的Text
视图,并在List
中添加两个LandmarkRow
初始化的视图作为两行显示。
现在我们预览显示以适合iOS的列表样式呈现的两个landmark
,这个List
不就是UITableView
吗?
第5节 使列表动态化
我们可以直接从集合中生成行,而不是单独指定列表的元素。
我们可以通过传递数据集合以及为集合中的每个元素提供视图的闭包来创建显示集合元素的列表。 该列表使用提供的闭包将集合中的每个元素转换为子视图。
- 1.删除
List
中两个静态的LandmarkRow
,然后将landmarkData
传递给List
的初始化方法中。
列表使用可识别的数据。 我们可以通过以下两种方式之一来识别数据:通过调用identified(by:)
方法,使用唯一标识每个元素的属性的键路径,或者使您的数据类型遵守Identifiable
协议。
- 2.通过从闭包返回
LandmarkRow
来完成动态生成的列表。
这为landmarkData
数组中的每个元素创建一个LandmarkRow
。
接下来,我们将通过向Landmark
类型添加Identifiable
协议来简化List
代码。
- 3.切换到
Landmark.swift并
声明符合可识别协议。
由于Landmark
类型已经具有Identifiable
协议所需的id
属性,因此没有其他工作要做。
- 4.切换回
LandmarkList.swift
并删除landmarkData
调用的identified(by:)
方法。
import SwiftUI
struct LandmarkList : View {
var body: some View {
List(landmarkData) { landmark in
LandmarkRow(landmark: landmark)
}
}
}
#if DEBUG
struct LandmarkList_Previews : PreviewProvider {
static var previews: some View {
LandmarkList()
}
}
#endif
从现在开始,我们将能够直接使用Landmark
元素的集合。
第6节 在列表和详细信息之间设置导航
列表正确呈现,但我们无法点击某个landmark
以查看它的详细信息页面。
通过将导航功能嵌入到NavigationView
中,然后将每行嵌套在NavigationButton
中以设置到目标视图的转换,可以将导航功能添加到列表中。
1.在
NavigationView
中嵌入动态生成的landmark
列表。2.调用
navigationBarTitle(_ :)
修饰符方法以在显示列表时设置导航栏的标题。
3.在列表的闭包内,将返回的行包装在
NavigationButton
中,将LandmarkDetail
视图指定为目标。4.我们可以通过切换到实时模式直接在预览中尝试导航。 单击“实时预览”按钮,然后点击地标以访问详细信息页面。
第7节 将数据传递到子视图
LandmarkDetail
视图中是使用写死的数据展示UI 的。 就像LandmarkRow
一样,我们需要在LandmarkDetail
中添加一个Landmark
类型的模型以作为其数据源显示view。
从子视图开始,我们将转换CircleImage
,MapView
和LandmarkDetail
以显示传入的数据,而不是将数据写死在代码中。
- 1.在
CircleImage.swift
中,将存储的图像属性添加到CircleImage
。
import SwiftUI
struct CircleImage : View {
var image: Image
var body: some View {
image
// 给图片添加圆角
.clipShape(Circle())
// 给圆角添加边框
.overlay(Circle().stroke(Color.gray, lineWidth: 4))
// 添加半径为10的阴影
.shadow(radius: 10)
}
}
#if DEBUG
struct CircleImage_Previews : PreviewProvider {
static var previews: some View {
CircleImage(image: Image("turtlerock"))
}
}
#endif
这是使用SwiftUI
构建视图时的常见模式。 自定义视图通常会包装并封装特定视图的一系列修饰符。
- 2.更新
CircleImage_Previews
,给其传递一个Turtle Rock
的图像使其可以正常预览。
- 3.在
MapView.swift
中,向MapView
添加一个坐标属性,并转换代码以使用该属性,而不是在代码中写死纬度和经度数据。
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
var coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ view: MKMapView, context: Context) {
let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
let region = MKCoordinateRegion(center: coordinate, span: span)
view.setRegion(region, animated: true)
}
}
struct MapView_Preview: PreviewProvider {
static var previews: some View {
MapView()
}
}
- 4.更新预览提供程序以传递数据数组中第一个
landmark
的坐标。
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
var coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ view: MKMapView, context: Context) {
let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
let region = MKCoordinateRegion(center: coordinate, span: span)
view.setRegion(region, animated: true)
}
}
struct MapView_Preview: PreviewProvider {
static var previews: some View {
MapView(coordinate: landmarkData[0].locationCoordinate)
}
}
5.在
LandmarkDetail.swift
中,将Landmark
类型的属性添加到LandmarkDetail
类型中,作为其model数据。6.更新预览以使用
landmarkData
中的第一个lanmark
。7.完成将所需数据传递给您的自定义类型。
8.最后,调用
navigationBarTitle(_:displayMode :)
方法,在显示详细视图时为导航栏指定标题。
- 9.在
SceneDelegate.swift
中,将rootViewController
的rootView
修改为为LandmarkList
,这样首页展示的就是列表页了。
当我们在模拟器中独立运行而不是预览时,我们的应用程序将以SceneDelegate
中定义的根视图开始。
- 10.在
LandmarkList.swift
中,将当前landmark
传递到目标LandmarkDetail
中,关键代码为NavigationButton(destination: LandmarkDetail(landmark: landmark))
。
import SwiftUI
struct LandmarkList : View {
var body: some View {
// 设置导航容器
NavigationView {
// 初始化一个类型TableView的view
List(landmarkData) { landmark in
// 点击cell时,将当前`landmark`传递到目标`LandmarkDetail`中。
NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
// 显示当前列表页的导航标题
.navigationBarTitle(Text("Landmarks"))
}
}
}
#if DEBUG
struct LandmarkList_Previews : PreviewProvider {
static var previews: some View {
LandmarkList()
}
}
#endif
- 11.切换到实时预览以查看从列表导航时详细视图显示正确的标志
第8节 动态生成预览
接下来,我们将向LandmarkList_Previews
预览提供程序添加代码,以显示不同设备大小的列表视图的预览。 默认情况下,预览会以活动方案中设备的大小进行渲染。 我们可以通过调用previewDevice(_ :)
方法来更改预览设备。
- 1.首先,将当前列表预览更改为以
iPhone SE
的大小呈现。
#if DEBUG
struct LandmarkList_Previews : PreviewProvider {
static var previews: some View {
LandmarkList()
// 以iPhone SE 设备的大小预览画布
.previewDevice(PreviewDevice(rawValue: "iPhone SE"))
}
}
#endif
我们可以提供Xcode
方案菜单中显示的任何设备的名称。
- 2.在画布中添加对多个设备的预览
在预览的列表中,使用设备名称数组作为数据,将LandmarkList
嵌入到ForEach
实例中。
ForEach
以与列表相同的方式对集合进行操作,这意味着我们可以在任何可以使用子视图的位置使用它,例如在堆栈,列表,组等中。 当数据元素是简单的值类型 - 就像在这里使用的字符串一样 - 我们可以使用\ .self
作为标识符的关键路径。
- 3.使用
previewDisplayName(_ :)
修饰符将设备名称添加为预览的标签。
- 4.我们可以尝试使用不同的设备来比较视图的渲染,所有这些都来自画布。
测试
- 1.除了List之外,这些类型中的哪一个提供了集合中的动态视图列表?
可选项:
1.Group
2.ForEath
3.UITableView
答案:2
- 2.我们可以从可识别元素集合中创建视图列表。 我们使用什么方法来调整不符合可识别协议的元素集合?
可选项:
1.func map(_:)
2.func sorted(by:)
3.func identified(by:)
答案:3
- 3.用哪种类型来使
List
的行可以导航到另一个视图?
可选项:
1.NavigationButton
2.UITableViewDelegate
3.NavigationView
答案: 1
- 4.哪些选项不是设置设备以预览视图的方法?
可选项:
- Change the simulator selected in the active scheme.
- Make a different choice in Canvas Settings in Xcode’s preferences.
- Specify one or more devices using the
previewDevice(_:)
method. - Connect your development device and click the Device Preview button.
答案:2