Persist favourites

In Chris’ SwiftUI - How To Handle User Input Tutorial favourites are lost on close of app. Can anyone advise how to persist these user selected favorites? User defaults? Core Data?

Probably the best way is to store the landmarks.id in an array and then save that to UserDefaults. When the App is next run, that saved array would then be retrieved from UserDefaults and the landmarks array updated to set isFavourite equal to true for each id saved in that array.

Hi Chris, many thanks for responding. You are unfortunately talking to a novice and need even more help with this. How do I save the array? Is there an example you could share with me. Thx Rob.

Hi Rob,

Yes. Have a look at this code I added to my version of the project.

UserData.swift: The init() function retrieves any saved favourites.

import Combine
import SwiftUI

final class UserData: ObservableObject {
    @Published var showFavoritesOnly = false
    @Published var landmarks = landmarkData

    //  Added all the code below to facilitate saving the favourites to UserDefaults
    var savedFavourites: [Int] = []

    init() {
        retrieveSavedFavourites()
    }

    func updateSavedFavourites() {
        for landmark in landmarks {
            if landmark.isFavorite && !savedFavourites.contains(landmark.id) {
                savedFavourites.append(landmark.id)
            } else if !landmark.isFavorite && savedFavourites.contains(landmark.id) {
                let idx = savedFavourites.firstIndex { $0 == landmark.id }
                guard idx != nil else { return }
                savedFavourites.remove(at: idx!)
            }
        }
        UserDefaults.standard.set(savedFavourites, forKey: "favourites")
    }

    func retrieveSavedFavourites() {
        if let retrievedFavourites = UserDefaults.standard.object(forKey: "favourites") as? [Int] {
            savedFavourites = retrievedFavourites
        }
        for idx in 0..<landmarks.count {
            if savedFavourites.contains(landmarks[idx].id) {
                landmarks[idx].isFavorite = true
            }
        }
    }
}

LandmarksList.swift: Added an onAppear modifier to cause the favourites to be updated from the saved favourites.

struct LandmarkList: View {
    @EnvironmentObject private var userData: UserData
    
    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $userData.showFavoritesOnly) {
                    Text("Show Favorites Only")
                }
                
                ForEach(userData.landmarks) { landmark in
                    if !self.userData.showFavoritesOnly || landmark.isFavorite {
                        NavigationLink(
                            destination: LandmarkDetail(landmark: landmark)
                                .environmentObject(self.userData)
                        ) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
        //  Added onAppear to iterate through the landmarks and update the
        //  favourites with to the saved favourites.
        .onAppear {
            userData.updateSavedFavourites()
        }
    }
}

LandmarkDetail.swift: Added a line of code to the Button action for the favourite so that the change is favourite is saved.

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(coordinate: landmark.locationCoordinate)
                .edgesIgnoringSafeArea(.top)
                .frame(height: 300)
            
            CircleImage(image: landmark.image)
                .offset(x: 0, y: -130)
                .padding(.bottom, -130)
            
            VStack(alignment: .leading) {
                HStack {
                    Text(verbatim: landmark.name)
                        .font(.title)
                    
                    Button(action: {
                        self.userData.landmarks[self.landmarkIndex].isFavorite.toggle()
                        //  Update saved favourites
                        userData.updateSavedFavourites()
                    }) {
                        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(verbatim: landmark.park)
                        .font(.subheadline)
                    Spacer()
                    Text(verbatim: landmark.state)
                        .font(.subheadline)
                }
            }
            .padding()
            
            Spacer()
        }
    }
}

Hope that helps.

1 Like

Oh my word!! I have tried so many different things but couldn’t get it to work. THIS THO IS BRILLIANT!! Thank you so much. Need to get Chris to add this to his series as this makes the landmark app so much more usable!! Brilliant. Thankyou.

No problem Robert.