Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Code Generation of .xcasset resources
import Foundation
/**
* Build script to generate a type safe wrapper around your projects .xcasset file.
* This will fail your build if you reference an image in your .xcasset file that has
* changed or been removed, as well as provide code completion help for all your images.
*
* Copy this file into a new `Run Phase` in your project, with `/usr/bin/env xcrun swift`
* specified for `Shell`.
*
* Configure the variables below:
*
* - outputPath: The location to write the generated file. This file must be manually
* added to your .xcodeproj.
* - path: The location to look for .imageset files (Which reside inside the .xcasset directory)
* - objcCompatible: A flag to use static methods or an enum.
*/
private struct Parameters {
static let outputFile = "./Assets.swift"
static let inputPath = "."
static let objcCompatible = false
static let separationCharacters = NSCharacterSet(charactersInString: "-_ ")
static let indentWidth = 4
}
// Transform image names in the xcasset to Tokens for use in code
func stringToCamelCase(input:String) -> String {
let parts = input.componentsSeparatedByCharactersInSet(Parameters.separationCharacters)
return parts.reduce("") { methodName, part in
methodName + (methodName == "" ? part : part.capitalizedString)
}
}
func stringToUpperCase(input:String) -> String {
let parts = input.componentsSeparatedByCharactersInSet(Parameters.separationCharacters)
return parts.reduce("") { methodName, part in
methodName + part.capitalizedString
}
}
// Formatting helper
func indent(lines:[String], level:Int = 1) -> [String] {
let space = "".join((0..<Parameters.indentWidth * level).map { (x:Int) in return " "})
return lines.map { "\(space)\($0)" }
}
// Functions to transform image names to lines of code
func imageNameToSwiftStaticFunc(imageName:String) -> String {
let methodName = stringToCamelCase(imageName)
return "static func \(methodName)() -> UIImage { return UIImage(named: \"\(imageName)\")! }"
}
func imageNameToSwiftEnum(imageName:String) -> String {
let methodName = stringToUpperCase(imageName)
return "case \(methodName) = \"\(imageName)\""
}
// Higher level transforms from image names to compilable code
let generated = ["/*",
" * WARNING: This file is automatically generated based on the contents",
" * of your .xcasset files. Do not modify.",
" */"]
func swiftImageExtension(imageNames:[String]) -> String {
let functions = indent(imageNames.map(imageNameToSwiftStaticFunc))
var lines = ["import UIKit", "extension UIImage {", "}"]
lines.splice(functions, atIndex: 2)
lines.splice(generated, atIndex: 0)
return "\n".join(lines)
}
func swiftImageEnum(imageNames:[String]) -> String {
let functions = indent(imageNames.map(imageNameToSwiftEnum), level:2)
var lines = ["import UIKit",
"extension UIImage {",
" enum AssetName : String {",
" }",
" convenience init!(asset:AssetName) {",
" self.init(named:asset.rawValue)",
" }",
"}"]
lines.splice(functions, atIndex: 3)
lines.splice(generated, atIndex: 0)
return "\n".join(lines)
}
// Obtain image names and pass them to a higher level transform function
if let paths = NSFileManager.defaultManager().subpathsAtPath(Parameters.inputPath) {
let imageNames = paths.filter {
$0.pathExtension == "imageset"
}.map { (x:String) -> String in
x.lastPathComponent.stringByDeletingPathExtension
}
let body = Parameters.objcCompatible ? swiftImageExtension(imageNames) : swiftImageEnum(imageNames)
try! body.writeToFile(Parameters.outputFile, atomically:true, encoding: NSUTF8StringEncoding)
}
else {
print(" warning: Unable to find any imageset files in \(Parameters.inputPath)")
}
@jwatson
Copy link

jwatson commented Jun 12, 2015

I like to stash my constants into a struct so that they're namespaced, e.g.:

private struct Parameters {
    static let outputPath = "./Output.swift"
}

And then refer to Parameters.outputPath. Might be overkill for this, but it's great for layout constants.

@jwatson
Copy link

jwatson commented Jun 12, 2015

Could you use reduce in lines 28 and 36? Also, hate that I can't comment on lines here.

@jwatson
Copy link

jwatson commented Jun 12, 2015

Stylistically I'd prefer

if let paths = opaths {
    let imageNames = paths.filter({
        return $0.pathExtension == "imageset"
    }).map({ (x:String) -> String in
        return x.lastPathComponent.stringByDeletingPathExtension
    })

    let body = objcCompatible ? swiftImageExtension(imageNames) : swiftImageEnum(imageNames)
    try! body.writeToFile(outputPath, atomically:true, encoding: NSUTF8StringEncoding)
}
else {
    // Complain about error.
}

Also, I believe you can omit the ()'s and use trailing closure syntax here, e.g.:

paths.filter {
}.map { (x: String) -> String in
}

@KingOfBrian
Copy link
Author

KingOfBrian commented Jun 13, 2015

Thanks @jwatson! I updated indent to use map + join instead too. Pretty sure it's slightly worse, but it doesn't have any var's! Also, I dropped return from the map functions. Not sure if it's correct or better, but thought i'd give it a shot.

@AliSoftware
Copy link

AliSoftware commented Jul 23, 2015

Hi @KingOfBrian,

You might be interested to know that I developed a similar tool to generate enums for assets — which actually can also generate enums for various other stuff like UIStoryboards and Localizable.strings, etc…

Anyway, nice to see that we had a similar idea, great minds think alike 😉
Don't hesitate to make suggestions or PR to my code if you see anything that could be improved)

@KingOfBrian
Copy link
Author

KingOfBrian commented Jul 27, 2015

Hey @AliSoftware -- That looks awesome! Will definitely point people in that direction instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment