Created: September 12, 2022
Code Generation Script for iOS

In this article, I will show you an example of how can you speedup development with code generation scripting.
How can you speed up development with scripted code generation?
For code generation, I usually use Python or Ruby as these languages have a lot of libs for the generation of boilerplate code. And also these languages are first-class citizenship on macOS.
Real-life example: I had a lot of color themes in an application and a lot of colors, my designer changed colors once every month and added a new theme once a year.
Information was provided in JSON format:
{
"MainTheme": {
"textPrimary": {
"light": "#000000",
"dark": "#FFFFFF"
},
"textSecondary": {
"light": "#8A8A7E",
"dark": "#8D8D94"
},
...
},
...,
"BrightTheme": {
...
}
}
I had theme name and color type name with HEX code of color for light and dark modes.
So how should I convert this into the code?
In common situation you can transform hex into UIColor like that:
extension UIColor {
convenience init(rgb: Int) {
self.init(
red: (rgb >> 16) & 0xFF,
green: (rgb >> 8) & 0xFF,
blue: rgb & 0xFF
)
}
}
let color = UIColor(rgb: 0xFFFFFF)
But it is really uncomfortable to use colors in this way. I prefer color literal because can see the color:
Yeah, it is much better. Let’s do it this way.
First of all, we should create a swift file where our colors will be — “AppColors.generated.swift”.
After that, as we are using Ruby we should prepare a project for using Ruby.
- Make sure Ruby and Gem are installed;
- Call the
bundle install
command in the project’s iOS root folder — it will install the required gems; Liquid
is one of the required gems — we will use it for creating templates.
Also, we should have raw resources for colors — JSON with colors. Let’s name it “ios_colors.json”.
The liquid is very useful for preparing templates. Example of our template “ColorsTemplate.liquid”:
import UIKit
/// Extension representing a pool of new colors
public extension UIColor {
static func getColor(lightMode: UIColor, darkMode: UIColor) -> UIColor {
if #available(iOS 13.0, *) {
return UIColor { (traits) -> UIColor in
return traits.userInterfaceStyle != .dark ? lightMode : darkMode
}
} else {
return lightMode
}
}
{% for keyVal in data %}
struct {{ keyVal[0] }} {
{% for key in keyVal[1] %}
public static let {{ key[0] }}: UIColor = getColor(
lightMode: #colorLiteral(
red: {{ key[1].lightRed }},
green: {{ key[1].lightGreen }},
blue: {{ key[1].lightBlue }},
alpha: {{ key[1].lightOpacity }}
),
darkMode: #colorLiteral(
red: {{ key[1].darkRed }},
green: {{ key[1].darkGreen }},
blue: {{ key[1].darkBlue }},
alpha: {{ key[1].darkOpacity }}
)
) ///{{ key[1].lightHex }} | {{ key[1].darkHex }}
{% endfor %}
}
{% endfor %}
}
Now the most interesting process — writing script. I will just leave code on Ruby:
require 'Liquid'
require 'json'class Colors
attr_accessor :lightRed,
:lightGreen,
:lightBlue,
:darkRed,
:darkGreen,
:darkBlue,
:lightHex,
:darkHex,
:lightOpacity,
:darkOpacity
def initialize(light, dark)
@lightHex = light
@darkHex = dark
if light.size > 7 then
*tempLight, alpha = light.match(/^#(..)(..)(..)(..)?$/).captures.map(&:hex)
@lightRed = (tempLight[0] / 255.0).round(2)
@lightGreen = (tempLight[1] / 255.0).round(2)
@lightBlue = (tempLight[2] / 255.0).round(2)
@lightOpacity = (alpha || 255) / 255.0
else
tempLight = light.match(/^#(..)(..)(..)$/).captures.map(&:hex)
@lightRed = (tempLight[0] / 255.0).round(2)
@lightGreen = (tempLight[1] / 255.0).round(2)
@lightBlue = (tempLight[2] / 255.0).round(2)
@lightOpacity = 1.0
end
if dark.size > 7 then
*tempDark, alpha = dark.match(/^#(..)(..)(..)(..)?$/).captures.map(&:hex)
@darkRed = (tempDark[0] / 255.0).round(2)
@darkGreen = (tempDark[1] / 255.0).round(2)
@darkBlue = (tempDark[2] / 255.0).round(2)
@darkOpacity = (alpha || 255) / 255.0
else
tempDark = dark.match(/^#(..)(..)(..)$/).captures.map(&:hex)
@darkRed = (tempDark[0] / 255.0).round(2)
@darkGreen = (tempDark[1] / 255.0).round(2)
@darkBlue = (tempDark[2] / 255.0).round(2)
@darkOpacity = 1.0
end
end
def to_hash
tempHash = Hash.new
tempHash['lightRed'] = @lightRed
tempHash['lightGreen'] = @lightGreen
tempHash['lightBlue'] = @lightBlue
tempHash['darkRed'] = @darkRed
tempHash['darkGreen'] = @darkGreen
tempHash['darkBlue'] = @darkBlue
tempHash['lightHex'] = @lightHex
tempHash['darkHex'] = @darkHex
tempHash['lightOpacity'] = @lightOpacity
tempHash['darkOpacity'] = @darkOpacity
tempHash
end
def to_json
to_hash.to_json
end
endclass Constants
private
TABLE_SEPARATOR = "----------------------------------------------------------------------------------"
JSON_TYPE = ".json"
COLORS_JSON_FOLDER = 'Utils/Utils/Resources/Colors/'
TEMPLATE_FILEPATH = 'Utils/Utils/Resources/Colors/ColorsTemplate.liquid'
COLORS_ENUM_FILEPATH = 'Utils/Utils/Generated/AppColors.generated.swift'
end# Find all colors json files
json_files = Dir.entries(Constants::COLORS_JSON_FOLDER).select {|f|
f.include? Constants::JSON_TYPE
}header_string = "Finded JSON files:"
header_string = "| #{header_string} |"
puts header_string# get all colors from files
color_duplicates = Hash.new
result_json_data = Hash.newjson_files.each do |filename|
context = File.read("#{Constants::COLORS_JSON_FOLDER}#{filename}")
@templateFile = File.read(Constants::TEMPLATE_FILEPATH)
@outputFile = Constants::COLORS_ENUM_FILEPATH
json = JSON.parse(context)
result_json_data["#{json.keys[0]}"] = Hash[json["#{json.keys[0]}"].map { |key, value|
[ key, Hash[ Colors.new(value['light'], value['dark']).to_hash ] ]
}]
end
puts Constants::TABLE_SEPARATOR# convert json feature toggles to enum
@template = Liquid::Template.parse(@templateFile) # Parses and compiles the template
@rendered = @template.render({'data' => result_json_data})
File.write(@outputFile, @rendered)
Ok, usually I save all scripts in the root directory as it is easy to use them. Name of our script file “convertColorsToStatic.rb”.
Let’s call in the terminal:
ruby convertColorsToStatic.rb
Voila:
Also I’m using scripts for creating events, feature toggles, strings, architecture components (for example, VIPER) and it saves me ton of time.