Home iOS & Swift Books Catalyst by Tutorials

8
Making Your App Feel at Home on macOS Written by Nick Bonatsakis

In the previous section, you learned how to turn your iPhone-only app into a great iPad app. As you now know, this is the first step in making a great Mac app.

In this chapter, you’re going to take things to the next level by making some adjustments that will really make your app shine when running on macOS via Catalyst. Throughout the rest of this section, you’ll go deeper on several other Mac-specific features. When you’re done, you’ll have the makings of a world-class Mac app.

By the end of this chapter, you will have learned how to:

  • Add a Mac-specific icon.
  • Take advantage of system colors.
  • Enable window-resizing.
  • Enhance your Settings bundle for running on Mac.
  • Make a handful of other minor Mac-related improvements.

Until now, you’ve been test running your app on Mac, but have been focused on iPad. That all changes right now. Ready to finally get your hands dirty with macOS? Fantastic, onward and upward!

It starts with an app icon

One of the first things you might have noticed about running Journalyst on macOS in previous chapters is that it uses the same icon on the Mac as it does on iPhone and iPad. While the icon style as of macOS Big Sur has skewed more towards the rounded rectangle style found on iOS, you still may want to tweak your app icon for macOS. Many of Apple’s stock apps now have rounded rectangle icons on macOS, but they are slightly different and distinctly Mac.

Apple has accounted for this fact and given you the ability to include a Mac-specific icon in your app’s bundle. And, it’s a pretty straight-forward task to do just that.

Open the starter project, then open Assets.xcassets. Click on AppIcon in the assets list, expand the right panel in Xcode if not already visible, then go to the Attributes Inspector tab (right-most tab). Finally, check the checkbox next to Mac.

You’ll now see several new slots for Mac icon size variants within the main editor. Inside the starter directory for this chapter, you’ll find a Mac App Icon folder that contains a spiffy new Mac icon in all the required sizes. Drag the appropriately sized icon into each slot to finish up.

Set the target device to My Mac, then build and run and you’ll find that your app now looks much more at home on the Mac, with an icon that has a larger book glyph.

Adding a touch of color

Another thing you may have noticed when running Journalyst on the Mac is that some of the colors seem off. That’s because how iOS uses color differs in some key ways from how it’s used on macOS.

Mac apps have traditionally adhered to some precise rules around how and where to use colors for standard system UI elements. Since most Mac apps lean heavily on these elements, they tend to have a very consistent look and feel. Conversely, until now, iOS hasn’t exposed system standard colors, and so iOS designs have tended to vary much more in their application of color.

In iOS 13, Apple added a consistent set of “system” colors in the form of static UIColor properties. Using these new properties will result in rendered colors that are accurate no matter which OS your app is running on. With this in mind, you’re now going to update a few key areas in your app to take advantage of these new colors so that your app adopts Mac-like colors when running on macOS.

The first area that looks somewhat out of place on the Mac is the background of the journal entry’s right-hand detail screen. Because its table view uses a grouped style, its background defaults to the .systemGroupedBackground color on all OS’s. On the Mac, this color renders as white, and this prevents you from differentiating the background from specific element areas.

To fix this, open EntryTableViewController.swift and append the following code to viewDidLoad():

#if targetEnvironment(macCatalyst)
view.backgroundColor = .secondarySystemBackground
#endif

Note the use of #if targetEnvironment(macCatalyst)#endif.

This is a compiler directive enabling you to include code conditionally — here only if you’re building for the Mac. Within this #if block, you then set the view’s background color to .secondarySystemBackground. The result is a Mac-appropriate off-white background color that’s visually distinct from the primary background color.

Next, notice that the journal entry cells in the left-hand sidebar still include lots of colors. On the Mac, data entry cells like this one typically use colored text sparingly. Additionally, the macOS selected color is user-defined and commonly darker than the standard iOS gray color. Together, these issues add up to journal entry cells that look out of place on the Mac.

Open EntryTableViewCell.swift and add the following method:

private func setupForMac() {
  //1
  dateLabel.textColor = .label
  //2
  dateLabel.highlightedTextColor = .white
  //3
  timeLabel.textColor = .secondaryLabel
  //4
  timeLabel.highlightedTextColor = .white
}

Here’s what this does:

  1. First, you set the normal color for the date label to the .label color, which will render as a dark gray in light mode.
  2. Next, you set the highlighted text color for the date label to .white. This is the color the label changes to when the cell is selected. Most of the time on the Mac, the system-selected color has a higher contrast, so white makes more sense here than on iOS.
  3. Now, you set the time label color to secondaryLabel. This is a secondary piece of information, so the color provided by the system will be less pronounced than .label.
  4. Finally, you set the time label highlighted color to .white just as you did for the date label.

Now, add the following code to awakeFromNib():

#if targetEnvironment(macCatalyst)
setupForMac()
#endif

Build and run and you’ll find the app looks much nicer now:

A word on typography

