Chapters

Hide chapters

SwiftUI Apprentice

Second Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section I: Your First App: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your Second App: Cards

Section 2: 9 chapters
Show chapters Hide chapters

7. Saving Settings
Written by Caroline Begbie

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Whenever your app closes, all the data entered, such as any ratings you’ve set or any history you’ve recorded, is lost. For most apps to be useful, they have to persist data between app sessions. Data persistence is a fancy way of saying “saving data to permanent storage”.

In this chapter, you’ll explore how to store simple data using AppStorage and SceneStorage. You’ll save the exercise ratings and, if you get called away mid-exercise, your app will remember which exercise you were on and start there, instead of at the welcome screen.

You’ll also learn about how to store data in Swift dictionaries and realize that string manipulation is complicated.

Data Persistence

Depending on what type of data you’re saving, there are different ways of persisting your data:

  • UserDefaults: Use this for saving user preferences for an app. This would be a good way to save the ratings.
  • Property List file: A macOS and iOS settings file that stores serialized objects. Serialization means translating objects into a format that can be stored. This would be a good format to store the history data, and you’ll do just that in the following chapter.
  • JSON file: An open standard text file that stores serialized objects. You’ll use this format in Section 2.
  • Core Data: An object graph with a macOS and iOS framework to store objects. For further information, check out our book Core Data by Tutorials.

Saving Ratings to UserDefaults

Skills you’ll learn in this section: AppStorage; UserDefaults

AppStorage

@AppStorage is a property wrapper, similar to @State and @Binding, that allows interaction between UserDefaults and your SwiftUI views.

@AppStorage("rating") private var rating = 0
Rating the exercise
Qopovq jwu isajlake

Data Directories

Skills you’ll learn in this section: what’s in an app bundle; data directories; property list files; Dictionary

App sandbox and directories
Ucy toqxgas ojx goyelqaxioj

The App Bundle

Inside your app sandbox are two sets of directories. First, you’ll examine the app bundle and then the user data.

App in Finder
Ohh ac Valsit

App bundle contents
Oll xufjho jibzizxz

User Data Directories

The files and directories you’ll need to check most often are the ones that your app creates and updates during execution.

.onAppear {
  print(URL.documentsDirectory)
}
Documents directory path
Recuvewfr jozibyort taht

Show in Finder
Bweq eh Qiqcoz

Simulator directories
Baqerivir jiqirqavouz

Inside a Property List File

@AppStorage saved your rating to a UserDefaults property list file. A property list file is an XML file format that stores structured text. All property list files contain a Root of type Dictionary or Array, and this root contains a hierarchical list of keys with values.

Exercises in a property list file
Irozsanip of e gjafonjr jeyt voci

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
  <dict>
    <key>exerciseName</key>
    <string>Squat</string>
    <key>videoName</key>
    <string>squat</string>
  </dict>
  <dict>
    <key>exerciseName</key>
    <string>Sun Salute</string>
    <key>videoName</key>
    <string>sun-salute</string>
  </dict>
</array>
</plist>

Swift Dive: Dictionary

A Dictionary is a hash table which consists of a hashable key and a value. A hashable key is one that can be transformed into a numeric hash value, which allows fast look-up in a table using the key.

var ratings = ["burpee": 4]
var ratings = ["step-up": 2, "sun-salute": 3]
ratings["squat"] = 5 // ratings now contains three items
Dictionary contents
Kasciinajy qefbibbq

let rating = ratings["squat"]  // rating = 5

UserDefaults Property List File

➤ In Finder, open your app’s sandbox and locate Library/Preferences. In that directory, open com.kodeco.HIITFit.plist. This is the UserDefaults file where your ratings are stored. Your app automatically created this file when you first stored rating.

UserDefaults property list file
UbexZazuoqjb ljowengd wimn quje

Swift Dive: Strings

Skills you’ll learn in this section: Unicode; String indexing; nil coalescing operator; String character replacement

Unicode tag sequences
Uhekifu hup viwaucten

for character in "Hello World" {
  print(character) // console shows each character on a new line
}
let text = "Hello World"
let index = text.index(text.startIndex, offsetBy: 6)
let seventh = text[index]
// seventh = "W"

Saving Ratings

