Transforming a Messy Swift Codebase to MVVM — A Case Study

Daniel Friyia
TribalScale
Published in
7 min readApr 5, 2022

Written by: Daniel Friyia, Agile Software Engineer, TribalScale

📫 Subscribe to receive our content here.

💬 Have any questions about transforming a swift codebase to MVVM? Click here to chat with one of our experts!

Photo Courtesy of Raagesh C on Unsplash

We’ve all been there, you get put in charge of a codebase written by another group of developers and you have to maintain it. Problem is, all those developers left the company awhile ago and you have no idea why any decisions were made. This isn’t always a bad experience. I can think of times where developers before me made good decisions and it was easy to dive in and start making changes 🙂. Recently though, my team had a really negative experience with a messy codebase that had basically no architecture to speak of 😞. In this article I’ll be talking about how we went about refactoring this app piece by piece into something we could maintain. Obviously every codebase is different so this article isn’t meant to be a strategy for all refactors. That being said, I hope you’ll pick up a few good ideas from our experience that you can implement when doing architectural refactors in your project.

Diagnosing the Health of an Inherited Codebase

When starting out with any project it’s first important to understand what we are dealing with. Obviously there can’t be any meaningful changes until we can identify what parts of the app are well built and which are not. When assessing a new codebase I like to think in terms of, what I feel, are the five pillars of any mobile or front-end project: User Interfaces, Navigation, State Management, Networking and Peripheral Management. Through this framework, I can get a feel for the app’s architecture and overall code health. There were the results of our diagnosis:

User Interface Development

In our case this app was written in Swift so it wasn’t a legacy project or anything. When building user interfaces, the previous developers used XIB files in combination with Swift View-Controller files. This was a bit annoying because the team had mostly worked with storyboards, but it wasn’t technically wrong or anything, so we felt we could leave this as-is.

Navigation

This app had no structured form of navigation. Because we weren’t using storyboards, we had no segues. The previous developers were simply getting the UIApplication root view controller and replacing it. Code snippets like this were randomly all over the place.

let vc = MyViewController()
UIApplication.shared.theKeyWindow?.rootViewController = vc

We would need to fix navigation before making any progress.

State Management

The state management of this code base was a crude form of Model-View-Controller (MVC). Although there are structured ways to do MVC, the developers before us didn’t use it. There were view controllers of 3000+ lines that were hard to navigate through. It would be important to create shorter, more focused files before moving forward.

Networking

The networking layer was another part of this app we didn’t feel needed rework. This is because the app used GraphQL and Apollo which generates most of the queries based on the schema you provide. Even if we wanted to re-write large portions of this, we wouldn’t be able to.

Peripheral Management

The final layer we assessed was the peripheral connection layer. This app used a Bluetooth device. The previous developers only exposed Bluetooth to one part of the app. Future feature development would require Bluetooth to be exposed to the entire app, so this piece wasn’t where it needed to be.

If I was to diagram the early architecture it would probably look something like this:

The architecture taken at face value seems simple. But it’s the lack of focused classes and objects that was leading to overloaded view controllers that were verbose and challenging to navigate.

The Refactor Begins

We diagnosed the app with having three main problems: state management, navigation and peripheral management. After fixing some initial bugs and negotiating refactor time with the client, we were off the the races.

Choosing Model-View-View-Model (MVVM)

The first decision we made as a team was that we were going to refactor to MVVM. As you’re probably aware, VIPER is another strong candidate for state management. Problem is, we only had a month or so for this refactor and it would take too long to build out the more sophisticated VIPER. Although MVVM is not as flexible as VIPER, it's no slouch. MVVM would enable us to separate our view logic from our business and navigation logic, effectively hitting two birds with one stone.

Refactoring Navigation

Once we were set on using MVVM, the first thing we needed to do was to fix the navigation structure of the app. The app was making screen transitions like this on the old architecture:

let vc = MyViewController(
val1: 1
val2: 2
)
UIApplication.shared.theKeyWindow?.rootViewController = vc

This meant that navigation logic and building view controllers were taking place in the host View Controller. Without a clear separation of concerns between navigation and the View Controllers, it would be impossible to make any progress on View Models.

We chose to use the coordinator pattern of navigation which allowed us to keep our navigation functionality in one place. For those who have never used the coordinator pattern before, it’s basically a way of managing access to navigation functions on a global UINavigationController. We paired this with an extension to the UINavigationController that allowed us to simplify navigation primitives such as pushing, popping, and presenting modals. At this point we could set up navigation using a pattern like the following:

func goToNewScreen(param1: AnyParam) {
let sampleVM = SampleViewModel(
param1: param1,
networking: self.networkingLayer,
user: self.userModel
)
let sampleCV = SampleViewController(viewModel: sampleVM)
self.push(sampleVC)
}

Then in the View Controller:

class MyViewController {
...
func transition() {
self.coordinator.goToNewScreen(
param1: sampleParam
)
}
}

At this point two of our problems were solved.

  1. We no longer had our business logic entangled with our view logic like what you see in the MVC pattern.
  2. We were able to remove navigation logic from view controllers. If I were to make a diagram at this point it would look like this:

There is definitely a lot more going on in this diagram, however, the app had a much better separation of concerns at this point. The Navigation Coordinator handled navigation, View Models handled business logic, View Controllers handled View Logic and the networking layer handled networking.

Peripheral Management

The final part of this project that needed rework is the peripheral layer. It was a Bluetooth object that was available in only one part of the app. To fix this, we started by transforming it into a singleton. This wasn’t enough though, we still needed to be able to access the delegate methods of Bluetooth from different screens. To do this, we created a superclass for all View Controllers that required Bluetooth connectivity. Extending this class would allow all core controllers to have access to Bluetooth without having to rewrite the protocol methods in all delegates over and over again.

Final Thoughts

In the end, the app architecture ended up looking like this:

As you can see there are a lot more layers to this app than we started out with. This ends up being a strength because every part of our codebase has a defined purpose. This makes the system not only easier to extend, but a lot easier to explain to stakeholders in plain English when the need arises.

Thanks for giving this a read! This refactor was actually quite fun and really allowed us to flex our OOP muscles. Hopefully hearing our story will empower you to make some of these decisions in your next major refactor.

Daniel is an Agile Software Developer specializing in Flutter and React-Native. As much as he likes cross platform, he is passionate about all types of mobile development including native iOS and Android and is always looking to advance his skill in the mobile development world.

TribalScale is a global innovation firm that helps enterprises adapt and thrive in the digital era. We transform teams and processes, build best-in-class digital products, and create disruptive startups. Learn more about us on our website. Connect with us on Twitter, LinkedIn & Facebook!

--

--

Daniel Friyia
TribalScale

Daniel is an Agile Software Developer specializing in all forms of mobile development, Native iOS, React-Native and beyond