What is a variant in SF Symbols

⋅ 12 min read ⋅ SwiftUI SF Symbols

Table of Contents

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

What is a variant in SF Symbols

SF Symbols defines several design variants such as fill, slash, circle. The variant isn't a new concept in SF Symbols.

Before iOS 15, we refer to these variants by putting a variant name to the end of the base symbol name, e.g., heart.fill, heart.slash.

Here are some variations of a heart symbol.

VStack(alignment: .leading) {
Label("Default", systemImage: "heart")
Label("Fill", systemImage: "heart.fill")
Label("Slash", systemImage: "heart.slash")
Label("Slash Fill", systemImage: "heart.slash.fill")
Label("Circle", systemImage: "heart.circle")
Label("Circle Fill", systemImage: "heart.circle.fill")
}

You can see that symbol can have many variants (heart.fill, heart.slash) and even multiple variants simultaneously (heart.slash.fill).

All of these symbols are just a variation of the default symbol, "heart".

Five variants of a heart symbol.
Five variants of a heart symbol.

Improvement in iOS 15

iOS 15, we have a new way to reference symbol variants without changing the name. We can do that with the symbolVariant(_:) modifier.

We have six SymbolVariants to choose from at the moment.

  1. none
  2. circle
  3. square
  4. rectangle
  5. fill
  6. slash

Here is how we use symbolVariant(_:) to yeild the same result I show earlier.

VStack(alignment: .leading) {
Label("Default", systemImage: "heart")
Label("Fill", systemImage: "heart")
.symbolVariant(.fill)
Label("Slash", systemImage: "heart")
.symbolVariant(.slash)
Label("Slash Fill", systemImage: "heart")
.symbolVariant(.slash.fill)
Label("Circle", systemImage: "heart")
.symbolVariant(.circle)
Label("Circle Fill", systemImage: "heart")
.symbolVariant(.circle.fill)
}

Instead of modifying the symbol name, we control a variant by setting the one you want via symbolVariant(_:) modifier.

You might wonder how this change is considered an improvement. It is even more verbose than the original implementation.

To understand the beauty of these changes, let's step back and learn the functions of variants.

What are usages of symbol variants

The default variant of most SF Symbols is an Outline style because it matches the appearance of a text (SF Pro font). But what about other variants? I think we can categorize variants into two groups by their usages.

  1. Variants that communicate states and actions.
  2. Variants which match visual UI elements or design language of platforms.

Variants that communicate states and actions

Variants can be used to communicate states and actions of UI elements. For example, you could use the slash variant to show that an item or action is unavailable.

Here is an example where we use a slash to communicate visibility and mute state.

A slash variant is used to communicate visibility and mute state.
A slash variant is used to communicate visibility and mute state.

Another example is to use the fill variant to indicate selection.

We can use a fill variant with a heart symbol to show whether users like something. And we use a fill variant with a circle to represent a checkbox.

A fill variant is used to communicate selection state.
A fill variant is used to communicate selection state.

Variants for platforms and UI elements

Another usage of variant is its appearance. The default outline variant works well in toolbars, navigation bars, and other places displaying a symbol alongside text. On the other hand, the fill variant gives a symbol more visual emphasis, making it a good choice for iOS tab bars.

iOS Tab bar items usually use a fill style because it has a large solid area, which is good for communicating a selection state.

iOS tab bars use fill variant symbols
iOS tab bars use fill variant symbols

What makes the situation more complicated is that the visual for the same element might not be the same on the different platforms. The macOS tab bars in the preference window are usually present in an outline variant with a highlighted background for the selected state.

Xcode preference window.
Xcode preference window.

Benefits of symbolVariant(_:)

As you can see from the last section, variants are more dynamic than we thought.

It used to represent a state which likely to change back and forth during the lifetime of an app.

It is also possible for the same UI element to present in different variants depending on the platform.

This is where the new API shine. Let's compare how symbol variants can help us improve for each usage.

Changing states

Here is an example of using the .slash variant to show a visible and an invisible state.

struct ContentView: View {
@State private var isVisible = false

var body: some View {
VStack {
// New API
Button {
isVisible.toggle()
} label: {
Image(systemName: "eye")
.symbolVariant(isVisible ? .slash: .none)
}
// Old API
Button {
isVisible.toggle()
} label: {
Image(systemName: isVisible ? "eye.slash": "eye")
}
}
.font(.largeTitle)
}
}