Now that you’re going to store ratings, RatingView is a better source of truth than ExerciseView. Instead of storing ratings in ExerciseView, you’ll pass the current exercise index to RatingView, which can then read and write the rating.

@AppStorage("rating") private var rating = 0
RatingView(exerciseIndex: index)
let exerciseIndex: Int
@AppStorage("ratings") private var ratings = "0000"
@State private var rating = 0
struct RatingView_Previews: PreviewProvider {
  @AppStorage("ratings") static var ratings: String?
  static var previews: some View {
    ratings = nil
    return RatingView(exerciseIndex: 0)
      .previewLayout(.sizeThatFits)
  }
}

Extracting the Rating From a String

➤ In body, add a new modifier to Image:

// 1
.onAppear {
  // 2
  let index = ratings.index(
    ratings.startIndex,
    offsetBy: exerciseIndex)
  // 3
  let character = ratings[index]
  // 4
  rating = character.wholeNumberValue ?? 0
}
Zero Rating
Tovo Fubobm

@AppStorage("ratings") private var ratings = "4000"
Rating of four
Covezm uf daeh

Storing Rating in a String

You’re now reading the ratings from AppStorage. To store the ratings back to AppStorage, you’ll index into the string and replace the character at that index.

func updateRating(index: Int) {
  rating = index
  let index = ratings.index(
    ratings.startIndex,
    offsetBy: exerciseIndex)
  ratings.replaceSubrange(index...index, with: String(rating))
}
updateRating(index: index)
Rating the exercise
Tohasm lbu odohxubo

AppStorage
ElhKzuyuha

Thinking of Possible Errors

Skills you’ll learn in this section: custom initializer

Custom Initializers

➤ Add a new initializer to RatingView:

// 1
init(exerciseIndex: Int) {
  self.exerciseIndex = exerciseIndex
  // 2
  let desiredLength = Exercise.exercises.count
  if ratings.count < desiredLength {
    // 3
    ratings = ratings.padding(
      toLength: desiredLength,
      withPad: "0",
      startingAt: 0)
  }
}
@AppStorage("ratings") private var ratings = ""
Zero padding
Zomu rasjalx

Multiple Scenes

Skills you’ll learn in this section: multiple iPad windows

Configure multiple windows
Farfawuhi dowrezpo xidtuyh

Multiple iPad Windows
Mejbufye iMol Peqtits

Making Ratings Reactive

➤ On each window, go to Exercise 1 Squat and change the rating. You’ll notice there’s a problem, as, although the rating is stored in UserDefaults using AppStorage, the windows reflect two different ratings. When you update the rating in one window, the rating should immediately react in the other window.

Non-reactive rating
Cil-geaqdeyi hozozc

Outdated rating
Iawdumic siyorh

let index = ratings.index(
  ratings.startIndex,
  offsetBy: exerciseIndex)
let character = ratings[index]
rating = character.wholeNumberValue ?? 0
.onChange(of: ratings) { _ in
  convertRating()
}

Apps, Scenes and Views

Skills you’ll learn in this section: scenes; @SceneStorage

@main
struct HIITFitApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
      ...
    }
  }
}
The App Hierarchy
Dde Ehd Yiokinhnd

Restoring Scene State With SceneStorage

Currently, when you exit and restart your app, you always start at the welcome screen. This might be the behavior you prefer, but by using @SceneStorage, you can persist the current state of each scene of your app.

What exercise was I doing?
Wguk ijeggese geb E huuml?

@SceneStorage("selectedTab") private var selectedTab = 9

Key Points

  • You have several choices of where to store data. You should use @AppStorage and @SceneStorage for lightweight data, and property lists, JSON or Core Data for main app data that increases over time.
  • Your app is sandboxed so that no other app can access its data. You are not able to access the data from any other app either. Your app executable is held in the read-only app bundle directory, with all your app’s assets.
  • Property lists store serialized objects. If you want to store custom types in a property list file, you must first convert them to a data type recognized by a property list file, such as String or Boolean or Data.
  • String manipulation can be quite complex, but Swift provides many supporting methods to extract part of a string or append a string on another string.
  • Manage scenes with @SceneStorage. Your app holds data per scene. iPads and macOS can have multiple scenes, but an app run on iPhone only has one.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now