Category: Mac

Xcode Multiplatform App Targets

Starting in Xcode 14, when you create a multiplatform app project, Xcode creates a single app target with destinations for each platform you want to support: iPhone, iPad, Apple TV and Mac. This article provides an introduction to multiplatform app targets.

When to Use the Multiplatform App Target

Use the Multiplatform App Target if you want people to buy one version of your app on the App Store and run the app on all the platforms you support: iPhone, iPad, AppleTV, and/or Mac. Use separate targets if you plan to charge for each platform separately.

If you plan to sell a Mac version of your app outside the App Store, create a separate app target. Use the multiplatform app target to sell on the App Store and the Mac app target to sell outside the App Store.

Viewing, Adding, and Removing Platform Destinations

Select the target from the target list on the left side of the project editor and click the General button at the top of the project editor to see a list of the app’s platform destinations:

AppTargetSupportedDestinations

The supported destinations list has buttons to add and remove destinations. If you created a project in an older version of Xcode, you must add destinations to have a single app target that supports iPhone, iPad, AppleTV, and Mac. Older versions of Xcode have separate targets for each platform.

Mac Destination Choices

When adding the Mac destination to an app target, you have the following choices:

  • Mac
  • Mac Catalyst
  • Designed for iPad

Choosing Mac uses SwiftUI and/or AppKit for the Mac app. Mac is the best choice for a new app, especially one that uses SwiftUI.

Choosing Mac Catalyst uses UIKit for the Mac app. Mac Catalyst is the best choice if you want to make a Mac version of an existing iOS app.

Choosing Designed for iPad runs the iPad version of the app on Macs with Apple Silicon chips. Choose Designed for iPad if you want a Mac version of your iOS app and don’t want to do any work converting the app.

Choosing the Platform to Build and Run

Xcode can build and run for only platform at a time. How do you specify the platform to build and run?

There’s a jump bar in the project window toolbar.

ChoosePlatformToRun

Click the right part of the jump bar to choose the platform: Mac, a connected iOS device, or an iOS simulator.

Compiling Files for Specific Platforms

Previous versions of Xcode have separate targets for each platform. If you have separate targets with code that should be compiled for a specific platform, make the file a member of that target. But you can’t do that with the multiplatform app target because there’s only one target. What do you do if you have platform-specific source code files?

Tell Xcode what platforms a source code file should compile for. Click the Build Phases button at the top of the project editor and examine the Compile Sources build phase.

FilterDestinations

Xcode initially sets each source code file to build for each destination. Click on the Filter column for a source code file to open a popover.

PlatformFilterPopover

Deselect the Allow any platform checkbox and deselect the destinations you don’t want to use. Now that file compiles only for the platforms you specified.

Using the Sparkle Framework in a Sandboxed App

Sparkle is a framework that simplifies updating Mac apps that are not on the Mac App Store. There is a guide for using Sparkle in a sandboxed app, but I found parts of it unclear so I’m writing about it in this article.

It is very important to set things up correctly with the initial version of your app. If you forget to add something to either the entitlements file or the Info.plist file, people will see the following alert when they try to install an update for your app:

SparkleUpdateErrorAlert

Overview

You have to perform one or more of the following steps to properly add Sparkle support to a sandboxed app:

  • Add the Installer Service (required)
  • Add the Installer Connection and Status Services (probably required)
  • Add the Downloader Service (maybe)

Most sandboxed apps only need to perform the first two steps. You may have to do more if you have more complex update requirements. See the sandboxing guide for those cases.

Add the Installer Service

To add the Installer Service to your app, you must add the following entry to the Info.plist file:

Add the Installer Connection and Status Services

To add the Installer Connection and Status Services to your app, you must add the following entry to your app’s entitlements file:

The entry is an array with two string items.

Add the Downloader Service

If you have the Outgoing Connections checkbox selected in the App Sandbox settings, you can skip this section. You’re done.

If your app does not allow outgoing connections (the Outgoing Connections checkbox is not selected), you must add the Downloader Service to update your app with Sparkle. Add the following entry to the Info.plist file:

Further Reading

Accessing the Sparkle Binary from its Swift Package

A common task when using the Sparkle framework is to update the appcast when you release an update to your app. Running the command to update the appcast requires access to the Sparkle binary. Finding the Sparkle binary can be difficult if you added Sparkle using the Swift Package Manager. But by using Xcode and the Finder, you can create a Terminal window in the right location to run Sparkle commands.

Overview

Running a Sparkle command when using the Swift Package Manager requires the following steps:

  1. Locate the Sparkle binary folder in the Finder.
  2. Go to the Sparkle binary folder in the Terminal.
  3. Run the Sparkle command.

Locate the Sparkle Binary Folder in the Finder

The easiest way to find the Sparkle binary folder is to use Xcode. The project navigator has an entry for Sparkle in the Package Dependencies section. Click the disclosure triangle next to the Sparkle entry.

AccessingSparkleToolsFromSwiftPackageManager

The Sparkle binary is in the Referenced Binaries section. Select the Sparkle binary, right-click, and choose Show in Finder to open the Sparkle binary folder in the Finder.

Go to the Sparkle Binary Folder in the Terminal

In the Finder you have to navigate one folder above the Sparkle folder to open the Sparkle folder in the Terminal. In the Finder toolbar is the name of the current folder, which should be Sparkle. Select the folder name, right-click and choose artifacts to move to the artifacts folder.

In the artifacts folder, select the Sparkle folder, right-click, and choose Services > New Terminal at Folder. A new terminal window will open at the correct location to run Sparkle commands.

Run the Sparkle Command from the Terminal

Now that you are in the right folder in the Terminal, you can run Sparkle commands. Since updating the appcast is the command you’ll run the most, I’ll use that as an example. To update the appcast, run the following command:

Where the path is the path to the folder where the appcast.xml file and the app file you are distributing (the .zip or .dmg file) reside on your Mac. I recommend placing those files in a place that is easy to reference so you don’t have to type an insanely long path to update the appcast.

Saving Passwords in the Keychain in Swift

The Keychain is the place to store small amounts of data securely, such as passwords and API tokens. In this article you’ll learn how to work with the Keychain in iOS and Mac apps using the Keychain Services framework.

Parts of a Keychain Action

There are four common actions to perform on keychain items: add an item, update an item, read an item, and delete an item. To perform a keychain action, create a query and run a Keychain Services function.

Query

A query tells Keychain Services what you want to do. A query is a Swift dictionary that you must cast to CFDictionary.

Every query must include an entry with the key kSecClass, which specifies the kind of item to add, update, read, or delete. There are the following Keychain item types:

  • Generic password, kSecClassGenericPassword
  • Internet password, kSecClassInternetPassword
  • Certificate, kSecClassCertificate
  • Cryptographic key item, kSecClassKey
  • Identity item, kSecClassIdentity

I’m going to focus on generic passwords in this article, as that’s what most apps use. You’re not limited to passwords when using generic passwords. I was able to use generic passwords to store OAuth tokens in the Keychain.

The query keys you can use depend on the class. You can find a list of possible keys in the following section of Apple’s documentation:

Item Class Keys and Values

Click the link for a class to see the available items. You can also read the documentation in Xcode by choosing Help > Developer Documentation.

Two common item attributes for generic passwords are services, kSecAttrService, and accounts, kSecAttrAccount.

The value you supply for the service is the text that appears for the item in the Keychain Access app on Mac. Make sure the text clearly shows the keychain item is part of your app. A generic value like password will be hard to find in the Keychain Access app.

The value for the account is the name of the password’s account. If you’re writing a Twitter client, Twitter would be a good value for the account.

Keychain Services Functions

Call the following functions to work with the Keychain for passwords:

  • SecItemAdd to add an item to the Keychain
  • SecItemUpdate to update an existing Keychain item
  • SecItemCopyMatching to read an item from the Keychain
  • SecItemDelete to delete an item from the Keychain

Adding a Keychain Item

Start by importing the Authentication Services framework. The Keychain Services API is in the Authentication Services framework.

You should also create a class for the Keychain functions.

Let’s start by writing a function to add an item to the Keychain.

The function starts by building a query. Tell Keychain Services that you’re adding a generic password. Supply the data, service, and account. The data is what you want to save in the Keychain.

After building the query, call the function SecAddItem to add the item to the Keychain. If the item is added to the Keychain successfully, SecAddItem returns the value errSecSuccess.

If the item already exists in the Keychain, SecAddItem returns the value errSecDuplicateItem. In this case update the existing item, which I cover next.

Updating an Existing Keychain Item

Let’s look at the code to update a keychain item.

Notice that the update function doesn’t include the data in the query. The code creates another dictionary for the data and passes it as an argument to the function SecItemUpdate.

Reading an Item from the Keychain

Saving items to the Keychain isn’t going to help unless your app can read the keychain items.

The big difference in the query is the kSecReturnData key, which tells Keychain Services you want to return data from the function call. The function SecItemCopyMatching returns its results as type AnyObject?. You saved the item as Data so you should return a Data object. If there is no matching item in the Keychain, SecItemCopyMatching returns nil.

Deleting an Item from the Keychain

The last major task to perform on Keychain items is to delete items from the Keychain.

The query for deleting an item is the simplest one. Supply the class, service, and account. Call SecItemDelete to delete the item from the Keychain.

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:

Using New API Features in Swift While Supporting Older OS Versions

Every year at WWDC Apple adds new features to their developer SDKs. Usually these new features require the upcoming version of iOS and macOS. You would like to use the new features while also supporting older versions of iOS and macOS. How do you do this?

Apple provides two Swift keywords to use both new code and old code in your apps: #available and @available.

#available

The #available keyword works with if and guard statements. Supply an operating system version version, and the code inside the if block (or after the guard) executes only on machines capable of running the code. Use the else block for code to run on older operating systems. The following example shows how to run one block of code on iOS 15 and another block on earlier iOS versions:

The following example shows how to run one block of code on macOS 12 and another block on earlier macOS versions:

What Does the Asterisk Do?

The asterisk in the last argument to #available tells the compiler to require the minimum deployment target for other platforms. You must always supply the asterisk to support future platforms Apple may add.

Checking for Multiple Platforms

Suppose you have a multiplatform SwiftUI project and you want to conditionally run code for iOS 15 and macOS 12. Supply the second platform operating system as an additional argument to #available. The following example checks for both iOS 15 and macOS 12:

guard Statements

Using available with a guard statement works best with code that should only run on newer versions of iOS and macOS. You can exit and do nothing on machines running on older operating systems.

@available

The @available keyword lets you mark a function, struct, or class as being available only on certain versions of iOS and macOS. Supply the same information as #available: an operating system version and an asterisk. The following code demonstrates the use of @available in a function:

Struct example:

Class example with multiple platforms:

Using File Wrappers in a SwiftUI App

What Is a File Wrapper?

A file wrapper is a bundle, which is a collection of one or more directories (folders) and files that appears as a single file in the Finder (Mac) or Files app (iOS). Most Mac applications use bundles. If you want to see what a bundle looks like, select an application, right-click, and choose Show Package Contents.

When should you use a file wrapper? Use a file wrapper when you want to save your app’s data in multiple files. Suppose you’re developing a website building app. A website can have multiple pages, plus folders and files for images, videos, and custom CSS. By using a file wrapper you can save all these files and have it look like a single file to the person using the app. Most apps don’t need to use file wrappers

Types of File Wrappers

The FileWrapper class has the following file wrappers:

  • Directory
  • Regular file
  • Symbolic link

The symbolic link file wrapper points to a file. The most common case of using a symbolic link file wrapper is to point to a large file (image, audio, or video file) to keep the wrapper from getting too big. I’m going to focus on directory and regular file wrappers in this article.

Making Your File Wrapper Appear as a Single File

Document-based apps are more likely to use file wrappers than shoebox apps. If you forget to configure the document to be a file wrapper, the document will appear as a folder instead of a single file.

To configure the document to be a file wrapper, perform the following steps:

  1. Select the project from the project navigator to open the project editor.
  2. Select the app from the Targets list in the project editor.
  3. Click the Info button at the top of the project editor.
  4. Click the disclosure triangle next to the Exported Type Identifiers section in the project editor.
  5. Enter com.apple.package in the Conforms To text field.
  6. Click the disclosure triangle next to the Imported Type Identifiers section in the project editor.
  7. Enter com.apple.package in the Conforms To text field.

Creating a Document File Wrapper

You must create a file wrapper when saving the document. If you create a document-based SwiftUI app, you should see the following function in the document struct’s Swift file:

This function is called when saving the document. Your code to create the file wrapper goes in this function.

To create a file wrapper you must perform the following tasks:

  • Create a directory file wrapper
  • Convert your app’s data to a Data object
  • Create a regular file wrapper
  • Add the file to a directory file wrapper

Creating a Directory File Wrapper

At a minimum you must create a root directory for the file wrapper. You will return this wrapper in the call to fileWrapper. To create a directory file wrapper, call the FileWrapper method directoryWithFileWrappers and supply an empty Swift dictionary.

Call directoryWithFileWrappers and supply an empty dictionary for any additional directories you want to create.

The root directory of a wrapper does not need a name because it does not appear in the Finder or Files app. But any other directories you create require a name. Set the preferredFilename property to name the directory.

Call the addFileWrapper method to add the directory to the root directory.

Convert App Data to a Data Object

Files in file wrappers store their data in a Data object. You must convert your app’s data to Data to use file wrappers. Converting to Data depends on what you are storing, but the following code converts a string to Data:

Creating a Regular File Wrapper

To create a file wrapper for a regular file, call the FileWrapper method regularFileWithContents and supply the Data object that contains the file’s data.

Set the preferredFilename property to name the file.

In a real app you won’t be hardcoding filenames often. Suppose you have a document that has a list of pages. You would use the page’s title as the filename instead of giving the file a specific name. Remember that you use file wrappers to save multiple files. If you have 20 files to save, hardcoding the name of each file is going to be a pain. The following code demonstrates how to save a collection of text files:

Call addFileWrapper to add a file to a directory.

Reading from a File Wrapper

At this point you know how to create a file wrapper and save data to it. The next step is to read data from the file wrapper when opening a document.

The SwiftUI document structure provides the following initializer to read data from a file wrapper:

This initializer is where you add the code to read from the file wrapper. To read from a file wrapper, you must perform the following tasks:

  • Access the directory holding the files
  • Read the individual files
  • Load the data from the file

Access the Directory Holding the Files

If you store all the files inside the root directory, you won’t need to write any code to access the directory. But if you have files inside a subdirectory, you must call the fileWrappers method and supply the name of the subdirectory as the dictionary value.

The SwiftUI initializer takes a read configuration as an argument. The ReadConfiguration struct has a file property that provides access to the root directory of the file wrapper.

Reading the Individual Files

Use the fileWrappers property to access the files in a directory file wrapper. Go through each file and load the data. Use the regularFileContents property to access the file contents.

Loading the Data

The file wrapper’s regularFileContents property gives you a Data object to load the file’s contents in your app. Loading the file’s contents depends on the type of data being stored. The following code loads a file’s contents into a string variable:

Sample Project

I have a sample project on GitHub that demonstrates saving data to a file wrapper. Look at the files Wiki.swift and Page.swift.

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.