Category: Mac

Creating a SwiftUI Toolbar Button with an Image and a Label

Many toolbar buttons in Mac and iOS apps include an image and a label. The Mac apps Finder, Mail, and Pages are examples of apps whose toolbar buttons have an image and a label.

If you try to create a button in a SwiftUI toolbar with an image and a label, you will notice the label does not appear. You must do the following to create a toolbar button with an image and a label:

  • Create a label style for the button.
  • Add a .labelStyle modifier to your button label and use the label style you created.

Creating a Label Style

To create a label style, you must create a struct that conforms to the LabelStyle protocol. Add a makeBody function to the struct that returns a SwiftUI view.

A label style for a toolbar button should create a VStack with the image first and the text label second. Supply a font to use for the image and the label in the VStack.

Add the .labelStyle Modifier

After creating the label style for your toolbar button, add a .labelStyle modifier to the button’s label and supply the name of the struct you created for the label style. The following code shows an example of a SwiftUI button that uses a custom label style:

Credits

The answer from Eduardo to the following Stack Overflow question helped me with the code for this article:

Add Button with Image and Text in ToolbarItem SwiftUI

Adding a Help Menu to a SwiftUI App

When you create and run a SwiftUI Mac app project, the Help menu has a menu item named AppName Help, where AppName is the name of your app. If you choose that menu item, an alert opens saying help isn’t available for the app.

Help Books

The included menu item works only if your app has a Help Book bundled in the app. Many of Apple’s apps have Help Books. Choose Help > Xcode Help in Xcode to see an example of a Help Book.

Creating Help Books is painful. Apple’s Help Book Programming Guide was last updated in 2013, and there aren’t many articles online on creating Help Books.

Take Users to Your Site

For most apps it’s easier to add a page to your app’s website with your app’s help and add a menu item to go that page. You can even include a link on the page for people to download a user guide for your app. Creating a menu item that takes someone to your site involves the following steps:

  • Create a view for the menu item.
  • Create a SwiftUI link for the menu item.
  • Add the menu item to the menu bar.

Creating the Link and Menu Item

A SwiftUI link takes two arguments. The first argument is the link name, which is the menu item text for menus. The second argument is the link destination. The following code shows an example of a menu item with a link:

Adding the Menu Item to the Menu Bar

Add a .commands modifier to the window group or document group in the App struct for your app. Add a command group for the menu, replacing the default Help menu item with your Help menu items.

Showing a SwiftUI sheet from a Mac Menu

Most articles on showing sheets in SwiftUI apps show an example of a button with a .sheet modifier that shows the sheet. But if you try to apply the example to a menu in a Mac app,

The sheet does not appear when you choose the menu item. How do you show the sheet when someone chooses the menu item?

  • Add a focused value for showing the sheet.
  • Set the focused scene value in your main view.
  • Add the menu item to show the sheet.
  • Add the menu item to the app’s menu bar.

Read the Accessing the Document in a SwiftUI Menu article to learn more about focused values and working with SwiftUI menus.

Add a Focused Value

Add a focused value that your app uses to control whether or not to show the sheet. In this article I’m going to use the example of a menu item that previews some content in a sheet.

Set the Focused Scene Value

After creating the focused value to show the sheet, you must make that value a focused scene value to show the sheet from a menu. Start by adding a property to the main SwiftUI view.

The next step is to add a .focusedSceneValue modifier to the view. The first argument is the name of the variable you created for the focused value. The second argument is a binding using the property you created in the view.

Apple added the .focusedSceneValue modifier in macOS 12.

Add the Menu Item

Menus in SwiftUI apps are SwiftUI views. To show a sheet from the menu, start by creating a property with the @FocusedValue property wrapper that contains the focused value to show the sheet.

Create a button for the menu item. The action for the button is to set the focused value’s wrapped value. The focused value for showing a sheet is a Boolean value so you give it the value of true, which shows the sheet when someone chooses the menu item.

Add the Menu to the App

After creating the menu you must add it to your app’s menu bar. Add a .commands modifier to the app’s window group or document group to add the menu to the menu bar.

Credits

Thanks to Jason Armstrong for his answer to the following Stack Overflow question:

