Where are your icons?
Using an icon font on iOS
If you work on a big mobile project you are facing a lot of issues to solve on a daily basis. And one of these issues is to deal with is the size of your app, and how you manage your assets.
Make a big app, with inumerous flows and you'll probably end up with a bunch of assets for icons, illustrations, fonts, and that marketing banner to promote something that for some weird reason is embed on the app.
While making some changes on our project, and make some updates with the design team, we decided to take a different approach regarding our icon's assets: Using a Font to store all the icons we needed.
A New Design System
At some point, our design team decided to improve the company's entire Design System. Basically studying all the visual components across multiple products and apps, organizing them, and defining patterns and components on the design and development side to be commonly used by everyone. A huuuge project, involving developers, designers, the product team, well… basically everybody.
And I got to work with the icons, illustrations, and other visual assets.
We decided to give it a shot on using a font to hold all of our icons. The idea came from one of the designers and it looked simple and promising: a single asset for all the icons, lightweight, and versioned between all of our platforms. The best of both worlds.
But we still had to understand: Using a font as icons inside the app would live up to our expectations? How would we migrate the old icons to the new ones? Would it be easy for the developers to use it on the project?
So many questions, but it was time to study and test a little bit more.
Wrapping the label complexity
Working with a font means that we are working with Characters and Strings. Each icon has a Unicode to identify it, basically the same as saying using"a"
will render an icon ✻
if your label is using the icons font.
So, first things first: Having the map of which code represents each icon is really important.
But we want them to behave like images. An icon is an image and there is no need to know that behind the icon displayed there is a UILabel hanging out, we just want the icon to be displayed and placed correctly.
That's when I decided to wrap the entire font and label logic on a view and hide everything we don't need on a daily base to use it. First, a way to identify the icons by name, not by its Unicode value.
Looking good! With this enum I could easily access the Unicode value for the icons I wanted, without having to know the exact code, I just had to use Icon.bag.rawValue
. But I still didn't want the developers to use a UILabel, set the font, and use the enum to get the icon. We still had more things to wrap.
To use the icon just like a regular UIImageView, I created an IconView, with a UILabel inside using the icon font.
This class is a UIView, with a label placed on the center of it. To build it, all we need is an Icon and color, and we'll set the icons rawValue
as the label text, and the color as the textColor
.
The label has some tricks: it is loaded with the icon font and has the text alignment centered, the font size is huge: 500
, and it's set to automatically scale itself depending on the available size with aminimumScaleFactor
of 0.01
.
The label's font size is big enough to fit an icon on the entire screen, and the scale factor allows it to shrink to the smallest size possible. This allows us to place an IconView, without worrying with the label inside and knowing that you can position and resize the view, and the label will just adapt itself to each context.
Making it easy to use
We have a view that wraps all of our font-string-icon logic, but I wanted to make it even easier for the developers to create and edit the icons they need.
I decided to use the callAsFunction
method inside the Icon enum, and by calling this method, it will create an IconView using the selected Icon.
This way, you don't really need to directly instantiate an IconView you just need to call an icon as a function let iconView = Icon.bag()
and everything will be handled internally and you just need to place your newly instantiated view.
But, what if someone needs to update the icon color, or the Icon itself? I added an observer on the IconView's icon and color properties.
This way, if you want, you can change the displayed icon and color at ease: iconView.icon = .market
or iconView.color = .red
.
Remember that we are keeping the label stuff all private inside the IconView. While using, it's not important to know that by changing the icon it's actually updating a label's text, on a higher level, we just want to change the icon.
Creating an Example
With everything coded, we had to make an example to really see if everything was working. I made a small app, with an icon on the center, a slider to allow change the size of the icon, and 2 pickers: one for the icon itself and another one for the color.
This test really helped us understand if the font was behaving correctly on any size and color (we actually found some issues on a few icons when looking at them on a larger scale, and they were addressed by our design team 😉).
Handling accessibility
What should be the accessibility of an icon? It depends on where you are using your icons but, mostly, icons are displayed associated with texts or individually as buttons.
Considering an icon to be just a regular image it should not have any accessibility text if you don't want it to. So, by default, I removed the IconView accessibility features.
But the property isAccessibilityElement
of the IconView is still public if you need to change it.
Also, if you are going to add any gestures to the IconView to make it behave like a button, remember to set isAccessibilityElement
to true
and also configure an accessibilityLabel
to it. And I also suggest using accessibilityTraits = .button
.
Improving for a long life project
The design system is a living thing and also are your company's icons.
We had to be prepared for updates, new icons, name changes, and new ways to use the icons.
The first thing that was not that good, and not very scalable, was using the Icon enum rawValue to get the Unicode value. Any new icon we'd have to manually insert it on the enum, and make sure we set the Unicode value correctly.
So, we decided to give a more versioned approach to the icons lib. We created a repository containing the font file (with the .ttf extension) and a JSON file describing the icons.
The JSON contains the name of the icon and the Unicode representation of it.
With that, inside the project, we could stop setting the Unicode value manually - which, let's be honest, is very susceptible to errors - and change how we fetched the Unicode value:
This way, we don't really need to know which Unicode represents each icon, we can simply finding them by their name! We just need to make sure that the JSON version we are using is compatible with our font.
Testing
We are ready to use the icon font, to create and use them inside views, to handle font updates… But what if something goes wrong?
What if a mistake on the font JSON file, or in the font file changed something on an icon. How could we prevent these kinds of mistakes?
That's why we added tests to the icon lib.
We could test if the JSON file has the expected Unicode value for each icon name, but that's not that good. The Unicode value can change between font versions and our tests would be useless. What we really want is to make sure that the final visual asset (the icon being displayed on-screen) is correct.
To do so, we made a snapshot test of the entire icon library, by creating a simple grid using UIStackViews and positioning all the icons at once.
Why not using individuals snapshots for every icon?
Well, you could, but you would end up having a lot of snapshots to handle, and having all the icons at once allowed us to see any changes on the lib as a whole, while still checking all the icons for individual issues.
Also, adding new icons, or removing can be detected on the snapshot failure, and if something goes wrong with the Unicode value and the icon is not rendered, it's very easy to detect an empty space on the snapshot and which icon is not working properly.
Wrapping up
Using an Icon Font is still an experiment for us, but until now, we've had many positive results. One of them being regarding our app size:
The entire font asset has something about 45Kb, storing more than 100 icons. And before, a single icon PNG/PDF file used to be around 5–25Kb.
If you are thinking about using this or a similar approach on your project, I totally encourage you to. But keep in mind that this does not only affect the developers, but also the design team on how will they create things and handle to the rest of the team later. Also, creating the font is already a big job by itself, and later handling its versions and updates.
If you are on an ongoing project, create plans to migrate your icons (I totally discourage you change everything right away) and test if the solution fits you in positioning, sizing, accessibility, quality, everything.
We still have a lot to test and to improve. One of the ideas I've had tese days is to create an IconButton
, similar to what we did for a regular view, but this time with a UIButton
and wrapping all the accessibility, actions, gestures logic inside it.
Try using it, try adapting it to your needs and you may come up with even better solutions!
✻ ❃ ✿ Keep coding everyone! ✿ ❃ ✻