Chapters

Hide chapters

macOS by Tutorials

First Edition · macOS 12 · Swift 5.5 · Xcode 13

Section I: Your First App: On This Day

Section 1: 6 chapters
Show chapters Hide chapters

5. Setting Preferences & Icons
Written by Sarah Reichelt

After finishing the last chapter, your app is feature complete. It downloads events for today or for a selected day. It displays the events in two different formats and allows searching and sorting. And it shows a list of the days with downloaded data, so you can swap between them.

In this chapter, you’re going to add the finishing touches that make your sample app into a real app you could distribute.

First, you’ll learn about app preferences and how to add a Preferences window. Next, you’ll update the app name and add an app icon. What image sizes do you need for the icon? How should you style the icon?

The last task will be adding your own information to the About box that Xcode creates for every Mac app.

Preferences

Nearly all macOS apps have a Preferences window, and they’re always accessed in the same way: via the Preferences… menu item in the app menu or with the Command-, shortcut.

If you write an iOS app with user settings, you have two choices. You can create your own settings view inside your app, or you can hook into the iOS Settings app and display options for your app in there.

For a macOS app, you’ll almost always create your own settings view. Some apps add a pane to System Preferences, but these are apps that are deeply embedded into the system, like mouse drivers or file system utilities.

Creating a Preferences View

Open the app project you were working on in the previous chapter or download the materials for this chapter and open the starter project.

In the Views group, add a new SwiftUI View file and call it PreferencesView.swift. Leave the default “Hello, World!” text in place for now, but add this size modifier:

.frame(width: 200, height: 150)

This will make sure that the window is big enough to see when you test it.

Next, you’ll hook this view up to the Preferences… menu item. Open OnTheDayApp.swift. Right now, the body contains a WindowGroup with its contents and a commands modifier. You’re going to add a second scene.

After the end of the commands modifier, add this:

Settings {
  PreferencesView()
}

This tells the app that, as well as having a WindowGroup scene, it now has a Settings scene. When you give Settings a view, SwiftUI will set up a Preferences… menu item and handle displaying your view whenever a user selects this menu item or presses its keyboard shortcut.

Build and run the app. Open the OnThisDay menu and use the new Preferences… menu item to open your preferences window:

Preferences menu item and window
Preferences menu item and window

There are a few interesting things about this window that make it different from the other windows you’ve opened so far in this app:

  1. You can’t open more than one copy of this window. You’re able to create as many instances of the main app window as you want, but the Preferences window will only ever open once. Select the menu item to open the window, click the main window and then press Command-,. The already-opened Preferences window will come to the front, but there will still only be one.

  2. It has a preset title: OnThisDay Preferences, but this is too long to fit in the window. To over-ride it, add this modifier after the frame modifier in PreferencesView.swift:

.navigationTitle("Settings")
  1. The maximize and minimize buttons are greyed out instead of being green and yellow. That’s because you set a fixed size for the view. In ContentView.swift, you set up a flexible frame for the main window, but a Preferences window will have fixed content, so a fixed size is fine.

Adding Tabs

Mac apps can have a lot of settings, especially the more complex apps. To make these less confusing, a very common design pattern is to split the settings into groups and use tabs to display each group. Xcode is a good example of this:

Xcode Preferences
Xcode Preferences

OnThisDay is not a very complicated app, and it doesn’t need a lot of settings, but you’re going to use this tabbed interface because it has some oddities you need to be aware of.

In PreferencesView.swift, replace Text with:

// 1
TabView {
  // 2
  Text("Tab 1 content here")
    // 3
    .tabItem {
      Image(systemName: "checkmark.circle")
      Text("Show")
    }

  // 4
  Text("Tab 2 content here")
    .tabItem {
      Image(systemName: "sun.min")
      Text("Appearance")
    }
}

And what does this do?

  1. Create a TabView to set up the tabbed interface.
  2. Set up the content for the first tab — a Text placeholder for now.
  3. Add the tabItem that will show at the top, using an Image from SF Symbols and a Text view.
  4. Repeat for the second tab.

Resume the preview and look at your tabs:

Previewing the tabs.
Previewing the tabs.

Nothing very surprising here; this looks like a standard set of Mac tabs. But you’re probably wondering why there’s an image in each tabItem when the tabs are far too small to display them.

Build and run the app, open the Preferences window and see what happens:

Preferences tabs in the app.
Preferences tabs in the app.

The tabs are completely different, and now you get to see the images. Also, the title of the window shows the selected tab’s title. This is yet another way a Preferences window differs from a normal window.

So the preview is no good for working with the tabs in a Preferences window, but it can still be useful when designing the tab content.

Setting up the Show Options

The first tab is the Show tab, and its options will control what you see in the app. You already have the ability to toggle Show Totals, which you’ll include here, but you’re going to add other controls to show or hide certain categories of events.

To keep the TabView simple and to avoid the Pyramid of Doom, add a new structure to PreferencesView.swift at the end of the file:

// 1
struct ShowView: View {
  // 2
  @AppStorage("showBirths") var showBirths = true
  @AppStorage("showDeaths") var showDeaths = true
  @AppStorage("showTotals") var showTotals = true

  // 3
  var body: some View {
    // 4
    VStack(alignment: .leading) {
      Toggle("Show Births", isOn: $showBirths)
      Toggle("Show Deaths", isOn: $showDeaths)
      Toggle("Show Totals", isOn: $showTotals)
    }
  }
}

Stepping through this, you:

  1. Initialize a new SwiftUI view.
  2. Declare three @AppStorage properties. You’ve already used showTotals, but the other two are new.
  3. Add a body, just like for any SwiftUI view.
  4. Inside the body, use a VStack containing three Toggle views. Each toggle is bound to one of the @AppStorage properties.

Note: The Pyramid of Doom is a phrase used to refer to deeply nested code, which ends up with an enormous pyramid of closing braces at the end.

You’ve seen that the preview isn’t much use for previewing the tabs, so re-purpose it to preview this.

In PreferencesView_Previews, replace PreferencesView() with:

ShowView()
  .frame(width: 200, height: 150)

Resume the preview to see your new view:

Previewing Show view.
Previewing Show view.

These settings are live, even in the preview. Run the app, and use the Display menu to change the setting for Show Totals. Come back to Xcode, resume the preview and the checkbox will reflect your new setting.

Applying the Show Choices

Your app already handles the different options for showTotals, but you need to apply the other two settings. You’ll do this by changing the event types listed in the sidebar.

First, open SidebarView.swift and add the two new @AppStorage properties to the one that’s there already:

@AppStorage("showBirths") var showBirths = true
@AppStorage("showDeaths") var showDeaths = true

In the top section of the list, you’re looping through EventType.allCases to create the display. Now you need to work out which of these cases to show.

So next, add this to SidebarView outside the body:

// 1
var validTypes: [EventType] {
  // 2
  var types = [EventType.events]
  // 3
  if showBirths {
    types.append(.births)
  }
  if showDeaths {
    types.append(.deaths)
  }
  // 4
  return types
}

What does this do?

  1. Create a computed property that returns an array of EventTypes.
  2. Define an array containing .events as there must always be something to display.
  3. Check the showBirths and showDeaths settings and append the matching cases to the array as required.
  4. Return the array for use in the loop.

Now, you can use this property. Replace the ForEach(EventType.allCases line with:

ForEach(validTypes, id: \.self) { type in

This will loop through your computed array instead of every possible EventType.

The last step is to show your new view in the TabView. Back in PreferencesView.swift, replace the first Text placeholder with:

ShowView()

Build and run the app now. Once you have events to display, open the Preferences window. Check and uncheck the three toggles and watch the sidebar change:

Toggling the Show options.
Toggling the Show options.

You may be wondering why Show Totals can be set from both the menu bar and the Preferences window. This is a common pattern in macOS apps. The menu bar will contain some unique features, but it will also provide quick access to commonly used features, usually with keyboard shortcuts. The Preferences window gathers all the app-wide settings into a single location.

Designing the Appearance Tab

As you may have guessed, the second tab in the Preferences window is also going to mimic some menu items, in this case the ones that set the app’s appearance: dark, light or automatic.

Like you did for the first tab, you’re going to add a new view to hold the contents of the second tab.

In PreferencesView.swift, at the end of the file, add this:

// 1
struct AppearanceView: View {
  // 2
  @AppStorage("displayMode") var displayMode = DisplayMode.auto

  // 3
  var body: some View {
    // 4
    Picker("", selection: $displayMode) {
      Text("Light").tag(DisplayMode.light)
      Text("Dark").tag(DisplayMode.dark)
      Text("Automatic").tag(DisplayMode.auto)
    }
    // 5
    .pickerStyle(.radioGroup)
  }
}

This is similar to the last view you added:

  1. Define a SwiftUI view.
  2. Give it access to the @AppStorage property you already created for this purpose.
  3. Add the body to set up what appears in the view.
  4. This time, the user can only select one of the possible options at a time, so a Picker is a logical interface option.
  5. Set the pickerStyle to display the options as a set of radio buttons.

To see how this will look, set the canvas to show a second preview by adding this to previews:

AppearanceView()
  .frame(width: 200, height: 150)

You’ve used the preview canvas to show the same view in different configurations, but it can also display different views.

Resume the preview to see this:

Previewing both tabs.
Previewing both tabs.

Check boxes are perfect for when the user can select any, all or none of the options. Radio buttons are the right interface element for when the user must choose one, and only one, of the possibilities.

Changing Appearances

Next, to make the new view show up in the Preferences window, replace the second Text placeholder in PreferencesView with:

AppearanceView()

Build and run, open the Preferences window and change the appearance. Open the Display menu and look at the Appearance submenu. The check mark in the menu matches the selected radio button in Preferences.

And changing either one immediately changes the other, as well as setting the display.

Setting appearance preferences.
Setting appearance preferences.

But wait just a second… All you did was add some radio buttons. How come this all just works?

Remember back when you set up the menus, you added an onChange modifier to ContentView in OnThisDay.swift? This watches for any changes to the @AppStorage property displayMode and applies the new setting.

You connected this new UI directly to displayMode, so whether you change the setting through the menu items or in the Preferences window, it triggers the onChange and applies the selected appearance.

And since the menu items and the radio buttons are all bound to displayMode, their visuals get updated at the same time. This is the beauty of SwiftUI.

Editing the App Name

One thing you haven’t addressed yet is the app’s name. As instructed, you called the project OnThisDay. Xcode can get very confused if you use spaces or any unusual characters in project names, so resist the impulse to use emojis or any other unusual characters and stick to very plain names, with no spaces or accented characters, for your project names.

But the project name appears all through the app. You’ve overridden the main window title, but look at the OnThisDay menu and the Help menu. Mouse over the app icon in the Dock. They’re all using OnThisDay with no spaces.

Fortunately, there’s an easy fix for this without renaming the project — you rename the target!

Select the project at the top of the Project navigator. Click the target name to select it, then press Return to start editing. Add the spaces to change the target name to On This Day and press Return again to complete the change:

Editing the target name.
Editing the target name.

After an edit like this, you need to do a clean build to make sure Xcode incorporates the change into the app, so press Command-Shift-K to clean the build folder.

Now build and run and check out the menus to see your new app name:

New app name
New app name

You can also change the app name by selecting the target, as before, and clicking the Build Settings tab. Scroll down to Packaging and double-click the Product Name value. It’s set to $(TARGET_NAME) by default. You can edit directly here, instead of changing the target name itself.

Editing the product name.
Editing the product name.

App Icons

So far, your app icon has looked a bit sad in the Dock. Take a look at the other icons in the Dock and open your Applications folder in icon mode to look at the icons there:

Icons in applications folder
Icons in applications folder

The standard Mac app icon is now a rounded square with a slight shadow. Select an icon in the Applications folder and you’ll notice that there is some transparent padding around the icon too.

If you’ve published an iOS app, you’ll know that you have to supply the icon image files as complete squares with no transparency. iOS rounds the corners for you. macOS doesn’t process the icons at all, so you have to supply exactly what you want to see.

The other difference between iOS and macOS icons is the number and size of image files required. You’ll be happy to hear that you need fewer images for a macOS icon set. :]

Open Assets.xcassets and select AppIcon to see the empty spaces you have to fill. The largest one is 1024 x 1024 pixels.

Creating a macOS App Icon Set

Our wonderful artist, Luke Freeman, has designed an icon for this app and he has supplied a 1024 x 1024 pixel image with no transparency or padding. Open the assets folder in the downloads for this chapter to find the file.

You could go to the trouble of editing the image and then creating all the different sizes, but there are tools out there to make this easier. Some only make icons for iOS apps, but Bakery is a free app in the Mac App Store which makes icon sets for all the Apple operating systems.

Start by downloading and installing Bakery from the Mac App Store.

Next, open Bakery and drag app-icon.png from the assets folder on to the large icon at the top of the window:

Adding your image to Bakery.
Adding your image to Bakery.

Then, click Generate icons which puts a floating window at the bottom left of your screen, displaying your icon:

Bakery floating window
Bakery floating window

In Xcode, drag this floating window into the list of assets in Assets.xcassets. This creates a new icon set called AppIcon-1.

Finally, delete AppIcon from the assets list and rename AppIcon-1 to AppIcon. This tidies up the assets catalog and means that you don’t have to change any settings to use your new icon.

Press Command-Shift-K to clean the build folder again. Build and run the app, then look in the Dock to see your beautiful new icon in action.

App icon in the Dock.
App icon in the Dock.

Note: Bakery has a warning about not using these icons for submission to the App Store. This only applies if you use Apple’s symbols or emojis to make an icon. Using your own artwork is perfectly OK.

Now that you have your icon in place, the app is very nearly done.

Configuring the About Box

Run the app, go to the On This Day menu and select About On This Day to see the default About box that Xcode has made for you:

Default About box
Default About box

As you can see, this is a simple display showing your shiny new icon, the app name and its version and build number.

You might think that this will be difficult to change, since Xcode generates it automatically, but there is a simple method.

Back in Xcode, press Command-N to open the New File dialog. In the search box at the top right, type empty, and select Empty from the Other section:

New empty file
New empty file

Then, click Next and call the file Credits.html. You must use this exact name or it won’t work.

This new file allows you to enter HTML and it will show up in the About box. You can include different HTML elements, links, even inline styling.

As a demonstration, add this HTML to the file:

<div style="text-align: center; 
    font-family: sans-serif; 
    font-size: 15px">
  <h2>Header</h2>
  <p style="color: green;">
  Here is some green text.
  </p>
  <a href="https://www.raywenderlich.com">
    Ray Wenderlich
  </a>
  <br>
  <a href="mailto:info@example.com?subject=On%20This%20Day">
    Email Me
  </a>
</div>

Build and run the app, open the About box, and you’ll see this:

About box showing HTML samples.
About box showing HTML samples.

This is a good example of the sort of things that you can display in the About box. Both the web link and the email link are active and the text is using the styles you set. The About box has grown taller to accommodate the extra text, but after it reaches a certain height, it will wrap the extra information in a scroll box.

Now that you know how to add text, links and styling to your About box, think about what you really should be showing there.

Use the link in the Help menu to go to the ZenQuotes.io web site. Scroll to the bottom of the page and look at Usage Limits and Attribution. That attribution link looks like the perfect thing to put in the About box.

Replace the contents of Credits.html with:

<div style="text-align: center;
    font-family: sans-serif;
    font-size: 15px">
    Historical event data provided by
    <a href="https://today.zenquotes.io/">ZenQuotes.io</a>
</div>

This keeps the styling and uses the suggested attribution text and link.

Build and run, open the About box and check the link:

About box showing attribution.
About box showing attribution.

You’ve taken the default About box, learned how to customize it and used it to give credit to the people providing the data for the app. Nice work.

Note: If you’re more comfortable with Rich Text than with HTML, add a file called Credits.rtf and edit that to add to your About box.

Help?

There is one last feature that you might be wondering about: Help. Select On This Day Help from the Help menu to see this rather discouraging message:

No help here
No help here

When you use one of Apple’s apps and you look for Help, you see Apple’s help book interface. It’s possible to use this to create help for your app, but learning that would need another complete book. And it ends up with a system that is slow and difficult to use.

In a later section of this book, you’ll learn how to hijack the Help menu item and show your own Help window, but this app is easy enough to use without any extra help. :]

