Home iOS & Swift Tutorials

SwiftGen Tutorial for iOS

Learn how SwiftGen makes it easy to get rid of magic strings in your iOS projects.

5/5 1 Rating

Version

  • Swift 5, iOS 14, Xcode 12

As a mobile developer, you might feel like you encounter magic daily. The lines of code you write get converted into apps that people all over the world can use. The tools Apple provides help make that magic come to life and make your life easier. As you journey further into software development, you might realize there’s one piece of magic you don’t like: the magic string.

Type safety, the concept that variables can only be of a specific type, provides developers with guardrails that keep their programs safe. Magic strings, however, introduce unsafe code into those applications. What is a magic string? In iOS development, you’ve encountered these many times. An example looks something like the following:

let color = UIColor(named: "green-apple")
self.title = "Welcome!"

This example shows "green-apple" and "Welcome!" written as strings directly in your code. It’s not a stretch to say all developers have found themselves guilty of this practice sometimes.

In fact, in iOS development, you don’t have much of a choice. Out of the box, Xcode doesn’t provide a way to avoid this practice.

Those who’ve worked in Android might find themselves cringing at code like this. Android development environments have a mechanism that converts app resources, such as strings, colors, images and fonts, into type-safe variables. There are many benefits to this. It:

  • Reduces the risk of misspelling.
  • Prevents unnecessary duplication of resources.
  • Provides resource checking at compilation time.
  • Helps when cleaning up old resources.
  • And more!

As stated, iOS and macOS developers don’t have access to a system that provides this resource type-safety out of the box.

Fortunately, there’s SwiftGen, a code generator for getting rid of magic strings in your app. Available as an open-source library on GitHub, you can add this to your iOS and macOS projects to bring type safety and compilation-time checking of all your assets.

In this tutorial, you’ll learn how to:

  • Set up your project with SwiftGen.
  • Define which assets you want to convert.
  • Define where the generated code will live.
  • Create templates that allow SwiftGen to generate code that will work with SwiftUI for fonts and colors.

Getting Started

To get started, click the Download Materials button at the top or bottom of this tutorial.

There are a few ways you can install SwiftGen to work with your environment:

  • CocoaPods
  • Homebrew
  • Mint
  • Directly download a zipped release

In this tutorial, you’ll use CocoaPods to manage SwiftGen.

Note: If you don’t have CocoaPods, don’t worry — the starter and final projects already have the dependency downloaded. :]

Open the workspace, named DrinksUp!.xcworkspace. Because this project uses CocoaPods, you won’t be able to work with DrinksUp!.xcodeproj directly.

Take a moment to look around in Xcode. The project is already in a completed state but uses strings to reference fonts, colors, images and strings. You’ll convert all these by the end of the tutorial.

Build and run and get familiar with the app.

DrinksUp initial launch screen

The app, DrinksUp!, is a way to track fun drinks you and your family have tried while visiting restaurants or at home.

Setting up SwiftGen

Start by opening Terminal and navigating to your starter project’s root directory. Next, enter the following command in Terminal:

./Pods/SwiftGen/bin/swiftgen config init

This will generate a configuration file, named swiftgen.yml, at your project root. If this file opens automatically in Xcode, go ahead and close it.

Next, in your project workspace, go to File ▸ Add Files to “DrinksUp!”…. Find swiftgen.yml. Be sure to uncheck Copy items if needed and select Create folder references.

Add SwiftGen.yml without copying file

Click the Add button. When done, you should see swiftgen.yml at the top of the Project navigator, like below:

SwiftGen yml file in Project navigator

Note: You can move this file to sit in the same place as shown, if Xcode didn’t add it the same way.

This file is where you’ll place the instructions telling SwiftGen which files you want to convert into generated code. The file type, YML, indicates it’s using YAML for its syntax. If you haven’t used YAML before, it’s simply a more readable way to view serialized data. You can think of it as JSON, simplified.

Now, replace the entire contents of swiftgen.yml with the following:

# 1
input_dir: DrinksUp!/
# 2
output_dir: DrinksUp!/Generated/

Here’s what you added:

  1. You’ve declared a variable, input_dir, or the input directory. This tells SwiftGen the root folder to navigate against for all the file paths you’ll add soon.
  2. Another variable that defines the output directory of the generated Swift files. By doing this, you make it easier to keep track of all the SwiftGen files.

Adding a Build Phase

To run SwiftGen, you’ll need to add a new build phase to your project. To do this, select your project in the Project navigator, select Build Phases. Select + and choose New Run Script Phase.

Add new run script to project

Rename the script to SwiftGen by double-clicking the current name, Run Script. Next, add the following inside the text field of the script:

if [[ -f "${PODS_ROOT}/SwiftGen/bin/swiftgen" ]]; then
  "${PODS_ROOT}/SwiftGen/bin/swiftgen"
else
  echo "warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it."
fi 

Finally, reorder the script to sit just after the script name [CP] Check Pods Manifest.lock. Your Build Phases should now look like the following:

SwiftGen script added and ordered

Build and run. If everything is set up properly, you shouldn’t have any errors. You won’t have anything in your Generated folder yet. That comes next.

Converting XCAssets

Now, you’re ready to start removing strings from your project! The first step will be to have SwiftGen generate code for XCAsset files in the project. Open swiftgen.yml and add the following to the end of the file:

## XCAssets
# 1
xcassets:
  # 2
  inputs:
    - Assets.xcassets
    - Colors.xcassets
  # 3
  outputs:
    # 4
    templateName: swift5
    # 5
    output: XCAssets+Generated.swift

Here’s what each of these lines means:

  1. Each file type, or template, you want to convert with SwiftGen requires an entry at the root level of swiftgen.yml. Here, this indicates you want SwiftGen to convert files that are an XCAsset.
  2. This list indicates which files SwiftGen should limit its conversion to.
  3. You need to tell SwiftGen how to generate the output.
  4. You must provide a template name. Here, swift5 is a default template provided by the SwiftGen team. You’ll learn how to use your own templates later.
  5. Finally, you provide the file name you want your new Swift code to generate into. Remember, you defined output_dir at the top of the file, which means it will output into Generated/XCAssets+Generated.swift.

Build and run. If you didn’t encounter any errors, your code generation worked!

Adding File

Expand the Generated group in Project navigator. At the moment, you still won’t find your new file. To add it, right-click Generated and select Add Files to “DrinksUp!”….

Add generated file to project

Select XCAssets+Generated.swift. Ensure Copy items if needed is not selected, then click Add. Now, open XCAssets+Generate.swift and look around. You’ll see the enum Asset. In the enum, you’ll find further enumerations defined that match the XCAsset catalogs you defined. For example:

  • Assets: Each of the images included in the project now has a static property defined.
  • Colors: All the app’s colors also have static properties available to reference.

Open Assets.xcassets. Notice there’s a group of images named launch-assets. But Assets declared all the static image properties at the same level. SwiftGen can maintain this organization for you but doesn’t do so by default. Open swiftgen.yml and replace the entire xcassets entry with the following:

## XCAssets
xcassets:
  inputs:
    - Assets.xcassets
    - Colors.xcassets
  outputs:
    templateName: swift5
    # 1
    params:
       # 2
       forceProvidesNamespaces: true
       # 3
       forceFileNameEnum: true
    output: XCAssets+Generated.swift

Here, you take advantage of SwiftGen’s ability to customize your code output by doing the following:

  1. Define params on your outputs node.
  2. forceProvidesNamespaces: This will maintain your namespacing found in the asset catalogs.
  3. This additional parameter ensures no matter how many file names you provided in inputs, SwiftGen will maintain separate enumerations to represent each asset catalog.

Build the project, then go back to XCAssets+Generated.swift. You’ll see Assets now contains a new enum named LaunchAssets to represent your folder structure.

Now, it’s time to use this newly generated code to remove any string references to images. Open DrinksListView.swift. You’ll see Image("milkshake") within the toolbar items added on the view. Replace the line with the following:

Image(Asset.Assets.milkshake.name)

Here, you’ve referenced the image name for milkshake. As of now, SwiftGen doesn’t support working directly with SwiftUI. You’ll learn how to add this yourself later. For now, you can still use what comes out of the box to load the image asset without referencing the string directly.

Using Additional Basic Templates

There are a few additional templates you can take advantage of, without any need for customization.

Working With Interface Builder

The app you’re working with is using SwiftUI. However, to showcase SwiftGen’s ability to work with Interface Builder, the sample project includes a storyboard and some view controllers. Start by generating code to support Interface Builder or Storyboards by adding the following to swiftgen.yml:

## Interface Builder
ib:
  inputs:
    # 1
    - .
  outputs:
    # 2
    - templateName: scenes-swift5
      output: IB-Scenes+Generated.swift
    # 3
    - templateName: segues-swift5
      output: IB-Segues+Generated.swift

Here, you’ve done the following:

  1. Indicated to SwiftGen that you want it to look for any interface builder supported file in your project’s root directory.
  2. One great thing about SwiftGen is that it separates the concept of Scenes from Segues. This indicates the file your scenes will output to.
  3. Finally, this indicates where all segue information should be output to.

Build your project. Add IB-Scenes+Generate.swift and IB-Segues+Generated.swift to the Generated group, like you did for XCAssets+Generated.swift.

Now, you can replace the scene or segue information in the app.

Start by opening InformationViewController.swift. In InformationViewController, replace the implementation of showAbout() with the following:

performSegue(
  withIdentifier: StoryboardSegue.Main.showAbout.rawValue, 
  sender: self)

All segues will generate as actual enum cases, which makes things easier when you’re checking which segue triggered. For the sake of simplicity, the code you added is just triggering the segue.

Next, inside InformationView, replace makeUIViewController(context:) with the following:

func makeUIViewController(context: Context) -> some UIViewController {
  StoryboardScene.Main.initialScene.instantiate()
}

Here, you’ve simplified the code required to instantiate the initial view controller of a storyboard. SwiftGen provides a helper method to quickly access your initial scene.

Build and run. Tap Learn More at the top right. You’ll see a modal appear, showing you some information about the app. This is calling into makeUIViewController(context:) to know which view to load.

Show Learn More screen

Now, tap the About button. You’ll trigger the segue you modified.

DrinksUp! About screen

Working With JSON

The last quick win you’ll add is support for JSON. Add the following to the end of swiftgen.yml:

## JSON
json:
  inputs:
    - Resources/
  outputs:
    templateName: runtime-swift5
    output: JSON+Generated.swift

This now provides a way to reference any JSON files you have in the project’s resources. You’ll use this to convert the mock data bundled with the app.

Build the project, then add JSON+Generated.swift to your Generated group.

Now, open Drink.swift. Completely remove the struct named MockData. Then, replace the extension for DrinkStore, found at the bottom, with the following:

extension DrinkStore {
  static var mockData: [Drink] {
    do {
      let data = try JSONSerialization.data(
        withJSONObject: JSONFiles.starterDrinks,
        options: [])
      let mockDrinks = try JSONDecoder().decode([Drink].self, from: data)
      return mockDrinks
    } catch {
      print(error.localizedDescription)
      return []
    }
  }
}

Here, you should notice your code references JSONFiles.starterDrinks. Open MockData.json and notice the first key in the file, named starterDrinks. SwiftGen has taken this top-level object and provided it as a static property on JSONFiles for you to reference as needed.

Build and run. You shouldn’t notice anything different from before — just the drinks showing up in the list.

DrinksUp Initial Launch

Working With Strings

Perhaps one of the biggest conveniences SwiftGen provides is the ability to use variables to reference localized strings. It’s a great practice to place any text you’ll present to your app users inside of localized strings or stringsdict files. But if you’ve done this, you know that once you get beyond a handful of strings, it becomes difficult to remember what strings are available. It also feels redundant that you have a string in your strings file and … a string in your code.

This project contains the following strings files:

  • Localizable.strings: Your run-of-the-mill strings file, made of keys and values.
  • Localizable.stringsdict: You should use stringsdict files whenever you need to worry about pluralized strings. This file type supports not only translating strings but also how to pluralize words for any of the variations a language requires.

To convert all your strings files, add the following to swiftgen.yml:

## Strings
strings:
  inputs:
    # 1
    - en.lproj
  outputs:
    - templateName: structured-swift5
      # 2
      params:
        publicAccess: true
      output: Strings+Generated.swift

You should know a few important things about what you added here:

  1. When you convert your strings files, you should use only one of the localized directories. For each language added, a new localized directory gets created. In this project, the only localization is English. If you add more languages, there’s no need to modify this entry to pick up those additional translations. Because the strings file should have a matching set of keys and values, you’ll reference the translations like you would if you weren’t using SwiftGen.
  2. You added a new parameter, named publicAccess. If you look around any of the generated files you’ve added, you notice all the types have the access modifier of internal. By using this parameter, you can change the access modifier of the generated enumerations to be public.

Build the project, then add Strings+Generated.swift to the Generated group. Before you convert all the strings in the app, it’s important to understand how this file is a bit different.

Understanding the File

Open Strings+Generated.swift. You should see the parent type created, named L10n. In this enum, you’ll see several subtypes generated inside of it: DrinkDetail, Navigation and DrinkList. These correspond with the strings declared in Localizable.strings.

Open Localizable.strings and see how it declares the first entry:

"DrinkList.Navigation.Title" = "Drinks";

Notice how the key is declared using a dot-namespace notation:

  • DrinkList: This indicates this string belongs on the Drink List screen.
  • Navigation: Indicates this string is to be used in the navigation bar.
  • Title: Finally, this indicates it’s the title in the navigation bar.

Now, you may choose to organize and name your strings differently. There’s nothing wrong with that — this naming style is used to show how SwiftGen will convert and organize your code. For each period you place in your string, SwiftGen generates an additional subtype.

Back in Strings+Generated.swift, you’ll see the static function drinksCount. SwiftGen makes it easy to work with pluralized strings. Instead of having to create references to localized strings and using string formatters, these generated functions make it easy to use a function that takes your pluralized string’s values.

Now, convert all the localized strings used in the app to point to the generated types. Start by opening DrinksListView.swift. Next, find the line of code:

Text("DrinkList.Navigation.Title")

Change this to:

Text(L10n.DrinkList.Navigation.title)

Wait a second … what is L10n? This is a shortcut for “localization”. You might have also seen “internationalization” abbreviated as i18n. If you count the letters between the first letter and last “n” in either word, you find 10 or 18 letters, respectively. Although this makes sense, wouldn’t it be nice to use a different name for your top-level strings type?

Open swiftgen.yml and add a property to your strings entry, right after publicAccess, so it looks like this:

strings:
  inputs:
    - en.lproj
  outputs:
    - templateName: structured-swift5
      params:
        publicAccess: true
        enumName: Strings
      output: Strings+Generated.swift

Here, you added the parameter enumName. This lets you change the type from “L10n” to “Strings.”

Build and run. This time, you should have a compilation error. This is because the type L10n is no longer available. Go to DrinksListView.swift and find:

Text(L10n.DrinkList.Navigation.title)

Now, replace it with:

Text(Strings.DrinkList.Navigation.title)

Now, your app is using the new type name you provided in the previous step.

Build your app. You should no longer have any compilation errors.

Note: If you still are getting errors, you might need to clean your project. Select Product ▸ Clean Build Folder, then build again.

Next find the property drinkCountString. This is the code that uses the stringsdict file to handle how to display the count of drinks on the list. Replace it with the following:

private var drinkCountString: String {
  Strings.drinksCount(drinkStore.drinks.count)
}

If you compare it to the code that was there before, you can see this is a much quicker way to reference pluralized strings.

You should convert all the strings in the project away from using strings. Open Localizable.strings and look at all the string keys. You should find each usage of these keys in a Swift file and swap it for the variables generated by SwiftGen.

When you get to swapping the rating text found in DrinkDetailView.swift, it will use a function to provide the string, like how you handled the drink count.

Creating Custom Templates

Up to this point, your swiftgen.yml file has used default templates provided by SwiftGen. All these are stored in the SwiftGen pod. If these templates don’t quite provide the functionality you want, you can create your own templates to generate code how you would like. Templates are built using Stencil, an open-source project that provides a templating language for Swift. In this section, you’ll learn how to modify existing templates and use them to generate your code.

If you look in the Project navigator, you see there’s a folder named Templates. In that, there are two subfolders: fonts and xcassets. With these, you’ll have SwiftGen provide support to use colors and fonts directly in SwiftUI.

Supporting SwiftUI Colors

To add SwiftUI Color support, open assets_swift5_swiftui.stencil. Things might be a bit overwhelming at first, because the file doesn’t have any code syntax support.

On line 13, add the following line of code:

import SwiftUI

Next, go back to swiftgen.yml. In your first entry, for xcassets, find the line where you define the template name:

templateName: swift5

Now, replace it with the following:

templatePath: Templates/xcassets/assets_swift5_swiftui.stencil

Here, you’ve changed from using templateName to templatePath. This tells SwiftGen to use your custom template rather than a built-in one.

Build the project, then go to XCAssets+Generated.swift. At the top, you’ll now see:

import SwiftUI

Because you added the import to the Stencil file, when the code was generated, it picked up this change and added the new import. Pretty cool, right?

Open assets_swift5_swiftui.stencil and replace:

// Add Support For SwiftUI Here

With the following:

{{accessModifier}} private(set) lazy var color: Color = {
  Color(systemColor)
}()

In this code, you get to see how to use Stencil a bit more. Here’s what you added:

  • {{accessModifier}}: This is how you tell Stencil how to replace with something provided during the code generation. If you look at line 11 of this file, you see accessModifier defined as a variable. By default, the access modifier is internal. If you remember from before, you saw how you can change this to public
  • The rest of this is actually just standard code. It creates a SwiftUI color from a UIKit color.
Note: There was one other modification made to the template as part of the starter materials. It changes the type Color to SystemColor.

Build the project again, then go back to XCAssets+Generated.swift. On line 62, you should see the code from your template, now generated as real code.

Now, you need to swap out any hard-coded reference to a color to use your new functionality. Open DrinksListView.swift and find where the foreground color is set on the navigation title text. Replace it with the following:

Text(Strings.DrinkList.Navigation.title)
  .font(Font.custom("NotoSans-Bold", size: 17, relativeTo: .body))
  .foregroundColor(Asset.Colors.textColor.color)

Here, you’ve switched away from using a hard-coded string, with true SwiftUI support to reference a color directly.

You can use colors directly in UIKit, as well. Open AppMain.swift. Change the following line of code:

appearance.backgroundColor = UIColor(named: "header")

To be this:

appearance.backgroundColor = Asset.Colors.header.systemColor

Here, systemColor is a reference to the platform specific color type. It’ll be UIColor if you’re on iOS, and NSColor if you’re on macOS.

Two files have colors declared using strings:

  • AppMain.swift
  • DrinksListView.swift

Finish converting the rest of the colors from using strings in each of these files following the example above.

Supporting SwiftUI Fonts

Just as SwiftUI’s Color isn’t currently supported in SwiftGen, neither is Font. Start by opening fonts_swift5_swiftui.stencil and adding the following import to the top of the file:

import SwiftUI

Next, replace this block of code found near the end of the file:

// Add Support For SwiftUI here
fileprivate extension Font {
}

With the following:

fileprivate extension Font {
  // 1
  static func mappedFont(_ name: String, textStyle: TextStyle) -> Font {
    let fontStyle = mapToUIFontTextStyle(textStyle)
    let fontSize = UIFont.preferredFont(forTextStyle: fontStyle).pointSize
    return Font.custom(name, size: fontSize, relativeTo: textStyle)
  }

  // 2
  static func mapToUIFontTextStyle(
    _ textStyle: SwiftUI.Font.TextStyle
  ) -> UIFont.TextStyle {
    switch textStyle {
    case .largeTitle:
      return .largeTitle
    case .title:
      return .title1
    case .title2:
      return .title2
    case .title3:
      return .title3
    case .headline:
      return .headline
    case .subheadline:
      return .subheadline
    case .callout:
      return .callout
    case .body:
      return .body
    case .caption:
      return .caption1
    case .caption2:
      return .caption2
    case .footnote:
      return .footnote
    @unknown default:
      fatalError("Missing a TextStyle mapping")
    }
  }
}

Here’s what you added:

  1. mappedFont(_:textStyle:) creates a custom font from a name and TextStyle. The style is used to get the standard, default font size each font should be.
  2. mapToUIFontTextStyle(_:) simply provides a 1:1 mapping of a SwiftUI TextStyle to a UIKit TextStyle

Next, find this comment in the middle of the file:

// Add Support For SwiftUI Here

Replace that with the following:

{{accessModifier}} func textStyle(_ textStyle: Font.TextStyle) -> Font {
  Font.mappedFont(name, textStyle: textStyle)
}

This block of code is similar to what you added to provide Color support. The only difference here is that it’s specific to providing fonts directly in SwiftUI.

Now, open swiftgen.yml and add the following entry:

## Fonts
fonts:
  inputs:
    - Resources/Noto_Sans
  outputs:
    templatePath: Templates/fonts/fonts_swift5_swiftui.stencil
    output: Fonts+Generated.swift

This app uses two fonts, both located in the group Resources:

  1. NotoSans
  2. NotoSans-Bold

This new entry just needs to know where the parent folder of the font you’d like to support is. Everything else is similar to all the other entries you’ve added before.

Build your app and add Fonts+Generated.swift to Generated. Once you do, open Fonts+Generated.swift. Here, you can see how the font family gets organized. You should see that NotoSans has the following variations available:

  • Regular
  • Bold
  • BoldItalic
  • Italic

Like all the other generated code in the app, it’s fairly easy to use. Open AppMain.swift and replace the first line of application(_:didFinishLaunchingWithOptions:) with the following:

let buttonFont = FontFamily.NotoSans.bold.font(size: 16)

Here, you directly set the font size to the bold NotoSans font.

Next, go to DrinksListView.swift, find the first reference to a font, in the NavigationLink, and replace it with the following:

Text(drinkStore.drinks[index].name)
  .font(FontFamily.NotoSans.bold.textStyle(.body))

Here, you take advantage of your customized template’s code that can generate a custom SwiftUI font, size to match a default TextStyle: in this case body.

Finally, finish converting all the usages of font throughout the app. In both DrinksListView.swift and DrinkDetailView.swift, you’ll find several places where font is set. Following the example above, you can convert the code away from using a string to the appropriate weight of NotoSans. Each of these placements already indicates which TextStyle they should have.

Build and run. Your app should still look identical to how it did when you started. But you should now have all resources referenced in a type-safe way!

DrinksUp Initial Launch

Where to Go From Here?

You can download the completed version of the project by clicking the Download Materials button at the top or bottom of this tutorial.

You now can use SwiftGen to:

  • Remove the need to use strings to reference resources throughout your app, whether you use SwiftUI or UIKit.
  • Customize the output files using built-in parameters.
  • Use your own templates to generate code.

To learn more about it, check out SwiftGen on GitHub. You can learn more about Stencil also on GitHub.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

1 rating

More like this

Contributors

Comments