cover

macOS by Tutorials

by Sarah Reichelt

Edition 2.0.1, August 2024
Copyright © 2024 Sarah Reichelt.

Notice of Rights

All rights reserved. No part of this book or corresponding materials (such as text, images, or source code) may be reproduced or distributed by any means without prior written permission of the copyright owner.

Notice of Liability

This book and all corresponding materials (such as source code) are provided on an “as is” basis, without warranty of any kind, express of implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in action of contract, tort or otherwise, arising from, out of or in connection with the software or the use of other dealing in the software.

Trademarks

All trademarks and registered trademarks appearing in this book are the property of their own respective owners.

Attribution

The author originally created portions of this work for the benefit of Kodeco, a community of developers who love to share their knowledge with the world. www.kodeco.com

License

By purchasing macOS by Tutorials, you have the following license:

  • You are allowed to use and/or modify the source code in macOS by Tutorials in as many apps as you want, with no attribution required.

  • You are allowed to use and/or modify all art, images and designs that are included in macOS by Tutorials in as many apps as you want, but must include this attribution line somewhere inside your app: “Artwork/images/designs: from macOS by Tutorials.

  • The source code included in macOS by Tutorials is for your personal use only. You are NOT allowed to distribute or sell the source code in macOS by Tutorials without prior authorization.

  • This book is for your personal use only. You are NOT allowed to sell this book without prior authorization, or distribute it to friends, coworkers or students; they would need to purchase their own copies.

All materials provided with this book are provided on an “as is” basis, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software.

All trademarks and registered trademarks appearing in this guide are the properties of their respective owners.

Preface

Welcome to the second edition of macOS by Tutorials.

The first edition of this book was published in April 2022 and a lot has changed in the Swift and SwiftUI world since then. macOS itself has also seen a lot of changes, with macOS Ventura and Sonoma, and there have been lots of improvements to Xcode.

In 2023, Kodeco (formerly raywenderlich.com) changed their approach from being similar to a library to becoming more like a school or college. Sadly, this meant that they were no longer going to publish books like this one. However, since it is now out-of-catalog, they have allowed me to take over the book and publish in my own name.

I’d like to acknowledge their assistance and pay tribute to the editors who worked with me on the original edition: Richard Critz, Audrey Tam and Ehab Amer. Special mention to Manda Frederick who was the book manager for the first edition and without whom, this book would never have been written. Also a big thank you to Matt Derrick, CEO of Kodeco, who agreed to and arranged the transfer of this book to me.

Every project in this book has been updated to macOS Sonoma and Xcode 15.

The major changes in this edition include:

  • The On This Day app uses NavigationSplitView instead of the older NavigationView.

  • On This Day implements the new Observation framework for its data.

  • The Time-ato app no longer imports the external LaunchAtLogin package but now uses Apple’s ServiceManagement library.

  • Creating an app intent in ImageSipper uses the new code-based system.

  • The workflow for distributing your app has been updated to reflect the changes in App Store Connect and Xcode.

I hope you enjoy this updated edition of macOS by Tutorials.

About the Author

I got hooked onto trying to make computers do what I told them a very long time ago and have never stopped loving it. I’m a keen evangelist for developing native Mac apps. When not at my computer, I love coffee, puzzles, reading and cooking — the day hasn’t started until the first cup of coffee is drunk and the crossword is done! — Sarah

Sarah

About the Language Engineer (Editor)

I’m a fan of words and the things you can do with them, and I’ve spent a decade floating between different forms of writing — academic, fiction, technical. I hope that in this book I’ve succeeded at easing the passage of ideas between Sarah’s brain and your own, and I ask only that you blame me for any typos, stray capitals, Australianisms, and unoxfordenised commas that have made it into the final manuscript. — Peter

Peter

Dedication

To Tim who is endlessly supportive, even when listening to me complain about all my bugs.

Introduction

What do I love about programming for the Mac?

I use three Apple devices every day. My iPhone is primarily for communication; my iPad is mostly for entertainment. But the device where I spend most of my time is my Mac. The Mac is the most powerful, flexible and unrestricted device Apple makes, and I love using it.

When writing Mac apps, I get to do so many things that iOS apps cannot do, are not allowed to do, or are not suited to. I can make beautiful, intricate, powerful apps that I use every day.

I use and write iOS apps, too. There is definitely a place for both, but I feel sad that so many developers don’t even consider the enormous possibilities of the Mac app development world.

I’m so happy that you’ve decided to consider those possibilities and join me on this journey!

What You Need

To follow along with this book, you’ll need the following:

  • A Mac computer with an Intel or M series processor, running macOS 14 or later. Any Mac that you’ve bought in the last few years will do, even a Mac mini or MacBook Air.

  • Xcode 15 or later. Xcode is the main development environment for building macOS Apps. It includes the Swift compiler, the debugger and other development tools you’ll need. You can download the latest version of Xcode for free from the Mac App Store.

  • A basic understanding of Swift and SwiftUI. If you’re new to Swift, you might want to read my macOS Apprentice book first.

Where to download the materials for this book

The source code for this book can be cloned or downloaded from the GitHub repository:

Click the link to open it in your browser. You’ll see a big green Code button. Click on that and on Download ZIP. This puts all the code files in your Downloads folder.

If you prefer a direct download without source control, use this link: https://github.com/trozware/mos_book_code/releases/latest and download Source code (zip).

Depending on your browser settings, you may need to double-click the ZIP file to expand it. That gives you a folder containing one sub-folder for each chapter. The text of each chapter tells you when you need to use any of these files or folders, but every chapter that involves coding has a final folder showing the end result of working through that chapter.

How to read this book

The chapters in each section are designed to take you from start to finish building a particular kind of app. While the book is fun from the first page to the last, if one section especially piques your interest, you’re free to dive right in there.

When you get to the code blocks, if the code is unfamiliar to you, then I strongly recommend that you type it in for yourself, rather than copy and paste it. Xcode’s autosuggestions will help you type, and having to get every detail correct will embed the new knowledge in your brain in a way that reading it can’t.

If you are using copy and paste for any of the code blocks, then go to Xcode’s Settings and in Text Editing → Indentation, turn on Re-Indent on paste. This will solve a lot of issues with the format of the text in the book differing from Xcode’s format.

This book is available as HTML, PDF or ePub. I recommend reading this book from the HTML version. It has good light and dark variations and the most accurate formatting. Also, the code blocks have a COPY button for when you want it, and there are no issues with pagination. My next favorite is PDF — use the light or dark version to suit your preference. My least favorite option is the ePub, as its display depends greatly on your ePub reader. Apple’s Books app makes copying any code sections particularly problematic as it adds copyright information to the clipboard every time.

If you find any errors or typos, please report them to me at [email protected]. For problems with the code, please open an issue at https://github.com/trozware/mos_book_code/issues.

This book is split into five sections:

Section I: Your First App: On This Day

Begin your journey developing for macOS by building a full-featured app using SwiftUI. The app, On This Day, accesses a public network API to collect information about events, births and deaths for a given date. Along the way, you’ll learn how to manage multiple windows, add menu and toolbar commands, and choose multiple display options. You’ll experience first-hand the power of SwiftUI and see just how easy it is to build an app that has all of the look and feel you expect in a macOS app.

Section II: Building a Menu Bar App

In this section, you’ll use AppKit to build a Pomodoro-style time tracking app that lives only in the macOS menu bar. Along the way, you’ll learn how to manage timers, update the menu in real-time, and integrate a SwiftUI view into an AppKit app. You’ll also learn about how macOS “sandboxes” apps to protect both them and the system itself.

Section III: Building a Document-based App

In this section, you’ll return to using SwiftUI and explore how to build a document-based app. You’ll create a Markdown editor — there can never be enough Markdown editors in the world! — that allows you to preview your text in real time. Along the way, you’ll add menu commands to change the styling of the preview and add formatting to your Markdown text.

Section IV: Advanced Wizardry

Because macOS has its roots in Unix, it provides a vast array of command line tools which allow power users to perform tasks ranging from system management to image manipulation. In this section, you’ll learn how to build a graphical front-end for one such command: sips. Once you’ve built your sips GUI, you’ll enable automation to allow your new command to appear in the Services menu and Shortcuts app. When you complete this section, you too will be a wizard!

Section V: Distributing Your macOS Apps

