Category: SwiftUI

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 property wrapper
  • 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 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 property wrapper.

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 any view that needs to access the project.

Pass the project variable to any view that needs to access the project.

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.

Disable a Text Field in a SwiftUI List Until Tapping Edit Button

SwiftUI lists support using text fields to change the name of list items. But if your list items are navigation links, you will run into some annoying behavior on iOS. You tap a list item expecting to go to the navigation link destination, but you end up changing the name of the list item.

When you tap a list item that is a navigation link, you normally want to go to the link destination. But you still want to be able to rename list items. In SwiftUI iOS apps, you can disable the list item’s text field until someone taps the Edit button. In edit mode you can tap the list item to rename it. When the person taps the Done button, disable the text field.

Variables to Add to the SwiftUI View

Start by adding two variables to your SwiftUI view that has the list. The first variable is an environment variable for the editing mode. This variable tracks when someone taps the Edit and Done buttons.

The second variable is a Boolean value that tracks if the text field is disabled. The name disabled is used by SwiftUI. I got a compiler error when I named the variable disabled so use another name for the variable.

The text field should be disabled until someone taps the Edit button.

Modifiers to Add to the Text Field

Add two modifiers to the text field. The first modifier is the .disabled modifier. Supply the Boolean variable you added to the view.

The second modifier is the .onChange modifier. The change you’re going to track is the change in edit mode. When someone taps the Edit button, the isEditing property is set to true. Set the Boolean variable to false, which enables editing in the text field. When someone taps the Done button, isEditing becomes false. Set the Boolean variable to true, which disables editing in the text field.

Sample Project

I have a sample project on GitHub. Look at the PageListView.swift file in the iOS folder to see the code for disabling the text fields in the list.

Moving List Items Using Drag and Drop in SwiftUI Mac Apps

Mac apps usually use drag and drop to rearrange items in a list. You search for how to move list items in SwiftUI, and you find a bunch of examples for iOS that use the .onMove modifier. But when you start to type .onMove in your Mac list, Xcode’s autocomplete shows only an .onMoveCommand modifier. If you try to use .onMove directly on a list in a Mac app, you’ll get a compiler error.

SwiftUI has an .onMoveCommand modifier to move items in a list in a SwiftUI Mac app, but it works with the keyboard, not with drag and drop. How do you use drag and drop to move items in a SwiftUI list in a Mac app?

.onMove Requires ForEach on Mac

The .onMove modifier is the modifier to use to move list items using drag and drop in SwiftUI Mac apps. But you must place a ForEach statement inside the list to enable .onMove. The ForEach statement takes two arguments: the array of items you’re showing in the list and an ID that uniquely identifies each list item.

Apply the .onMove modifier to the ForEach block. The .onMove modifier’s closure takes two arguments: the indices for the list items being moved and the drop destination. Call the array’s move method to reorder the list items.

The following code creates a list of a book’s chapters that you can rearrange using drag and drop:

Accessing the Document in a SwiftUI Menu

You’re making a SwiftUI document-based Mac app. You have a menu item in your app that performs an action on the document. How do you give the menu access to the document?

Use focused values and focused bindings.

Creating a Focused Value

A focused value provides a way for a SwiftUI app to observe values from the focused view or one of its ancestors. In a document-based SwiftUI Mac app, one of the focused view’s ancestors is the document window. By using a focused value your app can access the focused document.

To create a focused value, you must write two pieces of code. The first is a struct that conforms to the FocusedValueKey protocol. The second is a property for the document as an extension to the FocusedValues struct.

The struct creates a type alias for the document’s type, which is usually a binding to your document type.

The document variable is the value you will use when setting the focused value. The value must match this variable name.

Notice the type of the variable, DocumentFocusedValueKey.Value?. The first part of the type is the name of the struct you created for the focused value. The second part is the name of the type alias you created in the struct.

Setting a Focused Value

The .focusedValue modifier sets a focused value.

The name of the first argument must match the name of the variable you created in the FocusedValues extension.

Apple added the .focusedSceneValue modifier in iOS 15 and macOS 12. This modifier works much better than .focusedValue, which requires the person using the app to be focused on a text view or text field. If you can require iOS 15 and/or macOS 12, use .focusedSceneValue.

Where do you place the code to set the focused value? Attaching the focused value modifier to the content view in the app file is the most common place in a document-based SwiftUI app.

Focused Bindings

