Created: September 12, 2022

Code Generation Script for iOS

Aibek Nogoev.

Aibek Nogoev

iOS Developer in Jusan Bank

Mobile Development
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.

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.