Once you’ve written your app, you’ll want to distribute it to others so they can benefit from your creativity. On macOS, you have more distribution options than you do on iOS. In this section, you’ll explore the pros and cons of those options so you can choose which is best for you.

Section I: Your First App: On This Day

Begin your journey developing for macOS by building a full-featured app using SwiftUI. The app, On This Day, accesses a public network API to collect information about events, births and deaths for a given date. Along the way, you’ll learn how to manage multiple windows, add menu and toolbar commands and choose multiple display options. You’ll experience first-hand the power of SwiftUI and see just how easy it is to build an app that has all of the look and feel you expect in a macOS app.

If you haven’t already downloaded it, you can clone or download the source code and assets from the GitHub repository: https://github.com/trozware/mos_book_code

Chapter 1: Designing the Data Model

In this section, you’ll build a SwiftUI app called On This Day which pulls notable events for a day from an API and displays them in various ways. The app uses an interface style that appears in many macOS apps, with a navigation sidebar, details area, toolbar, menus, and settings window. You’ll end up with an app that looks like this:

The finished app
The finished app

When starting a new app, it’s tempting to jump right into the interface design, but this time you’ll start by working out the data models. There are two reasons for this. First, in a SwiftUI app, the data drives the display, so it makes sense to work out the data structure before you start laying out the interface. Secondly, this data comes from an external source and may not have the structure you’d like. Spending some time now to analyze and parse will save you a lot of time and effort later on.

In this chapter, you’ll use a playground to fetch the data, analyze the structure, and create the data models for the app.

Data model design is a vital first step in the development of any app, so working through this chapter will be a valuable experience. But, if you’re already familiar with downloading data, parsing JSON, and creating data structs and classes, feel free to skip ahead. In Chapter 2: Working With Windows, you’ll download and import the data model files used in this section and start building the user interface.

Where is the Data Coming From?

You’ll use the API from ZenQuotes.io. Go to today.zenquotes.io in your browser and look around the page. At the top, you’ll see an interesting historical event that happened on this day of the year, and you can scroll down to see more:

today.zenquotes.io
today.zenquotes.io

Keep scrolling until you get to the Quick Start heading. Underneath that, the Fetch Historical Events subsection gives you the format for the URL to access the data. Scroll all the way to the bottom and check the Usage Limits too. When testing, it’s easy to hit this limit, so one of your first tasks is to download a sample set of data to work with.

Under the Quick Start heading, follow the Read the full documentation link to get more information on the structure of the JSON returned by the API call. You’ll get to explore that in detail over the rest of this chapter.

Saving a Sample Data Set

If you haven’t already downloaded the supporting materials, follow the instructions in the Where to download the materials for this book section of the preface.

Open the playground from the starter folder for this chapter. It’s a macOS playground set up with some functions to get you going. The most important one is getDataForDay(month:day:), which takes in a numeric month and day, assembles them into a URLRequest and then uses URLSession to download the JSON from that URL.

If the data returned can be converted into a String, the function will save it to a file. But where should you save it? Unlike iOS, macOS gives you full access to the file system. The app sandbox may restrict this, as you’ll learn in later chapters, but in a playground, you can access everything. Since this is data you’re downloading, saving it to the Downloads folder makes the most sense, so now you need to work out the file path for the Downloads folder.

Working with the File System

Your first thought might be to build up the file path as a string. Maybe ~/Downloads would work. But remember not everyone uses English as their system language. My Downloads folder is at /Users/sarah/Downloads, but if I switch my system language to French, it’s at Utilisateurs/sarah/Téléchargements. You can’t assume there’ll be a folder called Downloads.

The Sources section of the playground contains Files.swift, which holds functions for saving and reading the sample data. Expand the Sources section, if it’s not already expanded, and open Files.swift.

Note
If you can’t see the files list, press Command-1 to open the Project navigator.

The first entry is a computed property called sampleFileURL which returns a URL. At the moment it returns this:

URL.downloadsDirectory.appending(path: "SampleData.json")
  • There’s only one line of code, so I’ve omitted the return keyword as it is implied. The property and functions in this file are public so that the other parts of the playground can access them.

  • The URL struct has a number of properties that you can use to access various system folders. These are properties of the URL struct itself, and not any particular URL.

  • downloadsDirectory is one of the standard system folders that you can locate, and this avoids any translation problems.

Once you have the URL for the user’s Downloads folder, append the sample data file name to create the final URL.

Getting the Data

Since URLSession uses await, getDataForDay(month:day:) is an async function. As a result, you must call it asynchronously, so its usage is wrapped in a Task. Click the Play button in the gutter beside the last line of the playground and wait while it goes off to the API server, gathers the data and returns it.

Note
If you don’t see a play button in the gutter, your playground is set to run automatically. Click and hold the play or stop button at the bottom of the code, and then choose Manually Run.

Once the download finishes, you’ll see a message in the console saying the playground has saved the sample data to your Downloads folder:

Saving sample data
Saving sample data
Note
If you don’t see the console, press Shift-Command-Y to open it at the bottom of the window.

Go to your Downloads folder and open SampleData.json. On my computer, it opens in Xcode, but you may have a different app set up to open JSON files:

Downloaded JSON
Downloaded JSON

Formatting the JSON

The app you used may have formatted the JSON into a more readable form, but as you can see, Xcode has not. Here’s a trick that makes formatting JSON a breeze on any Mac.

Select all the text in the JSON file and copy it. Open Terminal and type in the following line (don’t copy and paste it or you’ll overwrite the JSON that’s in the clipboard):

pbpaste | json_pp | pbcopy

Press Return.

This sequence of three shell commands pastes the clipboard contents to the json_pp command, which "pretty prints" it, then uses pbcopy to copy the neatly formatted JSON back into the clipboard. Under the hood, macOS calls the clipboard the pasteboard which is why it uses pbpaste and pbcopy.

The vertical bar is a pipe symbol. The output from pbpaste is piped json_pp and the output from that is piped to pbcopy.

Return to your original SampleData.json file, delete its contents, and press Command-V to paste in the pretty printed JSON. Save the file again. It should now look something like this:

Pretty printed JSON
Pretty printed JSON

If you make a mistake and lose the sample data, run the playground again to get it back.

Note
To know more about json_pp, go back to your Terminal, right-click the command, and choose Open man Page to open the built-in help page in a new window. You could also type man json_pp, but this shows the information in your working Terminal window. Using a new window makes it easier to read and test the command.

Using the Sample Data

Now that you have the data saved and formatted, you can start using it instead of calling the API server every time. This is faster and avoids hitting the usage limit.

To begin, comment out the entire Task section but don’t delete it in case you need to re-fetch the data later. Next, add this code to access the saved data instead:

if let data = readSampleData() {
  print(data.count)
}

Finally, run the playground again and you’ll see a number showing the amount of data in the sample file:

Reading sample data
Reading sample data

I deliberately chose February 29 for the sample day to minimize the amount of data. Presumably only a quarter of the usual number of interesting events happened on February 29th. :-) You may get a different number as the site adds and deletes events, but any number over zero shows that you have read some data from the sample file.

Exploring the JSON

To make it easier to examine the structure of the JSON data returned, turn on code folding. If you’re using Xcode, go to the menu bar and choose Xcode → Settings…​ → Text Editing → Display, then select the checkbox labeled Code folding ribbon:

Code folding
Code folding

Now you’ll be able to click in the ribbon beside the line numbers to collapse and expand the data nodes. By collapsing nearly all the nodes, you can see that the root structure of the layout contains four elements. data and date are the ones you need here. No way of confusing those two! :-) You can ignore the info and updated elements:

Collapsed JSON
Collapsed JSON

Inside data, there are three nodes — one for each of the three different types of event: Births, Deaths, and Events. The data inside each of them has the same structure, so after expanding Births to show the first one, you’ll see this:

JSON event data
JSON event data

The three top level elements are html, links and text. If this was for display in a web page, html would be important, but for an app, text is much more useful. Notice how it includes HTML entities and starts with the year.

The links section is oddly structured with the keys being numbers inside strings. Each link has three elements with "0" being the full HTML link, "1" containing the URL, and "2" holding the text for the link.

Decoding the Top Level

Now you’ve explored the JSON and know what you’re getting from the API server, it’s time to start decoding it. The overall data model for this JSON will be a struct called Day since it contains the information for a specific day. Day will have data and date properties. date is a string, so start with that.

