Skip to content

Instantly share code, notes, and snippets.

@davbeck
Last active January 18, 2023 01:35
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save davbeck/e944b3e05749e94617aa to your computer and use it in GitHub Desktop.
Save davbeck/e944b3e05749e94617aa to your computer and use it in GitHub Desktop.
Automatic Enum-based Xcassets in Swift
#!/usr/bin/env ruby
require "active_support"
def assetCatalogsAtPath(path)
results = []
contents = Dir.entries(path)
for file in contents
fileExtention = File.extname(file)
# puts "fileExtention: #{fileExtention}"
newPath = File.join(path, file)
if fileExtention == ".xcassets"
results.push(newPath)
elsif fileExtention.empty? && file != "Pods" && File.directory?(newPath) && file[0] != "."
results += assetCatalogsAtPath(newPath)
end
end
return results
end
def assetsInCatalog(catalog)
contents = Dir.entries(catalog)
assetMapping = {}
for fileName in contents
fileExtention = File.extname(fileName)
if fileExtention == ".imageset"
assetName = File.basename(fileName, fileExtention)
name = ActiveSupport::Inflector.camelize(assetName.gsub('-', '_'), true).gsub(/[^a-z ]/i, '')
assetMapping[name] = assetName
end
end
return assetMapping
end
def assetsSourceForCatalog(catalog)
assets = assetsInCatalog(catalog)
# puts "assets: #{assets}"
enumName = ActiveSupport::Inflector.camelize(File.basename(catalog, File.extname(catalog)).gsub('-', '_'), true) + "Asset"
# puts "enumName: #{enumName}"
enumNameInit = enumName[0, 1].downcase + enumName[1..-1]
enumCases = assets.collect { |name, assetName| " case #{name} = \"#{assetName}\"" }.join("\n")
# puts "enumCases: #{enumCases}"
%Q(import UIKit
private class #{enumName}Class: NSObject {}
extension UIImage {
public enum #{enumName}: String {
#{enumCases}
}
public convenience init!(#{enumNameInit}: #{enumName}, compatibleWithTraitCollection: UITraitCollection? = nil) {
self.init(named: #{enumNameInit}.rawValue, inBundle: NSBundle(forClass: #{enumName}Class.self), compatibleWithTraitCollection: compatibleWithTraitCollection)
}
}
)
end
path = ARGV[0]
# puts "path: #{path}"
catalogs = assetCatalogsAtPath path
# puts "catalogs: #{catalogs}"
for catalog in catalogs
source = assetsSourceForCatalog(catalog)
puts "source: #{source}"
enumName = ActiveSupport::Inflector.camelize(File.basename(catalog, File.extname(catalog)).gsub('-', '_'), true) + "Assets"
sourcePath = File.join(File.dirname(catalog), enumName) + ".swift"
File.open(sourcePath, 'w') { |file| file.write(source) }
end

In WWDC 2015 Session 411, Apple recomends creating enums for all of your xcassets in order to have better compile time checks. But manually creating enums for each asset is tedious, and if you make a mistake there, all of your compiler checks will lead you astray.

Instead, I wrote this script to automatically export all asset names as a Swift enum on build. If an asset is removed, the enum will be regenerated without that asset and anything that uses it will create a compile time error. And assuming Xcode supports Swift enum autocompletion one day, it should fill things in for you as you type as well.

Usage

I like to put any build scripts in their own folder under the project directory. I use bin. Then create a new agregate target in your project and add a run script build phase to it. Paste the following in to foward the action to your script folder:

$PROJECT_DIR/bin/assets.rb $PROJECT_DIR

Finally, add the new target as a dependency to your apps target. Now, whenever you build a Swift source file will be generated next to each xcassets file (excluding Cocoapods). The first time the script is run, you will need to add the generated file to Xcode. Subsequent runs will update the existing file. When you update assets, you will need to build before the enum will be available.

The first and only argument to the script should be the project directory. The script will search that directory for any and all xcassets and generate a .swift file for each one based on the name of the xcassets file. The enum is namespaced to that assets file. For most apps, there is a single Images.xcassets file for the entire project, so that will be the one that is used.

The enum file will look something like the following:

import UIKit

private class ImagesAssetClass: NSObject {}

extension UIImage {
    public enum ImagesAsset: String {
        case PhotoLikeSmall = "photo-like-small"
        case PhotoResponsesSmall = "photo-responses-small"
    }
    
    public convenience init!(imagesAsset: ImagesAsset, compatibleWithTraitCollection: UITraitCollection? = nil) {
        self.init(named: imagesAsset.rawValue, inBundle: NSBundle(forClass: ImagesAssetClass.self), compatibleWithTraitCollection: compatibleWithTraitCollection)
    }
}

Note that asset names will be normalized to standard swift enum camelcase.

We use a placeholder class to get the files bundle, so this will work safely from a framework as well. The name of the initializer and enum comes from the name of the .xcassets file. Xcode 7 uses "Assets.xcassets" by default now, which creates the weird "AssetsAsset" enum. A better approach is to name the xcassets file after your app or framework.

Once you have the enum file generated and added to Xcode, you can use it like the following:

self.likeButton.setImage(UIImage(imagesAsset: .PhotoLikeSmall), forState: .Normal)
self.commentButton.setImage(UIImage(imagesAsset: .PhotoResponsesSmall), forState: .Normal)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment