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 installcommand in the project’s iOS root folder — it will install the required gems; Liquidis 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.