A focused binding gives a SwiftUI view access to the focused value. To create a focused binding, declare a variable with the @FocusedBinding property wrapper and supply the name of the focused value in parentheses.

Using Focused Bindings in SwiftUI Menus

To use a focused binding in a SwiftUI menu, you must create a SwiftUI view for the menu items. Create the menu items in the group. The following example shows a menu with items to make text bold and italic:

Creating Menu Commands

If you have a menu with multiple menu items, creating a set of menu commands lets you keep the menu creation code in its own Swift file. To create a set of menu commands, supply the views inside the CommandMenu block.

Add the Menu to the App

The final step is to add the menu to the app.

By creating a struct for the menu commands, the code inside the app’s body is much cleaner.

Credits

I learned much of the information shared in this article from the following article by Lost Moa:

Accessing the document in the SwiftUI macOS menu commands

Creating a Master-Detail Interface in SwiftUI

Master-detail interfaces are common in iOS and Mac apps. Select an item from the master view to see additional information in the detail view. In SwiftUI you use two views to create a master-detail interface: NavigationView and NavigationLink.

A navigation view is the most common way to create a master-detail interface in SwiftUI. On iOS selecting an item from the master view, which is usually a SwiftUI list, takes you to the detail view. On macOS selecting an item from the master view shows the details in the detail view.

The following code shows how to create a navigation view:

In a real app you will pass data as arguments to the master view and detail view. The data depends on your app, which is why I didn’t supply any arguments in the code listing.

The article Passing Data to SwiftUI Views provides more detailed information on passing data to views.

The final part of creating a master-detail interface is to add a navigation link in the master view and set its destination to the detail view.

This example creates a navigation link for each item in the list. Selecting an item from the list fills the detail view with information about the selected item. In a real app you will pass the data to show in the detail view as an argument to the detail view.

Handling Selection

There are some situations where you may need to hold on to the selection. Mac apps need access to the selected item when deleting an item from a list.

In the master view create a property to store the selection. Make it optional and set its value to nil.

When someone selects an item from the list, the selectedItem property will store it. In the call to create the navigation link, add the tag and selection arguments. The tag is usually the current item. The selection is the selected item.

Sample Project

I put a sample project on master-detail interfaces on GitHub. It builds upon the project from the Make a Markdown Editor in SwiftUI article by displaying a list of Markdown pages to edit. Look at the files ContentView.swift and PageListView.swift for the code on making the master-detail interface.

Using a Scrollable Text View in a Mac SwiftUI App

In the article Using Text Views in a SwiftUI App I showed how to use a UIKit text view in a SwiftUI app. But if you use the code from that article in a Mac app, you’ll run into a problem. The text view won’t scroll when you reach the bottom of the text view. In this article you’ll learn how to make the text view scroll in a Mac app.

Much of the code for this article comes from the CodeEditor Swift package, which is a text editor for SwiftUI with syntax highlighting.

Create a Scroll View

The reason the text view won’t scroll is that Mac text views aren’t wrapped in scroll views like iOS text views are. When you create the text view, you have to create both a text view and a scroll view. Set the scroll view’s document view to the text view. Give the scroll view a vertical scroller. Return the scroll view.

Setting the autoresizing mask ensures the text view resizes properly when the window resizes.

Updating the View

When writing the updateNSView function, supply the scroll view as the argument. Use the documentView property of the scroll view to access the text view and update its contents.

Removing Items from SwiftUI Lists in Mac Apps

Most examples of removing items from SwiftUI lists use the .onDelete handler, which is not available for Mac apps. In this article I share what I learned to remove list items from SwiftUI Mac apps.

To remove items from SwiftUI lists in Mac apps, you must perform the following tasks:

  • Add a variable to the list view to store the selected list item.
  • If you are using a navigation link, supply a tag and selection when creating the link.
  • Make your struct or class conform to the Equatable and Hashable protocols.
  • Add the .onDeleteCommand handler to the list. The .onDeleteCommand handler is the handler SwiftUI Mac apps use to remove list items.

Add a Selection Variable to the List View

To remove an item from a SwiftUI list, the list view requires a variable to store the item you want to remove. Create an optional for the variable and set it to nil initially.

ListItemStruct is the name of the data structure in your app that you want to show in the list.

When you supply this selection when creating a navigation link, SwiftUI keeps track of the selected item in the list.

