代码下载
应用的首页是一个纵向滚动的地标类别列表,每一个类别内部是一个横向滑动列表。随后将构建应用的页面导航,这个过程中可以学习到如果组合各种视图,并让它们适配不同的设备尺寸和设备方向。
下载起步项目并跟着本篇教程一步步实践,或者查看本篇完成状态时的工程代码去学习,项目文件。
添加一个首页视图
已经创建了所有在应用中需要的视图,现在给应用创建一个首页视图,把之前创建的视图整合起来。首页不仅仅包含之前创建的视图,它还提供页面间导航的方式,同时也可以展示各种地标信息。
创建一个名为CategoryHome.swift的自定义视图文件,添加NavigationSplitView,这个NavigationSplitView将会容纳应用中其它不同的视图。配合使用NavigationLink及相关的修改器,就可以构建出应用的页面间导航结构,设置导航栏标题为Featured:
struct CategoryHome: View {
var body: some View {
NavigationSplitView {
Text("Hello, World!")
.navigationTitle("Featured")
} detail: {
Text("Select a Landmark")
}
}
}
创建地标类别列表
应用为了便于用户浏览各种类别的地标,将地标按类别竖向排列形成列表视图,对于每一个类别内的具体地标,又把它们按照水平方向排列,形成横向列表。组合使用垂直栈(vertical statck)和水平栈(horizontal stack)并给列表添加滚动。
首先从landmarkData.json文件读取类别数据。
1、在Landmark中,向Landmark结构添加Category枚举和category属性。landmarkData文件已经为每个地标数据包含了一个category值,该类别值为三个字符串值之一。通过匹配数据文件中的名称,可以让结构体遵守Codable协议来加载数据:
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var name: String
var park: String
var state: String
var description: String
var isFavorite: Bool
var category: Category
enum Category: String, CaseIterable, Codable {
case lakes = "Lakes"
case rivers = "Rivers"
case mountains = "Mountains"
}
private var imageName: String
var image: Image {
Image(imageName)
}
private var coordinates: Coordinates
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
}
2、在ModelData中,添加一个categories计算字典,使用Dictionary结构体的初始化方法init(grouping:by:),把地标数据的类别属性category传入作为分组依据,可以把地标数据按类别分组:
class ModelData {
var landmarks: [Landmark] = load("landmarkData.json")
var hikes: [Hike] = load("hikeData.json")
var categories: [ String: [Landmark] ] {
Dictionary(grouping: landmarks) { $0.category.rawValue }
}
}
3、在CategoryHome中创建一个modelData属性。现在需要访问categories,稍后还需要访问其他landmark数据。使用List显示地标数据的类别。Landmark.Category是枚举类型,它的值标识列表中每一种类别,可以保证类别不会有重复定义:
struct CategoryHome: View {
@Environment(ModelData.self) var modelData: ModelData
var body: some View {
NavigationSplitView {
List {
ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
Text(key)
}
}.navigationTitle("Featured")
} detail: {
Text("Select a Landmark")
}
}
}
#Preview {
CategoryHome().environment(ModelData())
}
创建地标行
Landmarks在水平滚动的一行中显示每个类别。添加一个新的视图类型来表示行,然后在新视图中显示该类别的所有地标。
重用在创建和组合视图中创建的Landmark视图的部分,以创建熟悉的Landmark预览。
1、创建一个名为CategoryItem的新自定义视图,显示一个地标:
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)
}
}
#Preview {
CategoryItem(landmark: ModelData().landmarks[0])
}
2、定义一个新的视图类型CategoryRow,用来展示地标类别行的内容。新建行视图需要存放地标具体类别的展示数据。在CategoryRow.swift中将类别的条目放入HStack中,并将其与类别名称分组到VStack中。为行内容指定一个高度,并把行内容嵌入到ScrollView中,以支持横向滑动。预览视图时,可以多增加几个地标数据,用来查看列表的滑动是否正常:
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)
}
}
}
#Preview {
let landmarks = ModelData().landmarks
return CategoryRow(categoryName: landmarks[0].category.rawValue, items: Array(landmarks.prefix(3)))
}
完成类别视图
将行和特色地标图像添加到类别主页。
1、更新CategoryHome的body,将类别信息传递给行类型的实例:
接下来,在视图的顶部添加一个特色地标。要做到这一点,需要从地标数据中获得更多信息。
2、在Landmark中,添加一个新的isFeatured属性,与添加的其他地标属性一样,这个布尔值已经存在于数据中,只需要声明一个新属性:
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var name: String
var park: String
var state: String
var description: String
var isFavorite: Bool
var isFeatured: Bool
var category: Category
enum Category: String, CaseIterable, Codable {
case lakes = "Lakes"
case rivers = "Rivers"
case mountains = "Mountains"
}
private var imageName: String
var image: Image {
Image(imageName)
}
private var coordinates: Coordinates
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
}
3、在ModelData中,添加一个新的计算特色地标数组,其中只包含将isFeatured设置为true的地标:
class ModelData {
var landmarks: [Landmark] = load("landmarkData.json")
var hikes: [Hike] = load("hikeData.json")
var categories: [ String: [Landmark] ] {
Dictionary(grouping: landmarks) { $0.category.rawValue }
}
var features: [Landmark] {
landmarks.filter { $0.isFeatured }
}
}
4、在CategoryHome中,将第一个特色地标的图像添加到列表的顶部。在后面的教程中,会将这个视图修改成一个交互式轮播图。目前,这个视图仅仅展示一张缩放和剪裁后的地标图片。把视图的边距设置为0,让展示内容可以尽量贴着屏幕边沿:
struct CategoryHome: View {
@Environment(ModelData.self) var modelData: ModelData
var body: some View {
NavigationSplitView {
List {
modelData.features[0]
.image
.resizable()
.frame(height: 200)
.clipped()
.listRowInsets(EdgeInsets())
ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: modelData.categories[key] ?? [])
}.listRowInsets(EdgeInsets())
}.navigationTitle("Featured")
} detail: {
Text("Select a Landmark")
}
}
}
在各节之间添加导航
现在所有类别的地标都可以在首页视图中展示出来,用户还需要能够进入应用其它页面的方法。使用页面导航和相关API来实现用户从应用首页到地标详情页、收藏列表页及用户个人中心页的跳转。
1、在CategoryRow.swift中,把CategoryItem视图包裹在NavigationLink视图中。CategoryItem这时做为跳转按钮的内容,destination指定点击NavigationLink按钮时要跳转的目标视图:
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(destination: LandmarkDetail(landmark: landmark)) {
CategoryItem(landmark: landmark)
}
}
}
}.frame(height: 185)
}
}
}
2、使用renderingMode(:)和foregroundColor(:)这两个属性修改器来改变地标类别项的导航样式。做为NavigationLink标签的CategoryItem中的文本会使用Environment中的强调颜色,图片可能以模板图片的方式渲染,这些都可以使用属性修改器来调整,达到最佳效果:
struct CategoryItem: View {
var landmark: Landmark
var body: some View {
VStack(alignment: .leading) {
landmark.image
.renderingMode(.original)
.resizable()
.frame(width: 155, height: 155)
.cornerRadius(5)
Text(landmark.name)
.foregroundStyle(.primary)
.font(.caption)
}.padding(.leading, 15)
}
}
3、接下来,将修改应用程序的ContentView,以显示一个TabView,让用户在刚刚创建的类别视图和现有的地标列表之间进行选择:
-
切换到ContentView并添加要显示的选项卡枚举。为选项卡选择添加一个状态变量,并给它一个默认值。
-
创建一个TabView来包装LandmarkList和新的CategoryHome。每个视图上的 tag(_😃 修饰符匹配选择属性可以取的一个可能值,因此当用户在用户界面中进行选择时,TabView可以协调显示哪个视图。
-
给每个Tab一个标签。确保实时预览打开,并尝试新的导航。
struct ContentView: View {
@State private var selection: Tab = .featuredenum Tab { case featured case list } var body: some View { TabView(selection: $selection) { CategoryHome().tabItem { Label("Featured", systemImage: "star") } .tag(Tab.featured) LandmarkList().tabItem { Label("List", systemImage: "list.bullet") } .tag(Tab.list) } }
}
#Preview {
ContentView().environment(ModelData())
}