因为 API 变更,此文章部份内容已失效,最新完整中文教程及代码请查看 github.com/WillieWangW…git
SwiftUI
表明将来构建 App 的方向,欢迎加群一块儿交流技术,解决问题。github
Landmarks
的主屏显示了一个滚动的分类列表,每一个分类中都有水平滚动的地标标记。经过构建这样的主导航,咱们来探究组合 view 是怎样适配不一样设备大小和方向的。swift下载项目文件并按照如下步骤操做,也能够打开已完成的项目自行浏览代码。bash
- 预计完成时间:20 分钟
- 项目文件:下载
如今咱们已经作好了 Landmarks
app 所需的全部 view,是时候给它们一个统一的 home view 了。 home view 不只包含了全部其余 view,还提供了浏览和显示地标的方法。微信
1.1 在一个新文件 Home.swift
中建立一个自定义 view CategoryHome
。session
Home.swiftapp
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 。ide
SceneDelegate.swiftui
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 。this
1.3 在 Landmarks
中添加一个 NavigationView
来组织别的 view 。
咱们在 app 中使用 NavigationView
和 NavigationButton
实例以及其余相关方法来构建分层导航结构。
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
复制代码
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
复制代码
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
复制代码
更新 CategoryRow
的 body
,给新的 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
复制代码
在用户点击一个地标去了解详情以前, 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
复制代码
如今,在 home view 中能够看到全部不一样分类的地标,用户须要一种方法来访问 app 中的每一个部分。使用 navigation
和 presentation
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 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(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())
}
}
}
复制代码