How we accomplished rebuilding Pairs iOS App continuously

Muukii (Hiroshi Kimura)
Eureka Engineering
Published in
8 min readDec 1, 2022

--

Refactoring is a popular topic among developers.

We launched Pairs iOS Application in 2012, and several times it was refactored, rebuilt over to allow business growth.
Using accumulation is nice to highlight the continuous use of the technique. There were so many changes.

The iOS Application development industry has also extended by using many technologies. As you know, Swift language is one of the biggest game changers.

The code base of applications is expected to grow gradually, so it will take a lot of work to maintain them accordingly.

This article shows you what we’ve done lately to bring the app to our future.

We rebuilt the app entirely and continuously

We started that work in June 2019, rebuilding the app entirely, maintaining a single codebase and while still adding new functionalities and publishing as we went.

Then finally, that work was completed in December 2021.

Why we continued working on the business

From measuring our codebases scale, we suspected that work would take quite a long time, at least over 1 year. Then we decided to begin the rebuilding concurrently because blocking our business roadmap would be a risk to us.

How we work on

Rebuilding concurrently is one of the hardest strategies.
Taking most of the time to work on business tasks, refactoring won’t move forward.
Product managers need to understand why software development needs this kind of work. And developers need to explain what is not just clean-up time, rather, it’s for pushing forward our business strongly.

This refactoring might appears quite simple at first sight — making a new system layer that provides integrations with our server. Then transfers all of the features into there.

But as you may know, it is super complex work.
It cannot always be done by simply reimplementing. We have no choice but to make partial implementations if had many dependencies — putting another complex integration with old and new systems carefully.
Otherwise, the application potentially might be unstable temporarily.

To complete as soon as possible, we took the following ways:

  • Let the roadmap items include refactorings if they are related — this is good because both motivations of engineering and business have the opportunity to make improvements at once. Developers can then take the chance to consider what is going to be needed in the future.
  • Run refactorings independently if there are no dependencies for roadmap items —this is a bit tougher to reason with because if the system is already working correctly, then the work is potentially redundant with little obvious benefit.
  • The development leader shows the team a clear-cut strategy and tactics for the development — What technology we use, how to use it, and why we use that, what we can get from that.

Making building blocks accelerates productivity

While working on the refactoring, we focused on making reusable building blocks rather than minimizing code or just re-implementing.

This works well for both refactoring and feature improvement.
Developers can use it for work and can accelerate development.

From this cycle, can find ideal building blocks and improve on them. We could then make more sophisticated blocks by iterating this cycle.

We often see similar user interfaces in several places. First, we might consider integrating into one that can change its behavior by passing parameters.

The number of code lines and types would decrease. However, complexity implicitly increases.

Rather, it is hard to accept modifications since the components are too tightly-coupled.

We should not do like this by having similar appearances or behaviors as the main motivation. It’s fine to keep it until something special comes.
Minimizing accompanies risks of losing flexibility.

If we make improvements, we can do the following from thinking in composition.

Instead, find possible smaller building blocks that are shared in different places. Then compose them for each use case.

By the way, I like this section on React design principles.

We prefer boring code to clever code. Code is disposable and often changes. So it is important that it doesn’t introduce new internal abstractions unless absolutely necessary. Verbose code that is easy to move around, change and remove is preferred to elegant code that is prematurely abstracted and hard to change.

Characteristic techniques

We show you what we’ve got through this work.

Drop UINavigationController and ViewController Presentation

The Pairs app stopped using UINavigationController and UIKit’s presentation APIs except for particular cases. Instead, we’ve built a container view controller (StackController) from scratch.

It’s like UINavigationController, stacking and popping view controllers for displaying. And there are more functions in addition.

For the presentation and flexible transitions.

StackController works for both push and presentation(modal) transitions.
It communicates with other instances about how the view controller operations should be forwarded to achieve the desired presentation behavior.
UIKit’s presentation works from any view controller associated with the responder chain.

This decision was a dramatic change for Pairs and us, and the following are the motivations for adopting it:

We need more flexibility with screen management; mixing push and presentation behavior results in unnecessary complexity and instability

Normally, UIKit offers us these APIs for screen management:

modality (presentation)

  • present
  • dismiss

hierarchical navigation (push)

  • push
  • pop

In the app, view controllers are shown as a modal or a navigation push from different sources — often from deep linking and jumping directly from other screens. It might be a bad user experience according to Human Interface Guideline, but sometimes we have to.

To support both modal and navigational push, we may need to do some tricks. Particularly, with navigation items the UINavigationBar may need to be created as a standalone subview or may be provided by the UINavigationController’s managed instance. Therefore, developers will take some additional work to support it — performing different operations during modal and during navigation push.

  • pushing a view controller into a navigation controller
  • wrapping a view controller with UINavigationController to have UINavigationBar.

On the other hand, presenting a view controller has restrictions. UIViewController is allowed to have just one other view controller as its presentedViewController. If the view controller receives a second view controller for presentation, it will be ignored silently. We have to do extra work to prevent it.

Taking advantage of presentation context and specifying modal presentation style helps us, although it makes custom transitions harder.
UIKit does not supports well custom transitions in a modal-transition-style except fullScreen or custom.

UIKit’s ecosystem is designed to make apps follow the Human Interface Guidelines as easy as possible. In contrast, it means we may have to do extra work for use cases outside what the Human Interface Guidelines were made for.

So we decided to take an approach detached to handle all of that manually.

More Interactive and Interruptible screen transitions

Our stack controller provides for more flexible view controller transitions.

Custom transitions using UIViewControllerAnimatedTransitioning have some limitations in modal presentation and push transition. It supports cancellation, which helps, but it is not enough.

Please take a look at what happens on the iOS Home Screen. It completely responds to user interaction— It is interruptible and cancellable at any point in the interaction.

  • opening an app
  • canceling the opening through the Home bar
  • moving back to the home screen
  • opening the app again by touch.

The following is the main concept of what we made.

Using Verge for state-management

Verge is a state-management framework. We built it, and Pairs has used it for over four years.

What’s next

SwiftUI

Pairs started using SwiftUI in particular views but still contained inside view controllers.
SwiftUI’s power is real. It’s much faster to prototype and make ready for production

However, it’s still under development. There are many places where we use workarounds to fix issues, and we also need to do more maintenance work when new iOS versions are released.

swift-concurrency

This is a stunning and delightful feature in Swift!
We’ve been working on converting RxSwift(Combine) and closure-based asynchronous operations to swift-concurrency.
It makes the codebase much simpler. We can then potentially modify operations that include asynchronous operations much easier. For example, changing the order of execution.

// before
await runA()
await runB()

// after
await runB()
await runA()

However, this won’t make concurrent programming easier actually. It just provides us with simpler syntax to describe operations that run concurrently.

Of course, Swift compiler works on raising warnings and errors from compiling concurrency code if there are unsafe operations.
However, we still need to have a deep understanding of concurrency programming to design code correctly.

Language features do not necessarily reduce the things we should learn about.

We believe technologies that give developers productivity also affect the company’s competitiveness.
We will continue to explore how we expand the usage of such technologies.

--

--