Context

Traditionally applications have been single-target. When projects evolve, and the target and the team grow, working with a monolith project becomes very hard:

Inspired by microsvervices, the Framework Oriented Programming project architecture pretends to reduce these issues by splitting the large application module, into smaller and atomic chunks.

In the next sections, we’ll dive into the definition of the architecture and how different code components would fit into all the modules of our projects.

The core idea of framework oriented programming is not something we’ve invented. You can find a lot of literature about scaling projects by splitting up your project in different services. Our aim is to apply all these principles, that help projects scale easily in other platforms, to scale our Xcode apps.

Why modularizing my apps?

Index

Principles


Core

Core Framework

Core is the framework at the bottom of the stack. It’s the responsible for providing the frameworks in upper levels with tools that they need to build their features. A few examples of these tools could be:

Notice that some of these tools will be a shared instance with a configuration that depends on the app. For example, your client will point to an URL that depends on the configuration that you are building. Similarly, the log level will be different in your Release build compared to your Debug build.

One possible way to get it done has a shared configuration that every new instance of the client will take by default:

class Client {

  static var appConfig: Config!
  let config: Config

  init(config: Config = Client.appConfig) {
    self.config = Client.config
  } 
}

Other tools, will be instantiated by the feature that needs it. Depending on the expensiveness of its creation, features might lazily load it, or get set up at startup time. A good example of these would be the Store:

import Core

class Feature {
  let store: Store<Entity>
  init() {
    store = Core.DiskStore(name: "feature")
  }
}

Features

Features framework allows dependency inversion with features. Feature A and B don’t know about each other, but they know about their interfaces because they’ve been defined in the Features framework. Without the dependency inversion in place, accessing B from A, creates an implicit dependency between these two frameworks. With such dependency, you can’t use A without importing B in a different application.

All the models that these interfaces (or protocols) expose should be part of Features as well.

Testing

Testing framework

Most times you’ll find yourself writing helpers or testing expectations that other teams might need as well. By extracting all of them in a framework, you make them reusable across all the feature frameworks.

UI

UI framework

For consistency in your applications designs, there are certain UI elements that are shared across the features, elements like fonts, colors, or custom views. It’s also a good place for UIKit and AppKit extensions that you come up with.

These elements can be placed in an UI framework that the feature frameworks depend on.

Feature

Feature frameworks represent one or multiple related features of your apps. Features are composed by business logic (data) and presentation (views). While the business logic is common for all the platforms, the presentation layer might differ because for example:

For that reason, features should be horizontally split into the Core and the UI frameworks.

Core

Feature Core framework

It contains the business logic of your features. It’s up to the teams to decide about the patterns that they want to follow inside the framework (MVC, MVVM, MVP, VIPER). Different use cases could be exposed as interactors that would be hooked from the feature UI framework.

UI

UI framework

The UI of your features will be in this framework. Since UIs will most likely be different between platforms, it’s very recommended to have one framework per platform:

Feature_iOS
Feature_macOS

Features can be a composition of views from other features. The result of that composition will be another view that will be exposed. These views should be able to respond to actions and trigger state updates.

If actions imply navigation to features in other frameworks, they should be delegated to the app.

Products

Products targets

The product targets are the top element in the stack. The result of its build is the app or extensions that users will use on their devices. Since features will be defined in the frameworks and will provide the views and view controllers that represent them, the responsibility of the app is hooking up all of them and define the navigation of the app.

As mentioned earlier, the app will set up Core tools at startup time and notify about the application lifecycle events to the components in lower levels that need to know about them.

Setup

Manual

Although all the targets for the frameworks can be on the same projects, keeping them in diferent project will make them completely independent from the others:

Frameworks/
  FreatureA
  FeatureB
Frameworks/
   Configuration/
      Framework.xcconfig

If you are building cross-platform frameworks it’s very easy to break the support for the platforms you’re not building for in your workflow. To prevent this, you can define a continuous integration step in your pipeline, that build the frameworks for all the platforms that they are supposed to support.

CocoaPods

If you prefer to use CocoaPods to create the stack it’s also possible:

  1. Create the frameworks as pods with pod lib create Feature.
  2. Update the .podspec accordingly specifying the deployment_target and defining their dependencies, either external or local.
  3. Add all the dependencies to the Podfile, being the first one Core and the last one the Features.
  4. Execute pod install. It’ll update the workspace to include these dependencies. Notice that CocoaPods will create schemes for building these dependencies individually.

With CocoaPods it’s easier to bring external dependencies. Otherwise, you’d need to appeal to Carthage, Git Submodules or Swift Package Manager.

Tools

Framework generation

Every time you create a new project for a framework, you need to repeat the same steps. Hopefully, there are tools that help you automate the creation and save a lot of time:

Pandora

Once you start modularizing your apps you’ll notice that you repeat the same steps every time you are about to create a new framework. Create the project, set the config, connect dependencies, add the example app… Hopefully we’re developers and we can automate things! And that’s what we did with Pandora. Pandora is a command line tool written in Ruby to automate Framework tasks.

Example: Creating a framework

pandora create Search com.myorg

SwiftPlate

Easily generate cross platform Swift framework projects from the command line. SwiftPlate will generate Xcode projects for you in seconds, that support:

Example: Creating a framework

swiftplate

Dependency managers

Useful for fetching external dependencies and integrate them into your frameworks-based projects.

Contribute

How to contribute?

How to setup the project

  1. Git clone theh repository with git clone https://gitlab.com/caramba/framework-oriented.git
  2. Install gem dependencies with bundle install
  3. Run the server with bundle exec jekyll serve
  4. Open http://127.0.0.1:4000

We’re looking forward to your improvements!

Share

If your project were already using a similar modularized setup, or you moved towards this direction, you can share your experience in this section. Open a merge request and do not hesitate to share it!

Thanks

Special thanks to all the contributors listed below that have helped to make this reference possible and spread the idea of modularizing code:

Resources

Talks

Articles

Further reading