Category: SwiftUI

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

Scrolling a SwiftUI Sprite View

SwiftUI provides a sprite view to show a SpriteKit scene in a SwiftUI app. Supply a SpriteKit scene to the sprite view.

SwiftUI provides a scroll view to scroll content that won’t fit in the main SwiftUI view. Place the sprite view inside the scroll view to scroll the scene.

If you build and run this code, you will notice the scroll view covers the sprite view so you can’t see the sprite view or interact with it. How do you keep the scroll view from blocking the sprite view?

Add a .frame modifier to the sprite view.

If you want to scroll the scene horizontally and vertically, supply the scrolling directions in an array when creating the scroll view.

Using a UIKit or AppKIt View in SwiftUI

When to Wrap a UIKit or AppKit View

There are two cases where you would wrap a UIKit or AppKit view in a SwiftUI app. The first case is your app needs a view that SwiftUI doesn’t have. If your app needs to show web content or a PDF file, you must use a WebKit web view or PDFKit’s PDF view. SwiftUI currently doesn’t have a native web view or PDF view.

The second case is your app needs to do something that a SwiftUI view can’t do. To let people edit rich text in your app, you must use UITextView or NSTextView. SwiftUI’s TextEditor view only allows plain text editing. Text views are a common control where you have to drop down to UIKit or AppKit because SwiftUI’s text editor is very limited currently.

Wrapping a UIKit or AppKit View

To wrap a UIKIt or AppKit view in a SwiftUI app, perform the following steps:

  • Create a struct that conforms to the UIViewRepresentable(iOS) or NSViewRepresentable(Mac) protocols.
  • Write a makeUIView or makeNSView function to create the UIKit or AppKit view.
  • Write an updateUIView or updateNSView function to handle view updates.

The following code creates a web view that displays HTML text:

Notice that makeUIView returns the view type. The view type is also the type of the first argument to updateUIView.

After creating the view struct, you can add the view as part of the body of a SwiftUI view the same way you would add one of SwiftUI’s native views.

Examples and Further Reading

I have a demo project on GitHub that wraps a web view in a multi-platform SwiftUI app.

The CodeEditor package wraps a text view that supports syntax highlighting. It’s a more complex example than my demo project.

The following articles have additional information on using UIKit and AppKit views in SwiftUI:

Fixing the Type () cannot conform to View Error in Swift

Why am I getting this error?

You are trying to do something inside the body of a SwiftUI view besides displaying views. Placing print statements in a SwiftUI view will give you this error. Using if statements to conditionally show a view may also generate this error.

Let’s look at a simple example that shows a name in a SwiftUI Text label.

The code in the example is trying to set the name to show inside the body computed property. But you can’t run arbitrary code, such as setting a variable’s value, inside the body property. The body property must return a SwiftUI view so the code generates the Type () cannot conform to View error.

Ways to fix the error

The easiest way to fix the error in the example is to set the name when creating the name variable.

Use the .onAppear modifier to do something when a SwiftUI view appears. The following code changes the name correctly:

You can also make the error go away by adding a return statement to explicitly return a view.

Remove print statements from your SwiftUI views. If you need to print something to debug your app, use Xcode breakpoint actions to print to Xcode’s console.

If you need to do something in a SwiftUI view, move the code out of the body property to a separate function and call the function in the view.

Showing a Swift Enum’s Values in a SwiftUI Picker

Swift enums let you create your own data types with a limited number of possible values. You may want to use a picker in a SwiftUI app to choose one of the enum’s values. Filling a picker with an enum’s values requires you to do two things.

  • Make the enum conform to the CaseIterable protocol.
  • Wrap the picker in a ForEach block, supplying an array of the enum’s values.

Conforming to CaseIterable

Your Swift enum must conform to the CaseIterable protocol to get an array of the enum’s values. In most cases, conforming to CaseIterable is easy. Add CaseIterable after a colon when creating the enum. The following code shows a simple enum that conforms to CaseIterable:

By conforming to CaseIterable, the enum gets an allCases property that contains an array of the enum’s values. Use the property in a picker or any other SwiftUI view that shows an array of items.

Wrap the Picker in a ForEach Block

To provide picker items for each enum value, wrap the picker in a ForEach block. Supply the enum’s allCases property in the ForEach block. Create a Text view or whatever view you want for the picker item. The following code demonstrates how to create a picker with items for each value in the IssuePriority enum from the previous section:

Working with Lists in Multiplatform SwiftUI Apps

One of SwiftUI’s best features is you can use it to make apps that run on both iOS and Mac. Almost every SwiftUI article you find online is about iOS development, but most of the material also applies to Mac as well.

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. This article provides guidance on writing list code that works in both iOS and Mac apps.

Differences in List Behavior

