The creation of a framework-based meta-app-skeleton for iOS

How we achieved a collection of modular components as building blocks for different medical applications.

Ben John
Liftric
6 min readSep 13, 2018

--

TL;DR

“With the utilisation of coordinator pattern (RxFlow) we were able to build, deploy and maintain modular reusable components for iOS and create a framework-based meta-app-skeleton: a collection of frameworks that together builds up a shell for instantiating new apps with a similar purpose, structure and user interface.”

Introduction

The prequel of this article is a small introduction about who we are and what we are up to and can be read here.

Rationale

As can be read in our introduction, we need to build an app for each physical rapid test. But as all apps will be very similar regarding purpose, structure and user interface, we tried to split our app and identify small components which should be reusable in every app. I believe the first idea which would come to every iOS developer’s mind immediately is “iOS frameworks”.

There exist tons of articles about how to use iOS frameworks for some sort of micro features architecture approach. A good start is “Getting Started with Reusable Frameworks for iOS Development”. Another post against a monolithic architecture is “Micro Features Architecture for iOS”. Both can be seen as an inspiration for this article.

Some sort of micro features architecture illustration implemented with iOS frameworks.

In this article we address the following issues when combining frameworks to a final app, depict the process and finally propose solutions which worked well for us. We are very open to feedback and questions.

Problems

  1. How to properly outsource views and define relations in frameworks?
  2. How to keep track of every possible navigation step?
  3. How to orchestrate the final app with such frameworks?

Solution

Storyboards and Segues

Working with Storyboards and Segues, one will experience the boundaries of the proprietary system by Apple very fast. Basically, Storyboards or other Interface Builder related files are just XML. Nonetheless, it is very hard to keep track of changes, resolve merge conflicts or even find out quickly which Storyboards are involved in a linear sequential navigation chain, especially when working with Storyboard references. When it comes to more complex navigation flows it is even harder to maintain those. Especially when you have multiple applications that might have similar navigation flows but with slight differences depending on the actual app configuration. For instance you may want to show parts of an onboarding process in applications for lay users but want to leave them out in apps which are made for professionals. In addition, the bigger your Storyboards grow, the slower Xcode becomes.

For a smaller app, consistent of just one Xcode project, it may makes sense to stick with the stuff Apple provides. But when building a much bigger app, or in our case, a framework-based meta-app-skeleton — which builds up a shell for instantiating new apps with a similar purpose, structure and user interface — you need to come up with a different approach.

The first idea that pops into an iOS-Developer's brain probably is to omit the usage of Segues and Storyboard references and to instantiate the view controller on your own in with custom boilerplate code like the following:

let storyboard = UIStoryboard(name: "SomeStoryboard", bundle: nil)
guard let viewController = storyboard.instantiateInitialViewController() else { return }
self.rootViewController.pushViewController(viewController)

You cannot always avoid boilerplate code, but this approach clearly lacks of a good concise navigation flow and we still would need to have more knowledge of our frameworks or provide functions for each possible reachable view.

Coordinator pattern

Next, we tried using our own implementation of Coordinator pattern (you can find a very good article on that pattern plus MVVM here). The basic idea behind coordinator pattern is that you have a simple unit which instantiates all your view models, view controllers and in addition, when and how they are presented to the user. Hence, all the navigation logic is represented in pure code, which makes it also a lot easier to maintain and work collaboratively.

Depict of basic coordinator pattern.

This again may work for small apps but as soon as your codebase starts growing or you need a more modular approach, i.e. abstract parts of it into modules/frameworks, for the sake of it, you would need to implement some sort of sub coordinators. In our case you can imagine each framework contains a sub coordinator as main entry point.

We started to imagine how our final solution could look like and defined the term Flow as a navigation area within our application, so a small independent and sealed scope. Hence, each application would have a RootFlow, acting as its main Coordinator that orchestrates all the sub-flows, which is responsible for creating and navigating to other flows. By dividing the app into sub flows the final App can easily be orchestrated with different frameworks, as every framework should be responsible for an own, complete and encapsulated Flow. By complete we mean that every navigation step is known beforehand and the Flow cannot be altered.

Other drawbacks from using Coordinator pattern are the necessity of writing the Coordinator pattern dozens of times, and the need to make heavy use of delegation patterns to let ViewModels communicate back with the Coordinator, for instance to trigger navigation actions.

RxFlow to the rescue

However, an approach to clearly and precisely define navigation steps within a flow and across other flows was still missing. After some research we found a library called RxFlow which implemented our fundamental idea and took it even further (big kudos to twittemb). It structures Navigation Flows using the core concepts of Flow, Step and Stepper.

A Flow is an encapsulated and sealed navigation area within your app. In a Flow you define all your navigation actions, such as presenting a view controller, jumping to other Flows, or ending the current Flow (i.e. if you are currently in some child Flow). The Flow also contains all the code for instantiating the different view controllers, managing corresponding view models and other dependencies. A Step is some reachable navigation state (mostly views) in your app. Steppers are entities which can emit Steps, thus alter the current navigation state. The Stepper triggers a Step which then leads to a navigation action in a Flow. NextFlowItems is a simple data structure which combines a presentable view and a Stepper. It tells RxFlow which Stepper can emit Steps next. I made a very small simplified example how the code looks like (more detailed gists are linked).

An example of an implemented Flow.
An example of an implemented Steps enum.
An example of an implemented Stepper.

The completion Step is provided by the parent Flow. A view model could also be a Stepper which feels most natural at least for us.

Depict of RxFlow architecture whereas ViewModels are used as Stepper.

By using RxFlow, we could reduce our boilerplate code compared to an own coordinator implementation significantly. Moreover, the concepts of Step and Stepper really helped us to realise our idea of an meta-app-skeleton which is orchestrated by several frameworks. RxFlow helps to get rid of the need to write your own coordinators and the delegation code between those. Internally, RxFlow uses RxSwift to observe navigation states.

Some problems along the way

Along the way we identified different problems, for instance using Core Data and some other Bundle bound functionality across frameworks. In your code you need to explicitly refer to your framework, otherwise the main Bundle is used. We added a small helper which can be used like Bundle.framework to access the frameworks context (e.g. which is indispensable for Core Data).

internal extension Bundle {
class var framework: Bundle {
return Bundle(for: FrameworkExampleNameMaker.self)
}
}
private class FrameworkExampleNameMaker {}

Conclusion

We outsourced all views in frameworks and use for each view controller a new fresh and clean Storyboard. The relations are expressed via Flows and Steps. The combination of both describes every possible navigation action.

With that being said, we are able to keep track of every possible navigation step through the sum of all Step enums.

The final app is orchestrated via an app flow, we call it RootFlow. This flow basically conducts the actions between frameworks.

RxFlow makes us kinda feel like these two cows right now.

Unresolved issues

You may have a problem if you need to customise one view controller in order to satisfy some special requirements for a particular app. So one upcoming story deals with “how to customise views and have proper visual representation in exchange for Storyboards”.

--

--