Type-safe assets in Swift
Whenever you need to refer to resources in your apps, whether it be colors, fonts or images, you typically don’t want to use magic strings to refer to them. You want to reference Swift types.
There are multiple third party tools that can help you with this, but if you don’t want to add yet another step to your build process for your project, or just want to have total control over the API you’re defining, this article is for you!
There’s obviously a little bit more boiler-plate with this approach, but you will get to customize and extend the types you create as much as you like.
If you are, however, looking for an automatic solution, I recommend trying out SwiftGen
Setup
Before we begin, we should define ourselves a Swift Package library where we can keep our resources and code (if you haven’t already).
Defining it in a package allows us to easily share it within our target and/or other targets, decouple it from the rest of the project, and will speed up the compilation of your main project. (More on those benefits in another article).
If you haven’t already, create a new package for your project:
We’ll call ours MyProjectKit
, save it to the project's root folder, and add it to the project.
Adding it to the project will allow us to easily edit the package locally in the same project (even though it is not actually a part of the project).
Your project structure should look something like this:
Starting out, both our package and library product have the same name MyProjectKit
.
We’ll quickly change our library name to Assets:
To make sure everything still builds, add the new Assets
library to the iOS target and build:
Now that everything is set up (for success!), let’s start coding!
Colors
Let’s begin by defining our colors!
When we created the project in Xcode, we got an Asset.xcassets
by default.
Instead of throwing everything into a single monolithic .xcassets
folder, let's rather create a new one for just your colors, named Colors.xcassets
.
We’ll save this file in a folder named Resources
in our newly created package:
Xcode knows how to process files like .xcassets in Swift Packages, so we don’t have to specify it in the package manifest.
Let’s add some colors:
Now that we’ve got some colors, let’s define a type that represents them:
import SwiftUIpublic enum AppColor: String {
case background
case text
}extension AppColor: CaseIterable {}public extension AppColor {
var color: Color { Color(rawValue, bundle: .module) }
}
Notice the use of semantic naming for the colors. We’re not naming colors based on their properties, but rather their purpose.
Because we’re in another module than our iOS app, we need to include
bundle: .module
, otherwise the target importing this will look for the colors in its own module.
Now we have a simple type that describes our colors, and a convenient computed property that gives us the Color
type to use for our view related code.
Refering to a color in a view is as simple as AppColor.background.color
.
Because it’s just a simple enum type, we can store this in our model/data layer without having to store a reference to SwiftUI’s Color
type. It would simply be an enum, and it would be up to the UI layer to interpret what color it should show.
Let’s try it out!
Add the color to your app’s root view like so:
import SwiftUI
import Assets@main
struct MyProjectApp: App {
var body: some Scene {
WindowGroup {
AppColor.background.color
.ignoresSafeArea()
}
}
}
You should now be able to run your app and see your beautiful pre-defined background color!
Images
Moving on, let’s apply the same concepts to our images!
I’ve added a new Images.xcassets
to our Assets
library:
Icon made by https://www.freepik.com from https://www.flaticon.com
Similarly as our color type, we’ll add a new type called AppImage
:
import Foundation
import SwiftUIpublic enum AppImage: String {
case hammer
case star
}public extension AppImage {
var image: Image {
switch self {
case .hammer: return Image(rawValue, bundle: .module)
case .star: return Image(systemName: "star.fill")
}
}
}
Because we’re creating our own implementation, we can decide how our
image
property is made. We fetch the hammer image from ourImages.xcassets
file, but the star is created from the builtSF Symbols
.
Let’s make sure it works. Update your MyProjectApp.swift
with this:
import Assets
import SwiftUI@main
struct MyProjectApp: App {
var body: some Scene {
WindowGroup {
ZStack {
AppColor.background.color
.ignoresSafeArea() VStack {
AppImage.hammer.image
AppImage.star.image
.resizable()
.frame(width: 100, height: 100)
}
}
}
}
}
Result:
Great success!
Fonts
Next up, we’ll tackle fonts.
Once again, we’ll define a new type; AppFont
import SwiftUIpublic enum AppFont {
case title
case subtitle
}public extension AppFont {
var font: Font {
switch self {
case .title:
return .custom("ComicNeue-Bold", size: 36)
case .subtitle:
return .body
}
}
}
We’re also using semantic naming here, similarly to our colors!
Our title font uses a custom font, while our subtitle simply uses one of the built in dynamic type fonts
For our title, I decided to download Comic Neue, because no app is complete without some sort of Comic font.
Since we’ve imported a font into our package folder, we need to declare it as a resource in the package manifest:
.target(name: "Assets", resources: [.process("Resources")]),
This makes sure our fonts are included when using/distributing this package.
There’s one more thing we have to do.
With fonts, it is not as easy as specifying the module like with the colors or images. We have to register the fonts with Core Text’s font manager.
Add this code snippet to AppFont.swift
:
extension AppFont {
private static var fonts = [(name: "ComicNeue-Bold", extension: "ttf")]
public static func registerFonts() {
for font in AppFont.fonts {
guard let fontURL = Bundle.module.url(forResource: font.name, withExtension: font.extension),
let provider = CGDataProvider(url: fontURL as CFURL),
let font = CGFont(provider)
else {
fatalError("Failed to register font \(font)")
} var error: Unmanaged<CFError>? CTFontManagerRegisterGraphicsFont(font, &error)
}
}
}
We also need to call this function on app startup:
@main
struct MyProjectApp: App {
init() {
AppFont.registerFonts()
}
...
Our custom font should now register and be usable within the app!
Let’s add some text:
import Assets
import SwiftUI@main
struct MyProjectApp: App {
init() {
AppFont.registerFonts()
} var body: some Scene {
WindowGroup {
ZStack {
AppColor.background.color
.ignoresSafeArea() VStack {
Text("U Can't Touch This")
.font(AppFont.title.font)
.foregroundColor(AppColor.text.color) Text("Stop! Hammer Time!")
.font(AppFont.subtitle.font)
.foregroundColor(AppColor.text.color) AppImage.hammer.image AppImage.star.image
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(AppColor.starred.color)
}
}
}
}
}
Note, I added a new
starred
color!
And there we have it, our title and subtitle font!
Tip
You might end up with a lot of assets, and to keep things tidy, you can seperate different domains with new enums:
public extension AppImage {
enum Tab: String {
case first
case second
case third public var image: Image {
switch self {
case .first:
return Image(systemName: "circle")
case .second:
return Image(systemName: "triangle")
case .third:
return Image(systemName: "rhombus")
}
}
}
}
And so the usage becomes:
AppImage.Tab.first.image
Summary
We’ve created types for our images, fonts and colors. They’re neatly contained in their own little library, that we can easily share with other projects.
I hope you enjoyed this article, have a nice day!