[译] 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 会保持一致。

相关文章
相关标签/搜索