Most SwiftUI apps that use lists use a navigation link to create master-detail interfaces. Select an item from the list to show additional information in the detail view. Add a call to NavigationLink in the master view and set its destination to the detail view.

To support more complex selection behavior, you must supply two additional arguments to the navigation link call: tag and selection. Usually the tag is the current list item you’re adding to the list. The selection is the variable you added to the list view.

The following code demonstrates how to show a list of a book’s chapters:

This example uses the improved syntax Apple added in Xcode 13 to bind list text fields to items in an array. If you’re using Xcode 12 you will have to use a Text to display the titles and remove the $ character from $chapter.

Make Your Struct/Class Conform to Equatable and Hashable Protocols

To use the tag and selection arguments in a navigation link, your struct or class must conform to the Equatable and Hashable protocols. Your project won’t compile until you make the struct or class conform to those protocols.

Making your struct or class conform to Equatable requires you to implement the == operator to check for equality.

Replace Chapter with the name of your struct or class. Do whatever comparisons you need to make to determine that two objects are equal. SwiftUI list items require a unique ID. That’s what I used to determine equality in the example.

To conform to the Hashable protocol, you must implement the hash function.

Inside the hash function, call the hasher’s combine function for each property in your struct or class. Supply the name of the property.

Add the .onDeleteCommand Handler to the List

The last step is to remove the item from the list. Add the .onDeleteCommand handler to the list to enable the Delete menu item in the Edit menu. Inside the block of code, you will use the selection variable you added to the list view to find the selection index. Use the selection index to remove the item from the list.

The firstIndex function returns the first selected item in the list. After getting the index of the selected item, remove that item from the array that populates the list.

Now let’s put the whole list view together.

Removing an Item with a Button and the Delete Key

At this point you can remove a list item by choosing Edit > Delete. But you may want to provide a button to remove an item. How do you remove list items by clicking a button?

Start by moving the code inside the .onDeleteCommand handler into its own function. Moving the deletion code into a separate function will also make the list code cleaner.

Now the .onDeleteCommand handler looks like the following:

Call the function in your button. Add a .keyboardShortcut handler to the button to remove list items with the Delete (Backspace) key.

I haven’t figured out how to add a keyboard shortcut for the Delete menu item in the Edit menu. Every example I’ve seen on creating keyboard shortcuts for menu items in SwiftUI uses custom menu items, not the menu items that Apple supplies. I’ll update the article if I ever find a solution.

Sample Project

I have a project on GitHub that supports removing items from lists. Look at the PageListView.swift and Wiki.swift files for the list removal code.

Another sample project for SwiftUI list item removal is the Feed Read project by TrozWare for the Back to the Mac conference.

Should I learn Swift or SwiftUI?

I see a lot of new iOS developers ask the question in the title of this article. The short answer is both. To write SwiftUI apps you must also learn Swift because Swift is the programming language SwiftUI uses. The rest of this article provides a more detailed explanation.

UIKit

In 2008 Apple gave developers a SDK (Software Development Kit) to let them make native iOS apps. The heart of the SDK was UIKit, a framework for creating the views and controls in iOS apps, such as buttons, text fields, and table views.

UIKit uses the Objective-C programming language. Using Objective-C worked well for existing Mac developers, who were already using Objective-C to make Mac apps. But the release of the iOS SDK attracted lots of new developers to the Apple developer community. These new developers found Objective-C’s syntax to look strange and found it difficult to understand. They wanted a language with a syntax that looked more familiar.

Swift

In 2014 Apple unveiled their solution for developers who hated Objective-C: the Swift programming language. Swift’s syntax looked more familiar to people who came to iOS development from web development and Windows development. Now iOS developers had two language choices for making iOS apps with UIKit: Swift and Objective-C.

SwiftUI

In 2019 Apple released the initial version of SwiftUI, a new framework for developing apps for all of Apple platforms: iOS, Mac, tvOS, and watchOS. SwiftUI has only one language: Swift. You must know Swift to use SwiftUI.

The Language and Framework Options for iOS Apps

Someone who wants to make iOS apps with Apple’s frameworks has three options.

  1. Use UIKit with Objective-C.
  2. Use UIKit with Swift.
  3. Use SwiftUI with Swift.

Which option should use choose? You won’t be making a mistake no matter which option you choose. But for someone new to iOS development, I recommend Option 3, write the app with SwiftUI using Swift. SwiftUI is the future of iOS development. You’re eventually going to have to learn it so you might as well learn it now.