Using JSON files to configure app styles

In this post I am going to write about how you can use JSON file to style your app. As a bonus point we will also see how different JSON files can be used to style different targets of the same app.

Styling is hard, especially when you have different screens or targets. Usually in the app you encode information in plist file or add ifdefs. Based on the current target we will then apply appropriate styles. This is not exactly feasible - Especially when you have multiple files and/or targets and code gets bloated with numerous if-else statements.

In this post, I will demonstrate the approach I have been using in my side projects. This is not novel or original in the sense, but I really like it and putting things in local JSON also implies that data can be fed and directed from server as well.

Let's start! Say you have a project named CustomStyles. Hypothetically you have 3 targets and each of them needs a custom style on its own. (Targets are CustomStyles, CustomStyles1 and CustomStyles2)

Now, we can either do if-else in the project or make a JSON files for individual projects. These JSON file will be read and an object of type AppStyle will be created which will dictate the styles to use.

To start with let's look at JSON file of one of the targets,

{
    "styles": {
        "background-color": "#d8dcff",
        "text-color": "#53e6da",
        "font-name": "AmericanTypewriter",
        "font-size": 16,
        "corner-radius": 30
    },
    "image": {
        "main": "horse",
        "chevron": "chevron"
    },
    "animation": true,
    "text": "App 1"
}

We will use the similar json file but with different values for other targets as well. I will use the same json file name for all 3 targets, but every file will be in its own target

Xcode will not allow you to create files with same name under same folder. Solution is to create 3 json files with same name, say app1.json and store them under different folders on file system. Once this is done you can drag and drop individual file in the project. Here's how it will look like

Their target membership is described below,

JSON/app1/app1.json - CustomStyles

JSON/app2/app1.json - CustomStyles1

JSON/app3/app1.json - CustomStyles2

As it is cleared from JSON, we have use following styles which are specific to targets,

  • background-color
  • text-color
  • font-name
  • font-size
  • corner-radius
  • main-image
  • chevron-image
  • animation
  • text

Creating AppStyle object

First off we read a JSON from local file structure. (For the practical purpose, this could well be coming back from server).

class JSONFileReader: NSObject {
    static func readJSONFile(with name: String) -> AppStyle? {
        guard let path = Bundle.main.path(forResource: name, ofType: "json") else { return nil }
        do {
            let jsonData = try NSData(contentsOfFile: path, options: NSData.ReadingOptions.mappedIfSafe)
            do {
                let jsonResult: NSDictionary = try JSONSerialization.jsonObject(with: jsonData as Data, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
                if let style = jsonResult as? NSDictionary {
                    return AppStyle(styleInformation: style)
                }
            } catch {
                print("Error occurred while trying to convert JSON data into Swift types")
            }
        } catch {
            print("Error occurred while trying to read contents of file \(name)")
        }
        /// Simply return a nil value when unpleasant situation is encountered.
        return nil
    }
}

Please note how we use only one JSON file name app1 for all 3 targets. This is because each of the three JSON file is specific to checked target. There are no two JSON files with same name in the same target. So depending on the target for which you are running project, appropriate JSON file will be picked up.

Once we successfully read from the file, a Dictionary object will be passed to AppStyle initializer which will return the initialized AppStyle object back.


class AppStyle {
    let animation: Bool
    var mainImage: UIImage?
    let chevronImage: UIImage
    var backgroundColor: UIColor?
    var textColor: UIColor?
    let cornerRadius: CGFloat
    let font: UIFont
    let text: String

    init(styleInformation: NSDictionary) {
        animation = (styleInformation["animation"] as? Bool) ?? false
        chevronImage = UIImage(named: Image.chevron.rawValue)!
        text = (styleInformation["text"] as? String) ?? ""

        if let imageInfo = styleInformation["image"] as? NSDictionary, let mainImageInfo = imageInfo["main"] as? String, let actualImage = UIImage(named: mainImageInfo) {
            mainImage = actualImage
        }

        if let styles = styleInformation["styles"] as? NSDictionary {

            backgroundColor = (styles["background-color"] as? String)?.hexStringToUIColor()
            textColor = (styles["text-color"] as? String)?.hexStringToUIColor()
            cornerRadius = (styles["corner-radius"] as? CGFloat) ?? 0.0
            if let fontName = styles["font-name"] as? String, let fontSize = styles["font-size"] as? Int, let assignedFont = UIFont(name: fontName, size: CGFloat(fontSize)) {
                font = assignedFont
            } else {
                font = UIFont.systemFont(ofSize: 14)
            }
        } else {
            cornerRadius = 0.0
            font = UIFont.systemFont(ofSize: 14)
        }
    }
}

// Initialize AppStyle object
let currentStyle = JSONFileReader.readJSONFile(with: "app1")

Once AppStyle object is ready, we can use it directly in our ViewController class. Below are some of the UI elements in this UIKit class which we will decorate using AppStyle object

let fullView = UIView()
let imageView = UIImageView()
let chevronImageView = UIImageView()
let textLabel = UILabel()

Obviously, we don't want to show them in vanilla appearance. So we will use our AppStyle object and use its properties to style these elements.

I have also sprinkled a code with comments for easier understanding


// Read the JSON file named app1.json. Every target gets its own app1.json. Proceed only if we could get valid AppStyle instance
if let currentStyle = JSONFileReader.readJSONFile(with: "app1") {

// Below is the demonstration of how we use different AppStyle properties to style the on-screen elements
    fullView.layer.cornerRadius = currentStyle.cornerRadius
    fullView.backgroundColor = currentStyle.backgroundColor
    imageView.image = currentStyle.mainImage
    chevronImageView.image = currentStyle.chevronImage
    textLabel.textColor = currentStyle.textColor
    textLabel.text = currentStyle.text
    textLabel.font = currentStyle.font

// Animation property dictates if image on the view port appears with an animation or not. Default duration is 2 seconds. You can also make it to fetch from app styles JSON document
    if currentStyle.animation {
        UIView.animate(withDuration: 2.0, animations: {
            self.imageView.alpha = 1.0
        })
    } else {
        imageView.alpha = 1.0
    }
}

For the sake of saving space, I have decided not to add style JSONs for all 3 app targets. However, I am adding the screenshots of how screens for those targets would look like

Target 1

target_1_screenshot

Target 2

target_2_screenshot

Target 3

target_3_screenshot

Please note how all these screens look similar in layouts, but only differ in styles including color and fonts. Also, having JSON in app may not be feasible all the time, if requirements imply you may even download the JSON directly from server based on the target for which app is currently run.

A source code of this project is included in the Github repository. Please let me know if you have any more thoughts about this approach