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.