SwiftUI之深入解析如何创建列表展示页和导航跳转详情页

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SwiftUI之深入解析如何创建列表展示页和导航跳转详情页相关的知识,希望对你有一定的参考价值。

一、前言

  • 地标详情页视图已经创建完成,我们需要提供一种方式让用户可以查看完整的地标列表,并且可以查看每一个地标的详情。地标详情页视图的创建,请参考我的博客:SwiftUI之深入解析如何创建和组合视图
  • 本文将分析如何创建一个可以展示任何地标信息的视图,并动态生成一个可滚动列表,用户可以点击列表项去查看地标的详细信息;优化视图显示时,可以使用 Xcode 画布来渲染多个不同设备大小下的预览视图。

二、样本数据

  • 自定义视图所展示的信息都直接被写死在代码中,那么如将何自定义视图传入样本数据进行展示:

  • 项目工程中的 Models->Landmark.swift 文件,声明了需要在应用中展示一个地标所需要信息的结构化名称,并通过导入 landmarkData.json 文件中的数据,生成一个地标信息数组:
import SwiftUI
import CoreLocation

struct Landmark: Hashable, Codable, Identifiable 
    var id: Int
    var name: String
    fileprivate var imageName: String
    fileprivate var coordinates: Coordinates
    var state: String
    var park: String
    var category: Category

    var locationCoordinate: CLLocationCoordinate2D 
        CLLocationCoordinate2D(
            latitude: coordinates.latitude,
            longitude: coordinates.longitude)
    

    enum Category: String, CaseIterable, Codable, Hashable 
        case featured = "Featured"
        case lakes    = "Lakes"
        case rivers   = "Rivers"
    


extension Landmark 
    var image: Image 
        ImageStore.shared.image(name: imageName)
    


struct Coordinates: Hashable, Codable 
    var latitude: Double
    var longitude: Double

  • 选择 Resources->landmarkData.json,这个样本数据文件如下所示:

三、创建行视图

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

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

  • 在一个 HStack 中嵌入一个 Text,修改这个 Text,让它使用 landmark 属性的 name 字段,在 Text 视图前面添加一个图片视图,在 Text 视图后面添加 Spacer 视图:
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 
        LandmarkRow(landmark: landmarkData[0])
    

  • 效果如下:

四、自定义行预览

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

  • 在 LandmarkRow_Previews 中,把 landmark 参数更新为 landmarkData 数组的第二个元素,预览视图会立即刷新反映第二个元素的渲染情况:
struct LandmarkRow_Previews: PreviewProvider 
    static var previews: some View 
        LandmarkRow(landmark: landmarkData[1])
            .previewLayout(.fixed(width: 300, height: 70))
    

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

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

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

五、创建地标列表

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

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

六、创建动态列表

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

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

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

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

七、设置从列表页到详情页的页面导航

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

  • 把动态生成的列表视图嵌套进一个 NavigationView 视图中:
struct LandmarkList: View 
    
    var body: some View 
        NavigationView 
            List(landmarkData)  landmark in
                LandmarkRow(landmark: landmark)
            
        
    

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

  • 在列表的闭包中,将每一个行元素包裹在 NavigationLink 中返回,并指定 ContentView 视图为目标视图:
struct LandmarkList: View 
    
    var body: some View 
        NavigationView 
            List(landmarkData)  landmark in
                NavigationLink(destination: ContentView()) 
                    LandmarkRow(landmark: landmark)
                
            
            .navigationBarTitle(Text("Landmarks"))
        
    

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


八、子视图传入数据

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

  • 在 CircleImage.swift 文件中,添加一个存储属性,命名为 image,这是一种在构建 SwiftUI 视图中很常用的模式,常常会包裹或封装一些属性修改器:
struct CircleImage: View 
    
    var image : Image
    var body: some View 
        image
            .clipShape(Circle())
            .overlay(
                Circle().stroke(Color.white, lineWidth: 4))
            .shadow(radius: 10)
    

  • 更新 CirleImage 的预览结构体,并传入 Turtle Rock 这个图片进行预览:
struct CircleImage_Previews: PreviewProvider 
    static var previews: some View 
        CircleImage(image: Image("turtlerock"))
    

  • 在 MapView.swift 中添加一个 coordinate 属性,并使用这个属性来替换写死的经纬度坐标:
struct MapView: UIViewRepresentable 
    
    var coordinate : CLLocationCoordinate2D
    
    func makeUIView(context: Context) -> MKMapView 
        MKMapView(frame: .zero)
    

    func updateUIView(_ uiView: MKMapView, context: Context) 
        let span = MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        uiView.setRegion(region, animated: true)
    

  • 更新 MapView 的预览结构体,并传入每一个地标的经纬度数据:
struct MapView_Previews: PreviewProvider 
    static var previews: some View 
        MapView(coordinate: landmarkData[0].locationCoordinate)
    

  • 在 ContentView.swift 中添加 landmark 属性,更新 ContentView 预览结构体,并传入第一个地标的数据,把对应子视图的数据传入:
struct ContentView: View 
    
    var landmark : Landmark
    
    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)
                
            
            .padding()
            Spacer()
        
    


struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView(landmark: landmarkData[0])
    

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

  • 在 App 入口 BuildingListsApp.swift 类中的 Main 函数中修改根视图为 LandmarkList:
@main
struct BuildingListsApp: App 
    var body: some Scene 
        WindowGroup 
            LandmarkList()
        
    

  • 在 LandmarkList.swift 中,传入当前行的地标数据到地标详情页 ContentView:
struct LandmarkList: View 
    
    var body: some View 
        NavigationView 
            List(landmarkData)  landmark in
                NavigationLink(destination: ContentView(landmark: landmark)) 
                    LandmarkRow(landmark: landmark)
                
            
            .navigationBarTitle(Text("Landmarks"))
        
    


struct LandmarkList_Previews: PreviewProvider 
    static var previews: some View 
        LandmarkList()
    

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

九、动态生成预览视图

  • 接下来要在不同尺寸设备上展示不同的预览视图,默认情况下,预览视图会选择当前 Scheme 选中的设备尺寸进行渲染,可以使用 previewDevice(_😃 修改器来改变预览视图的设备:

  • 改变当前预览列表,让它渲染在 iPhone 8 Plus 设备上,可以使用 Xcode Scheme 菜单上的设备名称来指定渲染设备:

  • 在列表的预览视图中,还可以把 LandmarkList 嵌套进入 ForEach 实例中,使用设备数组名作为数据。ForEach 运算作用在集合类型的数据上,就和列表使用集合类型数据一样,可以在子视图使用的任何场景下使用 ForEach,例如:stack、list、group 等。当元素数据是简单值类型时(例如字符串类型),可以使用 .self 作为 keypath 去标识:
struct LandmarkList_Previews: PreviewProvider 
    static var previews: some View 
        ForEach(["iPhone 8 Plus", "iPhone 11 Pro Max"], id: \\.self)  deviceName in
            LandmarkList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
        
    

以上是关于SwiftUI之深入解析如何创建列表展示页和导航跳转详情页的主要内容,如果未能解决你的问题,请参考以下文章

Swift之深入解析如何使用Swift UI实现3D Scroll效果

SwiftUI之深入解析如何创建和组合视图

SwiftUI之深入解析如何使用组合矩形GeometryReader创建条形(柱状)图

SwiftUI之深入解析如何使用组合矩形GeometryReader创建条形(柱状)图

Swift之深入解析SwiftUI布局如何自定义AlignmentGuides

iOS14中点击SwiftUI列表项导航跳转返回后外观呈灰色的解决