macOS by Tutorials
by Sarah Reichelt
Edition 2.0, July 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
- macOS by Tutorials
- License
- Preface
- Introduction
- Section I: Your First App: On This Day
- Chapter 1: Designing the Data Model
- Where is the Data Coming From?
- Saving a Sample Data Set
- Using the Sample Data
- Exploring the JSON
- Decoding the Top Level
- Going Deeper
- Processing the Links
- Making Day Easier to Use
- Identifying Data Objects
- Tidying up the Event Text
- Bringing it All Together
- Testing with Live Data
- Key Points
- Where to Go From Here?
- Chapter 2: Working With Windows
- Chapter 3: Adding Menus & Toolbars
- Chapter 4: Using Tables & Custom Views
- Chapter 5: Settings & Icons
- Chapter 6: Why Write a macOS App?
- Chapter 1: Designing the Data Model
- Section II: Building a Menu Bar App
- Section III: Building a Document-based App
- Section IV: Advanced Wizardry
- Section V: Distributing Your macOS Apps
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 olderNavigationView
. -
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’sServiceManagement
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
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
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:
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:
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 arepublic
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 theURL
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:
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:
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:
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:
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:
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:
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:
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:
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.
Processing the Links
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:
-
Change
links
into an array ofEventLink
objects. -
Tell the decoder what keys to use. This is essential when decoding the JSON manually.
-
Add a custom
init(from:)
for decoding. -
Use
CodingKeys
to get the data values from the decoder’s container using the specified keys. -
Decode the
text
element fromvalues
. This doesn’t need any further processing before assigning it to thetext
property. -
Decode the
links
element as a dictionary. -
Loop through the values in the dictionary and try to create an
EventLink
object from each one. -
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:
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:
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:
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:
-
Decode the
text
element fromvalues
exactly as before, but this time, assign it to a temporary constant. -
Split
rawText
using the HTML entity with a space on either side. -
If the split resulted in two parts, assign the first to
year
and the second totext
. If the text didn’t contain the entity or contained it more than once, setyear
to a question mark andtext
to the complete value from the decoder. -
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:
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]
}
}
-
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 theObservation
framework, so that’s something you’ll add later. -
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. -
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 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:
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.