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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SF Symbols for iOS: Getting Started
30 mins
- Getting Started
- Getting Acquainted with the App
- Importing Models and Data
- Adding TransportAPI Functionality
- Setting up TransportAPI
- Connecting the API
- Understanding SF Symbols
- Viewing Available Symbols
- Using SF Symbols
- Testing with Mock Data
- Naïve Mock Data Approaches
- Using Mock Data with Environment Variables
- Switching Between Mock Data and Actual Data
- Using the DebugData Scheme
- Understanding Restrictions on Using SF Symbols
- Creating Custom SF Symbols
- Making an “Information” Symbol
- Importing the Exclamation Mark Symbol
- Customizing the Exclamation Mark Symbol
- Using Custom Symbols
- Supporting Older Operating Systems
- Where to Go From Here?
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.
Viewing Available Symbols
Apple has released an SF Symbols app for macOS showcasing all the available symbols. Download the app and open it.
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:
- Add a new method,
image()
, toTFLLineStatus
. - Use the new
init(systemName:)
onImage
to create an image with the exclamationmark.octagon SF Symbol.
Search for exclamationmark.octagon in the SF Symbols app.
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:
- Calling
status.image()
, which you defined onTFLLineStatus
, to insert the status image into the leading side of theHStack
. - 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.
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.
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.
Name the new scheme Debug Data and click Close. Then, select Product ▸ Schemes ▸ Manage Schemes…, select the Debug Data scheme and select Edit….
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.
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:
- 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.
- Adds
LineStatus
items toDebugData
‘slineStatus
. This first set adds a different status code to each of the “real” tube lines. - The second set adds the remaining status codes to the fake tube lines you created.