iOS project evolution

This is a text version of my talk @ CocoaHeads Kazakhstan from Decemebr 2022

Since 2021 I have been a part of inDrive, and to be more specific - on city-to-city travel feature of the app, where people can find and create long-distance car rides. I wanted to tell a story of how the product I'm working at is Version 3.0, why Version 1.0 had been online for so long, and meanwhile, what happened to Version 2.0?

A little context

inDrive started as a ride-hailing service, simply put, passenger transportation, in Yakutsk in 2013, and the main "feature" of the service was the process of negotiating between passengers and drivers before a ride would happen, which was very helpful because of automatic fare changes during bad weather conditions.

Nowadays the iOS app is almost 90% Swift and uses many modern and useful frameworks and other technologies, but it was not always like this. Languages

To see how it used to be, I went on a journey through git commits to find out what it was like before. Archaeologist

The peer-peer model I was talking about earlier quickly showed its potential, and soon enough the decision was made to try out this model on other products (a.k.a verticals). One such vertical was Intercity. It was essentially an MVP that was made in a very short time and quickly rolled out to the major cities where the service was operating at the time. Intercity 1 (old)

At that point in time, the iOS project tried to follow Apple's guideline with the classic Model-View-Controller. This is a good option for small applications, but it has one big disadvantage - poor scalability. At some point, Navigation, Network and Error handling start to appear in the ViewController file, which causes it to grow uncontrollably.

MVP1 MVP2 MVP3

Inevitably, MVC turned into a Massive View Controller. It was a kind of chaos, which is absolutely normal for a startup. There was the main feature - city taxi service, and there was this new thing that was growing and wasn't planned ahead, and since the app wasn't modularized it was a growing mess. Which is a classic for startups, again.

For some time the product grew beyond MVP and was quite successful. The number of users grew, and with it the load on the service grew as well. Adding new features and fixing existing bugs in this situation became more and more difficult, and it wasn't just iOS either. The backend also had to be either fixed or remade..

At some point the decision was made to upgrade the product. In addition to a new, slightly more up to date design, the team decided to try new tech approaches to solving business problems. Intercity 2

Up to that point, the iOS team consisted of about 6 people, and there was no division into teams, it was pretty much one big team. With the new Intercity version, an actual team was formed. The new designs were made in Figma, communications were moved to Slack, and tasks were managed via Jira. In addition, the backend team decided to start implementing microservices, and backend-frontend interactions were boosted with Swagger. All of these services are still in use to this day seriousbusinesspeople

Let's go back to Swift

It was 2018, and at that time Swift 4.2 was released. In the days of Swift version 1 and 2 very few projects were remade with the new language, the transition to Swift 3 was quite a chore, but with the release of version 4 it began to seem that Swift is here to stay, and Apple was serious about their programming language. Besides, many iOS developers were eager to try Swift because at this point companies started to adapt to the new technology. Swift42 The problem was that none from the team had any serious experience with Swift. Another important issue was the architecture. As I said before, up until that point the project had no real architecture, so the idea was to start from scratch, make it well organized, extendable and testable. The good news was that the developers were free to choose whatever they wanted for the architecture.

Some popular architectures were looked at: MVVM, RIBs, even VIPER, but it seemed a bit off. Personally I'm glad they didn't pick VIPER. VIPER chart At the same time, Clean Swift, inspired by Bob Martin's Clean Architecture, was gaining popularity, and that's what they decided to use in the new project.

I won't go into great detail about Clean Swift, in short: Clean Swift focuses on the VIP triangle: View-Interactor-Presenter, these components communicate with each other through special data structures or models. Clean Swift So, for example, ViewController notifies Interactor about user interactions, Interactor passes the data to Presenter, and Presenter transforms the received data via a ViewModel into a ViewController. Speaking of navigation, we didn't have a unified router back then. Each screen had its own navigation. We used a common NavigationController (with its default pop, push, dismiss methods). All layout was done purely in code, using autolayout.

Pros Cons
• Simple and straightforward approach, as a result, a low entry threshold
• Ease of testing separate layers
• Large amount of boilerplate - files, protocols, and sometimes almost identical Request, Response, and ViewModel
• Navigation may become too complex since each screen has a separate navigation logic (this, however, can be done correctly)

There weren't many screens or much logic. In reality, the problem was that no one had any experience in Swift, and a lot of things were written in the manner of Objective-C (for example, there was a lot of force unwrapping, which at the time didn't seem like a huge deal). Since a lot of Objective-C code had to be reused in a new Swift project, but Swift had a neat Interop feature and using old code was pretty easy. For more on Swift and Objective-C Interoperability check out this WWDC15 session. Obj-C interop Of course, the deadlines were inevitably pushed back. Many features originally planned in the product were cut for the MVP release.

In addition to the new technologies, many service features changed with the new version, which ultimately led to the new version of Intercity being conceptually different from the old one. A few months after initial test runs, Intercity 2 was shut down, there was a rollback to the first version, and Version 2 was shelved. For a long time, Intercity 1, written in Objective-C, became the main version again, and version 2 was called IntercityOld.

It was time for a change

it was 2019, the main iOS project was already almost 7 years old, and it still didn't have any unified architecture and clear organization. It was a wild west. The app was actively growing, it was getting harder and harder to add new features, it was time for a change. A clear pattern would speed up development by making it easier to make common components, review each other and even transfer between team.

At that time, Redux, which came from the web, was gaining popularity in the mobile development world. UDF1

UDF

The concept is pretty simple: we can only go through the code in one direction. We have several entities: the main ones being View, Action and Store. On a View we call a certain Action - an event that can happen in the app (user clicked a button, text was changed, etc.) This Action is handled in a specific State via its Reducer. The State is located in a Store. This update notifies the View, and it updates the UI according to the State. UDF2

This wasn't very common in mobile development, however, both Apple and Google use UDF in their frameworks. If we look closely at SwiftUI, we will find many similarities with this scheme. And Google explicitly mentions Unidirectional Data Flow in the Jetpack Compose documentation.

Pros Cons
• Clear separation of logic. In other architectures, business logic often leaks into Controller/Presenter and other layers, while UDF gives clear instructions on how to organize the domain layer and get a reusable model
• Testability: for example, UI depends on its own data and is only concerned with rendering, so it's easy to write snapshot tests
• The approach is still not quite popular, so it has a slightly higher entry threshold
• Less obvious and intuitive debug process

Bonus

With Objective-C we've seen how quickly technology can change how and code can become obsolete. And as I said, UDF is similar to SwiftUI. That means it's easier to adapt to the new technology. SwiftUI

At inDrive we worked on our own library and it's simply named UDF. It's open source, so anyone is welcome to check it out and collaborate! UDF Logo

UDF worked perfectly, so it the plan was to gradually upgrade the whole app. At the same time, the business team needed an upgraded Intercity. That's how Intercity 3.0 came about. Actually, this is the current version of the product that was released in 2021 and is still being updated.

After the release of the MVP, we could finally remove the old, unused Intercity 2 code. Old project deleted Almost 32 thousand lines from the project went into oblivion, and we quietly exhaled as we continued working on the new version.

If you were reading carefully, you may have noticed that I said that we removed the second version, but I didn't say anything about the first one. The fact is that it still exists, and more importantly, it is still profitable.

2023 update: this talk was given in the end of 2022 when version 1.0 was still online. A bit later the product was remade from scratch and the old Objective-C code was finally removed

One more thing...

Sooner or later when you expand your product to new countries you come to realize that something that works in one place may not work in another. This can even apply to the less obvious things like naming: In English, the product is called Intercity. Sounds pretty self-explanatory. It turned out that in some languages the name wasn't clear enough to a user who had never used the service before. This is where A/B experiments come into play. The principle is quite simple - different things are tested on different groups of users.

Naming1 As a result of one of these experiments, it was decided to simply rename the service in Spanish from the not so obvious "Interurbano" to the more understandable "Ciudad a ciudad" ("City to city"), which increased the number of users and the total number of orders in the module. Naming2

One more thing...

To summarize: sometimes to make some product better - you can start from scratch. If necessary - even twice. In this case, we will certainly change the functionality. Something will turn out to be unused, something will be no longer relevant. You should not be afraid to delete large pieces of code to make your product better.

Thanks for reading!

December 23, 2022