The current SoundCloud iOS App was built back in 2014, resulting in a huge codebase consisting of both Objective-C and Swift, with multiple architectural patterns and lots of legacy code. With time, the team changed and the platform evolved, so for the current iOS Team, maintenance, bug fixing, and implementing new features are all challenges.
In 2017, our team of six engineers wanted to try out a clean architectural pattern and decided to use VIPER.
I first read about VIPER four years ago in a post published by objc.io. This post is probably the most well-known guide to VIPER. At the time, most iOS applications were using Apple’s Model View Controller (MVC) pattern. This pattern is well suited for relatively small teams where there is only one developer working on one of the components (the Model, the View, or the Controller) at a time. VIPER, on the other hand, is good for larger teams and for parallelizing work among them, particularly when an app is big and complex, which the SoundCloud app is.
In May 2017 when I joined SoundCloud, I was excited to improve the massive codebase with my previous learnings. Prior to starting at SoundCloud, I’d used VIPER on a project written in Swift in hopes of avoiding the obstacles of MVC. Overall, I had a really positive experience and I was looking forward to exploring it more, and my work at SoundCloud provided the perfect opportunity to do this.
Because I was the only one on the team familiar with VIPER, we organized a one-day workshop where I explained the VIPER pattern to the team and we rewrote an existing screen (the System Playlist) with it. The feedback from the team was quite positive and everyone was excited to start using it. As a result, we went on to write the new Mini Player feature using VIPER, and because it was successful, we’ve since used VIPER to write additional features.
Every time we set out to write a new feature, we begin with a project kickoff, which is where we define what we are doing and why. Only two engineers, the project’s point people, are present for the kickoff. Part of their responsibility is to organize an offsite to show the feature to the team and brainstorm on the implementation.
I always liked how hackathons are done. The fast-paced environment, the chaos, the freedom, the focus on the problem without any interruption, and the brainstorming with the people in the room are all things that enable productivity. We can quickly solve a problem and accomplish a shared goal in a much shorter period, all while having fun at the same time.
Offsites remind me of hackathons because they consist of a full day at another location, without any meetings and with food and drinks. In other words, they’re different than the usual work day. Working toward a goal together, all while having fun, helps with knowledge sharing and team bonding.
The day starts with an explanation of the feature in question and the UX, followed up by questions to clarify the functionalities. The intention here is to have everyone on the same page without any misunderstandings. Once we have enough information, we start on the Contract
for the new feature.
For us, the Contract
is the most important file, and it’s where all protocols of the VIPER module are defined. This provides a quick and easy overview of the behavior of the feature — as you can see here — and helps us organize our ideas.
As an example, here is how we might build a generic feature. It assumes some familiarity with VIPER, but for more details on the way we at SoundCloud use VIPER, see here.
We start with a simple application that shows a list of tracks, as seen in the image below.
As we can see:
We tend to follow a specific order to create the Contract
(Protocols). This doesn’t mean that a different order would be worse; this is just the way the conversation usually flows.
We start with the Presenting
protocol, which handles UI events as follows.
This will probably call the Interactor
to fetch the data:
viewDidLoad()
This will probably go to the Router
to show another screen or call the Interactor
to immediately add a new value:
didTapAdd(title: String, artist: String)
This will receive the index of the row and find which track should be deleted. It will probably call the Interactor
to delete the track:
didSwipeToDelete(at row: Int)
Next, we develop the Interacting
protocol because the UI interactions are already defined in the Presenting
protocol.
We found that using
delegate
overblocks
between the components is clearer and more organized, given we often call the samedelegate
in multiple methods.
This is how the Interacting
protocol will look:
fetchTracks()
addTrack(title: String, artist: String)
delete(track: Track)
It is time to go back to the View. So next is the InteractorDelegate
, which is implemented by the Presenter
. You can expect something like this:
didFetch(tracks: [Track])
handle(error: ServiceError)
The Viewing
protocol comes next, with the following:
update(viewModels: [TrackViewModel])
The above receives an array of ViewModels
to be displayed in the TableView
. Usually, when we delete something, we call the same method with the items updated. If necessary, we can create a method to update a specific item at the index.
show(error: String)
The above receives a ViewModel
or a String
to be shown.
showLoadingIndicator()
hideLoadingIndicator()
Let’s assume that tapping on a track will direct to another screen with more detail.
The Routing
protocol will just present the detail screen, like so:
presentDetailScreen()
It will get another Builder
to retrieve the module and to use the weak
ViewController
to show the next screen.
Before we start coding, we select the most challenging parts to work on. Depending on the project, it could be a UI interaction, or it could be the decoupling of a class, a service, or even some of the VIPER components.
We split the team in groups, with each group trying to solve the problem that is given to them. Sometimes it is just a spike that will be performed on a different project, but sometimes we will work on the main application together.
If the feature is straightforward, each group gets one of the VIPER parts. Then we start branching from a feature branch in which the Contract
will be updated.
As time goes on, we continue to communicate with one another, helping with the most difficult parts and explaining architectural decisions, which makes the team more aware of the entire development process. Thanks to a different environment without interruptions, we can have these discussions without disturbing other colleagues.
By the end of the day, we have a rough idea of how our feature is going to behave. Through this process, we are essentially creating a bridge, wherein each team is constructing from a different place and hoping that in the end, all the pieces fit together.
What we want to achieve on this day is to identify the possible problems we might have and try to solve or propose solutions while we are together and focused. As a bonus, we may have some of the components done by the end of the day, like a Presenter
or Interactor
.
After having an offsite, it’s easier to create user stories and estimate and define what needs to be done. To date, the iOS team at SoundCloud has had four offsites. Each one had different outputs and sizes, but all of them helped us share knowledge and brainstorm problems that could have been difficult to solve later.
Having a clean architecture has given us the possibility of splitting the work, which makes it easier to have six people working on the same feature. It also helps improve awareness of the areas that needed to be covered, thereby reducing unexpected issues.
As a bonus, we discovered that having everybody in a different working environment with one goal in mind helped us discover problems earlier, improved our shared knowledge and ownership of a feature, and bonded the team.
We are looking forward to building more features in this format and seeing what we’ll learn from it. We’ve found that every time we do it, our process evolves, we learn new things, and we’re able to refine our workflow. We hope that over time we can decrease assumptions, have more consistent estimations, reduce project risk, and be able to ship features with more confidence.