Category: iOS

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

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.

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.

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.

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.

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.

UPDATE

Apple deprecated NavigationView in iOS 16 and macOS 13. The replacements for NavigationView are NavigationStack and NavigationSplitView, but they require iOS 16+ and macOS 13+. If you want to support earlier OS versions, you must use NavigationView.

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.

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 Swift 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.