SwiftGen — How to neatly get rid of magic strings in iOS projects

It’s worth a try

Mobile@Exxeta
8 min readMay 8, 2023

In this story, we’ll introduce you to SwiftGen and explain how it can speed up your app development.

Foto von Pixabay: https://www.pexels.com/de-de/foto/brillen-oben-auf-der-buchseite-261857/

Why should you use SwiftGen?

Can you spot a problem in the following code at first glance?

self.title = NSLocalizedString("introScreen.footer.title", comment: "") 
let color = UIColor(named: "bg-color")
let image = UIImage(named: "welcome-logo")

The strings used in the code above refer to certain project resources; in this case, they are a color and image asset, declared in the project’s asset catalog and the string key supplied in the corresponding .strings file. Consider what would happen if one of the offered strings contained a typo? Most of the time, the compiler doesn’t throw any errors, but instead you end up with a wrongly selected or even nonexistent resource. The same applies to a different circumstance: when you want to translate a text that hasn’t been localized yet. When using only magic strings, it is difficult to verify that all of the texts in the app were correctly rewritten. And this is just where SwiftGen comes in handy. So what is SwiftGen?

SwiftGen is a tool that automatically generates Swift code for your project resources. So, the key benefits are:

  • More type-safety
  • Fewer typos when using strings
  • Less non-existing project resources
  • Less crashes at runtime
  • Additional autocompletion
  • Additional Quick Help suggestions

In the following, we show you, step by step, how you can set up SwiftGen easily.

SwiftGen installation

There are multiple ways to install SwiftGen. For the sake of this article, we describe just the CocoaPods installation:

  1. add pod 'SwiftGen','~> 6.0'to your project’s Podfile
  2. then you need to execute pod install --repo-update

SwiftGen setup

Afterwards, run the following command in the terminal, being careful to run it from the project root directory:

./Pods/SwiftGen/bin/swiftgen config init 

This creates a sample configuration file swiftgen.yml that tells SwiftGen which files should be converted into generated code and how. Add this file to your project workspace so it can be seen in Xcode’s project navigator. In addition, create a new group called Generated/ in the root_proj_dir/proj_name/ folder (see image below).

At this point, a new build phase should be added to your project to invoke SwiftGen. As the image shows, first you need to select the option New Run Script Phase under Build Phases.

New Run Script Phase

Secondly, rename the newly created build phase to SwiftGen and insert following script into its script section:

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, change the order of the script so it’s located right after [CP] Check Pods Manifest.lock. This is how the build phases should look now:

Build Phases

Creating a specific build phase for SwiftGen ensures that SwiftGen is invoked every time the build is run. This allows us to keep the generated code up-to-date and in sync with the latest project resources.

Configuration file

swiftgen.yml defines various parsers with corresponding parameters that should be considered when running SwiftGen. The code below shows how the config file is generally structured:

# swiftgen.yml 
# in the beginning, you can specify input_dir and output_dir, e.g., as follows:

input_dir: [project_name/] # optional

output_dir: [project_name/Generated/] # optional

# in general, swiftgen.yml contains the following section for each parser:
[parser_name]: # required
inputs: # required
- [path_to_input_folder_or_file]
outputs: # required
- templateName: [name_of_the_builtin_template] # required (either templateName or templatePath)
params: # optional
forceProvidesNamespaces: true # optional
publicAccess: true # optional
enumName: [name_of_the_generated_enum] # optional y
output: [path_to_output_file] # required

In case the config entries use a common input/output parent directory, there’s an option to specify them using the two top-level keys input_dir and output_dir at the beginning of the config file. Every input/output path in the rest of the config will then be expressed relative to these. These keys are optional and both default to “.”, which is the directory of the config file.

  1. [parser_name]: represents the chosen parser; this is the list of supported parsers: strings, xcassets, fonts, ib, coredata, json, yaml, plist, …
  2. inputs: required key, expects a path to the input file/folder as a value (relative to input_dir if specified)
  3. outputs: required key, expects at least 2 other subkeys: templateName and output
  4. templateName: required key, expects a builtin/predefined template name to be used (e.g., swift5, structured-swift5, scenes-swift5, segues-swift5). In case you want to use your own template, use the key templatePath instead and provide it with the path to the template.
  5. params: optional key, comes with several options that are available, for instance:
    forceProvidesNamespaces: forces to create a sub-namespace for each folder/group used in your Asset Catalogs when true
    publicAccess: access modifier of the generated enum will be public instead of internal when true
    enumName: defines a custom name for the generated enum
  6. output: required key, expects a path to the output file as a value (relative to output_dir if specified), in other words, where (in which file) should the code be generated.

Now, that the basic config structure is clear, let’s get started with generating type-safe code for the assets.

Generating XCAssets

Assume we want to use the following assets (images, colors) in the project:

Assets: Colors and Images

Normally, we access them using magic strings:

// Colors 
self.view.backgroundColor = UIColor(named: "bg-color")
let textColor = UIColor(named: "text-color")

// Images
let image = UIImage(named: "welcome-logo")

But let’s configure SwiftGen to generate more type-safe code for them so we don’t need to reference them with magic strings. So, add the following snippet to the swiftgen.yml:

 input_dir: SwiftGenDemo/ 

output_dir: SwiftGenDemo/Generated/

xcassets:
inputs:
- Resources/Assets.xcassets
- Resources/Colors.xcassets

outputs:
- templateName: swift5
params:
forceProvidesNamespaces: true
output: XCAssets+Generated.swift

Now, let’s build the project. The output file XCAssets+Generated.swift should be generated in the Generated folder. Add the generated file to the project workspace so it can be visible in Xcode (as well). The image below shows the most interesting part of the generated file.

Generated File — Asset enum

Now we can access the assets in the code as follows:

// Colors 
self.view.backgroundColor = Asset.Colors.bgColor.color
let textColor = Asset.Colors.textColor.color

// Images
let image = Asset.Assets.WelcomeAssets.welcomeLogo.image

Can you feel the difference? Hopefully, you can 😊

With this code you cannot reference a non-existing asset anymore. Moreover, there is always an autocompletion available for you, so you can decide what assets are available for you to use without having to open the corresponding asset catalogs repeatedly.

Generating localized strings

SwiftGen is probably the most useful and powerful when dealing with localized strings. When it comes to localization (localized strings), there are two file types that will be described in this section: .strings file and .stringsdict file.

Localizable.strings

Assume we have the following Localizable.strings:

Localizable

Ordinarily, we use them in the code this way:

// Localizable.strings 
label.text = NSLocalizedString("introScreen.footer.title", comment: "")
let btnTitle = NSLocalizedString("introScreen.body.btnText", comment: "")
button.setTitle(btnTitle, for: .normal)

Let’s add the corresponding section for the localized strings to the swiftgen.yml:

 strings: 
inputs:
- en.lproj # folder containing .strings & .stringsdict file
outputs:
- templateName: structured-swift5
params:
publicAccess: true
enumName: L # change the default enum name “L10n” to L
output: Strings+Generated.swift

Note: Don’t worry if you need to add more localizations/languages to your project in the future; there is no need to update the swiftgen.yml afterwards because each Localizable.strings file contains all the keys of the localized strings.

Now, build the project again. File Strings+Generated.swift should be generated inside the Generated folder. Add it to the project workspace as well as the previous generated file so it can be seen by Xcode.

Localized strings can finally be accessed like this:

// Localizable.strings 
label.text = L.IntroScreen.Footer.title
let btnTitle = L.IntroScreen.Body.btnText
button.setTitle(btnTitle, for: .normal)

Additional info: As the image below shows, autocompletion also displays the particular value for the given localized string, so you have a better overview of which string to use. Therefore, you don’t need to focus only on the given enum case (or localized string key) while coding.

Autocompletion

Moreover, after you click on the title within L.IntroScreen.Footer.title, the Quick Help inspector displays some more information about the string itself:

Quick Help inspector

…which is handy as well compared to pure magic strings.

Localizable.stringsdict

Slightly different usage applies to .stringsdict files. Let’s use the following Localizable.stringsdict file in the project to localize plurals for multiple languages:

Localizable

Normally, we use it:

// Localizable.stringsdict 
let applesCount = 10
let localized = NSLocalizedString("apples_found", comment: "")
let formatted = String(format: localized, applesCount)
label.text = formatted

There is nothing else to be added in the swiftgen.yml since we already included the strings parser with the input folder en.lproj, which also contains Localizable.stringsdict file. Therefore, just build the project again. File Strings+Generated.swift should update and, in the end, contain the following:

Strings+Generated.swift

With this generated, we can use the localized plurals as simply as it gets:

// Localizable.stringsdict 
let applesCount = 10
label.text = L.applesFound(applesCount)

Here you can see Xcode’s autocompletion with the helpful description:

Xcode Autcompletion

I hope you see the difference between the magic strings version and the SwiftGen version in this case, too.

Creating custom templates

If the built-in templates don’t suit your coding conventions or needs, you can also create your own templates. More information about it can be found in the SwiftGen documentation.

Conclusion

There are several SwiftGen alternatives, such as R.swift. Therefore, don’t be afraid to post a comment under the story if you have experience with any other tools of a similar nature and share your experience.

Summarized, the goal of this article is to raise awareness about SwiftGen and provide some basic instructions on how to use it practically. In general, SwiftGen is very powerful, and you can use it for different content:

  • Fonts (if you have custom font files)
  • Storyboards & XIBs (scenes and segues)
  • CoreData (CoreData models)
  • files (JSON, YAML, PLIST) — to parse custom files and generate code from their content

In conclusion, it’s never too late to start with SwiftGen 😊 It’s worth it (by Matúš Dobrotka).

--

--

Mobile@Exxeta

Passionate people @ Exxeta. Various topics around building great solutions for mobile devices. We enjoy: creating | sharing | exchanging. mobile@exxeta.com