因为 API 变更,此文章部份内容已失效,最新完整中文教程及代码请查看 github.com/WillieWangW…git
SwiftUI
表明将来构建 App 的方向,欢迎加群一块儿交流技术,解决问题。github
在
Landmarks
app 中,用户能够标记他们喜欢的地点,并在列表中过滤出来。要实现这个功能,咱们要先在列表中添加一个开关,这样用户能够只看到他们收藏的内容。另外还会添加一个星形按钮,用户能够点击该按钮来收藏地标。canvas下载起始项目文件并按照如下步骤操做,也能够打开已完成的项目自行浏览代码。swift
- 预计完成时间:20 分钟
- 初始项目文件:下载
首先,经过优化列表来清晰地给用户显示他们的收藏。给每一个被收藏地标的 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(_:)
方法来修改它们的颜色。框架
当 landmark
的 isFavorite
属性为 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))
}
}
复制代码
咱们能够自定义 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()
}
}
复制代码
为了让用户控制列表的过滤,咱们须要一个能够修改 showFavoritesOnly
值的控件。经过给切换控件传递一个 binding
来实现这个需求。
binding
是对可变状态的引用。当用户将状态从关闭切换为打开而后再关闭时,控件使用 binding
来更新 view 相应的状态
3.1 建立一个嵌套的 ForEach group
将 landmarks
转换为 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 使用实时预览并点击切换来尝试这个新功能。
为了让用户控制哪些特定地标被收藏,咱们先要把地标数据存储在 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
做为发布者。
PassthroughSubject
是 Combine
框架中一个简易的发布者,它把任何值都直接传递给它的订阅者。 SwiftUI
经过这个发布者订阅咱们的对象,而后当数据改变时更新全部须要更新的 view 。
UserData.swift
import SwiftUI
import Combine
final class UserData: BindableObject {
let didChange = PassthroughSubject<UserData, Never>()
}
复制代码
4.3 添加存储属性 showFavoritesOnly
和 landmarks
以及它们的初始值。
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)
}
}
}
复制代码
如今已经建立了 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
,打开实时预览来验证一切是否正常。
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 会保持一致。