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:
Favorites
class that has a Set
of resort IDs the user likes.add()
, remove()
, and contains()
methods that manipulate the data, while also saving any changes to UserDefaults
.Favorites
class into the environment.Swift’s sets already contain methods for adding, removing, and checking for an element, but we’re going to add our own around them we can 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:
@Observable
class Favorites {
// the actual resorts the user has favorited
private var resorts: Set<String>
// the key we're using to read/write in UserDefaults
private let key = "Favorites"
init() {
// load our saved data
// still here? Use an empty array
resorts = []
}
// returns true if our set contains this resort
func contains(_ resort: Resort) -> Bool {
resorts.contains(resort.id)
}
// adds the resort to our set and saves the change
func add(_ resort: Resort) {
resorts.insert(resort.id)
save()
}
// removes the resort from our set and saves the change
func remove(_ resort: Resort) {
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
:
@State private var favorites = Favorites()
Now inject it into the environment by adding this modifier to the NavigationSplitView
:
.environment(favorites)
Because that’s attached to the navigation split view, every view the navigation stack presents will also gain that Favorites
instance to work with. So, we can load it from inside ResortView
by adding this new property:
@Environment(Favorites.self) var favorites
Tip: Make sure you modify your ResortView
preview to inject an example Favorites
object into the environment, so your SwiftUI preview carries on working. This will work fine: .environment(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 favorites.contains(resort) {
favorites.remove(resort)
} else {
favorites.add(resort)
}
}
.buttonStyle(.borderedProminent)
.padding()
Now we can show a colored heart icon next to favorite resorts in ContentView
by adding this to the end of the label for our NavigationLink
:
if favorites.contains(resort) {
Spacer()
Image(systemName: "heart.fill")
.accessibilityLabel("This is a favorite resort")
.foregroundStyle(.red)
}
Tip: As you can see, the foregroundStyle()
modifier works great here because our image uses SF Symbols.
That finishes our project, so give it one last try and see what you think. Good job!
TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.