Calling a sheet from a menu item

Showing Local HTML Content in a WKWebView

Apple provides the WKWebView class to show web content in Swift apps. Most apps use web views to show content from websites, but you can also show local HTML in a web view. There are two common ways to show local HTML in a web view.

  • Loading HTML strings
  • Loading HTML files

Loading HTML Strings

The easiest way to display HTML content in a web view is to create a string for the HTML and call the loadHTMLString function.

A common reason to load an HTML string is to preview Markdown content. You can see examples of loading HTML strings in my WikiDemo and SwiftUIMarkdownEditor GitHub projects.

The biggest limitation of loading HTML strings and showing them in a web view is that images will not show. Inserting images in a HTML string is a security risk so the web view won’t display them.

Loading HTML Files

If you want to show images in a web view, you must create a HTML file and show it in the web view by calling the loadFileURL function. The function takes two arguments. The first argument is the URL of the file to show in the web view. The second argument is a URL that you give the web view access to read. You normally give read access to a folder that contains subfolders you want to show, such as image and CSS files.

I recently added code in an app to preview EPUB books in a web view. An EPUB book has an OEBPS folder that holds most of the book’s content. The OEBPS folder has the following subfolders:

  • A Text folder that contains the book’s chapters.
  • An Images folder that contains the book’s images.
  • A Styles folder that contains any CSS files to style the book.

To preview the book I needed to pass the URL of the current chapter’s HTML file as the first argument to loadFileURL and the URL of the OEBPS folder as the second argument. Giving the web view access to the OEBPS folder gives the web view access to the images and CSS files.

Running a Mac App Outside of Xcode

When you are developing a Mac app, you run it often from Xcode. But eventually you will run it outside of Xcode. You have two ways to run a Mac app outside of Xcode.

  • Locate the build folder
  • Archive and export the project

Locate the Build Folder

If you’re actively developing your app, locating the app from its build folder is the easiest way to run the app outside of Xcode. In Xcode choose Product > Show Build Folder in Finder to show the app’s build folder.

Go to the Finder. The app will be in a folder inside the Products folder. Usually the name of the folder is Debug or Release.

Archive and Export the Project

When you have your app at a point where you can use it regularly on your Mac or share with other people, you should archive and export the project to create an app file that is not buried deep inside Xcode’s Derived Data folder.

In Xcode choose Product > Archive to archive the project. Xcode’s Organizer window contains your app archives. Choose Window > Organizer in Xcode to open the Organizer.

OrganizerWindow

Click the Archives item on the left side of the Organizer to show your app’s archives. If you have archived multiple projects, you may have to choose your app from the app menu that is above the Archives item.

Select the archive from the archive list and click the Distribute App button to export the archive. When you click the Distribute App button, a sheet opens asking you to choose a method of distribution. Xcode provides the following distribution methods:

  • App Store Connect
  • Developer ID
  • Development
  • Copy App

The first two methods require a paid Apple developer membership. I’m not sure about the Development method. If you don’t have a paid membership or are making an app for personal use, choose Copy App and click the Next button. Choose a location to save the app and click the Export button.

Now you have an app file that you can run on your Mac just like any other app. You can also use the file to copy the app to another Mac.

The Other Distribution Methods

The App Store Connect distribution method is for apps that are going to be in the Mac App Store.

The Developer ID distribution method is for apps that you are going to have people download from your own website. When you choose Developer ID, Apple notarizes the app so that people who download and install your app can trust it. The How to Notarize a Mac App article has more details on the Developer ID distribution method.

The Development distribution method is for creating an app file to share with people you know. When I tried the Development distribution method for a Mac app, I saw no difference between the Development and Copy App distribution methods.

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.

Xcode 14 Update

Xcode 14 does not have a Referenced Binaries selection. Select the Sparkle 2.x item from the project navigator, right-click, and choose Show in Finder.

When you choose Show in Finder, the Finder takes you to a Sparkle folder inside a checkouts folder. That Sparkle folder contains source code, not the scripts you need to run.

Move one directory above the checkouts folder, which should be named Source Packages. Go to the artifacts folder. That’s where the Sparkle folder you need is.

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.