NS

Save User Favorite Struct to UserDefault in SwiftUI

SwiftUI has a built-in dictionary called UserDefault that can persist a little bit of data until you do not uninstall the app. Let us look at some of these terms in detail.

SwiftUI is an innovative, exceptionally simple way to build user interfaces across all Apple platforms with the power of Swift. Build user interfaces for any Apple device using just one set of tools and APIs. With a declarative Swift syntax that’s easy to read and natural to write, SwiftUI works seamlessly with new Xcode design tools to keep your code and design perfectly in sync. Automatic support for Dynamic Type, Dark Mode, localization, and accessibility means your first line of SwiftUI code is already the most powerful UI code you’ve ever written” — Apple Developer website

UserDefault class provides a programmatic interface for interacting with the defaults system. The defaults system allows an app to customize its behavior to match a user’s preferences. For example, you can allow users to specify their preferred units of measurement or media playback speed. Apps store these preferences by assigning values to a set of parameters in a user’s defaults database. The parameters are referred to as defaults because they’re commonly used to determine an app’s default state at startup or the way it acts by default. At runtime, you use UserDefaults objects to read the defaults that your app uses from a user’s defaults database. UserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value. When you set a default value, it’s changed synchronously within your process, and asynchronously to persistent storage and other processes. — Apple Developer website

In this article, You and I will work together to do the following:

  1. Create a Struct
  2. Render a List of Struct objects
  3. Create a Like Button
  4. Create a Favorite Class to Save to UserDefault
  5. Render the Favorite Objects

Let us get started with creating a Struct

struct Task {  
   var id = UUID()  
   var title: String  
}

Go to ContentView.swift, initialize an array of Task and add dummy data to it.

@State var tasks : \[Task\] = \[\]  
let firstElement = Task(title: "We Are Young"  
let secondElement = Task(title: "Memories")  
tasks.append(contentsOf: \[firstElement, secondElement\]

Now Tasks has some data to display. Let us go ahead and create a new file and call it Favorite.swift. This is where we will have the UserDefault Logic.


class Favorites: ObservableObject {
    private var tasks: Set<String>
    let defaults = UserDefaults.standard
    
    init() {
        let decoder = JSONDecoder()
        if let data = defaults.value(forKey: "Favorites") as? Data {
            let taskData = try? decoder.decode(Set<String>.self, from: data)
            self.tasks = taskData ?? []
        } else {
            self.tasks = []
        }
    }
    
    func getTaskIds() -> Set<String> {
        return self.tasks
    }
    
    func isEmpty() -> Bool {
        tasks.count < 1
    }
    
    func contains(_ task: Task) -> Bool {
        tasks.contains(task.id)
    }
    
    func add(_ task: Task) {
        objectWillChange.send()
        tasks.insert(task.id)
        save()
    }
    
    func remove(_ task: Task) {
        objectWillChange.send()
        tasks.remove(task.id)
        save()
    }
    
    func save() {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(Favorites) {
            defaults.set(encoded, forKey: "Favorites")
        }
    }
}

Let us look into what is happening in the file:

  1. You will notice that we are storing a Set<String> and not Set<Task>. This is because we don’t want to store the entire struct but just the id which we can use to extract the entire struct.
  2. I created multiple methods, just for usability. However, all you need is add(), remove() and save().
  3. getTaskIds() → returns a set of ids, this is what we will use to extract our struct (if required). Mostly, you can just use the contains, if all you want to do is check if the item is marked favorite.
  4. isEmpty() → I created it to show a different view where there are no favorites.
  5. contains() → We use this to check if the like button should be marked red (like = true) or not.
  6. add() → To add to favorites
  7. remove() → To remove from favorites
  8. save() → We use JSONEncoder to encode our string and save it in the Key called “Favorites”
  9. init() → We use JSONDecoder to decode the encoded strings in the Key called “Favorites”

Now lets’ create a like button that uses these functions to communicate with our user defaults. In your ContentView.swift, render the text with a like button for all tasks in tasks[]. Before that, you need to initialize the object of Favorite.

@ObservedObject var favorites = Favorites()
  VStack {
    Text(task.title)
    Button(action: {
      if self.favorites.contains(task) {
          self.favorites.remove(task)
      } else {
          self.favorites.add(task)
      }
    }) {
      HStack {
          Image(systemName: favorites.contains(task) ? "heart.fill" : "heart").foregroundColor(favorites.contains(task) ? .red : .white)
      }
    }
  }
}

Render the like button:

Let’s look at this file closely,

  1. It renders a system icon heart filled with red color if the favorites contain tasks. If it does not, it displays an empty heart with no color fill.
  2. As soon as you press the button, it checks if the favorite contains, if it does, it means the user is trying to un-favorite this tasks, so we remove it. If the favorite does not contain that task, it means the user is trying to favorite the task, and therefore, we add it to Favorites.

One thing to notice is, that we are sending the entire task (object of the struct) to Favorite, however, if you look closely to Favorite.swift, we are extracting the id from the object before storing so we don’t save an entire struct in UserDefaults.

If you would like to extract the objects saved in UserDefault Favorite. I implemented a simple method to return the Ids. You can use the method getTaskIds() as,

let taskIds: Set<String> = self.favorites.getTasksIds()  
//some folks don't like Set, you can easily convert it to an array,  
let taskArray = Array(taskIds)

That’s all folks !! Now you can save user favorites, or any sort of user settings using the template I provided above, without worrying about the complication of writing the back end. Happy Hacking !!