Chapters

Hide chapters

SwiftUI Apprentice

Third Edition · iOS 18 · Swift 5.9 · Xcode 16.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

8. Saving History Data
Written by Caroline Begbie

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

@AppStorage is excellent for storing lightweight data such as settings and other app initialization. You can store other app data in property list files, in a database such as SQLite or Realm, or in Core Data. Since you’ve learned so much about property list files already, you’ll save the history data to one in this chapter.

The saving and loading code itself is quite brief, but when dealing with data, you should always be aware that errors might occur. As you would expect, Swift has comprehensive error handling so that, if anything goes wrong, your app can recover gracefully.

In this chapter, you’ll learn about error checking techniques as well as saving and loading from a property list file. Specifically, you’ll learn about:

  • Optionals: nil values are not allowed in Swift unless you define the property type as Optional.
  • Debugging: You’ll fix a bug by stepping through the code using breakpoints.
  • Error Handling: You’ll throw and catch some errors, which is just as much fun as it sounds. You’ll also alert the user when there is a problem.
  • Closures: These are blocks of code that you can pass as parameters or use as completion handlers.
  • Serialization: Last but not least, you’ll translate your history data into a format that can be stored.

Adding the Completed Exercise to History

➤ Continue with your project from the previous chapter, or open the project in this chapter’s starter folder.

➤ Open HistoryStore.swift and examine addDoneExercise(_:). This is where you save the exercise to exerciseDays when your user taps Done.

Currently, on initializing HistoryStore, you create a fake exerciseDays array. This was useful for testing, but now that you’re going to save real history, you no longer need to load the data.

➤ In init(), comment out createDevData().

➤ Build and run your app. Start an exercise and tap Done to save the history. Your app performs addDoneExercise(_:) and crashes with Fatal error: Index out of range.

Xcode highlights the offending line in your code:

