第六篇 swiftUI Landmarks 组合复杂接口

292 阅读4分钟

前言

为地标添加一个分类列表,每个分类是一个支持水平滚动的列表。当您构建此视图并将其连接到现有视图时,您将探索组合视图如何适应不同的设备尺寸和方向。
按照步骤构建这个项目,或者下载完成的项目来自己探索。

第一节 添加一个分类视图

您可以创建一个分类视图对地标进行排序,这样可以提供一个浏览地标的不同方式,同时在顶部高亮显示一个特色地标

第一步 创建CategoryHome.swift

在您的项目Views分组下新增一个Categories分组,在Categories分组下创建CategoryHome.swift

import SwiftUI

struct CategoryHome: View {
    var body: some View {
        Text("Hello, World!")
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}

第二步 添加一个导航视图,用来组织不同的分类

您可以使用导航视图以及NavigationLink实例和相关修饰符在应用程序中构建分层导航结构。

第三步 将导航栏的标题设置为“Featured”。

该视图在顶部展示一个或多个特色地标

import SwiftUI

struct CategoryHome: View {
    var body: some View {
        NavigationView {
            Text("Hello, World!")
                .navigationTitle(Text("Feature"))
        }
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}

第二节 创建分类列表

分类列表展示了地标的所有分类,在每一行又展示了某一个分类的所有地标。
要做到这个效果,您需要将水平和垂直的stack结合起来使用,且给列表添加可滑动性 landmarkData.json读取分类信息

第一步 添加模型字段

Landmark.swift中添加category属性,以及Category枚举,Category枚举的内容和landmarkData.json相对应

import Foundation
import SwiftUI
import CoreLocation

struct Landmark: Hashable, Codable, Identifiable{
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String
    
    var imageName: String
    var image: Image {
        Image(imageName)
    }
    
    var isFavorite: Bool
    
    var category: Category
    
    enum Category: String, CaseIterable, Codable {
        case lakes = "Lakes"
        case rivers = "Rivers"
        case mountains = "Mountains"
    }
    
    private var coordinates: Coordinates
    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(latitude: coordinates.latitude, longitude: coordinates.longitude)
    }
    
    struct Coordinates: Hashable, Codable {
        var latitude: Double
        var longitude: Double
        
    }
}

第二步 修改Model​Data​.swift

ModelData.swift中,添加一个计算类别字典,将类别名称作为关键字,并为每个关键字添加一组相关的地标。

final class ModelData: ObservableObject {
   @Published var landmarks:[Landmark] = load("landmarkData.json")
    var hikes: [Hike] = load("hikeData.json")
    
    var categories: [String : [Landmark]] {
        Dictionary(
            grouping: landmarks,
            by: {$0.category.rawValue})
    }
}

第三步 在CategoryHome.swift中,创建一个modelData环境对象。

现在您可以访问分类信息了,稍后还可以访问其它地标信息

import SwiftUI

struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData
    
    var body: some View {
        NavigationView {
            Text("Landmarks content")
                .navigationTitle(Text("Feature"))
        }
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome().environmentObject(ModelData())
    }
}

第四步 将分类信息放入List中

使用列表在地标中显示类别。
Category案例名称标识列表中的每个项目,因为它是一个枚举,所以在其他类别中必须是唯一的。

import SwiftUI

struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData
    
    var body: some View {
        NavigationView {
            List {
                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    Text(key)
                }
                .navigationTitle(Text("Featured"))
            }
        }
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome().environmentObject(ModelData())
    }
}

第三节 创建分类的行

Landmarks一行展示一个分类,每行都支持滚动。
添加一个新的视图类来表示行,然后在新视图中显示该类别的所有地标。

第一步 创建CategoryRow.swift

添加分类的名称categoryName和该分类下的地标数组items

var categoryName: String 
var items: [Landmark]

第二步 展示分类名称

Text(categoryName) .font(.headline)

第三步 展示分类下的地标

import SwiftUI

struct CategoryRow: View {
    
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(categoryName)
                .font(.headline)
            
            HStack(alignment: .top, spacing: 0) {
                ForEach(items) { landmark in
                    Text(landmark.name)
                }
            }
        }
    }
}

struct CategoryRow_Previews: PreviewProvider {
    static var landmarks = ModelData().landmarks
    static var previews: some View {
        CategoryRow(categoryName: landmarks[0].category.rawValue, items: Array(landmarks.prefix(3)))
    }
}

第四步 布局

通过指定一个高度、添加padding并在滚动视图中包装HStack,为内容提供一些空间。 使用更大的数据采样更新视图预览可以更容易地确保滚动行为是正确的。


import SwiftUI

struct CategoryRow: View {
    
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)
            
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(items) { landmark in
                        Text(landmark.name)
                    }
                }
            }
            .frame(height: 185)
        }
    }
}

第五步 创建一个自定义视图CategoryItem.swift展示一个地标

import SwiftUI

struct CategoryItem: View {
    var landmark: Landmark
    
