Category: SwiftUI

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.

Scene Editor Development: Updating a SwiftUI List When Adding an Item

This article talks about the first big problem I encountered when starting to develop a SpriteKit scene editor in SwiftUI. It’s a problem every SwiftUI developer has seen: a view doesn’t update when the data changes.

Initial Code

My initial idea for the scene editor interface was to have a split view with three columns.

  • A scene graph to show the items in the scene
  • A canvas to place items in the scene
  • An inspector to view and edit details for an item

I started with the following SwiftUI view:

I used a SwiftUI sprite view for the canvas.

The document struct has a property that holds a SpriteKit scene.

GameScene is a subclass of SKScene, SpriteKit’s scene class.

The scene graph looks like the following:

My Initial Goal

My initial goal was to add a sprite to the scene by clicking or tapping on the canvas. The sprite’s location is the location of the click or tap. Adding the sprite also adds an item to the scene graph.

Problem: The SwiftUI List Doesn’t Update when Adding an Item

I ran the app and tried adding sprites to the canvas. The sprites appeared on the canvas, but the scene graph was empty.

I stepped through the code in the debugger and saw that the new sprites were in the scene’s array of children. When I saved the document the sprites appeared in the scene graph.

Adding sprites works, but the scene graph doesn’t update. This is a common problem SwiftUI developers run into. The user interface doesn’t update when the data changes.

Fix Attempts that Didn’t Work

There are two common approaches to get the user interface to update when the data changes. First, if your data model uses structs, use @State and @Binding.

Second, if your data model uses classes, make the class conform to ObservableObject. Add the @Published property wrapper to class members that you want to trigger a UI update when the member’s value changes.

I tried the following things to get the scene graph to update when adding a sprite to the scene:

  • Make GameScene conform to ObservableObject.
  • Add a @Published variable to GameScene that holds an array of the scene’s items, have GameScene conform to ObservableObject, and have the scene graph show the array of scene items.
  • Make Level a class with a @Published property for the scene and use @StateObject instead of @Binding in the scene graph.
  • Add a @Published variable that tracks whether the scene was edited and set it to true when placing the sprite.
  • Add a refresh ID to the GameScene class and change it when placing the sprite.
  • Add a refresh ID to the Level struct and change it when clicking on the canvas.
  • Add a property for the level to the GameSceneclass.
  • Give the scene graph a binding to a game scene instead of a level.
  • Give the scene graph a @StateObject property for the scene.

None of these attempts updated the scene graph after adding a sprite to the canvas.

The Solution

I struggled to fix this problem for weeks. Finally I asked a question on Reddit about the problem and learned what the problem was. The problem has two parts.

First, SwiftUI’s data flow system does not automatically track changes in SpriteKit scenes. I have to manually trigger changes when the scene changes.

Second, The binding for the level in the scene graph won’t trigger an update unless I make the level show a new scene. The app is a scene editor so I am never going to change the scene in a level. Because I don’t change scenes in a level, any changes I make to the level will not trigger a UI update.

The solution to the problem has the following parts:

  • Make GameScene conform to ObservableObject.
  • Send an objectWillChange message after placing the sprite.
  • Add a private property for the scene to the scene graph with the @ObservedObject property wrapper.
  • Add a private property for the level.
  • Use the private property for the scene in the ForEach block.

Sending the objectWillChange message required adding one line of code.

I had to add the following code to the SceneGraph struct:

The final thing I had to do was change the code of the ForEach block for the list to the following:

SwiftUI Open and Save Panels

AppKit has the NSOpenPanel and NSSavePanel classes to let people choose files to open and save in Mac apps. What is the SwiftUI equivalent to NSOpenPanel and NSSavePanel?

SwiftUI has .fileImporter and .fileExporter modifiers to let people choose files to open and save. Apply the modifier to a SwiftUI view, such as a button or a menu item. The .fileImporter and .fileExporter modifiers are available on both iOS and Mac.

File Importers

You must provide the following to .fileImporter:

  • A Boolean value that determines whether or not to show the panel
  • A list of allowed file types to open
  • A completion handler that runs when the panel closes

The following code lets someone open an image file:

If the person chooses a file and clicks the Open button, the result is .success, and the file importer provides the URL of the chosen file for you.

File Exporters

You must provide the following to .fileExporter:

  • A Boolean value that determines whether or not to show the panel
  • The document to export
  • The file type for the exported file
  • A completion handler that runs when the panel closes

You can also supply a default file name to the file exporter.

The following code exports a document to EPUB:

If the person chooses a location to save the file and clicks the Export button, the result is .success, and the file exporter provides the URL of the chosen file for you to write data from your app to the file.

Scene Editor Development: Intro

As a fun side project, I’m starting to develop a SpriteKit scene editor for iPad and Mac using SwiftUI. I decided to write about the development of this project.

This post is the first in a series of posts. Future posts will be more technical than this one.

Why Make a SpriteKit Scene Editor?

Doesn’t Xcode include a SpriteKit scene editor? Yes it does.

So why make a SpriteKit scene editor?

The Swift Playgrounds app lets people make SpriteKit games on an iPad. Use a SwiftUI sprite view to display a SpriteKit scene, and you can make a 2D game on an iPad.

Swift Playgrounds does not include a scene editor so you must build your scenes in code if you have only an iPad. By making a scene editor people on an iPad can create scenes for a game visually.

What Will I Write About?

Most of the time I’ll be writing about a problem I faced and how I solved it. I think that will interest the most people, solving problems in SwiftUI apps.

Where Can I Download the Editor?

The editor currently isn’t available to download because it’s not usable. Right now I’m in a prototyping stage to see what I can do with SwiftUI. I would also like to find a good name for the editor before I release an early version.

When I have the editor ready for others to use, I’ll make it available to download.

Keep in mind that progress may be slow at times. This is a side project that does things that not many SwiftUI apps do. Doing unusual things makes finding solutions to problems more difficult because people haven’t written articles or asked questions about them.

Scene Editor Development Article List