Inder Deep Singh | August 16, 2023 | 11 min read
Building Kimchi for hack-a-noodle 2022 – Zomato’s on-the-fly iOS UI rendering engine

Hack-a-Noodle, a Zomato-hosted exhilarating 24-hour hackathon that brought together creativity and extraordinary ideas, unfolded with two thrilling dimensions. On one side, the stage was set for external participants, bringing their exciting and innovative projects to life. The stakes were high, as these brilliant minds had a chance to win prizes and secure potential employment at Zomato. On the other side, Zomato’s own tech team formed dynamic groups of 5-6 members, diving head first into the challenge of crafting solutions.

The atmosphere buzzed with energy as all participants set out to create something extraordinary, whether it was an improvement or a complete disruption to the status quo. Throughout the 24 hours of non-stop coding, the office was a hub of inspiration and determination. A plethora of brilliant ideas were pitched and brought to life. Passionate individuals worked tirelessly through the night, driven by their motivation and excitement. Hack-a-Noodle was an unforgettable journey of imaginativeness, collaboration and ingenuity, leaving a lasting impact on everyone involved. The goal was to shape the tech future of Zomato and beyond.

In this blog, we’ll take you through the exciting journey of the in-house hackathon champions. The winning team was an ensemble of 5 brilliant minds from the iOS team: Mehul Srivastava, Nakul Sakhuja, Rajesh Budhiraja, Archana Kumari and Inder Deep Singh. Their extraordinary creation, christened as Kimchi, is a groundbreaking on-the-fly UI rendering engine tailored exclusively for iOS. So, let’s embark on a deep dive to uncover the captivating story behind Kimchi – exploring its essence, significance and the fascinating intricacies that brought it to life.

The Journey of Kimchi

Context

Before we start with what Kimchi is, let’s understand where Zomato was before this idea was conceived. 

Sushi

We had been using a standardized library of UI elements, known as Sushi, across its various apps. This system defines the characteristics of all basic UI elements like Label, Button, ImageView, IconView and more. Each of these views also have a model associated with them which is used to configure their look and feel. For instance, the LabelObject has attributes that are needed to configure a Label such as text, color, font and the number of lines.

Example of a Sushi LabelObject

{
    "label": {
        "text": "this is a label",
        "font": {
            "weight": "bold",
            "size": "500"
        },
        "color": {
            "type": "black",
            "tint": "500"
        },
        "number_of_lines": 1
    }
}

Sushi system allows us to maintain a uniform look, feel and behavior across all screens in the app. App developers are only required to arrange these Sushi elements within a card or a view according to the design requirements. We also have the objects for each element on the backend which are sent to the app, and the app can set the appearance accordingly. This also allows us to have reusable views as we can control the appearance of each element without any change in the app.

Limitations of Sushi

Although Sushi eases a lot of problems for us when it comes to making a UI, there are still things that it cannot do – the biggest of which is that the app governs how these Sushi elements are placed with respect to each other. So, in order to ship a new UI, we need to write code for it on the app using Sushi elements, which is nothing more than an arrangement of those elements. Once we have coded the arrangement, we do an app release, wait for a certain adoption level and then take the feature live. This process takes at least 3 weeks, which feels too sluggish considering our aspiration for speed and constant iteration.

This naturally begs the question, what if we move the arrangement of Sushi elements to the backend as well? That would give us the power to control the whole UI from the backend itself, in real-time. We had this idea for quite some time, but due to various constraints, we hadn’t been able to experiment with them until the arrival of Hack-a-Noodle 2022. The hackathon gave us the perfect opportunity to delve into this exciting possibility and bring it to life.

24 Hours of Hackathon

We thought this was the perfect opportunity to try and build something like Kimchi and jumped in without knowing if it was possible or not. We had 24 hours to figure it out and potentially create something. We drew inspiration from SwiftUI’s syntax to arrange Sushi elements in the view. We used containers like hStack, vStack and zStack to arrange elements in horizontal, vertical and z axes respectively.

