Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Composing Complex Interfaces

Willie edited this page Feb 10, 2020 · 9 revisions

组合复杂界面

Landmarks 的主屏显示了一个滚动的分类列表,每个分类中都有水平滚动的地标标记。通过构建这样的主导航,我们来探究组合视图是怎样适配不同设备大小和方向的。

下载项目文件并按照以下步骤操作,也可以打开已完成的项目自行浏览代码。

  • 预计完成时间:20 分钟
  • 项目文件:下载

1. 添加主视图

现在我们已经做好了 Landmarks app 所需的所有视图,是时候给它们一个统一的主视图了。 主视图不仅包含了所有其他视图,还提供了浏览和显示地标的方法。

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 app 的根,所以它需要一个方式去显示其他视图。

1.3 在 Landmarks 中添加一个 NavigationView 来组织别的视图。

我们在 app 中使用 NavigationViewNavigationButton 实例以及其他相关方法来构建分层导航结构。

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 app 以垂直的独立行视图显示所有分类,这给浏览提供了便利。我们可以通过组合垂直和水平 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))
 )
 //
 }
}

更新 CategoryRowbody ,给新的行视图类型传入分类信息。

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 app 的主视图需要显示地标的简易信息。

重新使用我们在 创建和组合 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 之间添加导航

现在,在主视图中可以看到所有不同分类的地标,用户需要一种方法来访问 app 中的每个部分。使用 navigationpresentation 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 buttonlabel 传递的文字会使用环境的强调色渲染,并且图像可能会被当做 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 在导航栏上添加一个按钮,当点击后将 showProfilefalse 切换到 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 ,并把它添加到预览中。

中 app 的环境中, 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())
 }
 //
 }
}

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /