因爲 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())
}
}
}
複製代碼