The Swift code generator for your assets, storyboards, Localizable.strings, … — Get rid of all String-based APIs!
Swift Ruby Shell
Permalink
Failed to load latest commit information.
.github Added PR template Jan 3, 2017
GenumKit Version bump (4.1.0) Jan 3, 2017
Pods Version bump (4.1.0) Jan 3, 2017
SwiftGen.playground - Playground tweaks Nov 21, 2016
SwiftGen.xcodeproj Multiple paths support for fonts, images and storyboards commands (#213) Dec 12, 2016
SwiftGen.xcworkspace Change Travis image to use Xcode 7.3 instead of 7.0 + avoid double-fi… Jun 5, 2016
SwiftGen Reorganize project as an app bundle (#204) Dec 3, 2016
UnitTests Snake cased keys with uppercase letters are correctly camel cased aga… Jan 1, 2017
documentation Template recursion (#237) Dec 18, 2016
scripts Fix SwiftLint scripts escaping of path with spaces Jun 28, 2016
swiftgen-cli Parse storyboard customModule property (#243) Dec 18, 2016
templates Snake cased keys with uppercase letters are correctly camel cased aga… Jan 1, 2017
.gitignore Update Rakefile to release new version easily Mar 13, 2016
.swift-version - Bump swift version and change log Nov 21, 2016
.swiftlint.yml Added "force_unwrapping" rule to SwiftLint config. Fixed affected tem… Jun 7, 2016
.travis.yml Fix Travis.yml Dec 4, 2016
CHANGELOG.md Adding reference to script to simplify localization of existing proje… Jan 17, 2017
Gemfile Adding a Gemfile Dec 4, 2016
Gemfile.lock Adding a Gemfile Dec 4, 2016
LICENSE README: Instructions for shell redirection of output Jul 19, 2015
Podfile Update to Stencil development (#234) Dec 12, 2016
Podfile.lock Version bump (4.1.0) Jan 3, 2017
README.md Adding reference to script to simplify localization of existing proje… Jan 17, 2017
Rakefile Clean up some warnings (xcode and lint) (#224) Dec 6, 2016
SwiftGen.podspec Version bump (4.1.0) Jan 3, 2017

README.md

SwiftGen

Build Status CocoaPods Compatible Platform

SwiftGen is a suite of tools written in Swift to auto-generate Swift code (or anything else actually) for various assets of your project:

There are multiple benefits in using this:

  • Avoid any typo you could have when using a String
  • Free auto-completion
  • Avoid the risk to use an non-existing asset name
  • All this will be ensured by the compiler.

Installation

Via CocoaPods

If you're using CocoaPods, you can simply add pod 'SwiftGen' to your Podfile.

This will download the SwiftGen binaries and dependencies in Pods/ during your next pod install execution and will allow you to invoke it via $PODS_ROOT/SwiftGen/bin/swiftgen in your Script Build Phases.

Via Homebrew

To install SwiftGen via Homebrew, simply use:

$ brew update
$ brew install swiftgen

Compile from source

Alternatively, you can clone the repository and use rake install to build the tool.
With this solution you're sure to build and install the latest version from master.

You can install to the default locations (no parameter) or to custom locations:

# Binary is installed in `./swiftgen/bin`, frameworks in `./swiftgen/lib` and templates in `./swiftgen/templates`
$ rake install
# - OR -
# Binary will be installed in `~/swiftgen/bin`, framworks in `~/swiftgen/fmk` and templates in `~/swiftgen/tpl`
$ rake install[~/swiftgen/bin,~/swiftgen/fmk,~/swiftgen/tpl]

Usage

The tool is provided as a unique swiftgen binary command-line, with the following subcommands:

  • swiftgen images [OPTIONS] DIR
  • swiftgen strings [OPTIONS] FILE
  • swiftgen storyboards [OPTIONS] DIR
  • swiftgen colors [OPTIONS] FILE
  • swiftgen fonts [OPTIONS] DIR

Each subcommand has its own option and syntax, but some options are common to all:

  • --output FILE or -o FILE: set the file where to write the generated code. If omitted, the generated code will be printed on stdout.
  • --template NAME or -t NAME: define the Stencil template to use (by name, see here for more info) to generate the output.
  • --templatePath PATH or -p PATH: define the Stencil template to use, using a full path.

You can use --help on swiftgen or one of its subcommand to see the detailed usage.

Additional documentation

You can also see in the wiki some additional documentation, about:

Choosing your template

SwiftGen is based on templates (it uses Stencil as its template engine). This means that you can choose the template that best fit your preferences to customize the generated code to your own conventions.

Bundled templates vs. Custom ones

SwiftGen comes bundled with some default templates for each of the subcommand (colors, images, strings, storyboard, fonts…), but you can also create your own templates if the defaults don't suit your coding conventions or needs. Simply store them in ~/Library/Application Support/SwiftGen/templates, then use the -t / --template option to specify the name of the template to use, or store them somewhere else (like in your project repository) and use -p / --templatePath to specify a full path.

💡 You can use the swiftgen templates command to list all the available templates (both custom and bundled templates) for each subcommand, list the template content and dupliate them to create your own.

Templates bundled with SwiftGen include:

  • A default template, compatible with Swift 2
  • A swift3 template, compatible with Swift 3
  • Other variants, like structured and dot-syntax / dot-syntax-swift3 templates for Strings, or osx variant for OS X macOS Storyboards.

For more information about how to create your own templates, see the dedicated documentation.

Don't hesitate to make PRs to share your improvements suggestions on the default templates 😉

Swift 3 support

As explained above, among other templates, Swift is bundled with a swift3 template for each of its subcommands.

If you're using Swift 3, don't forget to use -t swift3 in your invocation of swiftgen to tell SwiftGen to use those Swift 3 templates and generate Swift 3 compatible code.

Playground

The SwiftGen.playground available in this repository will allow you to play with the code that the tool typically generates, and see some examples of how you can take advantage of it.

This allows you to have a quick look at how typical code generated by SwiftGen looks like, and how you will then use the generated enums in your code.


UIImage and NSImage

swiftgen images /dir/to/search/for/imageset/assets

This will generate an enum Asset with one case per image asset in your assets catalog, so that you can use them as constants.

Example of code generated by the default template

// The Image type below is typealias'ed to UIImage on iOS and NSImage on OSX
enum Asset: String {
  case Green_Apple = "Green-Apple"
  case Red_Apple = "Red apple"
  case _2_Pears = "2-pears"

  var image: Image {
    return Image(asset: self)
  }
}

extension Image {
  convenience init!(asset: Asset) {
    self.init(named: asset.rawValue)
  }
}

Usage Example

let image1 = UIImage(asset: .Banana)   // iOS Prefered way
let image2 = NSImage(asset: .Banana)   // OS X Prefered way
let image3 = Asset.Apple.image // Alternate way

Localizable.strings

swiftgen strings /path/to/Localizable.strings

This will generate a Swift enum L10n that will map all your Localizable.strings keys to an enum case. Additionaly, if it detects placeholders like %@,%d,%f, it will add associated values to that case.

Example of code generated by the default template

Given the following Localizable.strings file:

"alert_title" = "Title of the alert";
"alert_message" = "Some alert body there";
"greetings" = "Hello, my name is %@ and I'm %d";
"apples.count" = "You have %d apples";
"bananas.owner" = "Those %d bananas belong to %@.";

Reminder: Don't forget to end each line in your *.strings files with a semicolon ;! Now that in Swift code we don't need semi-colons, it's easy to forget it's still required by the Localizable.strings file format 😉

The generated code will contain this:

enum L10n {
  /// Title of the alert
  case AlertTitle
  /// Some alert body there
  case AlertMessage
  /// Hello, my name is %@ and I'm %d
  case Greetings(String, Int)
  /// You have %d apples
  case ApplesCount(Int)
  /// Those %d bananas belong to %@.
  case BananasOwner(Int, String)
}

extension L10n : CustomStringConvertible {
  var description : String { return self.string }

  var string : String {
    /* Implementation Details */
  }
  ...
}

func tr(key: L10n) -> String {
  return key.string
}

Usage Example

Once the code has been generated by the script, you can use it this way in your Swift code:

let title = L10n.AlertTitle.string
// -> "Title of the Alert"

// Alternative syntax, shorter
let msg = tr(.AlertMessage)
// -> "Some alert body there"

// Strings with parameters
let nbApples = tr(.ApplesCount(5))
// -> "You have 5 apples"

// More parameters of various types!
let ban = tr(.BananasOwner(2, "John"))
// -> "Those 2 bananas belong to John."

Automatically replace NSLocalizedString(...) calls

This script from Lutzifer can be run inside the project to transform NSLocalizedString(...) calls to the tr(...) syntax.

This script from HuguesBR can be run inside the (annotated) project to transform specific string (see detail in the gist) syntax to the tr(...) syntax as well as populating the Localizable.strings's file

Dot Syntax Support

SwiftGen also has a template with dot syntax support. This is the recommended way of using the strings command in SwiftGen and will replace the current default in future versions.

The main advantage of this is a much more useful auto-completion. To use the template simply add the -t option, either using the template dot-syntax for Swift 2 or dot-syntax-swift3 if you're using Swift 3:

# Swift 2 compatible template
swiftgen strings -t dot-syntax /path/to/Localizable.strings
# Swift 3 compatible template
swiftgen strings -t dot-syntax-swift3 /path/to/Localizable.strings

Given the same Localizable.strings as above the usage will now be:

let title = L10n.AlertTitle
// -> "Title of the Alert"

let nbApples = L10n.Apples.Count(5)
// -> "You have 5 apples"

let ban = L10n.Bananas.Owner(2, "John")
// -> "Those 2 bananas belong to John."

Note that all dots within the key are converted to dots in code.

The maximum number of dots supported is five (deeper levels are rendered without dots after the fifth level).

UIStoryboard

swiftgen storyboards /dir/to/search/for/storyboards

This will generate an enum for each of your UIStoryboard, with one case per storyboard scene.

Example of code generated by the default template

The generated code will look like this:

protocol StoryboardSceneType {
    static var storyboardName : String { get }
}

extension StoryboardSceneType {
    static func storyboard() -> UIStoryboard {
        return UIStoryboard(name: self.storyboardName, bundle: nil)
    }

    static func initialViewController() -> UIViewController {
        return storyboard().instantiateInitialViewController()!
    }
}

extension StoryboardSceneType where Self: RawRepresentable, Self.RawValue == String {
    func viewController() -> UIViewController {
        return Self.storyboard().instantiateViewControllerWithIdentifier(self.rawValue)
    }
    static func viewController(identifier: Self) -> UIViewController {
        return identifier.viewController()
    }
}

protocol StoryboardSegueType : RawRepresentable { }

extension UIViewController {
  func performSegue<S : StoryboardSegueType where S.RawValue == String>(segue: S, sender: AnyObject? = nil) {
    performSegueWithIdentifier(segue.rawValue, sender: sender)
  }
}

struct StoryboardScene {
  enum Message : String, StoryboardSceneType {
    static let storyboardName = "Message"

    case Composer = "Composer"
    static func composerViewController() -> UIViewController {
      return Message.Composer.viewController()
    }

    case URLChooser = "URLChooser"
    static func urlChooserViewController() -> XXPickerViewController {
      return Message.URLChooser.viewController() as! XXPickerViewController
    }
  }
  enum Wizard : String, StoryboardSceneType {
    static let storyboardName = "Wizard"

    case CreateAccount = "CreateAccount"
    static func createAccountViewController() -> CreateAccViewController {
        return Wizard.CreateAccount.viewController() as! CreateAccViewController
    }

    case ValidatePassword = "Validate_Password"
    static func validatePasswordViewController() -> UIViewController {
        return Wizard.ValidatePassword.viewController()
    }
  }
}

struct StoryboardSegue {
  enum Message : String, StoryboardSegueType {
    case Back = "Back"
    case Custom = "Custom"
    case NonCustom = "NonCustom"
  }
}

Usage Example

// Initial VC
let initialVC = StoryboardScene.Wizard.initialViewController()
// Generic ViewController constructor, returns a UIViewController instance
let validateVC = StoryboardScene.Wizard.ValidatePassword.viewController()
// Dedicated type var that returns the right type of VC (CreateAccViewController here)
let createVC = StoryboardScene.Wizard.createAccountViewController()

override func prepareForSegue(_ segue: UIStoryboardSegue, sender sender: AnyObject?) {
  switch StoryboardSegue.Message(rawValue: segue.identifier)! {
  case .Back:
    // Prepare for your custom segue transition
  case .Custom:
    // Prepare for your custom segue transition
  case .NonCustom:
    // Prepare for your custom segue transition
  }
}

initialVC.performSegue(StoryboardSegue.Message.Back)

NSStoryboard

swiftgen storyboards --template storyboards-osx-default /dir/to/search/for/storyboards

This will generate an enum for each of your NSStoryboard, with one case per storyboard scene.

Example of code generated by the default template

The generated code will look like this:

protocol StoryboardSceneType {
  static var storyboardName: String { get }
}

extension StoryboardSceneType {
  static func storyboard() -> NSStoryboard {
    return NSStoryboard(name: self.storyboardName, bundle: nil)
  }

  static func initialController() -> AnyObject {
    guard let controller = storyboard().instantiateInitialController()
    else {
      fatalError("Failed to instantiate initialViewController for \(self.storyboardName)")
    }
    return controller
  }
}

extension StoryboardSceneType where Self: RawRepresentable, Self.RawValue == String {
  func controller() -> AnyObject {
    return Self.storyboard().instantiateControllerWithIdentifier(self.rawValue)
  }
  static func controller(identifier: Self) -> AnyObject {
    return identifier.controller()
  }
}

protocol StoryboardSegueType: RawRepresentable { }

extension NSWindowController {
  func performSegue<S: StoryboardSegueType where S.RawValue == String>(segue: S, sender: AnyObject? = nil) {
    performSegueWithIdentifier(segue.rawValue, sender: sender)
  }
}

extension NSViewController {
  func performSegue<S: StoryboardSegueType where S.RawValue == String>(segue: S, sender: AnyObject? = nil) {
    performSegueWithIdentifier(segue.rawValue, sender: sender)
  }
}

struct StoryboardScene {
  enum Anonymous_Osx: StoryboardSceneType {
    static let storyboardName = "Anonymous-osx"
  }
  enum Message_Osx: String, StoryboardSceneType {
    static let storyboardName = "Message-osx"

    case MessagesTabScene = "MessagesTab"
    static func instantiateMessagesTab() -> CustomTabViewController {
      guard let vc = StoryboardScene.Message_Osx.MessagesTabScene.controller() as? CustomTabViewController
      else {
        fatalError("ViewController 'MessagesTab' is not of the expected class CustomTabViewController.")
      }
      return vc
    }

    case WindowCtrlScene = "WindowCtrl"
    static func instantiateWindowCtrl() -> NSWindowController {
      guard let vc = StoryboardScene.Message_Osx.WindowCtrlScene.controller() as? NSWindowController
      else {
        fatalError("ViewController 'WindowCtrl' is not of the expected class NSWindowController.")
      }
      return vc
    }
  }
}

struct StoryboardSegue {
  enum Message_Osx: String, StoryboardSegueType {
    case Custom = "Custom"
    case Embed = "Embed"
  }
}

Usage Example

// Initial VC
let initialVC = StoryboardScene.Message_Osx.initialController()
// Dedicated type var that returns the right type of VC (CustomTabViewController here)
let messageDetailsVC = StoryboardScene.Message_Osx.instantiateMessageTab()
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) {
    switch StoryboardSegue.Message_Osx(rawValue: segue.identifier!) {
    case .Custom:
    // Prepare for your custom segue transition
    case .Embed:
        // Prepare for your embed segue transition
    }
}
initialVC.performSegue(StoryboardSegue.Message.Back)

UIColor and NSColor

swiftgen colors /path/to/colors-file.txt

This will generate a enum ColorName with one case per color listed in the text file passed as argument.

The input file is expected to be either:

  • a plain text file, with one line per color to register, each line being composed by the Name to give to the color, followed by ":", followed by the Hex representation of the color (like rrggbb or rrggbbaa, optionally prefixed by # or 0x). Whitespaces are ignored.
  • a JSON file, representing a dictionary of names -> values, each value being the hex representation of the color
  • a XML file, expected to be the same format as the Android colors.xml files, containing tags <color name="AColorName">AColorHexRepresentation</color>
  • a *.clr file used by Apple's Color Paletes.

For example you can use this command to generate colors from one of your system color lists:

swiftgen colors ~/Library/Colors/MyColors.clr

Generated code will look the same as if you'd use text file.

Example of code generated by the default template

Given the following colors.txt file:

Cyan-Color       : 0xff66ccff
ArticleTitle     : #33fe66
ArticleBody      : 339666
ArticleFootnote  : ff66ccff
Translucent      : ffffffcc

The generated code will look like this:

// The Color type below is typealias'ed to UIColor on iOS and NSColor on OSX
extension Color {
  /* Private Implementation details */
  ...
}

enum ColorName {
  /// <span style="display:block;width:3em;height:2em;border:1px solid black;background:#339666"></span>
  /// Alpha: 100% <br/> (0x339666ff)
  case ArticleBody
  /// <span style="display:block;width:3em;height:2em;border:1px solid black;background:#ff66cc"></span>
  /// Alpha: 100% <br/> (0xff66ccff)
  case ArticleFootnote
  /// <span style="display:block;width:3em;height:2em;border:1px solid black;background:#33fe66"></span>
  /// Alpha: 100% <br/> (0x33fe66ff)
  case ArticleTitle
  /// <span style="display:block;width:3em;height:2em;border:1px solid black;background:#ff66cc"></span>
  /// Alpha: 100% <br/> (0xff66ccff)
  case Cyan_Color
  /// <span style="display:block;width:3em;height:2em;border:1px solid black;background:#ffffff"></span>
  /// Alpha: 80% <br/> (0xffffffcc)
  case Translucent
}

extension Color {
  convenience init(named name: ColorName) {
    self.init(rgbaValue: name.rgbaValue)
  }
}

Usage Example

// iOS
UIColor(named: .ArticleBody)
UIColor(named: .ArticleFootnote)
UIColor(named: .ArticleTitle)
UIColor(named: .Cyan_Color)
UIColor(named: .Translucent)

// OS X
NSColor(named: .ArticleBody)
NSColor(named: .ArticleFootnote)
NSColor(named: .ArticleTitle)
NSColor(named: .Cyan_Color)
NSColor(named: .Translucent)

This way, no need to enter the color red, green, blue, alpha values each time and create ugly constants in the global namespace for them.

UIFont and NSFont

swiftgen fonts /path/to/font/dir

This will recursively go through the specified directory, finding any typeface files (TTF, OTF, …), defining a struct FontFamily for each family, and an enum nested under that family that will represent the font styles.

Example of code generated by the default template

// The Font type below is typealias'ed to UIFont on iOS and NSFont on OSX
struct FontFamily {
  enum Helvetica: String {
    case Regular = "Helvetica"
    case Bold = "Helvetica-Bold"
    case Thin = "Helvetica-Thin"
    case Medium = "Helvetica-Medium"

    func font(size: CGFloat) -> Font? { return Font(name:self.rawValue, size:size)}
  }
}

Usage

// Helvetica Bold font of point size 16.0
let font = FontFamily.Helvetica.Bold.font(16.0)
// Another way to build the same font
let sameFont = UIFont(font: FontFamily.Helvetica.Bold, size: 16.0) // iOS
let sameFont = NSFont(font: FontFamily.Helvetica.Bold, size: 16.0) // OS X

License

This code and tool is under the MIT License. See LICENSE file in this repository.

It also relies on Stencil, Commander and PathKit licenses.

Any ideas and contributions welcome!