While there is nothing you need to change to make your app’s text readable on the Mac, it’s worth noting UI content will be scaled down when running on macOS. According to Apple’s guidelines.

Content scaling. Text in the macOS version of an iPad app looks the same as it does in iOS because SF fonts are available on both platforms. However, the baseline font size in iOS is 17 pt, whereas the most common font size in macOS is 13 pt. To ensure that your text and interface elements are consistent with the macOS display environment, iOS views are automatically scaled down to 77%.

Keep this in mind if you have specific situations where this default scaling may cause issues, and take care to address those potential problems.

Sizing down window resizing

When running on an iPad, you have a few options for how a secondary window is sized. On Mac, freeform window sizing has been a platform feature since Apple “borrowed” multi-windowing from Xerox PARC in the 1980s. It would be a shame to let that historical bit of borrowing go to waste, so let’s make sure your app supports window resizing properly.

You can already resize the window when running on the Mac, but you’ll quickly find that the system doesn’t allow you to shrink the window down past a default size. It would be nice to have tighter control over the minimum and maximum window dimensions.

Open SceneDelegate.swift and add the following code to the beginning of scene(_:willConnectTo:options:):

if let scene = scene as? UIWindowScene {
  scene.sizeRestrictions?.minimumSize =
    CGSize(width: 768.0, height: 768.0)
  scene.sizeRestrictions?.maximumSize =
    CGSize(
      width: CGFloat.greatestFiniteMagnitude,
      height: CGFloat.greatestFiniteMagnitude
    )
}

sizeRestrictions is a property on UIWindowScene that allows you to control the minimum and maximum sizes for the window. In the above code, you set the minimum size to 768x768 and the maximum to CGFloat.greatestFiniteMagnitude which effectively means “as large as you want to make it.”

Now there’s one more thing you need to add that will come in handy later on in this chapter. Add the following notification definition to the top of SceneDelegate.swift:

extension Notification.Name {
  static var WindowSizeChanged = Notification.Name("com.raywenderlich.Journalyst.WindowSizeChanged")
}

And then, add the following method to the SceneDelegate class:

func windowScene(_ windowScene: UIWindowScene,
  didUpdate previousCoordinateSpace: UICoordinateSpace,
  interfaceOrientation previousInterfaceOrientation: 
  UIInterfaceOrientation,
  traitCollection previousTraitCollection: UITraitCollection) {
    NotificationCenter.default.post(
      name: .WindowSizeChanged, object: nil)
}

The above delegate method gets called whenever the window bounds change. When this happens, you issue the WindowSizeChanged notification so that any objects in your app can observe for this event.

Build and run, then try resizing the window. You’ll find that the minimum size has now changed to the custom value you specified above. Through the magic of auto-layout, everything looks fine regardless of whatever window size you ultimately choose.

Preferential treatment

In the previous section, you learned how to add a Settings bundle to expose app preferences to the iOS Settings app. By doing so, you also enabled a default Preferences window for your app when running on the Mac, accessible via the Journalyst ▸ Preferences menu item. The out-of-the-box screen looks like this:

Not too bad, especially for free. But you can do better!

First, you’ll notice that both tabs use the same default icon. The default icon is fine for the General tab, but it’d be nice to have a separate icon for the Sharing tab. Thankfully, the Settings bundle mechanism now supports adding custom icons, so that you can add some style.

To start things off, find sharing.png and sharing@2x.png in starter/Sharing Settings Tab Icon, and drop them into the top level of Settings.bundle. Then, to specify the icon for the Sharing tab, do the following:

  1. Find Sharing.plist inside the Settings bundle and open it.
  2. Right-click on the Root element and click Add Row.
  3. Set the key for the new row to Icon.
  4. Finally, set the value to sharing.

It’s pretty common for Mac preference panes to have descriptions for checkbox-based settings. Your current app lacks these descriptions, so you’ll add them to both boolean settings.

To add the description on the General tab:

  1. Open Root.plist in the Settings bundle.
  2. Expand Preference Items and then expand Item 1.
  3. Right-click Item 1 and click Add Row.
  4. Set the key to Description.
  5. Set the value to Enable to force a light background on the entry screen.

Now, add a description to the checkbox on the Sharing tab:

  1. Open Sharing.plist in the Settings bundle.
  2. Expand PreferenceSpecifiers and then expand Item 1.
  3. Right-click Item 1 and click Add Row.
  4. Set the key to Description.
  5. Set the value to Automatically include a custom signature when sharing journal entries.

The last preference enhancement you’re going to add is to show a confirmation prompt when the user turns on the journal entry signature setting on the Sharing tab. This is another feature that is specific to apps running on the Mac, so the plist entries will be ignored when running on iOS.

To add the confirmation dialog:

  1. Open Sharing.plist in the Settings bundle.
  2. Expand PreferenceSpecifiers and then expand Item 1.
  3. Right-click Item 1 and click Add Row.
  4. Set the key to TrueConfirmationPrompt.
  5. Set the type to Dictionary.