First, add this code to the playground:

struct Day: Decodable {
  let date: String
}

This establishes Day as a struct that conforms to the Decodable protocol. Since this data will never be re-encoded, there’s no need to conform to Codable which is a type alias for Decodable & Encodable.

To test this, replace print(data.count) with this:

do {
  let day = try JSONDecoder().decode(Day.self, from: data)
  print(day.date)
} catch {
  print(error)
}

Then run the playground again and you’ll see "February_29" printed in the console.

Note
If you ever get an error when running the playground saying that some type cannot be found in scope, this is because you’re running code that comes before the type declaration in the playground. Use the Execute Playground button in the divider between the code and the console instead. You may need to click the Stop Playground button first and then the Execute Playground button.

Going Deeper

Decoding the data element isn’t so straightforward as there are different types of data inside. It’s time to think about the lower level data models.

You can decode each entry in the "Births", "Deaths", and "Events" elements into an Event data model. Event needs two properties: text and links — you can ignore html. To set this up, add a new struct to the playground:

struct Event: Decodable {
  let text: String
  let links: [String: [String: String]]
}

For now, links is an ugly dictionary containing more dictionaries, but this is enough to get it decoding.

Next, insert the new data property into Day:

let data: [String: [Event]]

And lastly, add a second debug print after print(day.date):

print(day.data["Births"]?.count ?? 0)

Run the playground again and you’ll see the date and a number showing how many notable births fell on that day:

Decoding Day & Event
Decoding Day & Event

The final piece in the puzzle is the links, so create a new struct called EventLink to handle them:

struct EventLink: Decodable {
  let title: String
  let url: URL
}

This is the important data for each link, but the incoming JSON isn’t structured like this. To process the data as it comes in, Event needs to do some more work.

Right now, your Event struct stores its links in a dictionary, which decodes them but doesn’t make the links easy for the app to use. By adding a custom init(from:) to Event, you can process the incoming JSON into a more usable format.

Replace Event with this version:

struct Event: Decodable {
  let text: String
  // 1
  let links: [EventLink]
  // 2
  enum CodingKeys: String, CodingKey {
    case text
    case links
  }
  // 3
  init(from decoder: Decoder) throws {
    // 4
    let values = try decoder.container(keyedBy: CodingKeys.self)
    // 5
    text = try values.decode(String.self, forKey: .text)
    // 6
    let allLinks = try values.decode(
      [String: [String: String]].self,
      forKey: .links
    )
    // 7
    var processedLinks: [EventLink] = []
    for (_, link) in allLinks {
      if
        let title = link["2"],
        let address = link["1"],
        let url = URL(string: address) {
        processedLinks.append(EventLink(title: title, url: url))
      }
    }
    // 8
    links = processedLinks
  }
}

It was so clean and simple a moment ago and now look at it! Let’s step through this code and see what I did:

  1. Change links into an array of EventLink objects.

  2. Tell the decoder what keys to use. This is essential when decoding the JSON manually.

  3. Add a custom init(from:) for decoding.

  4. Use CodingKeys to get the data values from the decoder’s container using the specified keys.

  5. Decode the text element from values. This doesn’t need any further processing before assigning it to the text property.

  6. Decode the links element as a dictionary.

  7. Loop through the values in the dictionary and try to create an EventLink object from each one.

  8. Assign the valid entries to links.

To test that this is decoding the data correctly, add a third debug print statement under the other two. It force-unwraps the array of Births, which is a bad idea in production but is fine in a testing playground:

print(day.data["Births"]![0].links)

Now run the playground. This time, it’ll take a while to finish. As Event.init(from:) loops, you’ll be able to see the results on the right flickering. Playgrounds aren’t great at doing multiple loops, but this is very fast inside an app.

The link output isn’t wonderfully readable, but you can see they’re all there, each with a title and a URL:

Decoding EventLink
Decoding EventLink

Making Day Easier to Use

Now that you’re decoding the JSON and have created the basic data structure, it’s time to consider how the app will use this data and what you can add to make this easier.

Looking at Day first, it would be convenient to have a more direct way of accessing the various categories of events instead of using an optional like day.data["Births"] each time.

There are three types of events, so to avoid using magic strings as dictionary keys, start by adding this enum which describes them:

enum EventType: String {
  case events = "Events"
  case births = "Births"
  case deaths = "Deaths"
}

Conventionally, the cases in an enum start with a lowercase letter, but the raw string values are the title case strings that appear in the JSON, so they’ll work as keys to the data dictionary.

With the enum in place, add these computed properties to Day:

var events: [Event] { data[EventType.events.rawValue] ?? [] }
var births: [Event] { data[EventType.births.rawValue] ?? [] }
var deaths: [Event] { data[EventType.deaths.rawValue] ?? [] }

These properties use the raw value to return an array of the relevant events or an empty array.

Now you can change the inelegant debug print statements so they use no optionals and no force unwrapping:

print(day.births.count)
print(day.births[0].links)

The second feature that would be useful in Day is a nicer way to show the date. Right now, there’s an underscore between the month and the day. You could use a custom init(from:) to change the way you decode it, but you’re going to use another computed property instead. Add this to Day:

var displayDate: String {
  date.replacingOccurrences(of: "_", with: " ")
}

To test this, change the first of the debug print statements to:

print(day.displayDate)

Run the playground again to see updated date string:

Formatting the date
Formatting the date

Not much different to see here except for the formatted date, but you accessed the information much more easily.

Identifying Data Objects

Take a moment to think about how your app might display the information in Day. displayDate is a String and all ready for use. Then you have the arrays containing Events and EventLinks, which the views in your app will loop through in some manner. When looping through arrays of data in SwiftUI, it’s important that each element has a unique identifier. This allows the SwiftUI engine to track which elements have changed, moved or disappeared, so it can update the display as efficiently as possible.

The best way to do this is to make the model structs conform to Identifiable. This protocol requires the conforming type to contain a property called id, which can be anything, but is usually a string, a number or a unique ID. Some data might arrive with IDs already. In this case there’s nothing obviously unique, so you’ll add a UUID to each Event and EventLink.

Starting with EventLink, edit the struct declaration to include Identifiable and add an id property:

struct EventLink: Decodable, Identifiable {
  let id: UUID
  let title: String
  let url: URL
}

Run the playground again and you’ll see this error in the console:

Missing argument error
Missing argument error

This is in the Event struct’s init(from:) method where it creates each EventLink. Replace the processedLinks.append(EventLink(title: title, url: url)) line with:

processedLinks.append(
  EventLink(id: UUID(), title: title, url: url)
)