Proposed response for rendering 2 labels arranged horizontally

Aside from writing a custom implementation of zStack, we already had everything in Sushi and iOS’s UIKit. We used UIStackView to make hStack and vStack. We decided to implement Kimchi using UIKit and not SwiftUI because our current codebase was already in UIKit and we were working within a 24 hour deadline.

Once we had all the basic elements ready, we had to plug them into the app to see how they look and perform. We rendered our homepage using Kimchi and were blown away. The homepage rendered with Kimchi was virtually identical to our existing version, and even the scrolling performance held up reasonably well, especially given that we weren’t reusing the KimchiViews at all. We were so pumped to see this that we created another page in the app using Kimchi. The pull request was just a JSON file which, we believe, is kind of legendary and hilarious at the same time.

We presented the idea and implementation to the judges across various rounds. Everyone could see this was revolutionary. Although there were other great ideas which made it through to the finals of the hackathon, it was a unanimous decision to give the winning award to Kimchi. The speed of execution and business impact that this could potentially bring would only be limited by our capability of taking advantage of it. It basically eliminates the need for even an app developer to ship a new UI.

The Architecture of Kimchi

Looking at the response, you might be able to understand that Kimchi views are recursive in nature. However, let’s look at how the UI is rendered from the configuration using our previous example of two horizontally stacked labels.

Every KimchiView has exactly one subview in it. The type of this subview is decided by the response. For example, KimchiView created with type hStack will have one UIStackView with its axis set to horizontal. Now this hStack, will have an array of items which will all be KimchiViews as well. Their subview will be determined by their specific types. For example, the view hierarchy would look something like this.

KimchiView -> UIStackView -> [
KimchiView -> SushiLabel,
KimchiView -> SushiLabel
]

Therefore, you just create one KimchiView and it creates more views inside it recursively.

The reason for embedding every view inside KimchiView is that we want certain functionalities in every view. Some examples are:

  • Padding
  • Background Color/ Gradient
  • Corner Radius
  • Border
  • Shadow

We’re able to achieve this for any view that is supported by Kimchi without implementing it in every single view type. Also, doing it this way allows us to maintain these functionalities from one single place if anything needs to be changed in the future.

Here’s an example of a little complicated view using Kimchi. The response snippet along with the resultant image is provided below.

{
    "type": "vStack",
    "vStack": {
        "padding": {
            "all": 20
        },
        "alignment": "center",
        "bg_color": {
            "type": "red",
            "tint": "200"
        },
        "corner_radius": 12,
        "shadow": {
            "color": {
                "type": "grey",
                "tint": "700"
            },
            "offset": {
                "width": 0,
                "height": 0
            },
            "radius": 8,
            "opacity": 0.2
        },
        "items": [
            {
                "type": "image",
                "image": {
                    "url": "https://b.zmtcdn.com/data/featured_v2.jpg",
                    "aspect_ratio": 1,
                    "height": 50
                }
            },
            {
                "type": "label",
                "label": {
                    "text": "Kimchi",
                    "font": {
                        "weight": "bold",
                        "size": "500"
                    },
                    "color": {
                        "type": "black",
                        "tint": "500"
                    }
                }
            }
        ]
    }
}

Hackathon to Shipping

Now that everyone had seen how impactful Kimchi could be for our day-to-day workflow at Zomato, all eyes were set on us to ship it. Before we could ship it though, there were a few things that needed figuring out. These issues included —

  1. Response size
  2. Reusability

Response Size

Imagine if we have to render a horizontally scrolling rail of 100 identical looking cards in which only a few data fields change. This is a very common use case in any app, especially Zomato.

Prior to Kimchi we would only get the differing fields in the backend response, resulting in efficient response size. With Kimchi, we would also be expecting the layout config for all the 100 cards, even though it is not changing at all. This would result in an astronomical increase in response size.

