Category: Mac

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 Inteface 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 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 don’t currently have a sample project to demonstrate list item removal. If you want to see an example of a SwiftUI Mac app that lets you remove items from a list, take a look at the Feed Read example by TrozWare.

Resources for Learning Combine

Combine is Apple’s framework for handling asynchronous events in Swift. Combine and SwiftUI are the future for making iOS and Mac apps. This post lists resources for learning Combine.

Books

Websites

I don’t know of any websites focused solely on Combine, but the following websites have articles on Combine:

Saving Data When Your App Quits

A common scenario for iOS and Mac apps is to save data when someone quits the app. You have written a function to save the data. Where do you make the call when the app quits? This article answers that question, both for the SwiftUI app life cycle and the classic AppDelegate life cycle.

Using the SwiftUI App Life Cycle

In Xcode 12 Apple introduced a native SwiftUI app life cycle. The new life cycle has a scenePhase environment value that stores the app’s current phase, such as active, inactive, or background.

Add a property to the app struct for the scene phase. Add the .onChange modifier to the app’s window group or document group. Check if the phase is .background. If it is, make a call to save the data. The following code shows how to check when the app goes in the background:

Using the AppDelegate Life Cycle

If your Xcode project has a file named AppDelegate.swift, it is using the AppDelegate life cycle. Open the AppDelegate.swift function, and you should see multiple empty functions for handling app events.

An iOS app that wants to save when someone quits the app should make a call to save the data in the function applicationDidEnterBackground. This function is called when the person quits your app.

A Mac app should make the save call in applicationWillTerminate.

Using Environment Variables in Swift Apps

When to Use Environment Variables

Most Swift apps do not need environment variables. A common reason to use environment variables is for apps that call a website’s REST APIs. To gain access to someone’s account on the website, your Swift app uses a client ID and client secret from the website. Storing the client ID and client secret in environment variables is safer than storing them as strings in your code.

Suppose you are developing a git client app. Your app wants to push commits to GitHub. To push commits to GitHub you must create an OAuth app for your app in GitHub. The GitHub OAuth app contains a client ID and a client secret that your Swift app uses to authenticate the user so your Swift app can access the person’s GitHub account. GitHub recommends using environment variables to store the client ID and client secret.

Add the Environment Variables from Xcode’s Scheme Editor

The first step to using environment variables is to add them to your Xcode project. Use Xcode’s scheme editor to add environment variables to your app. Use the jump bar next to the Run and Stop buttons in the project window toolbar to open the scheme editor.

XcodeSetEnvironmentVariablesBlurred

Select the Run step on the left side of the scheme editor. Click the Arguments button at the top of the scheme editor to access the environment variables. Use the Add (+) button to add the environment variables.

Accessing Environment Variables in Your Swift Code

After adding environment variables, you can access them in your code. Use the ProcessInfo class to access environment variables. The class has a processInfo property that contains a dictionary of values. The name of the environment variable is a key in the dictionary. Use that key to get the environment variable’s value. The following code demonstrates how to access a variable containing a GitHub OAuth app’s client ID:

Make a Markdown Editor in SwiftUI

In this tutorial you will make a Markdown editor with live preview using SwiftUI. This editor will run on both iOS and Mac. It’s a nice example to demonstrate how to create a document-based, multi-platform SwiftUI app. Plus, it shows how to use a WebKit web view in SwiftUI.

This tutorial uses the new document-based app features Apple added in Xcode 12. You must be running Xcode 12.2+ to create a multi-platform SwiftUI app. The iOS app requires iOS 14+, and the Mac app requires macOS 11+. If you are running macOS 10.15 (Catalina) on your Mac, you can still make the editor. You won’t be able to run the Mac version.

To keep the tutorial from getting too long, I will gloss over some steps and explanations that I covered in the following articles:

Create the Project

Start by creating the project.

  1. Choose File > New > Project in Xcode.
  2. Click the Multiplatform button at the top of the New Project Assistant.
  3. Choose Document App from the list of application templates.
  4. Click the Next button.
  5. Name the project in the Product Name text field.
  6. Click the Next button.
  7. Choose a location to save the project.
  8. Click the Create button.

If you look at the project navigator, you will see Xcode creates the following folders for your project:

  • The Shared folder contains files that both the iOS and Mac app targets use.
  • The iOS folder contains files that the iOS app uses.
  • The Mac folder contains files that the Mac app uses.

In the Shared folder are Swift files for the app, document, and content view.

Run the project and you will see that you have a working plain text editor. You can create documents, edit text, save documents, and load documents.

Add the Ink Package

This project uses the Ink Markdown parser to convert the Markdown you type in the text editor to HTML that you will preview. Ink has Swift Package Manager support to simplify adding it to your Xcode project.

Select the name of your project from the project navigator to open the project editor. Select the project from the project editor and click the Swift Packages button at the top of the editor to add the Ink package.

A current limitation in Xcode’s Swift Package Manager support is you can add a package to only one target initially. To add Ink to the other app target, select the target from the project editor and add the Ink framework from the Frameworks, Libraries, and Embedded Content section.

AddSwiftPackageToSecondTarget

Add Web Views

To preview the Markdown you write, you need a web view to display the HTML. SwiftUI does not currently have a native web view so you must use a WebKit web view, WKWebView, for the preview.

WKWebView has both iOS and Mac support, but you’re going to have to create two web view files: one for the iOS app and one for the Mac app. The code is going to be almost identical. The reason you need two files is that SwiftUI has separate protocols for working with UIKit(iOS) and AppKit(Mac) views.

Create one new Swift file in the iOS folder in the project navigator and create a second file in the Mac folder. Make sure the file in the iOS folder is a member of the iOS app target. Make sure the file in the Mac folder is a member of the Mac app target. You can set the target membership for a file using the File Inspector on the right side of the project window.

Add the following code to the iOS Swift file:

The web view uses the SwiftUI and WebKit frameworks so you must import them. When you use a UIKit view in a SwiftUI app, the view must conform to the UIViewRepresentable protocol. The html property stores the HTML to display in the web view.

There are three functions. The init function initializes the web view with some HTML to display. The makeUIView function creates the web view. The updateUIView function loads the HTML string to show in the web view.

The Mac code is almost identical. Replace anything starting with UI with NS.

Parsing the Markdown

The next step is to write some code to parse the Markdown to HTML. Open the ContentView.swift file. Import the Ink framework.

Add the following computed property to the ContentView struct:

This code creates an Ink parser and calls a function that converts the document text into HTML. This code is much easier to write than creating your own Markdown parser.

Build the User Interface

The last step is to build the user interface. The user interface contains a text editor and a web view side by side.

The body property for the content view should look like the following code:

The code creates a horizontal stack so the text editor and web view are side by side. The text editor is on the left and contains the document’s text. The web view is on the right and displays the HTML you parsed from the Markdown text. The iOS app uses the iOS version of the web view. The Mac app uses the Mac version of the web view.

Run the project. Any Markdown you type in the text editor will appear as HTML in the web view. The Ink parser does not currently support creating code blocks with indentation. You must add three backticks on the line above and below the code to create a code block.

I have the project on GitHub for you to look at if you run into problems.

How to Notarize a Mac App

Notarizing a Mac app allows it to work better with Apple’s Gatekeeper feature in macOS. If you do not notarize your app, when someone downloads the app and tries to run it, an alert will open saying the app is from an untrusted developer. This alert may make people afraid to run your app. Notarizing your app keeps that alert from appearing and makes it easier for people to run your app. In this article you will learn how to notarize your Mac app so you can distribute it from your own website.

Prerequisites

You must have a paid developer account with Apple to notarize apps.

Your app must have the Hardened Runtime capability enabled to notarize it. Open the project editor and click the Signing & Capabilities button to see if you have enabled the Hardened Runtime capability

Hardened Runtime

High Level Notarization Steps

Take the following steps to notarize your Mac app:

  1. Archive your project in Xcode by choosing Product > Archive.
  2. Open Xcode’s Organizer window by choosing Window > Organizer.
  3. Select your archive from the list of archives.
  4. Distribute the app.
  5. Choose a distribution method.
  6. Choose a destination.
  7. Choose a code signing option.
  8. Review and upload.
  9. Wait for Apple to notarize.
  10. Export the notarized app.

I will go into more detail on Steps 3–10 in the rest of the article.

Finding Your Archive and Distributing

In Xcode 12 the Organizer window looks similar to the following screenshot:

ArchiveListInOrganizer

In the upper left window is a menu with a list of your archived apps. Choose your app from the list. Click the Archives item in the Organizer sidebar to see a list of archives for your app. Select an archive from the list.

Click the Distribute App button on the right side of the Organizer to start the process of notarizing your app.

Choose a Distribution Method

When you click the Distribute App button, a sheet opens with a list of distribution methods.

DistributionMethod

To notarize your app and upload it to your website, you should choose Developer ID. Click the Next button.

Choose a Destination

The next step is to choose a destination.

Destination

There are two destination options: Upload and Export. You must choose Upload to notarize the app. Click the Next button.

At this point, another sheet may appear with a checkbox to receive symbolicated crash reports from Apple. Select that checkbox and click the Next button.

Choose a Signing Option

Apple provides two methods to manage the signing of your app: Automatic and Manual.

SigningOptions

I recommend automatically manage signing. Click the Next button.

At this point you will be asked for your Mac user’s password. Enter it.

Review and Upload

Now you can review options for notarizing the app.

ReviewContent

If you find a mistake, click the Previous button to go back and fix the mistake. When things look good, click the Upload button to upload your app to Apple to notarize.

Wait for Apple to Notarize

If the upload is successful, Apple will send you an email when they finish notarizing the app. Normally notarizing takes 20–30 minutes.

Export the Notarized App

When the app is notarized, a button with the title Export Notarized App appears on the right side of the Organizer. You may need to scroll the window to see the button. Click that button to export the app to a location on your Mac. When you finish exporting, you can upload the app to your website for people to download.