[譯] SwiftUI 官方教程 (七)

因爲 API 變更,此文章部份內容已失效,最新完整中文教程及代碼請查看 github.com/WillieWangW…git

微信技術羣

SwiftUI 表明將來構建 App 的方向,歡迎加羣一塊兒交流技術,解決問題。github

組合複雜界面

Landmarks 的主屏顯示了一個滾動的分類列表,每一個分類中都有水平滾動的地標標記。經過構建這樣的主導航,咱們來探究組合 view 是怎樣適配不一樣設備大小和方向的。swift

下載項目文件並按照如下步驟操做,也能夠打開已完成的項目自行瀏覽代碼。bash

  • 預計完成時間:20 分鐘
  • 項目文件:下載

1. 添加 Home View

如今咱們已經作好了 Landmarks app 所需的全部 view,是時候給它們一個統一的 home view 了。 home view 不只包含了全部其餘 view,還提供了瀏覽和顯示地標的方法。微信

1.1 在一個新文件 Home.swift 中建立一個自定義 view CategoryHomesession

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 中使用 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())
        }
    }
}
複製代碼

相關文章
相關標籤/搜索