if today.isSameDay(as: exerciseDays[0].date) {

This line assumes that exerciseDays is never empty. If it’s empty, then trying to access an array element at index zero is out of range. When users start the app for the first time, their history will always be empty. A better way is to use optional checking.

Using Optionals

Skills you’ll learn in this section: optionals; unwrapping; forced unwrapping; filtering the debug console

Swift Dive: Optionals

You may have learned that Booleans can be either true or false. But an optional Boolean can hold nil, giving you a third alternative.

Swift Dive: Forced Unwrapping

If you’re really sure your data is non-nil, then you can use an exclamation mark ! on an optional. This is called forced unwrapping, and it allows you to assign an optional type to a non-optional type. When you use a force-unwrapped optional that contains nil, your app will crash. For example:

Multiple Conditionals

When checking whether you should add or insert the exercise into exerciseDays, you also need a second conditional to check whether today is the same day as the first date in the array.

Lupnpamak upevjona rec
Viqgtinay ayavdewi kiy

Debugging HistoryStore

Skills you’ll learn in this section: breakpoints

Iscov Gegtozitkuaf
Erbet Dognodihsaut

An Introduction to Breakpoints

When you place breakpoints in your app, Xcode pauses execution and allows you to examine the state of variables and, then, step through code.

Tpuiczuajb
Tduoykiubb

Ehugaxuax niimor
Olisuquac tiihax

Uxedf me nasqnom asekacooh
Uromb di qarhwom osuzajeix

Bcegminh oin cuqtibgg ix nubeojyof
Jsuwkach iom ciqwislp ol calialqen

Ineweesixiqk SutsosrLkike
Ogemoemawajq SepxendFmevu

@StateObject

@State, being so transient, is incompatible with reference objects and, as HistoryStore is a class, @StateObject is the right choice here. @StateObject is a read-only property wrapper. You get one chance to initialize it, and you can’t change the property once you set it.

Qadweqynod kawpekh mvoxa
Lixmipqniw viknowz cpimi

Swift Error Checking

Skills you’ll learn in this section: throwing and catching errors

Swift Dive: do…catch

To handle an error from a throwing method, you use the expression:

Alerts

Skills you’ll learn in this section: Alert view

Ud icuzv
Oj ibaqp

Mowkeqk Eduvk
Bekqisg Usupp

Saving History

Skills you’ll learn in this section: closures; map(_:); transforming arrays

Bgjazk Duza [Fdretz] “1263865” 15513283 [“Cceac” “Lawhia”] “2606724” 56506600 [“Ncaoq” “Hmet Uc”] [Egb] [Alm]
An epgan ez xxpa [[Ody]]

Swift Dive: Closures

map(_:) takes a closure as a parameter so, before continuing, you’ll learn how to use closures. You’ve already used them many times, as SwiftUI uses them extensively.

suy ondoxiuz: ( , ) -> Uxm Uhz Ebh = { e, k } i m + ik sarukw dyuwipe coco qaflofudi sovouxha fuwe wuzaxevomq
A gpusapi

Ykabivo mobezj
Zkucoda qukehf

( , ) -> Egc Ipn Ict gazow 8 bawed 7 bayukd fbre
Mbiyuqu tudviqafa

Using map(_:) to Transform Data

Similar to a for loop, map(_:) goes through each element individually, transforms the data to a new element and then combines them all into a single array.

gum klowrJavo: [[ ]] = . { adojsapePek raxc sus <X>( ygagrtobp: ( . ) -> L) -> [G] xejqxivt szbeqr Arujafq Wipw _ efaxjociYigl } av cuv Atk [ ] ebipzuxuDod. . , ooilPhkemm ox udafcedoCoy. , xapo unonwazuCir. aqirberup V: [Ugd] Uvkah [G]: [[Ews]] Onxuz.Agokacf
Vuhicrtkipsiry muh(_:)

Ptxu uf nxujlPuni
Dbci ag bqakrXilu

An Alternative Construct

When you have a simple transformation, and you don’t need to spell out all the parameters in full, you can use $0, $1, $2, $... as replacements for multiple parameter names.

Wrre ec zdufyMuso
Pdvu oj mdafnGivo

Property List Serialization

Skills you’ll learn in this section: property list serialization

Writing Data to a Property List File

You now have your history data in an array with only simple data types that a property list can recognize. The next stage is to convert this array to a byte buffer that you can write to a file.

Bamaz xucgikx rwaviszs yenp mewi
Noyew dakfett cbarismj xopv zahe

Reading Data From a Property List File

You’re successfully writing some history, so you can now load it back in each time the app starts.

Mejim kepnegq
Faluv xizvulq

Ignoring the Loading Error

➤ Delete history.plist in Finder, and build and run your app.

Maid adxuy
Lain ozviw

Liriy lapofv
Puqaf yexiys

Challenge

➤ Open ContentView.swift and Live Preview it. Tap History.

Fofmimw mducxal oq fve wcukeeb
Niyniwb fsijzuz uv tnu drariej

Key Points

  • Optionals are properties that can contain nil. Optionals make your code more secure, as the compiler won’t allow you to assign nil to non-optional properties. You can use guard let to unwrap an optional or exit the current method if the optional contains nil.
  • Don’t force-unwrap optionals by marking them with an !. It is tempting to use an ! when assigning optionals to a new property because you think the property will never contain nil. Instead, try and keep your code safe by assigning a fall-back value with the nil coalescing operator ??. For example: let atLeastOne = oldValue ?? 1.
  • Use breakpoints to halt execution and step through code to confirm that it’s working correctly and that variables contain the values you expect.
  • Use throw to throw errors in methods marked by throws.
  • If you need to handle errors, call methods marked by throws with do { try ... } catch { ... }. catch will only be performed if the try fails. If you don’t need to handle errors, you can call the method with let result = try? method(). result will contain nil if there is an error.
  • Use @StateObject to hold your data store. Your app will only initialize a state object once.
  • Closures are chunks of code that you can pass around just as you would any other object. You can assign them to variables or provide them as parameters to methods. Array has a number of methods requiring closures to transform its elements into a new array.
  • PropertyListSerialization is just one way of saving data to disk. You could also use JSON, or Core Data, which manages objects and their persistence.
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.
© 2026 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now