The problem is that a KimchiView takes only one configuration for initialisation. This configuration includes the static as well as dynamic part. Pre-Kimchi, this process had two steps –  

  • Static part was written in Swift and shipped in the app
  • Dynamic part was sent by backend and only it was set in the cards during reuse

So how do we separate the static & dynamic parts of the UI from a KimchiView?

We were back at the drawing board with this problem. We thought about how iOS implements the concept of Optionals. Optional is an enum which has two cases. First case is that there can be a value and the other is that there is no value, which means nil. What if we make our objects like Optionals? Either they hold the value or they hold the path of the value in the backend response. 

The Kimchi view config as well as the backend response is provided below.

{
    "type": "hStack",
    "hStack": {
        "items": [
            {
                "type": "label",
                "label": {
                    "ref": "title"
                }
            },
            {
                "type": "label",
                "label": {
                    "text": "this is label 2",
                    "color": {
                        "type": "red",
                        "tint": "500"
                    },
                    "font": {
                        "weight": "bold",
                        "size": "400"
                    },
                    "number_of_lines": 1
                }
            }
        ]
    }
}
{
    "title": {
        "text": "this is label 1",
        "color": {
            "type": "red",
            "tint": "500"
        },
        "font": {
            "weight": "bold",
            "size": "400"
        },
        "number_of_lines": 1
    }
}

In the code examples above, the view configuration part has the static part, i.e. the part that will remain the same in a list of these cards. Note that we have added “ref” instead of the object for that label in the view configuration. The value of this “ref”, “title”, is the key name where we are expecting the object for that label in the backend response.

With the successful implementation of the Optionals-like approach in iOS, the backend response became agnostic to whether Kimchi was used or not. The response structure remained unchanged from the pre-Kimchi era. This achievement not only solved the problem of response size increase but also offered the advantage of independence from implementing Kimchi on Android.

Although the team had plans to implement Kimchi on Android in the future, they prioritized its integration into their daily development processes on iOS. The reason behind this decision was to thoroughly test and explore the current implementation’s capabilities and limitations. While the current version of Kimchi efficiently handled UI rendering, the team aspired to evolve it into a comprehensive UI engine that could handle interactions, animations, and ultimately render entire screens.

We made a generic enum called “KimchiReferable”, which can either have a “ref” of type String or value of the generic type. This container is used to decode the layout configuration part of the Kimchi configuration.

public enum KimchiReferable<T: Decodable>: Decodable {
    
    case value(T)
    case ref(String)
    
    private enum CodingKeys: String, CodingKey {
        case ref
    }
    
    public init(from decoder: Decoder) throws {
        if let container = try? decoder.container(keyedBy: CodingKeys.self),
           let ref = try? container.decode(String.self, forKey: .ref) {
            self = .ref(ref)
            return
        }
        let value = try T(from: decoder)
        self = .value(value)
    }
}

Decoding the data part posed a challenge since the types of data were unknown. For any decodable object, we need to define the keys and value types on the app. However, for Kimchi, that context is in the layout configuration. To overcome this, the team decoded the data part as a dictionary of [String: Any] type. This allowed us to navigate to the required key as per the “ref” value in that dictionary, retrieving the data for that particular view. Additionally, they introduced  support for going to a specific path in the response like “top_container/title” and arrays like “subtitles[2]” in the “ref”.

After the modifications, our Kimchi view took two things for initialisation:

  • Layout configuration, which represents the static part of the UI
  • Data object which corresponds to the dynamic part of the UI

However, a challenge arose in setting the data for the view when it needed to be updated. While the current approach successfully created views during the initial rendering, the team needed a solution for uploading the data within the views for reuse and dynamic changes.

Reusability

In iOS’s implementation of CollectionViews, it reuses cells with new data to display an extensive number of items. Kimchi faced challenges in achieving similar efficiency. Instead of reusing the existing views, Kimchi’s current approach involved destroying the existing view and creating a new one based on the config, resulting in a more resource-intensive process. As a result, Kimchi’s reuse process was far from efficient.

With successful separation of static and dynamic data, implementing efficient reuse becomes feasible by setting data only in the KimchiViews showing dynamic data. However, an issue arises due to nested view hierarchy, as there are no direct  references to all subviews. 

To set data for specific views, we identified dynamic views with the “ref” type in their layout configs. We then stored their references in a dictionary of type “[String: KimchiView]” where the “ref” value served as the key and corresponding KimchiView as the value.

Additionally, we introduced a protocol called “KimchiReferableContainerDelegate”, which helped us in managing the references. It had a function to notify the “referableContainerDelegate” that the current KimchiView is referable. The root KimchiView had no “referableContainerDelegate,” while each KimchiView created by a KimchiView had its parent designated as the “referableContainerDelegate.” Within the implementation of the function in the protocol, two states were considered:

  1. If the view had its own “referableContainerDelegate,” it would notify that delegate about the view and its “ref.”
  2. If the view did not have its own delegate, it would be stored in the current KimchiView’s “referableSubviews” dictionary, using the “ref” as the key.

When a call was received to update data for a view using a new data object, we would look for the corresponding “ref” in the dictionary. Upon finding the match, the specific data object to the corresponding KimchiView was passed, ensuring the view’s data was accurately updated.

By this mechanism, our KimchiViews were just as reusable as our current views, leading to a massive performance improvement as only necessary  minimum work was being done in the reuse process.

Where We Stand: Running Kimchi on Zomato’s Homepage

We recently executed a successful pilot experiment on Zomato’s homepage, where we replaced our recommendation rail running the traditional UIKit view with Kimchi’s rendering. We integrated the layout configuration file into the app for this experiment, allowing us to evaluate Kimchi’s looks, feel and performance in the real world. The entire transition was achieved without any disruption to our backend and Android teams, as Kimchi effortlessly worked with the existing backend response. This pilot was successful as the transition was unnoticeable, affirming Kimchi’s seamless integration.

Upcoming Endeavors 

Now that our pilot has been successful, the goal is to make the Kimchi layout configurations back-end driven. This development would unlock the full potential of Kimchi by allowing us to ship new UI without any app release.

A slightly longer term goal is to create a tool for designers which would allow them to design UIs and export the Kimchi configs for them automatically. This would essentially remove the need for an app developer to code the UI into Swift. With this, the business and design teams would be able to test out new UIs and run experiments with even deeper controls in real time.

facebooklinkedintwitter

More for you to read

Technology

a-tale-of-scale-behind-the-scenes-at-zomato-tech-for-nye-2023
Zomato Engineering | February 29, 2024 | 6 min read
A Tale of Scale: Behind the Scenes at Zomato Tech for NYE 2023

A deep-dive into how Zomato handled the massive order volumes on New Year’s Eve. More than 3 million orders delivered in a single day!

Technology

switching-from-tidb-to-dynamodb
Kanica Mandhania | January 11, 2024 | 9 min read
Unlocking performance, scalability, and cost-efficiency of Zomato’s Billing Platform by switching from TiDB to DynamoDB

Zomato accomplished a seamless migration from TiDB to DynamoDB. The transition has empowered Zomato to efficiently serve its growing user base by managing four times more transactions, decreasing latency by 90% and reducing database costs by 50%.

Technology

how-we-increased-our-zomato-restaurant-partner-app-speed-by-over-90
Samarth Gupta | November 14, 2023 | 4 min read
How we increased our Zomato Restaurant Partner App speed by over 90%

Discover how we achieved a remarkable ~90% reduction in load times and improved the overall engineering health of the Zomato Restaurant Partner App.

Technology

how-we-improved-our-android-app-startup-time-by-over-20-with-baseline-profile
Dilip Sharma | October 13, 2023 | 4 min read
How we improved our Android app startup time by over 20% with Baseline Profile

Baseline Profiles have boosted our Android app’s startup speed by over 20%. This blog explores our journey from optimization techniques to tackling testing challenges that have resulted in a smoother, faster user experience.