NEW: Join my free 100 Days of SwiftUI challenge today! >>

Letting the user mark favorites

Paul Hudson    @twostraws   

The final task for this project is to let the user assign favorites to resorts they like. This is mostly straightforward, using techniques we’ve already covered:

  • Creating a new Favorites class that has a Set of resort IDs the user likes.
  • Giving it add(), remove(), and contains() methods that manipulate the data, sending update notifications to SwiftUI while also saving any changes to UserDefaults.
  • Injecting an instance of the Favorites class into the environment.
  • Adding some new UI to call the appropriate methods.

Swift’s sets already contain methods for adding, removing, and checking for an element, but we’re going to add our own around them so we can use objectWillChange to notify SwiftUI that changes occurred, and also call a save() method so the user’s changes are persisted. This in turn means we can mark the favorites set using private access control, so we can’t accidentally bypass our methods and miss out saving.

Create a new Swift file called Favorites.swift, replace its Foundation import with SwiftUI, then give it this code:

class Favorites: ObservableObject {
    // the actual resorts the user has favorited
    private var resorts: Set<String>

    // the key we're using to read/write in UserDefaults
    private let saveKey = "Favorites"

    init() {
        // load our saved data

        // still here? Use an empty array
        self.resorts = []
    }

    // returns true if our set contains this resort
    func contains(_ resort: Resort) -> Bool {
        resorts.contains(resort.id)
    }

    // adds the resort to our set, updates all views, and saves the change
    func add(_ resort: Resort) {
        objectWillChange.send()
        resorts.insert(resort.id)
        save()
    }

    // removes the resort from our set, updates all views, and saves the change
    func remove(_ resort: Resort) {
        objectWillChange.send()
        resorts.remove(resort.id)
        save()
    }

    func save() {
        // write out our data
    }
}

You’ll notice I’ve missed out the actual functionality for loading and saving favorites – that will be your job to fill in shortly.

We need to create a Favorites instance in ContentView and inject it into the environment so all views can share it. So, add this new property to ContentView:

@ObservedObject var favorites = Favorites()

Now inject it into the environment by adding this modifier to the NavigationView:

.environmentObject(favorites)

Because that’s attached to the navigation view, every view the navigation view presents will also gain that Favorites instance to work with. So, we can load it from inside ResortView by adding this new property:

@EnvironmentObject var favorites: Favorites

All this work hasn’t really accomplished much yet – sure, the Favorites class gets loaded when the app starts, but it isn’t actually used anywhere despite having properties to store it.

This is easy enough to fix: we’re going to add a button at the end of the scrollview in ResortView so that users can either add or remove the resort from their favorites, then display a heart icon in ContentView for favorite resorts.

First, add this to the end of the scrollview in ResortView:

Button(favorites.contains(resort) ? "Remove from Favorites" : "Add to Favorites") {
    if self.favorites.contains(self.resort) {
        self.favorites.remove(self.resort)
    } else {
        self.favorites.add(self.resort)
    }
}
.padding()

Now we can show a colored heart icon next to favorite resorts in ContentView by adding this to the end of the NavigationLink:

if self.favorites.contains(resort) {
    Spacer()
    Image(systemName: "heart.fill")
    .accessibility(label: Text("This is a favorite resort"))
        .foregroundColor(Color.red)
}

Tip: As you can see, the foregroundColor() modifier works great here because our image uses SF Symbols.

That mostly works, but you might notice a glitch: if you favorite resorts with longer names you might find their name wraps onto two lines even though there’s space for it to be all on one. This is yet another example where SwiftUI’s layout system allocates too much priority to spacers and not enough to text, so the fix for now – hopefully until Apple solves it soon! – is to adjust the layout priority of the VStack directly before the condition we just added:

VStack(alignment: .leading) {
    Text(resort.name)
        .font(.headline)
    Text("\(resort.runs) runs")
        .foregroundColor(.secondary)
}
.layoutPriority(1)

That should make the text layout correctly even with the spacer and heart icon – much better.

And that also finishes our project, so give it one last try and see what you think. Good job!

LEARN SWIFTUI FOR FREE I have a massive, free SwiftUI video collection on YouTube teaching you how to build complete apps with SwiftUI – check it out!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5