OneID iOS Team: 2020 In Review
2020 was a real challenge to our iOS team in OneID especially when it comes to productivity. Despite the difficulties, we are proud to say that we have addressed quite a few technical problems and adopted neat solutions to help our app scale and grow steadily alongside demanding business requirements. In this article we would like to present the accomplishments we made so far as a reflection of our work in 2020 — also to motivate more achievements in 2021 going forward.
Plugin Architecture
With the vision of building a super app, our iOS team started out building the revamped VinID app using Uber RIBs architecture in the hope that it effectively supports the scalability of the product. The framework has indeed been a great tool for our growing team to build features with complicated structures and business logic. With support for Deep Scope Hierarchies, RIBs architecture encourages separation of concerns, making isolated development of features possible.
RIBs however — like any of its counterparts — is not a perfect architecture. The deep scope hierarchies themselves require close-knit parent-child relationships in RIB tree structure. This is especially troublesome for our more complex features that allow customized configurations from different modules (e.g Deeplinking, QR code scanning), as the last thing we want is a complicated linear dependency graph. With the inspiration from Uber, our team experimented with Plugin Architecture since late 2019 and applied it widely in 2020. Our successful adoption of plugins are highlighted as follows:
- Easier integration into any shared feature for our large number of teams as the tight coupling between RIBs is now freed.
- Simplified process of controlling feature rollouts, quality and performance.
- Simpler code review and onboarding process thanks to clean code and flatter project structure.
Interface Targets and Dependency Injection
With the ever increasing complexity of VinID app, our team encourages modularization for clear separation of features and allow capacity for substitutions later on. However, as the app grows, linear dependency between modules gradually made project structure more complicated and build time suffer significantly.
To be more specific, since the last half of 2019 there had been a growing number of features integrating our checkout feature to utilize payment with VinID Pay e-wallet. With around 20 submodules depending on the concrete implementation of Checkout, every code change in the module caused the rebuild of all the others (which usually took around 400–600s). Although we had maintained a small demo project to improve development productivity, in 2020 our team decided to adopt Interface Targets to improve our dependency graph:
Essentially we built a separate module containing only protocols and models required for the integration of checkout features, and then redirected the dependency of feature modules to this instead. The implementation is then sent to the modules using dependency injection (i.e service locator) from the main app downwards. This has helped flatten dependency graph and improved build time significantly. The below figure compares build time for code changes in implementation module of Checkout:
As for dependency injection, we have migrated from heavy use of singletons to a more dynamic and robust system. Our team considered two solutions: Uber’s Needle framework and Resolver. Both have their own pros and cons, and we decided to move forward with Resolver for it being lightweight and easier for adaption.
CI/CD and Build System
One of the top priorities of OneID iOS team in 2020 was to increase development productivity, hence our focus on improving CI/CD, build system and automation.
Since mid 2019, our team migrated to Bazel as our primary build system, with Tulsi as Xcode project generator for local development. This effectively reduced our build time in CI and local builds by up to 400%. In 2020, we pushed optimizations further with the following highlights:
- Reviewed and optimized dependency graph using Bazel’s powerful query and analyst tools
- Maximized parallel builds with code refactoring and module restructuring
- Built report system to detect functions and classes with compile time exceeding limit using Swift Driver
- Applied custom Apple’s linker zld.
Our Mobile Tool team also optimized swift_library Bazel rules for build process by separating actions to generate Swift derived files (swiftmodules, swiftdocs, swiftinterfaces etc.) from actions to compiled Swift code (.o). This technique speeds up build time by 20–40% by promoting parallelism execution in multi-core CPU machines.
These optimizations along with numerous others have brought the build time of VinID iOS app on our automated system by 50%, down to around 14 minutes per build.
Additionally, our CI/CD tools also achieved notable milestones:
- 100% automated release process
- Workflows to manage and automatically update assets and localization
- Workflows to gather and report source code statistics
- Automated merge requests checks for code optimization and related JIRA tickets to have appropriate status and release versions.
- Dashboard system to track CI build time, app size, startup time, libraries and all modules pre-build time.
Performance and Tracing
Another objective of our team in 2020 was to better control and improve app performance. The metrics of our main focus were:
- App size
- App’s startup time
- Time to interactive: load time of main screens
- Scroll hitch rate
During Q1 and Q2 of 2020, our Platform team focused their efforts to optimize app’s launch time and load time of VinID app’s Home screen. So far, average startup time in 50 percentile is now 900ms — we aimed to further reduce this number to 250ms in 95 percentile. Details of the optimization process here and here.
On the other hand, our team also used Firebase Performance to keep track of other metrics (load time, FPS) for key screens and features. The data was then sent to BigQuery for calculation and finally exported to Google Data Studio for visualization.
Our metric measurements were not limited to just production builds but also handled during development and integration processes:
- Maintained a module named Lumber to easily trace app performance via Xcode Instruments
- Developed a report tool for tracking weight and load time of third-party libraries for comparison and timely update or replacement if needed.
- Set up nightly app profiling routine with Firebase Test Lab to control beta app startup time and pre-main time of libraries.
IDAssets
Resource management has always been one of the challenges to be solved when an app grows in complexity. In late 2019 our team decided that it was time to build up a mechanism to better control and optimize all assets in one place. Together with design team, we gradually built a new module named IDAssets as the one and only source of assets used for all modules of the app. Design team is in charge of the review and optimization process of any asset to be used, and a new CI workflow was set up to automatically update the resource module twice daily.
We also built a special tool to aid the migration process from traditional asset catalogs. The tool supports scanning images in asset catalogs and mapping ones from IDAssets module that have similar content, even those with different colors and sizes — providing a faster and less error-prone way for developers to replace assets.
Additionally we also set up a workflow to scan new resources added from merge requests or the file storage system to quickly spot any overweight or duplicated files. This workflow was set up to run when checking merge requests and nightly to generate reports and warnings to modules that need better asset optimization.
Further into 2020 our Mobile Tools team brought resource management a level higher by setting up a new tool to scan all app assets and remove ones that were not used in either code or XIB/Storyboards, which reduced roughly 25% of total asset weight. This was then added as a mandatory CI/CD step in the workflow of released builds.
Design System and Snapshot Testing
By the end of 2019, VinID iOS app had grown into more than 50 feature modules. With the extensive number of features implemented by several distributed teams in 2 cities, the consistency of user interfaces across different modules slightly went out of hand. Acknowledging this problem, in the beginning of 2020, our design and mobile engineer teams decided to gather a dedicated team to build a revamped design system to improve productivity of both teams.
After 3 phases of development, our teams have built and improved more than 35 UI components so far, with detailed style guides for designers as well as integration documentations for developers. These guides have significantly reduced required efforts for UI designers when creating designs from mockups, while also increased code reusability and consistency.
After the active development phases, Design System module has then been open to contributions from all other modules as needed. To ensure quality of UI components in future updates, our team has also maintained snapshot testing for most widely used components with all their different states. This testing is set up as an additional step in our CI workflow for merge requests, triggered when there is any code change to the respective UI components.
Conclusion
Looking back 2020 was an exciting year as our team has learned a lot from our technical challenges on the way to build a stable and robust app at scale. There are far more problems ahead of us — in terms of performance, automation advancements, code health control as well as in-house solutions that we aim to solve in 2021. Here’s to another year of inspirations, productivity and creativity! 🥂🍾
Special thanks to my colleagues Đặng Thái Sơn and Nguyễn Chí Công for contributions to this article.