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 Jun 24, 2019 · 9 revisions

组合复杂界面

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

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

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

1. 添加 Home View

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

1.1 在一个新文件 Home.swift 中创建一个自定义 view CategoryHome

Home.swift

import SwiftUI
struct CategoryHome: View {
 var body: some View {
 Text("Landmarks Content")
 }
}
#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
 static var previews: some View {
 CategoryHome()
 }
}
#endif

1.2 修改 SceneDelegate ,把显示的地标列表换成 CategoryHome view 。

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
 let window = UIWindow(frame: UIScreen.main.bounds)
 window.rootViewController = UIHostingController(rootView: CategoryHome().environmentObject(UserData()))
 self.window = window
 window.makeKeyAndVisible()
 }
}

现在 home view 成了 Landmarks app 的根,所系它需要一个方式去显示其他 view 。

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

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

Home.swift

import SwiftUI
struct CategoryHome: View {
 var body: some View {
 NavigationView {
 Text("Landmarks Content")
 }
 }
}
#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
 static var previews: some View {
 CategoryHome()
 }
}
#endif

1.4 把导航栏设置成 Featured

Home.swift

import SwiftUI
struct CategoryHome: View {
 var body: some View {
 NavigationView {
 Text("Landmarks Content")
 .navigationBarTitle(Text("Featured"))
 }
 }
}
#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
 static var previews: some View {
 CategoryHome()
 }
}
#endif

2. 创建一个分类列表

Landmarks app 以垂直的独立 row 显示所有分类,这给浏览提供了便利。我们可以通过组合垂直和水平 stacks ,并给列表添加滚动来完成此需求。

2.1 使用 Dictionary 结构的初始化方法 init(grouping:by:) 把地标组合到分类中,输入地标的 category 属性。

初始化项目文件给每个地标包含了预设的分类。

Home.swift

import SwiftUI
struct CategoryHome: View {
 var categories: [String: [Landmark]] {
 .init(
 grouping: landmarkData,
 by: { 0ドル.category.rawValue }
 )
 }
 
 var body: some View {
 NavigationView {
 Text("Landmarks Content")
 .navigationBarTitle(Text("Featured"))
 }
 }
}
#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
 static var previews: some View {
 CategoryHome()
 }
}
#endif

2.2 在 Landmarks 中使用 List 来显示分类。

Landmark.Category 会匹配列表中每一项的 name ,这些项目在其他分类中必须是唯一的,因为它是枚举。

Home.swift

import SwiftUI
struct CategoryHome: View {
 var categories: [String: [Landmark]] {
 .init(
 grouping: landmarkData,
 by: { 0ドル.category.rawValue }
 )
 }
 
 var body: some View {
 NavigationView {
 List {
 ForEach(categories.keys.sorted().identified(by: \.self)) { key in
 Text(key)
 }
 }
 .navigationBarTitle(Text("Featured"))
 }
 }
}
#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
 static var previews: some View {
 CategoryHome()
 }
}
#endif

3. 给 Landmarks 添加 Row

Landmarks 在一个水平滚动的 row 上显示每个分类。添加一个新的 view 类型来表示 row ,然后在这个新 view 中显示该分类所有的地标。

3.1 定义一个新的自定义 view 来保存 row 的内容。

这个 view 需要保存显示特定地标分类的信息以及对应的地标。

CategoryRow.swift

import SwiftUI
struct CategoryRow: View {
 var categoryName: String
 var items: [Landmark]
 
 var body: some View {
 Text(self.categoryName)
 .font(.headline)
 }
}
#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
 static var previews: some View {
 CategoryRow(
 categoryName: landmarkData[0].category.rawValue,
 items: Array(landmarkData.prefix(3))
 )
 }
}
#endif

更新 CategoryRowbody ,给新的 row 类型传入分类信息。

CategoryRow.swift

import SwiftUI
struct CategoryRow: View {
	var categoryName: String
 var items: [Landmark]
 var categories: [String: [Landmark]] {
 .init(
 grouping: landmarkData,
 by: { 0ドル.category.rawValue }
 )
 }
 
 var body: some View {
 NavigationView {
 List {
 ForEach(categories.keys.sorted().identified(by: \.self)) { key in
 CategoryRow(categoryName: key, items: self.categories[key]!)
 }
 }
 .navigationBarTitle(Text("Featured"))
 }
 }
}
#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
 static var previews: some View {
 CategoryHome()
 }
}
#endif

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)
 }
 }
 }
}
#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
 static var previews: some View {
 CategoryRow(
 categoryName: landmarkData[0].category.rawValue,
 items: Array(landmarkData.prefix(3))
 )
 }
}
#endif

3.4 调用 frame(width:height:)row 的空间大一些,然后把 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(showsHorizontalIndicator: false) {
 HStack(alignment: .top, spacing: 0) {
 ForEach(self.items) { landmark in
 Text(landmark.name)
 }
 }
 }
 .frame(height: 185)
 }
 }
}
#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
 static var previews: some View {
 CategoryRow(
 categoryName: landmarkData[0].category.rawValue,
 items: Array(landmarkData.prefix(4))
 )
 }
}
#endif

4. 组合 Home View

在用户点击一个地标去了解详情之前, Landmarks app 的 home 界面需要显示地标的简易信息。

重新使用我们在 创建和组合 view 中的 view 来创建类似但更简单的 view 预览,它们用来显示地标分类和特征。

4.1 在 CategoryRow 下面创建一个自定义 view CategoryItem ,然后用新 view 替换包含地标名称的 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(showsHorizontalIndicator: 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(forSize: 155)
 .cornerRadius(5)
 Text(landmark.name)
 .font(.caption)
 }
 .padding(.leading, 15)
 }
}
#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
 static var previews: some View {
 CategoryRow(
 categoryName: landmarkData[0].category.rawValue,
 items: Array(landmarkData.prefix(4))
 )
 }
}
#endif

4.2 在 Home.swift 中添加一个简单的 view FeaturedLandmarks ,用来显示只有被标记了 isFeatured 的地标。

我们会在稍后的教程中把这个 view 转换成一个可交互的轮播。目前,它显示一个缩放并裁剪后的地标特征图片。

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 }
 }
 
 var body: some View {
 NavigationView {
 List {
 FeaturedLandmarks(landmarks: featured)
 .scaledToFill()
 .frame(height: 200)
 .clipped()
 ForEach(categories.keys.sorted().identified(by: \.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(forSize: 250).resizable()
 }
}
#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
 static var previews: some View {
 CategoryHome()
 }
}
#endif

4.3 把地标预览两边的 edge insets 都设置成 zero ,这样内容就可以展开到显示的边缘。

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 }
 }
 
 var body: some View {
 NavigationView {
 List {
 FeaturedLandmarks(landmarks: featured)
 .scaledToFill()
 .frame(height: 200)
 .clipped()
 .listRowInsets(EdgeInsets())
 
 ForEach(categories.keys.sorted().identified(by: \.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(forSize: 250).resizable()
 }
}
#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
 static var previews: some View {
 CategoryHome()
 }
}
#endif

5. 在 Sections 之间添加导航

现在,在 home view 中可以看到所有不同分类的地标,用户需要一种方法来访问 app 中的每个部分。使用 navigationpresentation API 可以从 home view 中导航到详情 view ,收藏列表和用户的 profile

5.1 在 CategoryRow.swift 中,把现有的 CategoryItem 包装在一个 NavigationButton 中。

分类项本身是按钮的 label ,它的目标是卡片中显示地标的详情 view 。

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(showsHorizontalIndicator: false) {
 HStack(alignment: .top, spacing: 0) {
 ForEach(self.items) { landmark in
 NavigationButton(
 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(forSize: 155)
 .cornerRadius(5)
 Text(landmark.name)
 .font(.caption)
 }
 .padding(.leading, 15)
 }
}
#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
 static var previews: some View {
 CategoryRow(
 categoryName: landmarkData[0].category.rawValue,
 items: Array(landmarkData.prefix(4))
 )
 }
}
#endif

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(showsHorizontalIndicator: false) {
 HStack(alignment: .top, spacing: 0) {
 ForEach(self.items) { landmark in
 NavigationButton(
 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(forSize: 155)
 .renderingMode(.original)
 .cornerRadius(5)
 Text(landmark.name)
 .color(.primary)
 .font(.caption)
 }
 .padding(.leading, 15)
 }
}
#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
 static var previews: some View {
 CategoryRow(
 categoryName: landmarkData[0].category.rawValue,
 items: Array(landmarkData.prefix(4))
 )
 }
}
#endif

5.3 在 Home.swift 中,在 tab bar 中点击 profile icon ,添加一个模态 view 来显示用户的 profile

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 }
 }
 
 var body: some View {
 NavigationView {
 List {
 FeaturedLandmarks(landmarks: featured)
 .scaledToFill()
 .frame(height: 200)
 .clipped()
 .listRowInsets(EdgeInsets())
 
 ForEach(categories.keys.sorted().identified(by: \.self)) { key in
 CategoryRow(categoryName: key, items: self.categories[key]!)
 }
 .listRowInsets(EdgeInsets())
 }
 .navigationBarTitle(Text("Featured"))
 .navigationBarItems(trailing:
 PresentationButton(destination: Text("User Profile")) {
 Image(systemName: "person.crop.circle")
 .imageScale(.large)
 .accessibility(label: Text("User Profile"))
 .padding()
 }
 )
 }
 }
}
struct FeaturedLandmarks: View {
 var landmarks: [Landmark]
 var body: some View {
 landmarks[0].image(forSize: 250).resizable()
 }
}
#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
 static var previews: some View {
 CategoryHome()
 }
}
#endif

5.4 添加一个 navigation button 来 home 界面,它指向一个包含所有地标的可过滤列表。

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 }
 }
 
 var body: some View {
 NavigationView {
 List {
 FeaturedLandmarks(landmarks: featured)
 .scaledToFill()
 .frame(height: 200)
 .clipped()
 .listRowInsets(EdgeInsets())
 
 ForEach(categories.keys.sorted().identified(by: \.self)) { key in
 CategoryRow(categoryName: key, items: self.categories[key]!)
 }
 .listRowInsets(EdgeInsets())
 
 NavigationButton(destination: LandmarkList()) {
 Text("See All")
 }
 }
 .navigationBarTitle(Text("Featured"))
 .navigationBarItems(trailing:
 PresentationButton(destination: Text("User Profile")) {
 Image(systemName: "person.crop.circle")
 .imageScale(.large)
 .accessibility(label: Text("User Profile"))
 .padding()
 }
 )
 }
 }
}
struct FeaturedLandmarks: View {
 var landmarks: [Landmark]
 var body: some View {
 landmarks[0].image(forSize: 250).resizable()
 }
}
#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
 static var previews: some View {
 CategoryHome()
 }
}
#endif

5.5 在 LandmarkList.swift 中,移除包装地标列表的 NavigationView ,并且把它添加到预览中。

app 的上下文中, LandmarkList 会始终显示在 Home.swift 声明的导航 view 上。

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 {
 NavigationButton(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 によって変換されたページ (->オリジナル) /