For Event, you want to do something similar - conform to Identifiable and add an id property. In this case, the declaration initializes the UUID itself. Replace struct Event: Decodable { with:

struct Event: Decodable, Identifiable {
  let id = UUID()

If you had used this technique for EventLink, you’d have seen a warning about an immutable property which won’t be decoded. This isn’t a problem with Event, because you’ve set up the CodingKeys, which tell the decoder which properties to use and which to ignore.

Tidying up the Event Text

Now that you’re prepared for looping through the events and links, it’s time to look at the text for the events. In your debugging print statements, replace the line printing the links with this one and run the playground again:

print(day.births[0].text)

In the console, you’ll see "1468 – Pope Paul III (d. 1549)" or something similar. You can see the text string starts with the year and then uses the HTML entity for an en dash to separate this from the information. For display purposes, it seems like it’d be useful to separate these two parts into distinct properties.

First, add a year property to Event. You may be tempted to convert the year into an Int, but remember that some events happened a long time ago and may include "BC" or "BCE", so the years need to remain as strings:

let year: String

Replace the line in init(from:) that sets text with this:

// 1
let rawText = try values.decode(String.self, forKey: .text)
// 2
let textParts = rawText.components(separatedBy: " – ")
// 3
if textParts.count == 2 {
  year = textParts[0]
  // 4
  text = textParts[1].decoded
} else {
  year = "?"
  // 4
  text = rawText.decoded
}

Let’s step through this code:

  1. Decode the text element from values exactly as before, but this time, assign it to a temporary constant.

  2. Split rawText using the HTML entity with a space on either side.

  3. If the split resulted in two parts, assign the first to year and the second to text. If the text didn’t contain the entity or contained it more than once, set year to a question mark and text to the complete value from the decoder.

  4. Decode any HTML entities in the text using the String extension from the start of the playground.

Time to add yet another debug print statement:

print(day.births[0].year)

Run the playground again and you’ll see something like this:

Splitting the text
Splitting the text

Bringing it All Together

So far, you’ve created a series of data structs: Day, Event and EventLink. Now, it’s time to pull them all together into a class, which is the primary data model in your app.

Add this definition to the playground:

// 1
class AppState {
  // 2
 var days: [String: Day] = [:]
  // 3
  func getDataFor(month: Int, day: Int) ->  Day? {
    let monthName = Calendar.current.monthSymbols[month - 1]
    let dateString = "\(monthName) \(day)"
    return days[dateString]
  }
}
  1. Create a class for this data object. When you use it in the app, you’ll set it up as @Observable so your SwiftUI views can watch it and respond to any changes. At the moment, playgrounds don’t support the Observation framework, so that’s something you’ll add later.

  2. Define a dictionary of Day data objects, indexed on their date. When this class becomes @Observable, any SwiftUI views observing this object get notified whenever this property changes.

  3. Add a convenience method for returning a Day for the supplied month number and day number, if it’s available.

To test this, go to the top of the playground and add these lines immediately after the import line:

let appState = AppState()
let monthNum = 2
let dayNum = 29

func testData() {
  if let day = appState.getDataFor(
    month: monthNum, day: dayNum
  ) {
    print(day.displayDate)
    print("\(day.deaths.count) deaths")
  } else {
    print("No data available for that month & day.")
  }
}

This creates an AppState object, sets the month and day and then adds a function for testing the result. These definitions need to be at the top because playgrounds run from top to bottom and these have to be set up before anything tries to use them.

Scroll back down to where you read the sample data file and printed some debugging information. Replace all of the print statements with the following:

appState.days[day.displayDate] = day
testData()

Run the playground and you’ll see a result like this in the console:

Testing AppState
Testing AppState

Testing with Live Data

As a final check, how about re-enabling the actual download and making sure your code can process live data correctly? Right now, the download saves the data to a text file, so you need to change the download function to make it decode this data into a Day and return it.

First, find getDataForDay(month:day:) and replace its signature line with this one which sets it to return a Day:

func getDataForDay(month: Int, day: Int) async throws ->  Day {

Next, below where you save the data to a file, add this chunk, which attempts to decode the downloaded data into a Day and throws an error if it can’t:

do {
  let day = try JSONDecoder().decode(Day.self, from: data)
  return day
} catch {
  throw FetchError.badJSON
}

Finally, comment out the entire code block that begins if let data = readSampleData() { and add the following after it:

Task {
  do {
    let day = try await getDataForDay(
      month: monthNum, day: dayNum
    )
    appState.days[day.displayDate] = day
    testData()
  } catch {
    print(error)
  }
}
Tip
Click the code folding ribbon to the left of if let data = readSampleData() { to collapse the block into a single line. Triple-click the collapsed line to select the entire block, then press Command-Slash to comment it out.

This is similar to the Task you used to get the sample data, but this version waits for the decoded Day to come back, adds it to appState's days, and calls the test function.

If there’s a download error or a decoding error, the catch block prints the error.

Click the Execute Playground button again. You’ll see a message reporting the saved file path, and then you’ll see the debug report:

Testing live data
Testing live data

For fun, try changing monthNum and dayNum at the top of the playground and running it again to fetch some different data. Add more print statements to testData() if you want to see what you’ve got.

Key Points

  • Designing your data model is an important step in app building.

  • Playgrounds make iterating through the data design much easier than doing it in an app where you have to build and run after each change.

  • When getting data from an external source, you have no control over the format, but it’s still possible to process the data to suit your app.

  • Computed properties are useful for making specific data easily accessible.

  • If you were building an iOS app, you could have gone through a similar process with similar code.

  • Starting with the data is a solid way to start developing any app, especially when the data comes from an external source.

Where to Go From Here?

This chapter may have seemed like hard work when you wanted to get started building a real macOS app, but in the next chapter, you’ll see how this preliminary work means that the app can start taking shape quickly.

Where To Buy?

This is the end of the sample chapter. To purchase the complete book, go to Gumroad.