[譯] SwiftUI 官方教程 (四)

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

微信技術羣

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

處理用戶輸入

Landmarks app 中,用戶能夠標記他們喜歡的地點,並在列表中過濾出來。要實現這個功能,咱們要先在列表中添加一個開關,這樣用戶能夠只看到他們收藏的內容。另外還會添加一個星形按鈕,用戶能夠點擊該按鈕來收藏地標。canvas

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

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

1. 標記用戶收藏的地標

首先,經過優化列表來清晰地給用戶顯示他們的收藏。給每一個被收藏地標的 LandmarkRow 添加一顆星。bash

1.1 打開起始項目,在 Project navigator 中選擇 LandmarkRow.swift微信

1.2 在 spacer 的下面添加一個 if 語句,在其中添加一個星形圖片來測試當前地標是否被收藏。session

SwiftUI block 中,咱們使用 if 語句來有條件的引入 view 。閉包

LandmarkRow.swiftapp

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image(forSize: 50)
            Text(landmark.name)
            Spacer()

            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .imageScale(.medium)
            }
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarkData[0])
            LandmarkRow(landmark: landmarkData[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}
複製代碼

1.3 因爲系統圖片是基於矢量的,因此咱們能夠經過 foregroundColor(_:) 方法來修改它們的顏色。框架

landmarkisFavorite 屬性爲 true 時,星星就會顯示。稍後咱們會在教程中看到如何修改這個屬性。

LandmarkRow.swift

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark

    var body: some View {
        HStack {
            landmark.image(forSize: 50)
            Text(landmark.name)
            Spacer()

            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .imageScale(.medium)
                    .foregroundColor(.yellow)
            }
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarkData[0])
            LandmarkRow(landmark: landmarkData[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}
複製代碼

2. 過濾 List View

咱們能夠自定義 list view 讓它顯示全部的地標,也能夠只顯示用戶收藏的。爲此,咱們須要給 LandmarkList 類型添加一點 state

state 是一個值或一組值,它能夠隨時間變化,而且會影響視圖的行爲、內容或佈局。咱們用具備 @State 特徵的屬性將 state 添加到 view 中。

2.1 在 Project navigator 中選擇 LandmarkList.swift ,添加一個名叫 showFavoritesOnly@State 屬性,把它的初始值設爲 false

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = false

    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
複製代碼

2.2 點擊 Resume 按鈕來刷新 canvas

當咱們對 view 的結構進行更改,好比添加或修改屬性時,須要手動刷新 canvas

2.3 經過檢查 showFavoritesOnly 屬性和每一個 landmark.isFavorite 的值來過濾地標列表。

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = false

    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                if !self.showFavoritesOnly || landmark.isFavorite {
                    NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
複製代碼

3. 添加控件來切換狀態

爲了讓用戶控制列表的過濾,咱們須要一個能夠修改 showFavoritesOnly 值的控件。經過給切換控件傳遞一個 binding 來實現這個需求。

binding 是對可變狀態的引用。當用戶將狀態從關閉切換爲打開而後再關閉時,控件使用 binding 來更新 view 相應的狀態

3.1 建立一個嵌套的 ForEach grouplandmarks 轉換爲 rows

若要在列表中組合靜態和動態 view ,或者將兩個或多個不一樣的動態 view 組合在一塊兒,要使用 ForEach 類型,而不是將數據集合傳遞給 List

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = true

    var body: some View {
        NavigationView {
            List {
                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
複製代碼

3.2 添加一個 Toggle view 做爲 List view 的第一個子項,而後給 showFavoritesOnly 傳遞一個 binding

咱們使用 $ 前綴來訪問一個狀態變量或者它的屬性的 binding

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    @State var showFavoritesOnly = true

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
複製代碼

3.3 使用實時預覽並點擊切換來嘗試這個新功能。

4. 使用 Bindable Object 進行存儲

爲了讓用戶控制哪些特定地標被收藏,咱們先要把地標數據存儲在 bindable object 中。

bindable object 是數據的自定義對象,它能夠從 SwiftUI 環境中的存儲綁定到 view 上。 SwiftUI 監視 bindable object 中任何可能影響 view 的修改,並在修改後顯示正確的 view 版本。

4.1 建立一個新 Swift 文件,命名爲 UserData.swift ,而後聲明一個模型類型。

UserData.swift

import SwiftUI

final class UserData: BindableObject  {

}
複製代碼

4.2 添加必要屬性 didChange ,使用 PassthroughSubject 做爲發佈者。

PassthroughSubjectCombine 框架中一個簡易的發佈者,它把任何值都直接傳遞給它的訂閱者。 SwiftUI 經過這個發佈者訂閱咱們的對象,而後當數據改變時更新全部須要更新的 view 。

UserData.swift

import SwiftUI
import Combine

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()
}
複製代碼

4.3 添加存儲屬性 showFavoritesOnlylandmarks 以及它們的初始值。

UserData.swift

import SwiftUI
import Combine

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()

    var showFavoritesOnly = false
    var landmarks = landmarkData
}
複製代碼

當客戶端更新模型的數據時,bindable object 須要通知它的訂閱者。當任何屬性更改時, UserData 應經過它的 didChange 發佈者發佈更改。

4.4 給經過 didChange 發佈者發送更新的兩個屬性建立 didSet handlers

UserData.swift

import SwiftUI
import Combine

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()

    var showFavoritesOnly = false {
        didSet {
            didChange.send(self)
        }
    }

    var landmarks = landmarkData {
        didSet {
            didChange.send(self)
        }
    }
}
複製代碼

5. 在 View 中接受模型對象

如今已經建立了 UserData 對象,咱們須要更新 view 來將 UserData 對象用做 app 的數據存儲。

5.1 在 LandmarkList.swift 中,將 showFavoritesOnly 聲明換成一個 @EnvironmentObject 屬性,而後給 preview 添加一個 environmentObject(_:) 方法。

一旦將 environmentObject(_:) 應用於父級, userData 屬性就會自動獲取它的值。

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject var userData: UserData

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(landmarkData) { landmark in
                    if !self.showFavoritesOnly || landmark.isFavorite {
                        NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
            .environmentObject(UserData())
    }
}
複製代碼

5.2 將 showFavoritesOnly 的調用更改爲訪問 userData 上的相同屬性。

@State 屬性同樣,咱們能夠使用 $ 前綴訪問 userData 對象成員的 binding

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject var userData: UserData

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $userData.showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(landmarkData) { 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 {
        LandmarkList()
            .environmentObject(UserData())
    }
}
複製代碼

5.3 建立 ForEach 對象時,使用 userData.landmarks 做爲其數據。

LandmarkList.swift

import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject var userData: UserData

    var body: some View {
        NavigationView {
            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 {
        LandmarkList()
            .environmentObject(UserData())
    }
}
複製代碼

5.4 在 SceneDelegate.swift 中,給 LandmarkList 添加 environmentObject(_:) 方法。

若是咱們不是使用預覽,而是在模擬器或真機上構建或運行 Landmarks ,這個更新能夠確保 LandmarkList 在環境中持有 UserData 對象。

SceneDelegate.swift

import UIKit
import SwiftUI

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: LandmarkList()
                .environmentObject(UserData())
        )
        self.window = window
        window.makeKeyAndVisible()
    }

    // ...
}
複製代碼

5.5 更新 LandmarkDetail view 來使用環境中的 UserData 對象。

咱們使用 landmarkIndex 訪問或更新 landmark 的收藏狀態,這樣就能夠始終獲得該數據的正確版本。

LandmarkDetail.swift

import SwiftUI

struct LandmarkDetail: View {
    @EnvironmentObject var userData: UserData
    var landmark: Landmark

    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }

    var body: some View {
        VStack {
            MapView(landmark: landmark)
                .frame(height: 300)

            CircleImage(image: landmark.image(forSize: 250))
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(caption)
                    Spacer()
                    Text(landmark.state)
                        .font(.caption)
                }
            }
            .padding()

            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)
    }
}

struct LandmarkDetail_Preview: PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}
複製代碼

5.6 切回 LandmarkList.swift ,打開實時預覽來驗證一切是否正常。

6. 給每一個 Landmark 建立收藏按鈕

Landmarks app 如今能夠在已過濾和未過濾的地標視圖之間切換,但收藏的地標還是硬編碼的。爲了讓用戶添加和刪除收藏,咱們須要在地標詳情 view 中添加收藏夾按鈕。

6.1 在 LandmarkDetail.swift 中,把 landmark.name 嵌套在一個 HStack 中。

LandmarkDetail.swift

import SwiftUI

struct LandmarkDetail: View {
    @EnvironmentObject var userData: UserData
    var landmark: Landmark

    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }

    var body: some View {
        VStack {
            MapView(landmark: landmark)
                .frame(height: 300)

            CircleImage(image: landmark.image(forSize: 250))
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                HStack {
                    Text(landmark.name)
                        .font(.title)
                }

                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(caption)
                    Spacer()
                    Text(landmark.state)
                        .font(.caption)
                }
            }
            .padding()

            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)
    }
}

struct LandmarkDetail_Preview: PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}
複製代碼

6.2 在 landmark.name 下面建立一個新按鈕。用 if-else 條件語句給地標傳遞不一樣的圖片來區分是否被收藏。

在按鈕的 action 閉包中,代碼使用持有 userData 對象的 landmarkIndex 來更新地標。

LandmarkDetail.swift

import SwiftUI

struct LandmarkDetail: View {
    @EnvironmentObject var userData: UserData
    var landmark: Landmark

    var landmarkIndex: Int {
        userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }

    var body: some View {
        VStack {
            MapView(landmark: landmark)
                .frame(height: 300)

            CircleImage(image: landmark.image(forSize: 250))
                .offset(y: -130)
                .padding(.bottom, -130)

            VStack(alignment: .leading) {
                HStack {
                    Text(landmark.name)
                        .font(.title)

                    Button(action: {
                        self.userData.landmarks[self.landmarkIndex].isFavorite.toggle()
                    }) {
                        if self.userData.landmarks[self.landmarkIndex].isFavorite {
                            Image(systemName: "star.fill")
                                .foregroundColor(Color.yellow)
                        } else {
                            Image(systemName: "star")
                                .foregroundColor(Color.gray)
                        }
                    }
                }

                HStack(alignment: .top) {
                    Text(landmark.park)
                        .font(caption)
                    Spacer()
                    Text(landmark.state)
                        .font(.caption)
                }
            }
            .padding()

            Spacer()
        }
        .navigationBarTitle(Text(landmark.name), displayMode: .inline)
    }
}

struct LandmarkDetail_Preview: PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
            .environmentObject(UserData())
    }
}
複製代碼

6.3 在 LandmarkList.swift 中打開預覽。

當咱們從列表導航到詳情並點擊按鈕時,咱們會在返回列表後看到這些更改仍然存在。因爲兩個 view 在環境中訪問相同的模型對象,所以這兩個 view 會保持一致。

相關文章
相關標籤/搜索