Category: iOS

Creating a UIKit Xcode Project

A common problem people learning UIKit have when following a tutorial is that what they see in Xcode does not match what the tutorial shows. The tutorial shows a view controller file, but they see a content view file.

The most common cause of this problem is creating an Xcode project that uses SwiftUI instead of UIKit. When you create a new project in Xcode, the default is to create a SwiftUI project.

To create an Xcode project that uses UIKit, you must create an iOS app project and tell Xcode to use a storyboard for the user interface. UIKit uses storyboards, and SwiftUI doesn’t.

Create an iOS App Project

In Xcode choose File > New > Project to create a new project. The New Project Assistant opens.

XcodeCreateiOSAppProject

Click on iOS at the top of the assistant to see the iOS app project templates. Choose either App or Document App. If you’re not sure, choose App. Click the Next button.

Choose Storyboard for the User Interface

When you click the Next button, the New Project Assistant takes you to the next step in creating the project.

XcodeChooseStoryboard

Choose Storyboard from the Interface menu to create a UIKIt project.

If you don’t see an Interface menu (or a menu with a similar name), click the Previous button and make sure you choose an iOS app project. Xcode’s multi-platform project templates create SwiftUI projects.

Dealing with the init(destination:tag:selection ) was deprecated in iOS 16.0 message

People starting out with SwiftUI development may get a message like the following from Xcode when building their project:

`init(destination:isActive:label:)’ was deprecated in iOS 16.0: use NavigationLink(value:label:) inside a NavigationStack or NavigationSplitView

‘init(destination:tag:selection????️ )’ was deprecated in iOS 16.0: use NavigationLink(value????️ ), or navigationDestination(isPresented:destination:), inside a NavigationStack or NavigationSplitView

Why are you getting this message?

You are getting this message because you are using NavigationView in your user interface.

The original version of SwiftUI used navigation views to navigate from one screen to another in an app and for master-detail interfaces with split views on iPad. In iOS 16 Apple replaced NavigationView with NavigationStack and NavigationSplitView for navigation.

The message is telling you to replace NavigationView in your app with either NavigationStack or NavigationSplitView.

NavigationStack and NavigationSplitView don’t run on earlier iOS versions. If you want your SwiftUI app to run on iOS 15, you must use NavigationView.

Is the message a problem?

Not currently. The message is a compiler warning, not an error. Your project will build and run, assuming there are no errors in the rest of your code.

Eventually Apple will stop supporting iOS 15. A future version of Xcode will drop support for building an app that runs on iOS 15. For reference Xcode 14 does not support iOS versions older than iOS 11. When Apple releases a version of Xcode that does not support building an app that runs on iOS 15, the message will be a problem.

How do you remove the message?

Use either NavigationStack or NavigationSplitView instead of NavigationView in your app. Apple provides the following guide to migrate code from NavigationView:

Migrating to new navigation types

If you are following a tutorial that uses NavigationView, find a more recent tutorial that uses the newer navigation APIs.

Highlighting the Selected Item Button in a SwiftUI List

If you use a button for list items in SwiftUI, the list item does not highlight when you select it. This is expected behavior for a bordered button. But some apps need to use buttons that look like text to perform actions when selecting the list item. If you have a button that looks like text, you want to highlight the selection so people can easily see the selected item.

How do you highlight the selected item? You must do two things to highlight the selected item. First, you must set the background color for the list row. Second, you must set the foreground color for the list item.

Setting the Background Color

Add a .listRowBackground modifier to the button to set the background color. Use the accent color for the selected item to highlight it and the clear color for the other items to match the standard selection highlighting.

Setting the Foreground Color

When you select an item from a SwiftUI list in an app running in light mode, the text color for the item changes from a dark color to a light color. You must set the color based on the current color scheme or else the text of the selected item will be tough to read.

Start by adding an @Environment property to your SwiftUI view that stores the current color scheme: light or dark.

Next, create computed properties for the item color based on whether or not it’s selected. Use white for the selected item. For unselected items make the text white in dark mode and black in light mode.

Finally add a .foregroundColor modifier to the button. Use the selected item color for the selected item and the unselected item color for the other items.

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 Mastodon client, Mastodon 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.