因爲 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 會保持一致。