Category: SwiftUI

How to Learn SwiftUI to Develop Mac Apps

A common complaint I hear online from Mac developers is that all the SwiftUI learning material focuses on iOS development, and there’s nothing on Mac development. This article provides advice on learning SwiftUI for Mac development.

A lot of iOS SwiftUI learning material also works on Mac

Mac developers have to accept the fact that iOS development is much more popular than Mac development. People creating SwiftUI tutorials, videos, and courses are going to focus on iOS developers.

The good news with SwiftUI is that most of the iOS learning material also applies to Mac development. Apple created SwiftUI so developers can create user interfaces for all of their platforms with one framework. If you find a SwiftUI tutorial or course that you like, such as Hacking with Swift’s 100 Days of SwiftUI course, you can use the tutorial or course to make Mac apps. You may have to make some changes to get things to work on Mac.

Lists are one area of SwiftUI where there are large differences between iOS and Mac. If you read an article about lists and try to use the code in a Mac app, you’ll run into problems. The following articles cover those differences:

You can use AppKit views in SwiftUI Mac apps

SwiftUI is not a finished framework, especially on Mac. Some views that Mac apps need do not have native SwiftUI equivalents, and some of SwiftUI’s views do not work well on Mac. A workaround for SwiftUI’s Mac limitations is to use AppKit views in your app.

To use an AppKit view in a SwiftUI Mac app, create a struct that conforms to the NSViewRepresentable protocol. My Using a UIKit or AppKIt View in SwiftUI article provides more details on using AppKit views in a SwiftUI app.

SwiftUI resources for Mac development

To address complaints about a lack of learning material on Mac development, I compiled a list of Mac development resources. Many of those resources cover using SwiftUI for Mac development.

This site has a number of SwiftUI articles, most of which apply to Mac development. You can see a complete list of articles on the Articles page.

Dealing with the init(destination:tag:selection ) was deprecated in iOS 16.0 message

People starting out with SwiftUI development may get a message like the following from Xcode when building their project:

`init(destination:isActive:label:)’ was deprecated in iOS 16.0: use NavigationLink(value:label:) inside a NavigationStack or NavigationSplitView

‘init(destination:tag:selection????️ )’ was deprecated in iOS 16.0: use NavigationLink(value????️ ), or navigationDestination(isPresented:destination:), inside a NavigationStack or NavigationSplitView

Why are you getting this message?

You are getting this message because you are using NavigationView in your user interface.

The original version of SwiftUI used navigation views to navigate from one screen to another in an app and for master-detail interfaces with split views on iPad. In iOS 16 Apple replaced NavigationView with NavigationStack and NavigationSplitView for navigation.

The message is telling you to replace NavigationView in your app with either NavigationStack or NavigationSplitView.

NavigationStack and NavigationSplitView don’t run on earlier iOS versions. If you want your SwiftUI app to run on iOS 15, you must use NavigationView.

Is the message a problem?

Not currently. The message is a compiler warning, not an error. Your project will build and run, assuming there are no errors in the rest of your code.

Eventually Apple will stop supporting iOS 15. A future version of Xcode will drop support for building an app that runs on iOS 15. For reference Xcode 14 does not support iOS versions older than iOS 11. When Apple releases a version of Xcode that does not support building an app that runs on iOS 15, the message will be a problem.

How do you remove the message?

Use either NavigationStack or NavigationSplitView instead of NavigationView in your app. Apple provides the following guide to migrate code from NavigationView:

Migrating to new navigation types

If you are following a tutorial that uses NavigationView, find a more recent tutorial that uses the newer navigation APIs.

Highlighting the Selected Item Button in a SwiftUI List

If you use a button for list items in SwiftUI, the list item does not highlight when you select it. This is expected behavior for a bordered button. But some apps need to use buttons that look like text to perform actions when selecting the list item. If you have a button that looks like text, you want to highlight the selection so people can easily see the selected item.

How do you highlight the selected item? You must do two things to highlight the selected item. First, you must set the background color for the list row. Second, you must set the foreground color for the list item.

Setting the Background Color

Add a .listRowBackground modifier to the button to set the background color. Use the accent color for the selected item to highlight it and the clear color for the other items to match the standard selection highlighting.

Setting the Foreground Color

When you select an item from a SwiftUI list in an app running in light mode, the text color for the item changes from a dark color to a light color. You must set the color based on the current color scheme or else the text of the selected item will be tough to read.

Start by adding an @Environment property to your SwiftUI view that stores the current color scheme: light or dark.

Next, create computed properties for the item color based on whether or not it’s selected. Use white for the selected item. For unselected items make the text white in dark mode and black in light mode.

Finally add a .foregroundColor modifier to the button. Use the selected item color for the selected item and the unselected item color for the other items.

Creating a SwiftUI Toolbar Button with an Image and a Label

Many toolbar buttons in Mac and iOS apps include an image and a label. The Mac apps Finder, Mail, and Pages are examples of apps whose toolbar buttons have an image and a label.

If you try to create a button in a SwiftUI toolbar with an image and a label, you will notice the label does not appear. You must do the following to create a toolbar button with an image and a label:

  • Create a label style for the button.
  • Add a .labelStyle modifier to your button label and use the label style you created.

Creating a Label Style

To create a label style, you must create a struct that conforms to the LabelStyle protocol. Add a makeBody function to the struct that returns a SwiftUI view.

A label style for a toolbar button should create a VStack with the image first and the text label second. Supply a font to use for the image and the label in the VStack.

Add the .labelStyle Modifier

After creating the label style for your toolbar button, add a .labelStyle modifier to the button’s label and supply the name of the struct you created for the label style. The following code shows an example of a SwiftUI button that uses a custom label style:

Credits

The answer from Eduardo to the following Stack Overflow question helped me with the code for this article:

Add Button with Image and Text in ToolbarItem SwiftUI

Adding a Help Menu to a SwiftUI App

When you create and run a SwiftUI Mac app project, the Help menu has a menu item named AppName Help, where AppName is the name of your app. If you choose that menu item, an alert opens saying help isn’t available for the app.

Help Books

The included menu item works only if your app has a Help Book bundled in the app. Many of Apple’s apps have Help Books. Choose Help > Xcode Help in Xcode to see an example of a Help Book.

Creating Help Books is painful. Apple’s Help Book Programming Guide was last updated in 2013, and there aren’t many articles online on creating Help Books.

Take Users to Your Site

For most apps it’s easier to add a page to your app’s website with your app’s help and add a menu item to go that page. You can even include a link on the page for people to download a user guide for your app. Creating a menu item that takes someone to your site involves the following steps:

  • Create a view for the menu item.
  • Create a SwiftUI link for the menu item.
  • Add the menu item to the menu bar.

Creating the Link and Menu Item

A SwiftUI link takes two arguments. The first argument is the link name, which is the menu item text for menus. The second argument is the link destination. The following code shows an example of a menu item with a link:

Adding the Menu Item to the Menu Bar

Add a .commands modifier to the window group or document group in the App struct for your app. Add a command group for the menu, replacing the default Help menu item with your Help menu items.

Showing a SwiftUI sheet from a Mac Menu

Most articles on showing sheets in SwiftUI apps show an example of a button with a .sheet modifier that shows the sheet. But if you try to apply the example to a menu in a Mac app,

The sheet does not appear when you choose the menu item. How do you show the sheet when someone chooses the menu item?

  • Add a focused value for showing the sheet.
  • Set the focused scene value in your main view.
  • Add the menu item to show the sheet.
  • Add the menu item to the app’s menu bar.

Read the Accessing the Document in a SwiftUI Menu article to learn more about focused values and working with SwiftUI menus.

Add a Focused Value

Add a focused value that your app uses to control whether or not to show the sheet. In this article I’m going to use the example of a menu item that previews some content in a sheet.

Set the Focused Scene Value

After creating the focused value to show the sheet, you must make that value a focused scene value to show the sheet from a menu. Start by adding a property to the main SwiftUI view.

The next step is to add a .focusedSceneValue modifier to the view. The first argument is the name of the variable you created for the focused value. The second argument is a binding using the property you created in the view.

Apple added the .focusedSceneValue modifier in macOS 12.

Add the Menu Item

Menus in SwiftUI apps are SwiftUI views. To show a sheet from the menu, start by creating a property with the @FocusedValue property wrapper that contains the focused value to show the sheet.

Create a button for the menu item. The action for the button is to set the focused value’s wrapped value. The focused value for showing a sheet is a Boolean value so you give it the value of true, which shows the sheet when someone chooses the menu item.

Add the Menu to the App

After creating the menu you must add it to your app’s menu bar. Add a .commands modifier to the app’s window group or document group to add the menu to the menu bar.

Credits

Thanks to Jason Armstrong for his answer to the following Stack Overflow question:

Calling a sheet from a menu item

Scene Editor Development: Showing a SwiftUI View Based on a Selection

The inspector for the scene editor has separate SwiftUI views for each kind of item in a SpriteKit scene. There’s a sprite inspector for sprites, a label inspector for labels, and so on.

When someone selects an item from the scene graph, the inspector should show the correct view. If someone selects a sprite, the inspector view should show the sprite inspector. How do you show the correct view based on the selection?

My solution is to get the class name for the selected item, add a switch statement to the view body, and show the correct view based on the class name.

Getting the Class Name

The inspector has a property that contains the selected item from the scene graph.

Because each kind of item in a SpriteKit scene has its own class, I can get the name of the class for the node property and show the correct view based on the name of the class.

NSObject is the base class for every SpriteKit class. The NSObject class has a nameOfClass property for Mac, but not iOS. But I saw on Stack Overflow that you can add an extension to NSObject to get the name of the class on iOS.

Adding a Switch Statement to the SwiftUI View

Now that I can get the names of the SpriteKit classes, I can add a switch statement in the SwiftUI view body and show the correct view based on the class of the selected item in the scene graph.

I can safely cast with as! because I know the class of the selected item.

Using ReferenceFileDocument to Save SwiftUI Documents

When you create a SwiftUI Document App project, the Swift file for the document contains a struct for the document that conforms to FileDocument. Using a struct for the document works in most cases, but you can make the document a class by conforming to ReferenceFileDocument.

When to Make Your SwiftUI Document a Class

The most common reason to use a class for a SwiftUI document is to update SwiftUI views when a property in the document changes. Using the @Published property wrapper for a property triggers updates to SwiftUI views when the property’s value changes. But the @Published property wrapper works only for classes.

If you are going to pass the document to many views, it can be more effective to make the document a class and use @StateObject, @ObservedObject, or @EnvironmentObject to pass the document to other views.

If you can’t mark the document as changed when using a struct, you can mark the document as changed by making the document a class and registering with the undo manager.

Changing the Document from a Struct to a Class

Apple’s Swift file for the document creates a struct.

You must make the following changes to change the document from a struct to a class:

  • Change struct to class
  • Change FileDocument to ReferenceFileDocument
  • Have the document conform to ObservableObject

Creating a Snapshot

You must add a snapshot function to your document’s class to use ReferenceFileDocument. The snapshot function creates a snapshot of the document’s current state that SwiftUI uses to save the document.

Replace GameScene with the data type your document saves. Replace scene with a property in your document class.

Marking the Document as Changed

If you find your document isn’t marked as changed when you make changes to the document, register your document with the undo manager by calling the registerUndo function. Usually you call registerUndo from a view’s .onAppear modifier.

Acknowledgements

The answers to the following questions on Stack Overflow helped me write this article:

Scene Editor Development: Unable to Open Scenes

I ran into a problem where I was unable to open the scenes I created in the editor. When I choose File > Open in the Mac version, the scene file is disabled. The scene file appears to save as the wrong type.

The Document Type and UTIs

The scene editor saves scenes as binary property lists. I used the following identifier for the document type, imported UTI, and exported UTIs:

That identifier is the UTType for binary property lists.

The type conforms to the following UTI:

I set the file extension for the UTIs to .sks.

The Problem

When I saved a scene I got the following error message in Xcode’s console:

Error Domain=NSCocoaErrorDomain Code=256 “The autosaved document “TestScene” could not be reopened. SpriteKit Editor cannot open files in the “property list” format.”

I also noticed the saved scene had the extension .plist instead of .sks.

The Solution

I created a custom identifier for the document type and UTIs. The document type conforms to the binary property list type, com.apple.binary-property-list.

Scene Editor Development: SwiftUI Navigation Issues

I have the following main view in the scene:

When selecting an item from the scene graph, I want to show the details in the inspector. I created a navigation link to do this.

The problem is that when I select an item from the scene graph, the inspector replaces the sprite view.

I took a look at the new NavigationSplitView view Apple added in iOS 16 and macOS 13 to handle the navigation. But I found the three column view doesn’t suit this app.

The behavior of a three column split view is that selecting an item from the first column affects the second column, and selecting an item from the second column affects the third column.

But I don’t want someone to have to select an item from the sprite view (scene canvas) to view it in the inspector. Selecting an item from the scene graph should show the details in the inspector.

Workaround

The workaround is to move the sprite view from the center column to the left column. Now the navigation link from the scene graph to the inspector works the way I want it to work.

For now I’m sticking with NavigationView to support iOS 15, which has a version of Swift Playgrounds that can make apps. It also allows me to support macOS 11+. But I may switch to a two column navigation split view if I need it.