To avoid showing your users this sad help dialog, you’re going to eliminate it.

Open Controls/Menus.swift and find where you set up the CommandGroup adding the Button that links to ZenQuotes.io. Right now it’s set to appear before the standard Help menu item.

Change the CommandGroup line to:

CommandGroup(replacing: .help) {

This will delete the On This Day Help menu item and show your own item in its place:

Edited Help menu
Edited Help menu

That’s much neater than showing a non-functional help menu item.

Challenge

Edit Credits.html so that it shows ways for users to contact you about your app:

  • Use a mailto link for email. Make sure it includes a subject, so you know what the email is about.
  • Add a web link to connect to your GitHub or Twitter page.

Key points

  • Nearly all macOS apps have a Preferences window. These windows operate differently and display tabs differently than normal app windows.
  • Menus are often used as shortcuts to features that are also available elsewhere in the app.
  • Using @AppStorage makes it easy to apply settings changes, no matter what part of the interface changes them.
  • Once you’ve finished the coding part of an app, don’t forget to set up the visuals: app icon and app name.
  • An About box comes as standard, but it’s easy to customize it using HTML.

Where to Go From Here?

Look at how much you’ve accomplished in these five chapters!

You started by designing data models to suit the API. Then, you built a conventional Mac app to display that data.

After that you looked at menus, toolbars, tables and custom views.

And finally, you added the finishing touches that make an app look polished and complete.

Great work! You’ve learned a lot — and you have an interesting app.

Apple has a section of their Human Interface Guidelines dedicated to the App Icon. This is well worth a read before you start creating your own icon images.

If you use Sketch or Photoshop, download Apple’s macOS Design Resources to get started.

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.