Data Persistence in SwiftUI

Jun 20 2024 · Swift 5.9, iOS 17.2, Xcode 15.1

Lesson 02: Persisting Data in JSON Format

Demo

Episode complete

Play next episode

Next
Transcript

Writing Data in JSON Format

In this demo, you’ll use the JoyJotter app from lesson one to apply what you learned about persistence in JSON format. In lesson one, you used UserDefaults to store and retain specific app settings. Now, your goal is to persist the entire collection of jokes. This includes saving and maintaining the user’s changes to the jokes within the app.

Open the starter project for this lesson. It’s the same as the final version you reached in lesson one. Open JoyJotterVM. Then, add the writeDataOf method at the end of this file’s methods:

func writeDataOf(jokes: [Joke]) {
  do {
    // 1
    let fileURL = try FileManager.default
      .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
      .appendingPathComponent("savedJokes.json")
    print("Document Directory Path:", fileURL.path)

    // 2
    if FileManager.default.fileExists(atPath: fileURL.path) {
      try FileManager.default.removeItem(at: fileURL)
    }

    // 3
    try JSONEncoder().encode(jokes).write(to: fileURL)
    print("Data written successfully.")
  } catch {
    print("Error writing Jokes: \(error.localizedDescription)")
  }
}

Here’s how this code saved the jokes array to the Document directory:

  1. In the first part, you create the file path where you’ll save the jokes file. You get the Document directory for the app, then append savedJokes.json to it to create a unique path to your file.
  2. You check if the file already exists at the specified file URL. If you find the file, remove it. This step ensures that any existing file with the same name is deleted before writing the new data.
  3. Finally, you encode the jokes, then write them to the file URL. This step completes the process of saving the encoded joke data to the specified file in the Document directory. The method prints some messages to the console to debug the above steps and check that all steps have been completed successfully.

The next consideration is determining the appropriate timing for writing jokes using this method. In this app, you’ll use the second approach that you learned in the previous section. You’ll save the jokes when the app is about to move to the background. Now, add the observation for the willResignActiveNotification notification at the end of the JoyJotterVM initializer:

let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(
  self,
  selector: #selector(appMovedToBackground),
  name: UIApplication.willResignActiveNotification,
  object: nil
)

This code adds an observer to detect when the app will move to the background and call the appMovedToBackground method. Next, add the implementation for the appMovedToBackground method:

@objc func appMovedToBackground() {
  print("App moved to background")
  writeDataOf(jokes: jokes)
}

Now, each time the app is about to move to the background, it’ll write the jokes to the specified directory in the app sandbox. Build and run the app. Make any changes you want to the jokes, then press the home button in the simulator. This causes the app to go to the background and fire the method to save the jokes into the Document directory. Notice the console messages indicating that the app moved to the background before typing the document’s path, at the end, verifying that the data has been written successfully.

Now, take a moment to check the Document directory for your simulator. You can find it by navigating to the Document directory path as you learned in the previous section:

cd ~/Library/Developer/CoreSimulator/Devices/{YOUR_SIMULATOR_UDID}/data/Containers/Data/Application/{APP_CONTAINER_UDID}/Documents

Inspecting this directory, you’ll spot the savedJokes.json file, snug in its cozy JSON format, ready to delight your app’s users!

Hooray, you’ve successfully saved those jokes in your app’s Document directory! Now, stop the app and relaunch it with a sprinkle of optimism, and — oh dear, the jokes are still stuck in their default outfits! Wait, what did you miss? Well, you missed a crucial step. You need to read those jokes every time you kickstart your app. It’s like waking up your jokes from their JSON slumber. You’ll do that now.

Reading Data

Open JoyJotterVM. Then, add the readDataOfJokes method to read the jokes from the Document directory.

static func readDataOfJokes() -> [Joke]? {
  do {
    // 1
    let fileURL = try FileManager.default
      .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
      .appendingPathComponent("savedJokes.json")

    // 2
    if FileManager.default.fileExists(atPath: fileURL.path) {
      let data = try Data(contentsOf: fileURL)
      let jokes = try JSONDecoder().decode([Joke].self, from: data)
      print("Data read successfully.")
      return jokes
    } else {
      print("No jokes found at path:", fileURL.path)
      return nil
    }
  } catch {
    print("Error reading Jokes: \(error.localizedDescription)")
    return nil
  }
}

Here’s how this code reads the jokes array from the Document directory:

  1. You create the file path where the jokes file should be located the same way that you did in the writeDataOf method.
  2. You attempt to read the data from the specified file URL. Then, you decode the data and return the saved jokes array.

Determining where to read the jokes is a straightforward decision. It’s necessary to update them with each new app launch to incorporate the latest changes from previous sessions. So, the optimal approach is to perform this task during the creation of JoyJotterVM.

Open AppMain. Then, replace the joyJotterVM property to try to read from the jokes you saved first:

@StateObject private var joyJotterVM = JoyJotterVM(jokes: JoyJotterVM.readDataOfJokes() ?? JoyJotterVM.basicJokes)

This property will try to read the jokes from the Document directory and inject it into the JoyJotterVM instance. If it fails, it’ll use the basic jokes instead. This ensures that the jokes are retained across app launches.

When you build and run the app now, notice the message in the console indicating that the data was read successfully. Great news — you’ve successfully saved and brought back the jokes every time the app starts!

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction Next: Conclusion