SwiftUI 02-构建列表和导航(Building Lists and Navigation)

简介:学习SwiftUI,通过swiftUI 构建列表和导航

Demo 链接

简介

此示例是记录学习SwiftUI的过程,原文出自
SwiftUI Essentials Building Lists and Navigation

SwiftUI 01-创建和组合视图 (Creating and Combining Views) 中创建了landmark的详情页,本节我们做landmark的列表页。

我们将创建可以显示任何landmark信息的视图,并动态生成一个滚动列表,用户可以点击该列表查看landmark的详细视图。要微调UI,我们将使用Xcode的画布以不同的设备大小呈现多个预览。

下载项目文件以开始构建此项目,并按照以下步骤操作。

第一节 了解示例中的数据

在第一个教程中, 我们直接把数据写死在视图的代码中。现在我们将学习把数据传递给自定义视图以供其显示。
首先下载入门项目并熟悉示例数据。

2f8f9d15-348e-4c7f-b53d-be31a5d8c457.png

  • 1.在Project导航器中,选择Models> Landmark.swift
    Landmark.swift声明了一个Landmark的结构体,用于存储应用程序需要显示的所有landmark信息,根据landmarkData.json中其中一组数据的所有字段构建的landmark的model。

  • 2.在Project导航器中,选择Resources> landmarkData.json

我们将在本教程的其余部分以及随后的所有内容中使用此示例数据。

我们将在此以及以下每个教程中创建多个view的类。

第2节 创建行视图

我们将在本教程中构建的第一个视图是用于显示每个地标的详细信息的行。 此行视图将信息存储在其显示的地标的属性中,以便一个视图可以显示任何地标。 稍后,您将多个行组合成一个地标列表。

6f1c3da5-34c7-4c27-ba77-270ed2a29272.png

  • 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的模型。

Snip20190609_41.png

要修复预览,我们需要修改预览中初始化LandmarkRow()的代码。

  • 4.在LandmarkRow_Previews的预览静态属性中,将landmark参数添加到LandmarkRow()初始化方法的参数中,指定landmarkData数组的第一个元素。

预览显示文本Hello World。

修复后,您可以为行构建布局。

  • 5.将现有文本视图嵌入HStack中。

  • 6.修改文本视图以使用landmark属性的name

  • 7.通过在文本视图之前添加图像来完成行。

Snip20190609_42.png

第3节 自定义行预览

Xcode的画布自动识别并显示当前编辑器中符合PreviewProvider协议的任何类型。 预览提供程序返回一个或多个视图,其中包含用于配置大小和设备的选项。

我们可以从预览提供程序自定义返回的内容,以准确呈现对我们最有帮助的预览。

de4301fa-bf9c-45ac-b7f2-bbb990ccb41b.png

  • 1.在LandmarkRow_Previews中,将landmark参数更新为landmarkData数组中的第二个元素。

修改后预览立即更改以显示第二个model的数据而不是第一个。

  • 2.使用previewLayout(_ :)方法设置列表中这一行的cell大小。

我们可以在previews中使用group返回多行cell预览。

  • 3.将返回的行换放到一个Group中,然后在Group中再添加一个LandmarkRow

Snip20190609_43.png

Snip20190609_44.png

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列表

使用SwiftUIList类型时,可以显示特定于平台的视图列表。 列表的元素可以是静态的,就像我们目前创建的堆栈的子视图一样,或者是动态生成的。 您甚至可以混合静态和动态生成的视图。

c91b6546-1230-43a2-8867-2f0e445edb99.png

  • 1.创建一个名为LandmarkList.swift的新SwiftUI视图。

  • 2.用List替换模板中默认的Text视图,并在List中添加两个LandmarkRow初始化的视图作为两行显示。

现在我们预览显示以适合iOS的列表样式呈现的两个landmark,这个List不就是UITableView吗?

Snip20190609_45.png

第5节 使列表动态化

我们可以直接从集合中生成行,而不是单独指定列表的元素。

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

3f2f8071-97e1-481e-92a2-efb18be01ec7-1.png

  • 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中以设置到目标视图的转换,可以将导航功能添加到列表中。

c6f21df9-1d6d-42d4-b21b-0a2588f4cd97.png

  • 1.在NavigationView中嵌入动态生成的landmark列表。

  • 2.调用navigationBarTitle(_ :)修饰符方法以在显示列表时设置导航栏的标题。

Snip20190609_48.png

  • 3.在列表的闭包内,将返回的行包装在NavigationButton中,将LandmarkDetail视图指定为目标。

  • 4.我们可以通过切换到实时模式直接在预览中尝试导航。 单击“实时预览”按钮,然后点击地标以访问详细信息页面。

Snip20190609_49.png

第7节 将数据传递到子视图

LandmarkDetail视图中是使用写死的数据展示UI 的。 就像LandmarkRow一样,我们需要在LandmarkDetail中添加一个Landmark类型的模型以作为其数据源显示view。

从子视图开始,我们将转换CircleImageMapViewLandmarkDetail以显示传入的数据,而不是将数据写死在代码中。

cb49732f-cb3e-4e77-a2e6-84f7f7618502.png

  • 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中,将rootViewControllerrootView修改为为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(_ :)方法来更改预览设备。

475e4ac3-b605-4309-a294-e9d2efa6f1ab.png

  • 1.首先,将当前列表预览更改为以iPhone SE的大小呈现。
#if DEBUG
struct LandmarkList_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkList()
            // 以iPhone SE 设备的大小预览画布
            .previewDevice(PreviewDevice(rawValue: "iPhone SE"))

    }
}
#endif

我们可以提供Xcode方案菜单中显示的任何设备的名称。

Snip20190609_50.png

  • 2.在画布中添加对多个设备的预览
    在预览的列表中,使用设备名称数组作为数据,将LandmarkList嵌入到ForEach实例中。

Snip20190609_51.png

ForEach以与列表相同的方式对集合进行操作,这意味着我们可以在任何可以使用子视图的位置使用它,例如在堆栈,列表,组等中。 当数据元素是简单的值类型 - 就像在这里使用的字符串一样 - 我们可以使用\ .self作为标识符的关键路径。

  • 3.使用previewDisplayName(_ :)修饰符将设备名称添加为预览的标签。

Snip20190609_52.png

  • 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.哪些选项不是设置设备以预览视图的方法?

可选项:
1. Change the simulator selected in the active scheme.
2. Make a different choice in Canvas Settings in Xcode’s preferences.
3. Specify one or more devices using the previewDevice(_:)method.
4. Connect your development device and click the Device Preview button.

答案:2

推荐阅读

目录