Estimated reading time:
Three years ago, we built our iOS SDK using a limited number of classes to manage all functionalities. Although this approach fulfilled our initial objectives, newly added features necessitated extensive maintenance, and it became challenging to support this architecture as it was becoming outdated.
Consequently, we made the strategic choice to reconstruct the SDK entirely, prioritizing a future-proof approach. In this article, we will explore the considerations and thought processes that went into the SDK’s reconstruction.
What does “future-proof” mean?
To start, it’s important to establish what we mean by “future-proof.” A future-proof SDK is one that has been crafted with the ability to adapt and remain valuable even as technology, user experience and market trends evolve over time. Simply put, a future-proof SDK can weather changes in the industry and continue providing lasting benefits to its users.
The key characteristics are:
- Modularity: The SDK should be designed in a modular way, with each module serving a specific function. This allows greater flexibility and easier integration with new technologies.
- Scalability: The SDK should be designed to scale as your product grows without sacrificing its performance or its stability.
- Flexibility: The SDK should be flexible enough to adapt to new use cases and market trends. This means that it should be easy to customize and extend as needed.
After examining various SDK architectures, we focused our studies on one in particular: modular architecture.
Modular architecture involves dividing all code and classes into separate modules and establishing a contract for communication between them. Each module is independent, which makes it effortless to add or remove modules as necessary, and enables module reuse across different parts of the codebase, reducing the duplicated code.
Big picture to define SDKs
To enhance our flexibility, it’s imperative to precisely define the products we’re delivering and identify which SDKs we need to construct. In our case, we provide two products: an Ads SDK for displaying ads and a Choice Manager SDK (CM SDK) for displaying user consent.
As both of these SDKs require common modules like network access and device settings access, creating a specialized SDK that incorporates these modules could be advantageous. This approach can simplify development and guarantee consistency across all our SDKs.
To simplify the integration process, creating a wrapper SDK that allows publishers to call a single method to start all necessary SDKs helps simplify the integration method. This approach also reduces the complexity of integration and makes it easier for publishers to use our SDKs.
We have 4 SDKs
List of all SDKs and how they are implemented
Defining different modules
Once we have split the SDKs, the next step is to delve deeper into each one to define the various modules needed. To accomplish this, we must identify the different features we have and group them by type. These types represent our modules, and we can construct the architecture based on these modules.
For example, if we look at the Ads SDK, we want to display Mobile Rich Media Ad Interface Definition (MRAID) Ads, but in the future, we may display ads with a video player via Interactive Media Ads (IMA). Ads can be displayed as fullscreen, banners, or thumbnails. We have two types: the ways that we display ads and the container of the ad.
In our case, we have created two modules:
- The ad displayer, and behind that: the MRAID Webview
- The Ad container, and behind that: the different containers like fullscreen, banner, and thumbnail.
We also created a module for user and publisher interaction (the user can click on the ad which induces actions and the publisher can show the ad): The ad controller, is then in charge of controlling all the actions.
Visual representation of an ad flow
Displayers and containers never call ads directly to avoid having several action sources.
Create the different module
After defining the different modules, the next step is to establish a communication contract between them. This is among the most critical elements to define, as it will ultimately determine how future-proof the architecture will be.
To define the contract, we create an interface that is as generic as possible. It’s essential to avoid passing objects directly through the interface but rather through another interface. By doing so, it becomes much simpler to create and pass another class that conforms to the interface.
For example, with the ad container, we create an interface that is as simple and generic as possible:
@property (nonatomic, strong, read-only) NSString *name;
@property (nonatomic, assign, read-only) OGAAdContainerStateType type;
@property (nonatomic, strong, read-only) id<OGAAdDisplayer> displayer;
– (BOOL)display:(id<OGAAdDisplayer>)displayer error:(OguryError * _Nullable * _Nullable)error;
To create a new state, we need to define the behavior on display and on clean. After that, it is ready to be added to the code at the right place without having to change the logic, avoiding regression.
Constraint of communication
In some cases, it’s essential to maintain communication between different SDKs. Most developers might consider creating a public API and communicating through it, but this approach involves a hard dependency between both SDKs, which is not ideal.
To be as flexible and future-proof as possible, we must find another way to communicate between SDKs.
In our particular case, we decided to create an event bus system to transmit information between SDKs without dependencies.
The Core module generates an event bus during initialization and provides it to the Ads and CM SDKs, this event bus is similar to a notification center. The SDK can create an event with a topic name, and all registered SDKs that listen to this topic will receive the event.
This approach is highly flexible and future-proof. If we need to send an existing event to a new SDK, we simply register the new SDK to the event’s topic without making any modifications to the sender SDK.
Event bus communication
Once SDKs are pushed to production, they can remain in use for a prolonged period. Therefore, it’s crucial to ensure flexibility and future-proofing to be able to have an impact on already deployed SDKs. One way to achieve this is by using a remote configuration.
A remote configuration involves the SDK pinging a server at regular intervals and upon initialization. The server responds with a list of parameters that tell the SDK which options to take, such as stopping or activating a feature.
A remote configuration also presents an excellent opportunity for A/B testing. We can activate a feature in a subset of the deliveries to compare results and make data-driven decisions.
To continue building future-proof SDKs, we have adopted a modular architecture that separates the code into independent modules, which can be easily added or removed. This approach saves time in development and testing while minimizing the risk of regression issues.
Although there are some challenges in communication between modules, we overcame them by using an event bus system that allows information to be sent between SDKs without creating dependencies.
Our modular architecture already proved its worth, like when we added the ability to display StoreKit Ads with SKAdNetwork. We simply added the StoreKit module, which implemented the Ad Container State module, and inserted it in the right place. This took only two weeks of development and one week for testing, which is much faster and more efficient than the old architecture and reduces the risk of regression issues.
Share this article: