SF Symbols for iOS: Getting Started

Learn to use SF Symbols, both existing and custom, to show data in an engaging way. By Tom Elliott.

4.5 (8) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Understanding SF Symbols

Now that the basic app is up and running, you’ll spend the rest of this tutorial learning how to add some pizazz in the form of SF Symbols.

SF Symbols are currently available in three versions:

  • Version 1.1 is available on iOS/iPadOS/tvOS 13 and watchOS 6.
  • Version 2.0 is available on iOS/iPadOS/tvOS 14 and watchOS 7.0.
  • Version 2.1 is available on iOS/iPadOS/tvOS 14.2 and watchOS 7.2.

All versions are also available with macOS Big Sur.

As well as adding nearly 900 symbols, Version 2 of SF Symbols also introduced over 160 multicolor symbols, localized variants and improvements to how symbols can be aligned horizontally.

Note: Some symbols have changed their name between versions. While SF Symbols supports old names for backward compatibility, you should make sure that any symbols you use in your app work on all versions you intend to support.

Viewing Available Symbols

Apple has released an SF Symbols app for macOS showcasing all the available symbols. Download the app and open it.

The SF Symbols macOS App

The left-hand panel acts as a filter, limiting which symbols are shown based on their category.

The top pane allows you to:

  • Alter the font and weight of the displayed symbols.
  • Switch the layout between grid or list.
  • Toggle multicolor preview.
  • Filter symbols by name.

When you click the i button on the top bar, a right-hand pane opens. This pane provides a detail view of any selected symbol, including which platforms it’s available on and any restrictions for its use.

Finally, the main pane displays all the relevant symbols based on the options selected.

Using SF Symbols

It’s finally time to bling up your app. In Xcode, open TFLLineStatus.swift in the LineData group. This file defines an enum containing all the line status values that the API supports. There are a lot of them!

At the end of the file, before the final closing brace, add the following code:

// 1
func image() -> Image {
  switch self {
  default:
    // 2
    return Image(systemName: "exclamationmark.octagon")
  }
}

In this code, you:

  1. Add a new method, image(), to TFLLineStatus.
  2. Use the new init(systemName:) on Image to create an image with the exclamationmark.octagon SF Symbol.

Search for exclamationmark.octagon in the SF Symbols app.

The 'exlamationmark.octagon' SF Symbol

Next, you’ll use this image when displaying the status for a line. Open LineStatusRow.swift.

Add the following to body as the first child of HStack, before the VStack:

// 1
status.image()
  // 2
  .font(.title)
  .padding(.trailing)
  .foregroundColor(lineColor.contrastingTextColor)

Here, you are:

  1. Calling status.image(), which you defined on TFLLineStatus, to insert the status image into the leading side of the HStack.
  2. Setting font style, padding and foreground color properties on the image using view modifiers. The foreground color is set such that it contrasts nicely with the row’s background color.

Notice how you can call font(_:) on Image. Because SF Symbols are designed to work with the San Francisco font system, they automatically pick the right variant based on the font you provide. Neat!

Build and run the app.

Adding your first SF Symbol

Voilà, you’ve just added your first SF Symbol into the app. Congratulations! :]

But, using the same symbol for every status code isn’t too helpful for the user. To fix that, go back to TFLLineStatus.swift. Place the following in the body of switch before default:

case .closed:
  return Image(systemName: "exclamationmark.octagon")
case .suspended:
  return Image(systemName: "nosign")
case .severeDelays:
  return Image(systemName: "exclamationmark.arrow.circlepath")
case .reducedService:
  return Image(systemName: "tortoise")
case .busService:
  return Image(systemName: "bus")
case .minorDelays:
  return Image(systemName: "clock.arrow.circlepath")
case .goodService:
  return Image(systemName: "checkmark.square")
case .changeOfFrequency:
  return Image(systemName: "clock.arrow.2.circlepath")
case .notRunning:
  return Image(systemName: "exclamationmark.octagon")
case .issuesReported:
  return Image(systemName: "exclamationmark.circle")
case .noIssues:
  return Image(systemName: "checkmark.square")
case .plannedClosure:
  return Image(systemName: "hammer")
case .serviceClosed:
  return Image(systemName: "exclamationmark.octagon")
case .unknown:
  return Image(systemName: "questionmark.circle")

In this code, you’re picking out several common status codes and providing custom SF Symbols for each. Any codes not specified will continue to use the exclamationmark.octagon symbol from the switch’s default case.

Build and run the app again. Your experience may vary from the image below depending on the state of the Tube system at the time you’re running the app. But hopefully, you’ll see many types of statuses displaying different images.

Using multiple SF Symbols

Neat! Hopefully, you’re starting to see how powerful SF symbols can be!

Testing with Mock Data

In the previous section, you chose different SF Symbols for different line statuses. However, you haven’t yet been able to see how each of them looks, since your app only renders the current status of the lines. Now, you’ll explore using mock data to test the full range of statuses.

Naïve Mock Data Approaches

You could wait around until each status occurs in real life, then quickly open the app. But you might be waiting a long time. :]

Another option is to add many Swift UI previews to LineStatusRow, setting the properties appropriately. This works, but it’s clumsy.

Each preview displays on its own on a phone screen background. Interactivity isn’t available, and worst of all, because LineStatusRow is a purely presentational view, you’re only checking that the values you provide in the preview are rendered correctly.

Another approach would be using unit tests and mock data. This is a pretty good approach but still lacks the interactivity element.

Using Mock Data with Environment Variables

Another approach that may be more useful is to configure your app with mock data based on an environment variable. That way, you can choose to build your app with whatever data you wish and play with the app on the simulator or your device as if it were the real thing.

In Xcode, select Product ▸ Scheme ▸ Edit Schemes… and select Duplicate Scheme.

Duplicating Schemes

Name the new scheme Debug Data and click Close. Then, select Product ▸ Schemes ▸ Manage Schemes…, select the Debug Data scheme and select Edit….

Editing Schemes

Select Run in the left-hand menu and then the Arguments tab. Click the + icon under Environment Variables and create a new environment variable called USE_DEBUG_DATA with a value of true. Click Close.

Adding Environment Variables

Your app now has two schemes, identical except that the DebugData scheme passes your new environment variable into the build environment.

Next, open DebugLineData.swift and add the following code immediately after the import declarations:

// 1
let bakerlooLineDebug = LineData(
  name: "BakerlooDebug", 
  color: Color(red: 137 / 255, green: 78 / 255, blue: 36 / 255))
let centralLineDebug = LineData(
  name: "CentralDebug", 
  color: Color(red: 220 / 255, green: 36 / 255, blue: 31 / 255))
let circleLineDebug = LineData(
  name: "CircleDebug", 
  color: Color(red: 255 / 255, green: 206 / 255, blue: 0 / 255))
let districtLineDebug = LineData(
  name: "DistrictDebug", 
  color: Color(red: 0 / 255, green: 114 / 255, blue: 41 / 255))
let hammersmithAndCityLineDebug = LineData(
  name: "Hammersmith & CityDebug",
  color: Color(red: 215 / 255, green: 153 / 255, blue: 175 / 255))
let jubileeLineDebug = LineData(
  name: "JubileeDebug", 
  color: Color(red: 106 / 255, green: 114 / 255, blue: 120 / 255))
let metropolitanLineDebug = LineData(
  name: "MetropolitanDebug", 
  color: Color(red: 117 / 255, green: 16 / 255, blue: 86 / 255))
let northernLineDebug = LineData(
  name: "NorthernDebug",
  color: Color(red: 0 / 255, green: 0 / 255, blue: 0 / 255))
let piccadillyLineDebug = LineData(
  name: "PiccadillyDebug",
  color: Color(red: 0 / 255, green: 25 / 255, blue: 168 / 255))
let victoriaLineDebug = LineData(
  name: "VictoriaDebug",
  color: Color(red: 0 / 255, green: 160 / 255, blue: 226 / 255))

Then, add the following between the square brackets of lineStatus inside the debugData constant declaration at the bottom:

// 2
LineStatus(line: bakerlooLine, status: .specialService),
LineStatus(line: centralLine, status: .closed),
LineStatus(line: circleLine, status: .suspended),
LineStatus(line: districtLine, status: .partSuspended),
LineStatus(line: hammersmithAndCityLine, status: .plannedClosure),
LineStatus(line: jubileeLine, status: .partClosure),
LineStatus(line: metropolitanLine, status: .severeDelays),
LineStatus(line: northernLine, status: .reducedService),
LineStatus(line: piccadillyLine, status: .busService),
LineStatus(line: victoriaLine, status: .minorDelays),
LineStatus(line: waterlooAndCityLine, status: .goodService),
LineStatus(line: dlr, status: .partClosed),
// 3
LineStatus(line: bakerlooLineDebug, status: .exitOnly),
LineStatus(line: centralLineDebug, status: .noStepFreeAccess),
LineStatus(line: circleLineDebug, status: .changeOfFrequency),
LineStatus(line: districtLineDebug, status: .diverted),
LineStatus(line: hammersmithAndCityLineDebug, status: .notRunning),
LineStatus(line: jubileeLineDebug, status: .issuesReported),
LineStatus(line: metropolitanLineDebug, status: .noIssues),
LineStatus(line: northernLineDebug, status: .information),
LineStatus(line: piccadillyLineDebug, status: .serviceClosed),
LineStatus(line: victoriaLineDebug, status: .unknown)

This code:

  1. Creates several “fake” tube lines. Your app has 21 status codes, but only 12 lines. So you created an additional 9 lines to make sure there are enough lines to display each code.
  2. Adds LineStatus items to DebugData‘s lineStatus. This first set adds a different status code to each of the “real” tube lines.
  3. The second set adds the remaining status codes to the fake tube lines you created.