Skip to content

c4arl0s/HandlingUserInput

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

49 Commits
 
 
 
 
 
 
 
 

Repository files navigation

  1. 1. Mark the User’s Favorite Landmarks
  2. 2. Filter the List View
  3. 3. Add a Control to Toggle the State
  4. 4. Use an Observable Object for Storage
  5. 5. Adopt the Model Object in Your Views
  6. 6. Create a Favorite Button for Each Landmark

In the Landmarks app, a user can flag their favorite places, and filter the list to show just their favorites. To create this feature, you’ll start by adding a switch to the list so users can focus on just their favorites, and then you’ll add a star-shaped button that a user taps to flag a landmark as a favorite.

Step 1

Open the project you finished in the previous tutorial, and select Landmark.swift in the Project navigator.

Step 2

Add an isFavorite property to the Landmark structure.

The landmarkData.json file has a key with this name for each landmark. Because Landmark conforms to Codable, you can read the value associated with the key by creating a new property with the same name as the key.

import Foundation
import SwiftUI
import CoreLocation

struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String
    var isFavorite: Bool
    
    private var imageName: String
    
    var image: Image {
        Image(imageName)
    }
    
    private var coordinates: Coordinates
    
    struct Coordinates: Hashable, Codable {
        var latitude: Double
        var longitude: Double
    }
    
    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(latitude: coordinates.latitude, longitude: coordinates.longitude)
    }
}

Step 3

Select LandmarkRow.swift in the Project navigator.

Step 4

After the spacer, add a star image inside an if statement to test whether the current landmark is a favorite.

In SwiftUI blocks, you use if statements to conditionally include views.

if landmark.isFavorite {
    Image(systemName: "start.fill")
}

