Due to API changes, part of this article has been invalid, please check the latest complete Chinese tutorial and codeGithub.com/WillieWangW…
Wechat Technology Group
SwiftUI represents the direction of building App in the future. Welcome to join us to exchange technology and solve problems.
Add group needs to apply now, you can add my wechat first, note “SwiftUI”, I will pull you into the group.
Processing user input
In the Landmarks App, users can mark their favorite locations and filter them out in a list. To do this, we first add a switch to the list so that users can see only their favorites. There will also be a star button that users can click to bookmark landmarks.
Download the initial project file and follow the steps below, or open the completed project and browse the code yourself.
- Estimated completion time: 20 minutes
- Initial project file: Download
1. Mark the user’s favorite landmarks
First, optimize the list to clearly show users their favorites. Add a star to each LandmarkRow.
1.1 Open the start Project and select Landmarkrow.swift from the Project Navigator.
1.2 Add an if statement under spacer to add a star image to test whether the current landmark is bookmarked.
In SwiftUI Block, we use the if statement to conditionally introduce a View.
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)
}
}
}
}
struct LandmarkRow_Previews: PreviewProvider {
static var previews: some View {
Group {
LandmarkRow(landmark: landmarkData[0])
LandmarkRow(landmark: landmarkData[1])
}
.previewLayout(.fixed(width: 300, height: 70))
}
}
Copy the code
1.3 since the system images are vector-based, we can modify their colors through foregroundColor(_:) method.
Stars are displayed when landmark’s isFavorite property is true. We will see how to modify this property later in the tutorial.
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))
}
}
Copy the code
2. Filter List Views
We can customize the List View to show all the landmarks, or we can just show the user’s favorites. To do this, we need to add a little state to the LandmarkList type.
State is a value or set of values that can change over time and affect the behavior, content, or layout of a view. We add State to the view with an @State attribute.
2.1 Select LandmarkList. Swift in the Project Navigator and add an @state property called showFavoritesOnly with its initial value set to 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()
}
}
Copy the code
2.2 Click Resume to refresh the Canvas.
When we make changes to the view’s structure, such as adding or modifying properties, we need to manually refresh the canvas.
2.3 Filter the list of landmarks by checking the showFavoritesOnly property and the value of each 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()
}
}
Copy the code
3. Add controls to switch states
To allow users to control list filtering, we need a control that modifies the showFavoritesOnly value. This requirement is implemented by passing a Binding to the switch control.
A binding is a reference to mutable state. When the user switches state from closed to open and then closed again, the control uses binding to update the corresponding state of the view
3.1 Create a nested ForEach group to convert Landmarks to Rows.
To combine static and dynamic views in a List, or to combine two or more different dynamic views together, use the ForEach type instead of passing a collection of data to a 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()
}
}
Copy the code
3.2 Add a Toggle View as the first child of the List View and pass a binding to showFavoritesOnly.
We use the $prefix to access the binding of a state variable or its attribute.
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()
}
}
Copy the code
3.3 Use live preview and click Toggle to try this new feature.
4. Use Bindable Object for storage
To give the user control over which particular landmarks are bookmarked, we first store the landmark data in a Bindable Object.
A Bindable Object is a custom object for data that can be bound to a View from storage in the SwiftUI environment. SwiftUI monitors any changes in the Bindable Object that may affect the View and displays the correct version of the View after the changes.
4.1 Create a new Swift file named userdata.swift and declare a model type.
UserData.swift
import SwiftUI
final class UserData: BindableObject {
}
Copy the code
4.2 Add the required property didChange, using PassthroughSubject as the publisher.
PassthroughSubject is a simple publisher in the Combine framework that passes any value directly to its subscribers. SwiftUI subscribes to our objects through this publisher and then updates all views that need to be updated when the data changes.
UserData.swift
import SwiftUI
import Combine
final class UserData: BindableObject {
let didChange = PassthroughSubject<UserData, Never>()
}
Copy the code
4.3 Adding showFavoritesOnly and Landmarks and their initial values.
UserData.swift
import SwiftUI
import Combine
final class UserData: BindableObject {
let didChange = PassthroughSubject<UserData, Never>()
var showFavoritesOnly = false
var landmarks = landmarkData
}
Copy the code
The Bindable Object needs to notify its subscribers when the client updates the model’s data. When any property changes, UserData should publish the change through its didChange publisher.
4.4 Create didSet Handlers for the two attributes that send updates through the didChange publisher.
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)
}
}
}
Copy the code
5. Accept model objects in the View
Now that the UserData object has been created, we need to update the View to use the UserData object as the data store for our app.
5.1 in landmarklist. swift, replace the showFavoritesOnly declaration with a @environmentobject property, and add a EnvironmentObject (_:) method to preview.
Once environmentObject(_:) is applied to the parent, the userData property automatically retrieves its value.
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())
}
}
Copy the code
5.2 Change the call to showFavoritesOnly to access the same property on userData.
Like the @state attribute, we can use the $prefix to access the binding of userData object members.
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())
}
}
Copy the code
5.3 When creating a ForEach object, use userdata.landmarks as its data.
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())
}
}
Copy the code
5.4 add environmentObject(_:) to LandmarkList in scenedelegate. swift.
If we build or run Landmarks on a simulator or real machine instead of using previews, this update ensures that LandmarkList holds UserData objects in the environment.
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 controllerlet window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIHostingController(
rootView: LandmarkList()
.environmentObject(UserData())
)
self.window = window
window.makeKeyAndVisible()
}
// ...
}
Copy the code
5.5 Update the LandmarkDetail View to use the UserData object in the environment.
We use landmarkIndex to access or update landmark’s collection status so that we always get the correct version of this data.
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())
}
}
Copy the code
5.6 Cut back to LandmarkList.swift and open live Preview to verify that everything is working.
6. Create favorites buttons for each Landmark
The Landmarks App can now switch between filtered and unfiltered views of Landmarks, but the favorites are still hard-coded. To allow users to add and remove favorites, we need to add a favorites button in the Landmark Details View.
6.1 In Landmarkdetail. swift, landmark.name is nested in an 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())
}
}
Copy the code
6.2 Create a new button under landmark.name. Use if-else conditions to pass different images to landmarks to distinguish between favorites.
In the button’s Action closure, the code updates the landmark using the landmarkIndex that holds the userData object.
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())
}
}
Copy the code
6.3 Open preview in LandmarkList.swift.
When we navigate from the list to the details and click the button, we will see that the changes are still there when we return to the list. Since both views access the same model object in the environment, the two views will be consistent.