Add the following sub-items as String key value pairs to TrueConfirmationPrompt, again by right-clicking and then choosing Add Row for each:

  • Type -> PSConfirmationPrompt
  • Title -> Confirm Share Name
  • Prompt -> Turning this on will share your name with the journal entry
  • ConfirmText -> Enable
  • DenyText -> Don't Enable

Now, Build and run. Then, open the preferences pane by clicking Journalyst ▸ Preferences. Your hard work has paid off, and you now have a much more Mac-like preferences window. Try enabling and disabling the various settings to see how things work.

Pretty slick right? Just a few more odds and ends to take care of and you’ll be on your way.

A few more odds and ends

Another thing that may be nagging your better Mac sensibilities is the look of the sidebar. On Mac, sidebars for split views tend to be styled in such a way that they let the content beneath bleed through, applying a blur.

Adding this behavior to your split view’s side bar is a simple one-liner. Open up RootSplitViewController.swift and append the following to the end of viewDidLoad():

splitViewController.primaryBackgroundStyle = .sidebar

That one line of code will keep the sidebar unchanged on iOS, but when running on the Mac, it will now look like the standard macOS sidebar.

Next, you have a minor scrollbar-related tweak to make. As you learned back in Chapter 1, when any UIScrollView instance is used, and your app is running on the Mac, the scroll bars that are only visible on demand on iOS are styled as Mac scrollbars and visible at all times. E.g., when someone is actively scrolling.

Note: By default scrollbars will auto-hide on Mac when using a trackpad. You may not experience any scrollbar related issues if you have this setting enabled in System Preferences and don’t use a mouse. If you’d like to see what it looks like for your users who use a mouse, open System Preferences, choose General, and under the “Show scroll bars” setting, choose “Always”.

On the journal entry detail screen, the images appear in a UICollectionView that is laid out horizontally. On iOS, the scroll bars aren’t really necessary, since the way the list extends off the side of the screen makes it pretty apparent that it is a scrollable element. But on the Mac, there’s no touch interaction, so scrollbars are how you typically move the content area.

Open EntryTableViewController.swift and update the #if targetEnvironment(macCatalyst) code block at the end of viewDidLoad to match this:

#if targetEnvironment(macCatalyst)
view.backgroundColor = .secondarySystemBackground
collectionView.showsHorizontalScrollIndicator = true
#endif

Now, when running on the Mac, the collection view that renders images will have a scrollbar visible if the content extends past the view frame. That’s not quite enough to make this work. You still need to let the collection view know when the window bounds change, or else it won’t know to update the scrollbar state. For example, if you start with a window that is very wide and can accommodate all the images, but then resize to a much smaller window width, the collection view needs to change from not showing scrollbars to showing them.

Remember the WindowSizeChanged notification you added to SceneDelegate earlier in this chapter? It’s time to make use of it!

Still in EntryTableViewController.swift, add the following code to viewDidLoad():

NotificationCenter.default.addObserver(
  self,
  selector: #selector(handleWindowSizeChanged),
  name: .WindowSizeChanged,
  object: nil
)

The above code should be pretty familiar, as you have done many times already. You are subscribing to a notification, in this case, WindowSizeChanged.

Next, add the implementation for the handleWindowSizeChanged() handler method like so:

@objc func handleWindowSizeChanged() {
  collectionView.reloadData()
}

This will force the collection view to reconsider whether it should be showing scrollbars or not, based on its current bounds.

Lastly, there is one more minor tweak you’ll want to make to how sidebar cells are rendered. At present, whenever you make changes to the text of a journal entry, the sidebar cell shows a preview of that text. This looks fine on an iPad, where cell heights commonly vary, but on the Mac, they tend to be more uniform, so it would be nice not to show that text preview in this case.

Open EntryTableViewCell.swift and add the following code to the didSet of the entry property:

#if targetEnvironment(macCatalyst)
summaryLabel.isHidden = true
#endif

Pretty straight-forward! You’re again using the #if targetEnvironment(macCatalyst) check to only run the code when on the Mac, in this case, hiding the summary label.

Build and run one more time, then bask in the glory that is your app, one huge step closer to being a marvelously Mac-like macOS app. Try saying that ten times fast!

Key points

  • Including a Mac-specific icon for your Catalyst app is easy and helps make the app feel more at home on the Mac.
  • There are many ways to leverage system colors to improve the styling of your iOS app when it runs on the Mac.
  • You need to consider window resizing when your app runs on the Mac.
  • Mac preferences panels get lots of functionality for free for Catalyst apps, but you can go further with some extra effort.

Where to go from here?

In this chapter, you took the first steps towards really making your iOS app shine when running macOS, taking advantage of various styling methods, window resizing, preferences, and more. But there are still some glaring omissions, things you’d expect to see in a great Mac app.

In the next chapter, you’re going to learn how to replace those navigation bars that look so out of place on the Mac, with native Mac toolbars.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC