iOS stack @ Kapten
An introductory blog post on how we do iOS things
Hey guys! 👋
This is our first iOS blog post at Kapten (ever). We intend to do more so this one will be a sort of introductory one. We will present the current state of Kapten’s iOS stack so you can see what we do with hopes that some day you can come check it out by yourselves.
First, we are going to describe our architecture components on the macro level. Then we will dive at the micro level with a presentation of our MVVM integration. Finally, we will show you what we and our favorite iOS SRE (yes it’s a new thing!) developed to help us in our daily work.
On the macro level, everything iOS is in a mono repository. It wasn’t always the case. In fact we migrated only a few months back (end of 2018).
Why a mono repository?
Well, we have more and more iOS developers and there is an increasing need to maintain coherent coding and design styles. Having a mono repository helps with that because all the code is shared all the time.
Furthermore, it allows us to make changes faster without the need to release multiple frameworks, thereby increasing our productivity.
How is it composed?
The macro components are organized in the following manner:
We have 2 applications in the mono repo: the Rider App and the Driver App.
Like the names suggest, the Rider App is the app centered on the rider’s experience. The app’s main features are: signup, ordering a ride, rider profile management, the loyalty program, in ride experience, etc.
The Driver App, on the other hand, is centered on the driver’s experience. That means managing ride proposals, GPS, real-time driver state, driver performance statistics and so forth.
The purpose of this framework is to be able to integrate into our rider app and its extensions. This is where we put our application specific logic.
Its necessity emerged during the implementation of the Apple Maps app extension. We realized that we needed to share some of the code between the main app and the extension. RiderKit was created and its boundaries were defined around that example.
This framework is integrated in RiderKit and the Rider App. This is where we put our application specific models and API route definitions. Think Data Transfer Objects, domain-specific route definitions, models, and errors, etc.
DriverKit is integrated in the Driver App only. It is quite similar to the Rider's NetworkKit framework. The Driver App doesn't have any extensions right now, so there was no need to extract some of the application specific logic from the App to another Framework. It is something that we might do in the future though.
Caboose is a homemade network abstraction layer, built on top of Alamofire. It is integrated in NetworkKit and DriverKit. It is used to abstract the recurring networking logic that can happen when calling an HTTP route. It will determine if the call was successful, or failure, and if it fails, why (Is it because of bad connectivity? HTTP error? API route specific error?).
Having 99.9% crash-free sessions is cool, but it doesn't mean your app isn't buggy. With that in mind, we decided to work on a homemade logging framework, which we called FluxDelux. It is highly configurable, and very much awesome! We plan on putting it open source soon.
This is where we put pieces of code with high re-usability features. There, we have custom functional operators, a representation of NonEmptyArray, our custom Result definition, etc. To be honest, a lot of the stuff here comes from our intellectual forefathers at PointFree.
In the future, we plan on migrating more and more code in there that have the properties of being used in both the Rider App and the Driver App. We want it to be the common base containing, for example, FluxDelux and Caboose's interface definitions and some class extensions.
RxMVVM with Coordinators
We have been refining our architecture for almost 3 years (#architecturegoals). It was a lot of fun and learning. We believe in having reached a point where our final draft is pretty definitive. It is scalable and robust. It allows us to create templates for view models, and unit tests which in turn, maintains our code coherence.
Anyway, the best way to describe an architecture is with a diagram, so there it is. 🙂
We have all read about MVC’s Massive View Controllers. We believe MVVM with Coordinators is a great way to move a lot of code away from the View Controllers. The model and logic part now resides in the View Model and the navigation and presentation part has been moved to the Coordinator.
Functional Reactive Programming
We have integrated FRP everywhere in our stack. We code reactive and we think reactive. Why? Because RxSwift, the framework we use, gives us a set of amazing tools that makes complicated stuff now safe and easy.
- Asynchronicity - Left to right “callback hell” becomes top to bottom easy to read code.
- Composition and Transformation - Streams can be composed and transformed through the use of a huge set of functional Rx operators.
- Multi-threading - Changing threads is as easy as calling another operator on your stream.
- Error handling - By using Rx, you are automatically given an amazing error handling system (error propagation, error recovery, one API only).
On iOS, RxSwift is often used with RxCocoa. RxCocoa is a Framework on top of Cocoa using RxSwift. It catapults the Cocoa API into the FRP world. For example, it allows you to access the event of a button tap through the Rx API (which would be accessed with button.rx.tap). This makes coding iOS apps using Reactive Extensions very practical.
We have been using Dependency Injection quite a lot as well. Our method of implementing DI in our stack was inspired by Krzysztof Zabłocki's blog post Using protocol compositon for dependency injection.
Before using DI, we had singletons hanging around in View Models, which made it difficult to unit test. Now our View Models are 100% predictable. Down to the timeline of occurring events (thanks RxSwift).
What does it look like?
Below is an example of what a View Model looks like in our stack. This is what we would code when developing new features and/or new screens. Its framework brings velocity in the development cycle and robustness.
A View Model can be described as a set of inputs and a set of outputs. Since we highly use RxSwift, the inputs are often Observers and the outputs Observables.
We can see in the example above how inputs are transformed into outputs using Rx operators.
Often our View Models are initialized with injected dependencies. In this example, the object responsible for calling the login API is injected through the use of the LoginAPI protocol. When unit testing, we can inject a mock version of LoginAPI and check that it was called correctly and that with a given output, it had the expected consequences in the View Model.
We have developed a set of scripts (lanes) to ease our daily lives. The scripts are all coded in Ruby on top of Fastlane, which allows us to stay CI independent.
- Pulling translations - A script is running every night, which pulls our translations, and creates a Pull Request on Github if changes are detected. The following morning, a developer checks the PR. If the CI is passing, it is merged, or else a manual fix has to be done. Indeed, sometimes a change in translations can result in bugs, like a missing placeholder. The failed CI case allows us to stay robust to these.
- Testing - Run Unit and UI Tests
- Deploy Branch - We have a script for building an app on a specific git branch, very useful for QA testing.
- Deploy Applications - Building and deploying our apps, both on App Store Connect and Fabric for internal beta testing.
At Kapten, there is a rigorous system of code reviews through the use of Github Pull Request templates. A PR can be merged only if a set of rules is validated. Those rules include an increase in test coverage, commit messages adopting a specific nomenclature and the validation of a peer.
After the PR is validated, the app is built from that branch, and sent to the QA and Product team. The product team validates the acceptance tests, and the QA engineer makes sure there were no regressions and no new bugs were introduced.
Product quality is one of our core values here @ Kapten, working on the mobile side we need to be even more cautious than our back-end friends. Indeed our app being a distributed software, once we ship a bug in production the process to have it fixed can be very tedious. To address this, a few processes were put in place.
Every Thursdays we build and deploy an internal beta. An App Store build is released on Wednesday every 2 weeks. Therefore 1 out of 2 internal betas is actually released which allows us to catch bugs earlier.
Before building the beta that will eventually be released, we do a sanity check. That means QA engineers get together and test the main flows of the app. When that clears, the internal beta is built and sent on both Fabric and App Store Connect.
Wrapping it up
It is always enlightening to look back at where we were a couple of years ago and see what an incredible journey it has been. We have done our best to try to induct best practices and grow the app alongside the business. It is not always an easy task to do but we believe the environment here @ Kapten is really helping us keeping a good balance between the product and the tech.
As our business continues to scale, with the opening of new cities, we must face the never ending evolution of the ecosystem. Those challenges translate into new features waiting to be built, and technical issues wanting to be addressed. If this little introduction has piqued your interest, don’t hesitate to contact us as we are always looking for talents to join our team.