组合复杂界面
Landmarks
的主屏显示了一个滚动的分类列表,每个分类中都有水平滚动的地标标记。通过构建这样的主导航,我们来探究组合视图是怎样适配不同设备大小和方向的。
下载项目文件并按照以下步骤操作,也可以打开已完成的项目自行浏览代码。
* 预计完成时间:20 分钟
* 项目文件:下载
1. 添加主视图
现在我们已经做好了 Landmarks
yinBiao 所需的所有视图,是时候给它们一个统一的主视图了。 主视图不仅包含了所有其他视图,还提供了浏览和显示地标的方法。
1.1 在一个新文件 Home.swift
中创建一个自定义视图 CategoryHome
。
Home.swift
import SwiftUI
struct CategoryHome: View {
var body: some View {
Text("Landmarks Content")
}
}
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
}
}
1.2 修改 SceneDelegate
,把显示的地标列表换成 CategoryHome
视图。
SceneDelegate.swift
import SwiftUI
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Use a UIHostingController as window root view controller
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(
//
rootView: CategoryHome()
//
.environmentObject(UserData())
)
self.window = window
window.makeKeyAndVisible()
}
}
}
现在主视图成了 Landmarks
yinBiao 的根,所以它需要一个方式去显示其他视图。
1.3 在 Landmarks
中添加一个 NavigationView
来组织别的视图。
我们在 yinBiao 中使用 NavigationView
和 NavigationButton
实例以及其他相关方法来构建分层导航结构。
Home.swift
import SwiftUI
struct CategoryHome: View {
var body: some View {
//
NavigationView {
Text("Landmarks Content")
}
//
}
}
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
}
}
1.4 把导航栏设置成 Featured
。
Home.swift
import SwiftUI
struct CategoryHome: View {
var body: some View {
NavigationView {
Text("Landmarks Content")
//
.navigationBarTitle(Text("Featured"))
//
}
}
}
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
}
}
2. 创建一个分类列表
Landmarks
yinBiao 以垂直的独立行视图显示所有分类,这给浏览提供了便利。我们可以通过组合垂直和水平 stack
,并给列表添加滚动来完成此需求。
2.1 使用 Dictionary
结构的初始化方法 init(grouping:by:)
把地标组合到分类中,输入地标的 category
属性。
初始化项目文件给每个地标包含了预设的分类。
Home.swift
import SwiftUI
struct CategoryHome: View {
//
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarkData,
by: { $0.category.rawValue }
)
}
//
var body: some View {
NavigationView {
Text("Landmarks Content")
.navigationBarTitle(Text("Featured"))
}
}
}
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
}
}
2.2 在 Landmarks
中使用 List
来显示分类。
Landmark.Category
会匹配列表中每一项的 name
,这些项目在其他分类中必须是唯一的,因为它是枚举。
Home.swift
import SwiftUI
struct CategoryHome: View {
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarkData,
by: { $0.category.rawValue }
)
}
var body: some View {
NavigationView {
//
List {
ForEach(categories.keys.sorted(), id: \.self) { key in
Text(key)
}
}
.navigationBarTitle(Text("Featured"))
//
}
}
}
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
}
}
3. 给 Landmarks 添加行视图
Landmarks
在一个水平滚动的行视图上显示每个分类。添加一个新的视图类型来表示行视图,然后在这个新视图中显示该分类所有的地标。
3.1 定义一个新的自定义视图来保存行视图的内容。
这个视图需要保存显示特定地标分类的信息以及对应的地标。
CategoryRow.swift
import SwiftUI
struct CategoryRow: View {
//
var categoryName: String
var items: [Landmark]
var body: some View {
Text(self.categoryName)
.font(.headline)
}
//
}
struct CategoryRow_Previews: PreviewProvider {
static var previews: some View {
//
CategoryRow(
categoryName: landmarkData[0].category.rawValue,
items: Array(landmarkData.prefix(3))
)
//
}
}
更新 CategoryRow
的 body
,给新的行视图类型传入分类信息。
CategoryRow.swift
import SwiftUI
struct CategoryHome: View {
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarkData,
by: { $0.category.rawValue }
)
}
var body: some View {
NavigationView {
List {
ForEach(categories.keys.sorted(), id: \.self) { key in
//
CategoryRow(categoryName: key, items: self.categories[key]!)
//
}
}
.navigationBarTitle(Text("Featured"))
}
}
}
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
}
}
3.3 在一个 HStack
中显示分类中的地标。
CategoryRow.swift
import SwiftUI
struct CategoryRow: View {
var categoryName: String
var items: [Landmark]
var body: some View {
HStack(alignment: .top, spacing: 0) {
ForEach(self.items) { landmark in
Text(landmark.name)
}
}
}
}
struct CategoryRow_Previews: PreviewProvider {
static var previews: some View {
CategoryRow(
categoryName: landmarkData[0].category.rawValue,
items: Array(landmarkData.prefix(3))
)
}
}
3.4 调用 frame(width:height:)
让行视图的空间大一些,然后把 stack
包装在一个 ScrollView
中。
使用很长的数据样本更新预览来确保可以正确滚动。
CategoryRow.swift
import SwiftUI
struct CategoryRow: View {
var categoryName: String
var items: [Landmark]
var body: some View {
//
VStack(alignment: .leading) {
Text(self.categoryName)
.font(.headline)
.padding(.leading, 15)
.padding(.top, 5)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.items) { landmark in
Text(landmark.name)
}
}
}
.frame(height: 185)
}
//
}
}
struct CategoryRow_Previews: PreviewProvider {
static var previews: some View {
CategoryRow(
categoryName: landmarkData[0].category.rawValue,
items: Array(landmarkData.prefix(4))
)
}
}
4. 组合主视图
在用户点击一个地标去了解详情之前, Landmarks
yinBiao 的主视图需要显示地标的简易信息。
重新使用我们在 创建和组合 view 中的视图来创建类似但更简单的视图预览,它们用来显示地标分类和特征。
4.1 在 CategoryRow
下面创建一个自定义视图 CategoryItem
,然后用新视图替换包含地标名称的 Text
。
CategoryRow.swift
import SwiftUI
struct CategoryRow: View {
var categoryName: String
var items: [Landmark]
var body: some View {
VStack(alignment: .leading) {
Text(self.categoryName)
.font(.headline)
.padding(.leading, 15)
.padding(.top, 5)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.items) { landmark in
//
CategoryItem(landmark: landmark)
//
}
}
}
.frame(height: 185)
}
}
}
//
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 CategoryRow_Previews: PreviewProvider {
static var previews: some View {
CategoryRow(
categoryName: landmarkData[0].category.rawValue,
items: Array(landmarkData.prefix(4))
)
}
}
4.2 在 Home.swift
中添加一个简易视图 FeaturedLandmarks
,用来显示只有被标记了 isFeatured
的地标。
我们会在稍后的教程中把这个视图转换成一个可交互的轮播。目前,它显示一个缩放并裁剪后的地标特征图片。
Home.swift
import SwiftUI
struct CategoryHome: View {
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarkData,
by: { $0.category.rawValue }
)
}
//
var featured: [Landmark] {
landmarkData.filter { $0.isFeatured }
}
//
var body: some View {
NavigationView {
List {
//
FeaturedLandmarks(landmarks: featured)
.scaledToFill()
.frame(height: 200)
.clipped()
//
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: self.categories[key]!)
}
}
.navigationBarTitle(Text("Featured"))
}
}
}
//
struct FeaturedLandmarks: View {
var landmarks: [Landmark]
var body: some View {
landmarks[0].image.resizable()
}
}
//
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
}
}
4.3 把地标预览两边的 edge insets
都设置成 zero
,这样内容就可以展开到显示的边缘。
Home.swift
import SwiftUI
struct CategoryHome: View {
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarkData,
by: { $0.category.rawValue }
)
}
var featured: [Landmark] {
landmarkData.filter { $0.isFeatured }
}
var body: some View {
NavigationView {
List {
FeaturedLandmarks(landmarks: featured)
.scaledToFill()
.frame(height: 200)
.clipped()
//
.listRowInsets(EdgeInsets())
//
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: self.categories[key]!)
}
//
.listRowInsets(EdgeInsets())
//
}
.navigationBarTitle(Text("Featured"))
}
}
}
struct FeaturedLandmarks: View {
var landmarks: [Landmark]
var body: some View {
landmarks[0].image.resizable()
}
}
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
}
}
5. 在 Sections 之间添加导航
现在,在主视图中可以看到所有不同分类的地标,用户需要一种方法来访问 yinBiao 中的每个部分。使用 navigation
和 presentation
API 可以从主视图导航到详情视图,收藏列表和用户简介 。
5.1 在 CategoryRow.swift
中,把现有的 CategoryItem
包装在一个 NavigationButton
中。
分类项本身是按钮的 label
,它的目标是卡片中显示地标的详情视图。
CategoryRow.swift
import SwiftUI
struct CategoryRow: View {
var categoryName: String
var items: [Landmark]
var body: some View {
VStack(alignment: .leading) {
Text(self.categoryName)
.font(.headline)
.padding(.leading, 15)
.padding(.top, 5)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.items) { landmark in
//
NavigationLink(
destination: LandmarkDetail(
landmark: landmark
)
) {
CategoryItem(landmark: landmark)
}
//
}
}
}
.frame(height: 185)
}
}
}
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 CategoryRow_Previews: PreviewProvider {
static var previews: some View {
CategoryRow(
categoryName: landmarkData[0].category.rawValue,
items: Array(landmarkData.prefix(4))
)
}
}
注意:在 Xcode 11 beta 6 中,如果你在一个 List
中嵌套了 ScrollView
,并且这个 ScrollView
包含一个 NavigationLink
,那么当用户点击它时,这个链接并不会导航到目标视图。
5.2 通过应用 renderingMode(_:)
和 color(_:)
修饰符改变分类项的导航外观。
我们给作为 navigation button
的 label
传递的文字会使用环境的强调色渲染,并且图像可能会被当做 template image
。我们可以修改任何一种行为来满足设计。
CategoryRow.swift
import SwiftUI
struct CategoryRow: View {
var categoryName: String
var items: [Landmark]
var body: some View {
VStack(alignment: .leading) {
Text(self.categoryName)
.font(.headline)
.padding(.leading, 15)
.padding(.top, 5)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.items) { landmark in
NavigationLink(
destination: LandmarkDetail(
landmark: landmark
)
) {
CategoryItem(landmark: landmark)
}
}
}
}
.frame(height: 185)
}
}
}
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)
//
.foregroundColor(.primary)
//
.font(.caption)
}
.padding(.leading, 15)
}
}
struct CategoryRow_Previews: PreviewProvider {
static var previews: some View {
CategoryRow(
categoryName: landmarkData[0].category.rawValue,
items: Array(landmarkData.prefix(4))
)
}
}
5.3 在 Home.swift
中,在标签栏中点击简介图标,添加一个模态视图来显示用户的简介。
当 showProfile
状态变量设置为 true
时,SwiftUI 将显示用户简介占位符。当用户关闭模态后,将 showProfile
设置回 false
。
Home.swift
import SwiftUI
struct CategoryHome: View {
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarkData,
by: { $0.category.rawValue }
)
}
var featured: [Landmark] {
landmarkData.filter { $0.isFeatured }
}
//
@State var showingProfile = false
//
var body: some View {
NavigationView {
List {
FeaturedLandmarks(landmarks: featured)
.scaledToFill()
.frame(height: 200)
.clipped()
.listRowInsets(EdgeInsets())
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: self.categories[key]!)
}
.listRowInsets(EdgeInsets())
}
.navigationBarTitle(Text("Featured"))
//
.sheet(isPresented: $showingProfile) {
Text("User Profile")
}
//
}
}
}
struct FeaturedLandmarks: View {
var landmarks: [Landmark]
var body: some View {
landmarks[0].image.resizable()
}
}
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
}
}
5.4 在导航栏上添加一个按钮,当点击后将 showProfile
从 false
切换到 true
。
Home.swift
import SwiftUI
struct CategoryHome: View {
var categories: [String: [Landmark]] {
//
.init(
//
grouping: landmarkData,
by: { $0.category.rawValue }
)
}
var featured: [Landmark] {
landmarkData.filter { $0.isFeatured }
}
@State var showingProfile = false
//
var profileButton: some View {
Button(action: { self.showingProfile.toggle() }) {
Image(systemName: "person.crop.circle")
.imageScale(.large)
.accessibility(label: Text("User Profile"))
.padding()
}
}
//
var body: some View {
NavigationView {
List {
FeaturedLandmarks(landmarks: featured)
.scaledToFill()
.frame(height: 200)
.clipped()
.listRowInsets(EdgeInsets())
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: self.categories[key]!)
}
.listRowInsets(EdgeInsets())
}
.navigationBarTitle(Text("Featured"))
//
.navigationBarItems(trailing: profileButton)
//
.sheet(isPresented: $showingProfile) {
Text("User Profile")
}
}
}
}
struct FeaturedLandmarks: View {
var landmarks: [Landmark]
var body: some View {
//
landmarks[0].image(forSize: 250).resizable()
//
}
}
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
}
}
5.5 添加一个导航链接,指向可以过滤所有地标的列表,这样主屏幕就完成了。
Home.swift
import SwiftUI
struct CategoryHome: View {
var categories: [String: [Landmark]] {
//
Dictionary(
//
grouping: landmarkData,
by: { $0.category.rawValue }
)
}
var featured: [Landmark] {
landmarkData.filter { $0.isFeatured }
}
@State var showingProfile = false
var profileButton: some View {
Button(action: { self.showingProfile.toggle() }) {
Image(systemName: "person.crop.circle")
.imageScale(.large)
.accessibility(label: Text("User Profile"))
.padding()
}
}
var body: some View {
NavigationView {
List {
FeaturedLandmarks(landmarks: featured)
.scaledToFill()
.frame(height: 200)
.clipped()
.listRowInsets(EdgeInsets())
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: self.categories[key]!)
}
.listRowInsets(EdgeInsets())
//
NavigationLink(destination: LandmarkList()) {
Text("See All")
}
//
}
.navigationBarTitle(Text("Featured"))
.navigationBarItems(trailing: profileButton)
.sheet(isPresented: $showingProfile) {
Text("User Profile")
}
}
}
}
struct FeaturedLandmarks: View {
var landmarks: [Landmark]
var body: some View {
//
landmarks[0].image.resizable()
//
}
}
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
}
}
5.6 在 LandmarkList.swift
中,移除包装地标列表的 NavigationView
,并把它添加到预览中。
中 yinBiao 的环境中, LandmarkList
将始终显示在 Home.swift
中声明的导航视图中。
LandmarkList.swift
import SwiftUI
struct LandmarkList: View {
@EnvironmentObject var userData: UserData
var body: some View {
//
//
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Favorites only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
//
//
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
//
NavigationView {
LandmarkList()
.environmentObject(UserData())
}
//
}
}