Both methods achieve the same result.

Using the slash variant to represent visibility state.
Using the slash variant to represent visibility state.

The difference between an old and new API might look small, just changing from hard-coded string to view modifier. But it is quite an improvement if you ask me.

Since variant is now extracted from a string to a struct, SymbolVariants, it can be applied to any symbols without touching a symbol name.

Here is a naive implementation of SlashButton, which you can toggle between a slash and non-slash variant symbol.

struct SlashButton: View {
@Binding var isSlash: Bool
let systemName: String

var body: some View {
Button {
isSlash.toggle()
} label: {
Image(systemName: systemName)
.symbolVariant(isSlash ? .slash: .none)
}
}
}

We can use it like this. You can see that we don't have to worry about the name of the slash variant, "eye.slash".

SlashButton(isSlash: $isVisible, systemName: "eye")

Platforms and UI dependent

The benefit of the symbol variant for this case is more prominent. Let's use tab bars as an example. Apple suggests that we should use the fill variant symbol for tab bars. The following are how we implement tab bars with and without a symbolVariant(_:).

Before iOS 15

Before iOS 15, you might need to do something like this.

TabView {
Label("photo.on.rectangle", systemImage: "photo.on.rectangle")
.tabItem {
Label("Library", systemImage: "photo.on.rectangle.fill")
}
Label("heart.text.square", systemImage: "heart.text.square")
.tabItem {
Label("For You", systemImage: "heart.text.square.fill")
}
Label("rectangle.stack", systemImage: "rectangle.stack")
.tabItem {
Label("Albums", systemImage: "rectangle.stack.fill")
}
Label("magnifyingglass", systemImage: "magnifyingglass")
.tabItem {
Label("Search", systemImage: "magnifyingglass.fill")
}
}
.font(.largeTitle)

If you run the app, this is what you will get. The last tab bar has no image since we want to use a fill variant for symbols, but the magnifyingglass symbol doesn't have a fill variant.

Implement tab bars using fill variant symbols.
Implement tab bars using fill variant symbols.

There are two problems right here.

  1. You need to know that you should use a fill variant for a tab bar in iOS. This might not be obvious for newcomers or those with no time to keep up with all the Apple guidelines.
  2. You have to figure out whether your symbols got a fill variant or not and if you somehow mistype it like our magnifyingglass.fill, you will end up with an empty image.

iOS 15

In iOS 15, symbolVariant(_:) can greatly help us on this. You can apply .fill variant for all symbol by apply .symbolVariant(.fill) on a parent view, TabView.

TabView {
Label("photo.on.rectangle", systemImage: "photo.on.rectangle")
.tabItem {
Label("Library", systemImage: "photo.on.rectangle")
}
Label("heart.text.square", systemImage: "heart.text.square")
.tabItem {
Label("For You", systemImage: "heart.text.square")
}
Label("rectangle.stack", systemImage: "rectangle.stack")
.tabItem {
Label("Albums", systemImage: "rectangle.stack")
}
Label("magnifyingglass", systemImage: "magnifyingglass")
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
}
.symbolVariant(.fill)
.font(.largeTitle)

For symbols that don't support fill variant, like "magnifyingglass", remains unaffected.

Implement tab bars using fill variant symbols.
Implement tab bars using fill variant symbols.

In fact, what I did in the above example is just for demonstration purposes. You don't even need to do that in iOS 15. SwiftUI is very smart. It automatically applies the right variant for UI elements. In this case, the fill variant is applied for items that appear in the content closure of the tab bar items of a TabView. So, you can remove .symbolVariant(.fill), and the result would be the same.

TabView {
Label("photo.on.rectangle", systemImage: "photo.on.rectangle")
.tabItem {
Label("Library", systemImage: "photo.on.rectangle")
}
Label("heart.text.square", systemImage: "heart.text.square")
.tabItem {
Label("For You", systemImage: "heart.text.square")
}
Label("rectangle.stack", systemImage: "rectangle.stack")
.tabItem {
Label("Albums", systemImage: "rectangle.stack")
}
Label("magnifyingglass", systemImage: "magnifyingglass")
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
}
.font(.largeTitle)

Content inside .tabItem automatically got a fill variant.

Implement tab bars using fill variant symbols.
Implement tab bars using fill variant symbols.

As you can see, the two problems that we mentioned earlier are gone.

We don't have to know which variant is suitable for which UI. We can just put a default version of a symbol, and SwiftUI will handle the rest for us.

And we don't need to care whether a symbol would break if the specified variant does not exist, as you can see from our magnifyingglass symbol. There are a few behaviors that you should know about symbolVariant(_:), which I'm going to go through in the next section.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

How to use symbolVariant(_:)

We have seen some behaviors of symbolVariant(_:) in previous examples, but let's repeat it for completeness.

Basic

Use symbolVariant(_:) modifier with a SymbolVariants value to make all the SF Symbols in that view hierarchy use the same variant.

We apply .symbolVariant(.fill) to the bottom HStack, so every symbol in that stack use the same variant, .fill.

VStack(spacing: 20) {
HStack(spacing: 20) {
Image(systemName: "heart")
Image(systemName: "folder")
Image(systemName: "person")
Image(systemName: "magnifyingglass")
}

HStack(spacing: 20) {
Image(systemName: "heart")
Image(systemName: "folder")
Image(systemName: "person")
Image(systemName: "magnifyingglass")
}
.symbolVariant(.fill) // Shows filled variants, when available.
}
.font(.largeTitle
Every symbol in the bottom HStack got a fill variant.
Every symbol in the bottom HStack got a fill variant.

Accumulate effect

If you apply the modifier more than once, its effects accumulate. Alternatively, you can apply multiple variants in one call.

VStack(spacing: 20) {
HStack(spacing: 20) {
Image(systemName: "heart")
Image(systemName: "folder")
Image(systemName: "person")
Image(systemName: "magnifyingglass")
}
// 1
.symbolVariant(.circle.fill)

HStack(spacing: 20) {
Image(systemName: "heart")
Image(systemName: "folder")
Image(systemName: "person")
Image(systemName: "magnifyingglass")
}
// 2
.symbolVariant(.circle)
.symbolVariant(.fill)
}
.font(.largeTitle)

1 Apply multiple variants in one call.
2 The effect from two modifiers is added up.

The result from both .symbolVariant(.circle.fill) and .symbolVariant(.circle).symbolVariant(.fill) are the same.

.symbolVariant(.circle.fill) and .symbolVariant(.circle).symbolVariant(.fill) resulting in the same effect.
.symbolVariant(.circle.fill) and .symbolVariant(.circle).symbolVariant(.fill) resulting in the same effect.

Order

Most of the time, the order of SymbolVariants doesn't matter, and it doesn't need to match the real symbol's name.

For example, the "heart.circle.fill" symbol can be build from both Image(systemName: "heart").symbolVariant(.circle.fill) and Image(systemName: "heart").symbolVariant(.fill.circle).

The only case where the order is matters is when you apply more than one shape variant. The one closest to the symbol takes precedence.

For example, the following images got all three shape variants, but render differently.

 VStack(spacing: 20) {
VStack(spacing: 20) {
Image(systemName: "heart")
.symbolVariant(.square) // This shape takes precedence for the first heart.
Image(systemName: "heart")
}
.symbolVariant(.circle) // This shape takes precedence for the second heart.
Image(systemName: "heart")
}
.symbolVariant(.rectangle) // This shape takes precedence for the third heart.
.symbolVariant(.square)
.symbolVariant(.circle)
.font(.largeTitle)
The shape variant that closest to the symbol takes precedence.
The shape variant that closest to the symbol takes precedence.

Opt-out

You can clear a variant effect by directly setting the symbolVariants environment value to .none.

In this example all symbols got the .fill variant from the parent. Set .symbolVariant(.none) won't work due to accumulate effect. The only way to clear variants and start fresh is by setting .environment(\.symbolVariants, .none).

HStack {
Image(systemName: "heart")
Image(systemName: "heart")
.symbolVariant(.none) // This won't work
Image(systemName: "heart")
.environment(\.symbolVariants, .none)
}
.symbolVariant(.fill)
.font(.largeTitle)
Clear variants by set symbolVariants environment value to none.
Clear variants by set symbolVariants environment value to none.

Read more article about SwiftUI, SF Symbols, or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
How to decode enums ignoring case in Swift Codable

Learn how to make a case insensitive enum.

Next
What is image rendering mode in iOS

When using an image in iOS, you have an opportunity to control how you want the image to be rendered. Most of the time, you don't need to care about this, but when images do not render the way you want, make sure you know how to customize them.

← Home