Complete LandmarkRow.swift file:

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark
    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)
            
            Spacer()
            
            if landmark.isFavorite {
                Image(systemName: "star.fill")
            }
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarks[0])
            LandmarkRow(landmark: landmarks[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}
Screenshot 2023-11-14 at 11 59 53 p m

Step 5

Because system images are vector based, you can change their color with the foregroundStyle(_:) modifier.

if landmark.isFavorite {
    Image(systemName: "star.fill")
        .foregroundStyle(.yellow)
}

Complete code:

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark
    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)
            Spacer()
            
            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .foregroundStyle(.yellow)
            }
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarks[0])
            LandmarkRow(landmark: landmarks[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}
Screenshot 2023-11-15 at 12 05 05 a m

Running Landmarks app.

Screenshot 2023-11-15 at 12 07 49 a m

You can customize the list view so that it shows all of the landmarks, or just the user’s favorites. To do this, you’ll need to add a bit of state to the LandmarkList type.

State is a value, or a set of values, that can change over time, and that affects a view’s behavior, content, or layout. You use a property with the @State attribute to add state to a view.

Screenshot 2023-11-23 at 9 16 10 p m

Step 1

Select LandmarkList.swift in the Project navigator.

Step 2

Add a @State property called showFavoritesOnly, with its initial value set to false.

Because you use state properties to hold information that is specific to a view and its subviews, you always create state as private.

import SwiftUI

struct LandmarkList: View {
    @State private var showFavoritesOnly = false
    var body: some View {
        NavigationView {
            List(landmarks) { landmark in
                NavigationLink {
                    LandmarkDetailView(landmark: landmark)
                } label: {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationTitle("Landmarks")
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}

Step 3

When you make changes to your view’s structure, like adding or modifying a property, the canvas automatically refreshes.

If the canvas isn’t visible, select Editor > Canvas to show it.

Step 4

Compute a filtered version of the landmarks list by checking the showFavoritesOnly property and each landmark.isFavorite value.

Screenshot 2023-11-19 at 12 38 50 p m
var filteredLandmarks: [Landmark] {
    landmarks.filter { landmark in
        (!showFavoritesOnly || landmark.isFavorite)
    }
}
Screenshot 2023-11-19 at 12 43 37 p m

Step 5

Use the filtered version of the list of landmarks in the List.

Step 6

Change the initial value of showFavoritesOnly to true to see how the list reacts.

Screenshot 2023-11-19 at 12 54 01 p m Screenshot 2023-11-19 at 12 55 26 p m
import SwiftUI

struct LandmarkList: View {
    @State private var showFavoritesOnly = true
    
    var filteredLandmarks: [Landmark] {
        landmarks.filter { landmark in
            (!showFavoritesOnly || landmark.isFavorite)
        }
    }
    
    var body: some View {
        NavigationView {
            List(filteredLandmarks) { landmark in
                NavigationLink {
                    LandmarkDetailView(landmark: landmark)
                } label: {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationTitle("Landmarks")
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}

To give the user control over the list’s filter, you need to add a control that can alter the value of showFavoritesOnly. You do this by passing a binding to a toggle control.

A binding acts as a reference to a mutable state. When a user taps the toggle from off to on, and off again, the control uses the binding to update the view’s state accordingly.

Screenshot 2023-11-21 at 10 56 16 a m

Step 1

Create a nested ForEach group to transform the landmarks into rows.

To combine static and dynamic views in a list, or to combine two or more different groups of dynamic views, use the ForEach type instead of passing your collection of data to List.

import SwiftUI

struct LandmarkList: View {
    @State private var showFavoritesOnly = true
    
    var filteredLandmarks: [Landmark] {
        landmarks.filter { landmark in
            (!showFavoritesOnly || landmark.isFavorite)
        }
    }
    
    var body: some View {
        NavigationSplitView {
            List {
                ForEach(filteredLandmarks) { landmark in
                    NavigationLink {
                        LandmarkDetailView(landmark: landmark)
                    } label: {
                        LandmarkRow(landmark: landmark)
                    }
                }
                .navigationTitle("Landmarks")
            }
        } detail: {
            Text("Select a Landmark")
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
Screenshot 2023-11-23 at 10 16 53 p m Screenshot 2023-11-23 at 10 12 42 p m

Step 2

Add a Toggle view as the first child of the List view, passing a binding to showFavoritesOnly.

You use the $ prefix to access a binding to a state variable, or one of its properties.

Screenshot 2023-11-23 at 10 30 32 p m
import SwiftUI

struct LandmarkList: View {
    @State private var showFavoritesOnly = true
    
    var filteredLandmarks: [Landmark] {
        landmarks.filter { landmark in
            (!showFavoritesOnly || landmark.isFavorite)
        }
    }
    
    var body: some View {
        NavigationSplitView {
            List {
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }
                
                ForEach(filteredLandmarks) { landmark in
                    NavigationLink {
                        LandmarkDetailView(landmark: landmark)
                    } label: {
                        LandmarkRow(landmark: landmark)
                    }
                }
                .navigationTitle("Landmarks")
            }
        } detail: {
            Text("Select a Landmark")
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
Screenshot 2023-11-23 at 10 32 11 p m

Step 3

Before moving on, return the default value of showsFavoritesOnly to false.

@State private var showFavoritesOnly = false
Screenshot 2023-11-23 at 10 35 28 p m

Step 4

Use the Live preview and try out this new functionality by tapping the toggle.

Screen Recording 2024-02-11 at 1 48 10 p m 2024-02-11 1_50_40 p m

To prepare for the user to control which particular landmarks are favorites, you will first store the landmark data using the Observable() macro.

Screenshot 2024-02-11 at 1 56 26 p m

With Observation, a view in SwiftUI can support data changes without using property wrappers or bindings. SwiftUI watches for any observable property changes that could affect a view, and displays the correct version of the view after a change.

Step 1

In the project's navigation pane, select Model Data.

Step 2

Declare a new model type using the Observable() macro.

Screenshot 2024-02-11 at 2 11 15 p m

SwiftUI updates a view only when an observable property changes and the view´s body reads the property directly.

Step 3

Move the landmarcks array into the model.

Screenshot 2024-02-11 at 2 17 00 p m

Now that you have created the ModelData object, you need to update your views to adopt it as the data store for your app.

Screenshot 2024-02-11 at 2 31 00 p m

Step 1

In LandmarkList, add an @Environment property wrapper to the view, and an environment(_:) modifier to the preview.

Screenshot 2024-02-11 at 2 55 19 p m

The modelData property gets ist value automatically, as long as the environment(_:) modifier has been applied to a parent. The @Environment property wrapper enables you to read the model data of the current view. Adding an environment(_:) modifier passes the data object down through the environment.

Step 2

Use modelData.landmarks as the data when filtering landmarks.

Screenshot 2024-02-11 at 3 00 56 p m

Step 3

Update the LandmarkDetail view to work with the ModelData object in the environment.

Screenshot 2024-02-11 at 3 07 16 p m

Step 4

Update the LandmarkRow preview to work with the ModelData object.

Screenshot 2024-02-11 at 3 15 13 p m

Step 5

Update the ContentView preview to add the model object to the environment, which makes the object available to any subview.

Screenshot 2024-02-11 at 3 19 05 p m

A preview fails if any subview requires a model object in the environment, but the view you are previewing does not have the environment(_:) modifier. Next, you will update the app instance to put the model object in the environment when you run the app in the simulator or on a device.

Step 6

Update the LandmarksApp to create a model instance and supply it to ContentView using the environment(_:) modifier.

Screenshot 2024-02-11 at 3 27 38 p m

Step 7

Switch back to LandmarkList to verify that everything is working properly.

Screen Recording 2024-02-11 at 3 33 46 p m 2024-02-11 3_35_42 p m

The Landmarks app can now switch between a filtered and unfiltered view of the landmarks, but the list of favorite landmarks is still hard coded. To allow the user to add and remove favorites, you need to add a favorite button to the landmark detail view.

Step 1

You will first create a reusable Favorite button.

Screenshot 2024-02-11 at 5 18 45 p m

Step 2

Add an isSet binding that indicates the button´s current state, and provide a constant value for the preview.

Screenshot 2024-02-11 at 5 28 51 p m

The binding property wrapper enables you to read and write between a property that stores data and a view that displays and changes the data. Because you use a binding, changes made inside this view propagate back to the data source.

Step 3

Create a Button with an action that toggles the isSet state, and that changes its appearence based on the state.

Screenshot 2024-02-11 at 5 37 37 p m

The title string that you provide for the button’s label doesn’t appear in the UI when you use the iconOnly label style, but VoiceOver uses it to improve accessibility.

As your program grows, it is a good idea to add hierarchy. Before moving on, create a few more groups.

Step 4

Collect the general purpose CircleImage, MapView, and FavoriteButton files into a Helpers group, and the landmark views into a Landmarks group.

Screenshot 2024-02-11 at 5 43 26 p m

Next, you’ll add the FavoriteButton to the detail view, binding the button’s isSet property to the isFavorite property of a given landmark.

Step 5

Switch to LandmarkDetail, and compute the index of the input landmark by comparing it with the model data.

Screenshot 2024-02-11 at 5 51 00 p m

To support this, you also need access to the environment’s model data.

Step 6

Inside the body property, add the model data using a Bindable wrapper. Embed the landmark´s name in an HStack with a new FavoriteButton; provide a binding to the isFavorite property with the dollar sign ($).

Screenshot 2024-02-11 at 6 00 31 p m

Use landmarkIndex with the modelData object to ensure that the button updates the isFavorite property of the landmark stored in your model object.

Step 7

Switch back to LandmarkList, and make sure the Live preview is on.

step7

As you navigate from the list to the detail and tap the button, those changes persist when you return to the list. Because both views access the same model object in the environment, the two views maintain consistency.

About

Handling User Input

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages