Igor Kulman

Generating boilerplate Swift code with GYB

· Igor Kulman

How many times how you copied and pasted some code in your current codebase because there was no good way to abstract it? Maybe because it was some repeating code required by a framework or mapping of some data transfer structures.

Writing such boilerplate code is an error-prone waste of time, especially when there is a much better way: generating that code.

There are a few tools to help you do that, one of the most flexible of them being GYB.

What is GYB?

GYB is a lightweight templating system that allows you to use Python to generate text files. Those text files can be Swift source code files or anything else you need.

GYB is used in the Swift standard library codebase so it works well for generating Swift source code and it is a proven technology that works.

You can download GYB to your project from the Swift repository on Github

wget https://github.com/apple/swift/raw/master/utils/gyb
wget https://github.com/apple/swift/raw/master/utils/gyb.py
chmod +x gyb

I put it directly to the project directory and include it in the source control for simpler build and CI/CD setup.

Creating GYB templates

GYB templates for generating Swift source file look like Swift source files with some Python snippets for code generation.

This is probably best shown on an actual example. Let’s say you have a list of permissions that the app needs to support. Those permissions are then a part of a protocol and of a few structs.

You can create a template that generates the protocol from a CSV with the permissions

%{
  import csv
  permissions = []

  source_file = open('Data/permissions.csv', 'rb')
  for line in csv.DictReader(source_file, delimiter = ','):
      permissions.append(line["Name"])
}%
import Foundation

public enum Permission {
    case Allow
    case Deny
    case Undefined
}

public protocol Permissions {
    % for permission in permissions:
    var ${permission}: Permission { get }
    % end
}

This GYB template really looks like a Swift source file just with one difference. Instead of manually adding properties for all the permissions you just use a for loop to have those properties generated.

You can use % code: ... % end to manage control flow, like using the for loop in this example, or %{ code } to evaluate Python code, like printing the permission names read from the CSV file.

The resulting Swift file might look something like this

import Foundation

public enum Permission {
    case Allow
    case Deny
    case Undefined
}

public protocol Permissions {
    var accessPhotos: Permission { get }
    var accessVideos: Permission { get }
    var accessMicrophone: Permission { get }
    var accessLocation: Permission { get }
    var accessCalendar: Permission { get }
    var fileSharing: Permission { get }
    var openIn: Permission { get }
    var copyPaste: Permission { get }
    var emailConversation: Permission { get }
    var inviteMembers: Permission { get }
    var leaveConversation: Permission { get }
    var attachments: Permission { get }
}

You can then also generate different implementation of this protocol, for example parsing those permissions from the backend or from an MDM

%{
  import csv

  permissions = []

  source_file = open('Data/permissions.csv', 'rb')
  for line in csv.DictReader(source_file, delimiter = ','):
      permissions.append(line["Name"])
}%
import Foundation

struct AppConfigPermissions: Permissions {
    % for permission in permissions:
    let ${permission}: Permission
    % end

    init(config: [String: Any]) {
        let getPermissionForKey = { (key: String) -> Permission in
            if let value = config[key] as? Bool {
                switch value {
                case true:
                    return .Allow
                case false:
                    return .Deny
                }
            }
            return .Undefined
        }

        % for permission in permissions:
        ${permission} = getPermissionForKey("allow${permission[:1].upper() + permission[1:]}")
        % end
    }
}

generates

import Foundation

struct AppConfigPermissions: Permissions {
    let accessPhotos: Permission
    let accessVideos: Permission
    let accessMicrophone: Permission
    let accessLocation: Permission
    let accessCalendar: Permission
    let fileSharing: Permission
    let openIn: Permission
    let copyPaste: Permission
    let emailConversation: Permission
    let inviteMembers: Permission
    let leaveConversation: Permission
    let attachments: Permission

    init(config: [String: Any]) {
        let getPermissionForKey = { (key: String) -> Permission in
            if let value = config[key] as? Bool {
                switch value {
                case true:
                    return .Allow
                case false:
                    return .Deny
                }
            }
            return .Undefined
        }

        accessPhotos = getPermissionForKey("allowAccessPhotos")
        accessVideos = getPermissionForKey("allowAccessVideos")
        accessMicrophone = getPermissionForKey("allowAccessMicrophone")
        accessLocation = getPermissionForKey("allowAccessLocation")
        accessCalendar = getPermissionForKey("allowAccessCalendar")
        fileSharing = getPermissionForKey("allowFileSharing")
        openIn = getPermissionForKey("allowOpenIn")
        copyPaste = getPermissionForKey("allowCopyPaste")
        emailConversation = getPermissionForKey("allowEmailConversation")
        inviteMembers = getPermissionForKey("allowInviteMembers")
        leaveConversation = getPermissionForKey("allowLeaveConversation")
        attachments = getPermissionForKey("allowAttachments")
    }
}

If you then need to add support for a new permission in the future, you just add it to the CSV file and the Swift code gets regenerated. No need to add the permissions manually to the protocol or to its different implementations.

Generating code

With the GYB templates created you now need to make Xcode generate the Swift source files from those templates. Ideally only when the templates change, not on every build.

I use a script that relies on a naming convention. The template is always called SomeFile.swift.gyb and it generated SomeFile.swift.

This script

for INFILE in ${!SCRIPT_INPUT_FILE_*};
do
    file=${!INFILE}
    if [ ${file: -4} == ".gyb" ]; then
        echo "Processing template $file"
        "${PROJECT_DIR}/support/gyb" --line-directive '' -o "${file%.gyb}" "$file"
    fi
done

is added as a build phase of the Xcode project

Code generation build phase

The script traverses all the files given as Input files in the Xcode build phase settings. If a file is a GYB template, it gets processed into a Swift code file.

Properly defining Input files and Output files is important because Xcode will run this build phase only when any of them changes or is missing. This is also the reason for including the CSV file in Input files even if it is not a GYB template file.

This build phase needs to be added before the Compile sources build phase so the files are generated before Xcode tries to build the project.

Sample project

As you might have noticed, setting up GYB generation in Xcode is not exactly trivial so I have created a sample project on Github to help you see everything properly set up and working.

See also