    var body: some View {
        VStack(alignment: .leading) {
            landmark.image
                .resizable()
                .frame(width: 155, height: 155)
                .cornerRadius(5)
            
            Text(landmark.name)
                .font(.caption)
        }
        .padding(.leading, 15)
    }
}

struct CategoryItem_Previews: PreviewProvider {
    static var previews: some View {
        CategoryItem(landmark: ModelData().landmarks[0])
    }
}

第六步 CategoryRow.swift中使用CategoryItem

import SwiftUI

struct CategoryRow: View {
    
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)
            
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(items) { landmark in
                        CategoryItem(landmark: landmark)
                    }
                }
            }
            .frame(height: 185)
        }
    }
}

struct CategoryRow_Previews: PreviewProvider {
    static var landmarks = ModelData().landmarks
    static var previews: some View {
        CategoryRow(categoryName: landmarks[0].category.rawValue, items: Array(landmarks.prefix(6)))
    }
}

第四节 完成分类视图

将行和特色图像添加到类别主页

第一步 更新Category​Home中行信息

import SwiftUI

struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData
    
    var body: some View {
        NavigationView {
            List {
                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    CategoryRow(categoryName: key, items: modelData.categories[key]!)
                }
                .navigationTitle(Text("Featured"))
            }
        }
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome().environmentObject(ModelData())
    }
}

接下来,您将会在顶部添加一个特色地标,您要在landmark数据模型中添加需要的信息

第二步 在Landmark​.swift中添加isFeatured属性,该属性代表是否是特色地标

var isFeatured: Bool

第三步 修改ModelData.swift

添加一个计算属性features数组,其中包含所有特色的地标

    var features: [Landmark] {
        landmarks.filter {$0.isFeatured}
    }

第四步 在顶部添加特色地标图片

在后面的教程中,您将把这个视图变成一个交互式旋转木马。目前,它通过缩放和裁剪的预览图像显示其中一个特色地标

modelData.features[0].image 
.resizable() 
.scaledToFill() 
.frame(height: 200) 
.clipped()

第五步 充满屏幕

设置两种地标的预览边缘偏移为0,这样整个内容将会充满到屏幕边缘

import SwiftUI

struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData
    
    var body: some View {
        NavigationView {
            List {
                modelData.features[0].image
                    .resizable()
                    .scaledToFill()
                    .frame(height: 200)
                    .clipped()
                    .listRowInsets(EdgeInsets())
                
                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    CategoryRow(categoryName: key, items: modelData.categories[key]!)
                }
                .listRowInsets(EdgeInsets())
            }
            .listStyle(.grouped)
            .navigationTitle(Text("Featured"))
        }
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome().environmentObject(ModelData())
    }
}

添加导航

为不同分类的地标添加点击跳转功能

第一步 CategoryRow 添加跳转

import SwiftUI

struct CategoryRow: View {
    
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)
            
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(items) { landmark in
                        NavigationLink {
                            LandmarkDetail(landmark: landmark)
                        } label: {
                            CategoryItem(landmark: landmark)
                        }

                    }
                }
            }
            .frame(height: 185)
        }
    }
}

struct CategoryRow_Previews: PreviewProvider {
    static var landmarks = ModelData().landmarks
    static var previews: some View {
        CategoryRow(categoryName: landmarks[0].category.rawValue, items: Array(landmarks.prefix(6)))
    }
}

第二步 修改CategoryItem

通过应用renderingMode(:)和foregroundColor(:)修改器来更改CategoryItem的导航外观。
作为导航链接标签传递的文本使用环境的强调色进行渲染,图像可以作为模板图像进行渲染。您可以修改任一行为以最适合您的设计。
接下来,您将修改应用程序的主要内容视图,以显示选项卡视图,用户可以在您刚刚创建的类别视图和现有的地标列表之间进行选择。

第三步 取消固定预览,切换到ContentView并添加要显示的选项卡的枚举。

    enum Tab {
        case featured
        case list
    }

第四步 为选项卡选择添加一个状态变量,并为其指定一个默认值。

@State private var selection: Tab = .featured

第五步 添加选项卡

创建一个选项卡视图,用于包装LandmarkList以及CategoryHome。 每个视图上的tag(_:)修饰符与选择属性可以采用的一个可能值相匹配,因此当用户在用户界面中进行选择时,TabView可以协调显示哪个视图。

        TabView(selection: $selection) {
            CategoryHome()
                .tag(Tab.featured)
            LandmarkList()
                .tag(Tab.list)
        }

第六步 给选项卡设置图标和文案

import SwiftUI

struct ContentView: View {
    
    @State private var selection: Tab = .featured
    
    enum Tab {
        case featured
        case list
    }
    
    var body: some View {
        TabView(selection: $selection) {
            CategoryHome()
                .tabItem({
                    Label("Feature", systemImage: "star")
                })
                .tag(Tab.featured)
            
            
            LandmarkList()
                .tabItem({
                    Label("List", systemImage: "list.bullet")
                })
                .tag(Tab.list)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(ModelData())
    }
}

运行结果