Projects People Resources Semesters Blog About
Data Persistence
🗃️

Data Persistence

Last Updated: 8 October, 2022

Often we generate content in our apps and want to store it somewhere. There are a variety of ways to do that in our apps, ranging from simple to more complex. Here are some (but not all) of the ways you can persist data generated by your apps.

User Defaults

Sometimes you want to store really simple data such as a user preferences or if a user has openned the app before so you can decide to show a tutorial or not. The best way to store this small, simple data is in UserDefaults. You can store up to 512kb before the app starts to feel sluggish—so specific keys, but not an entire database.

SwiftUI wraps UserDefaults in a very simple property wrapper, @AppStorage. Suppose we have this simple button:

struct ContentView: View {
    @State private var tapCount = 0

    var body: some View {
        Button("Tap count: \(tapCount)") {
            tapCount += 1
        }
    }
}

And suppose we want to store how many times this button has been pressed. Let’s add a writing to user defaults after we update the tap count:

struct ContentView: View {
    @State private var tapCount = 0

    var body: some View {
        Button("Tap count: \(tapCount)") {
            tapCount += 1
						UserDefaults.standard.set(self.tapCount, forKey: "tapCount")
        }
    }
}

Let’s also make sure we updated our default value for tapCount to represent the stored value in UserDefaults when the app launches:

struct ContentView: View {
    @State private var tapCount = UserDefaults.standard.integer(forKey: "tapCount")

    var body: some View {
        Button("Tap count: \(tapCount)") {
            tapCount += 1
						UserDefaults.standard.set(self.tapCount, forKey: "tapCount")
        }
    }
}

We use the same name for the key (tapCount) to set and retrieve, similar to using a HashMap/Dictionary.

SwiftUI also provides a property wrapper to make this process even easier. Let’s replace what we wrote above using the @AppStorage property wrapper.

struct ContentView: View {
    @AppStorage("tapCount") private var tapCount = 0

    var body: some View {
        Button("Tap count: \(tapCount)") {
            tapCount += 1
        }
    }
}

We use the key “tapCount” the same way, and also set a default value. The big change is we don’t have to remember to write our updates to UserDefaults manually when we modify the value of tapCount.

Documents

This is a great method for saving data created by users such as documents (hence the name of where you save this stuff). If you’re making a networking app you’ll want to look at the next method.

Your best approach for implementing this will be to follow the linked tutorials. The ideas behind the approach are to get the URL for the user’s documents folder. To encode your data to JSON and write it to said folder. To write your data, you’ll need to be using structs which have adopted the Codable protocol. To read data, you’ll decode the data at the given URL from JSON back into the structures in your app.

Key things to keep track of are that you now manipulate your data as you were before, but make sure you write to Documents periodically (normally after committing changes like Submit buttons) to make sure you’re saving data when the user expects you to.

Core Data + CloudKit

Going to mention it here, but keep in mind this option requires an Apple Developer account for sync to work. It’s a really powerful option, but probably not the best choice for your Oasis project becuase of this. For an Oasis project, I recommend you use Firebase for similar functionality (and a much better opportunity at mentor help).

CoreData is how I setup persistence in a lot of my apps, but starting from scratch you’ll be better off learning Firebase and offline for Firebase now that it’s on SPM (it wasn’t when I was getting started).

This video is a great place to start if you’re hoping to use this method:

Firebase: Cloud Firestore

Cloud Firestore lets you store data in the cloud so that users can sync data across devices, manage sign-ins, and share data with other users. BeReal was built on it.

This linked tutorial is fantastic and does a far better job than I could. When it mentions the AppDelegate.swift file, use the following from the Firebase setup documentation instead of the separate AppDelegate.swift file that the tutorial provides:

import SwiftUI
import FirebaseCore


class AppDelegate: NSObject, UIApplicationDelegate {
  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    FirebaseApp.configure()

    return true
  }
}

@main
struct YourApp: App {
  // register app delegate for Firebase setup
  @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate


  var body: some Scene {
    WindowGroup {
      NavigationView {
        ContentView()
      }
    }
  }
}

If you want to add Offline caching to your project as well so that users can access data without an internet connection, you’ll want to follow the below page from the Firebase Documentation. Firebase will manage syncing changes between local and online data and updates the online data once the device has an internet connection again.

Firebase has much more in its own fantastic documentation and tips linked in their console, so be ready to read and learn since they provide lots of great resources.