The main reason you can’t use the same list code on iOS and Mac is because the way people interact with lists is different in iOS and Mac.

iOS apps have an Edit button that people use to delete and move items. When someone wants to delete a list item, they tap the Edit button. Each item has a Delete button next to it. Tapping the button deletes the item. Because each list item has a Delete button, you don’t have to keep track of the selected items.

Mac apps don’t have an Edit button for deleting and moving list items. In a Mac app, people delete list items by selecting them and either pressing the Delete key or clicking a Delete button. People move items by selecting them and dragging them to the desired destination. When developing a Mac app that uses lists, you must keep track of the selected item.

Tip: Create Separate List Views for iOS and Mac App Targets

Because list behavior is so different between iOS and Mac apps, you should create separate SwiftUI list views for the iOS and Mac versions of your app. If you try to support iOS and Mac in the same view, your code is going to be littered with #if os() checks, making the code tough to read.

You can share a list view if the list only displays data. If your app doesn’t allow people to delete and move items, it doesn’t require an Edit button on iOS. In that case you can share the view.

Deleting List Items

iOS

Deleting a list item on iOS requires the following steps:

  • Place the list items in a ForEach block.
  • Add an .onDelete modifier to the ForEach block.
  • Write a function to delete the item from its array.

You can’t apply the .onDelete modifier directly to a list on iOS. That’s why you must place the list items in a ForEach block.

Call the function to delete the item in .onDelete.

The function to delete the item takes an IndexSet as an argument. Call the array’s remove function to remove the item from the array.

Mac

Deleting a list item on Mac requires the following steps:

  • Keep track of the selected item.
  • Add an .onDeleteCommand modifier to the list.
  • Write a function to delete the item from its array.

The usual way to keep track of the selected list item is to add a @State property to the view.

Call the function to delete the item in .onDeleteCommand.

Removing an item from an array in a Mac app is trickier. Call the array’s firstIndex function to find the selected item in the array. If the item is in the array, call the remove function to delete the item.

Moving List Items

iOS

Moving list items on iOS requires the following steps:

  • Place the list items in a ForEach block.
  • Add an .onMove modifier to the ForEach block.
  • Add a function to move the item in its array.

You can’t apply the .onMove modifier directly to a list on iOS. That’s why you must place the list items in a ForEach block.

Call the move function in .onMove

The move function takes two arguments: a location for the source and a location for the destination. Call the array’s move function to move the items in the array.

Mac

Moving list items on Mac requires the following steps:

  • Add an .onMove modifier to the list.
  • Add a function to move the item in its array.

You have to do a little more work on Mac with the .onMove modifier, providing the arguments in the closure to call the move function.

The move function is the same as iOS.

Want More Articles Like This?

This article is an example of the exclusive articles for Swift Dev Journal newsletter subscribers. Subscribers also get a guide on going from tutorials to making your first app, discounts on books, and any benefits I decide to add later.

Use the form in the sidebar to subscribe to the newsletter. The sidebar should be on the right side of the page on desktop or at the end of this article on mobile. If you don’t see the sidebar, go to the Contact page and send me an email.

Passing Data to SwiftUI Views

A common question I see from people learning SwiftUI is how to pass data from one view to another. There are three ways to pass data in SwiftUI apps.

  • Use @State and @Binding property wrappers
  • Use @StateObject and @ObservedObject property wrappers
  • Use @EnvironmentObject property wrapper

You’re going to use the first two ways more than you’ll use @EnvironmentObject.

Use @State and @Binding for Structs

If you use structs for your data, you must use @State and @Binding to pass data. Use the @State property wrapper in the master view and the @Binding property wrapper in any view where you want to pass the data.

Suppose you have a wiki app. The wiki consists of a list of pages. There are two views: the master view has a list of pages, and the detail view shows the contents of the selected page.

The master view needs to store the selected page.

The detail view needs access to the selected page.

The following line of code demonstrates how to pass the selected page from the master view to the detail view:

When using bindings, you must put the $ character before the name of the @State variable.

The master and detail view point to the same page. Any changes you make in the detail view will appear in both views.

I have a demo project on GitHub that provides a full example on using @State and @Binding.

Use @StateObject and @ObservedObject for Classes

Use the @StateObject property to pass a class instance to other views. Using @StateObject has the following requirements:

  • The class must conform to the ObservableObject protocol.
  • Any properties you want automatically updated must use the @Published property wrapper.
  • The SwiftUI views must use the @StateObject and @ObservedObject property wrappers.

Suppose you have a Project class that has a list of issues.

By making the class conform to ObservableObject and using @Published for the issues array, you can use @StateObject in the view that owns the project.

Pass the project variable to any view that needs to access the project. The other views should use @ObservedObject so that all the views are referring to the same project.

Remember that @StateObject is the class equivalent of @State and @ObservedObject is the class equivalent of @Binding.

I have an issue tracking project on GitHub that demonstrates passing data to views using @StateObject.

Use @EnvironmentObject for Data You Want Any View to Access

Create an environment object when you have a piece of data you want any SwiftUI view to access.

A common situation where you would use environment objects is an app that uses Core Data. When working with Core Data, your app needs to access the managed object context to add, update, and delete entities. Apple provides an environment object for the managed object context so you don’t have to create one. Use the @Environment property wrapper to access the managed object context.

If you need to create your own environment object, you must create a class that conforms to ObservableObject.

In the SwiftUI view, use the @EnvironmentObject property wrapper to access your environment object.

Use the .environmentObject modifier to pass the environment object to another view.

Document Types in SwiftUI Apps

SwiftUI supports making document-based apps that let people create and save documents. To ensure the documents have the right file extension and save and load correctly, you must configure the app’s document types. There’s not much information online about document types in SwiftUI apps so I’m sharing what I learned in this article.

Accessing the Document Type Settings

Take the following steps to access the document type settings for your SwiftUI project:

  1. Select the project file from the left side of Xcode’s project window to open the project editor.
  2. Select your app from the target list on the left side of the project editor.
  3. Click the Info button at the top of the project editor.

DocumentTypeSettings

There are three sections of document type settings.

  • Document Types
  • Exported Type Identifiers
  • Imported Type Identifiers

A new SwiftUI document-based app project gives you a plain text editor. Unless you’re making a plain text editor, you must change the document type settings.

Document Types

Every document type in a SwiftUI app requires you to supply the following information:

  • Name
  • Identifier
  • Handler Rank

The name describes the document. Enter a descriptive name in the text field.

The identifier uniquely identifies the document type. An iOS app has a Types text field to enter the identifier. Apple has a list of system defined types for common file types. But you can also define your own type using the following convention:

I found in an app I’m developing that the document had the wrong file extension and type on the Mac until I made the DocumentName part of the identifier match the file extension for the document. I didn’t have this problem on iOS.

The handler rank tells Xcode whether your app owns the document type. In most cases you can choose Default.

Mac document types also require you to choose whether your app can edit documents of that type or just view them. Choose Editor to let people edit documents in your app.

Exported Type Identifiers

You need an exported type identifier for each document type your app owns. If you create a custom document type for your app, you must add an exported type identifier for the custom document type. Do not add exported type identifiers for file types your app exports.

You must supply the following information for an exported type identifier:

  • Description
  • File extension
  • Identifier
  • Conforming identifiers

If you are adding a description for a custom document type, the description usually matches the name of the document type.

Enter the file extension without the leading dot in the Extensions text field.

The identifier should match the identifier for the document type.

You may be able to avoid entering conforming identifiers, but most document types require them. The conforming identifiers are a list of identifiers the type identifier conforms to. For example if you have a custom document type that saves the document in a file package, it must conform to com.apple.package. If you look at the list of system defined types, the Apple System Base Types section has the most common conforming types.

There are also fields to enter a MIME type and a reference URL for the exported type identifier. If your document type has a MIME type or a reference URL, enter them in the appropriate fields.

Imported Type Identifiers

Imported type identifiers are for types your app uses but doesn’t own. Xcode provides an imported type identifier for your document type. If your app imports other file types, you must add imported type identifiers for the other types. If you’re making an image editor, you would add imported type identifiers for the image file formats your app supports, such as PNG and JPEG files.

You supply the same information for imported type identifiers as you do for exported type identifiers so I’m not going to repeat the information here.

SwiftUI Code for Document Types

Open the Swift file for the document struct and you should see the following code:

If you changed the document type, you should replace the string com.example.plain-text with the identifier for your app’s document type. You should also change the variable name exampleText to something that better describes your document type.

Notice that the init for UTType has an importedAs argument. The UTType class reference in Apple’s documentation has a list of inits you can use for your document types.

The readableContentTypes property contains an array of the document types your app can read. There should be an array entry for each imported type identifier in your app. Apple has Swift enums for common document types.

SwiftUI documents also have a writableContentTypes property. This property defaults to matching the readable content types. If your app exports to a type that is not in readableContentTypes, you must add a variable for the writable content types and supply the writable types.

Summary

The most important points about SwiftUI document types are the following:

  • Document types must have a unique identifier.
  • Add exported type identifiers for any custom document types you make.
  • Add imported type identifiers for any file types your app can import.
  • You need a UTType property in your code for the document type.
  • You need to supply a readableContentTypes array in your code with an entry for each imported type identifier.

I have a WikiDemo project on GitHub that has its own document type. Clone the project to examine the settings for the document type, exported type identifiers, and imported type identifiers.