Components 1.1.0

Components 1.1.0

Maintained by Bartlomiej Nowak.



  • By
  • BartÅ‚omiej Nowak

Components

Bitrise status Carthage compatible CocoaPods version Swift version

Code organization components optimized for minimizing the coupling between separate functionalities in an iOS application.

The framework consists of three main and two supporting object types:

  • Router - Decides which module should be opened
  • Navigator - Allows for view hierarchy agnostic presentation of view controllers
  • Module - Fully encapsulates a specific piece of functionality
  • Builder - Instantiates modules and prepares them for use
  • ModuleContainer/Container - Contains and injects dependencies into other objects

The framework is designed for each separate component to be as generalized as possible. You are free to replace them with your own implementations, they just have to conform to the existing protocols.

What does it do?

This is a dynamic approach to structuring an iOS application, offering unique possibilities:

  • Flexible view presentation that allows you to push or present a view in any place dynamically
  • Eases extracting frameworks and completely separating their internals from the rest of the application fabric. Your only point of contact can be the Module with a Container
  • Functional modules can be implemented using any sort of architectural pattern, be it MVC, MVVM+C, VIPER or Redux, just use the Module as the top-level element
  • Building deep-linking straight into the structure of the application

And it's easily testable.

How do I install this thing?

Cocoapods

Add this to your Podfile:

pod 'Components'

Do a pod install and you're ready to go.

Carthage

Add this to your Cartfile:

github "bartlomiejn\components"

Do a carthage update --platform ios and integrate the framework into your project using your prefered approach.

Manual

Clone the repository into a directory:

git clone https://github.com/bartlomiejn/components

Once it's there:

  1. Drag the .xcodeproj file into your workspace.
  2. Edit the application scheme settings and add Components-iOS before your application target.
  3. Open the general pane in project settings and add the framework to either Embedded binaries or Linked Frameworks or Libraries.

How do I use it?

Go to your AppDelegate and type in this code:

import Components

func application(
  _ application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?
) -> Bool {
    window = UIWindow(frame: UIScreen.main.bounds)
    // 1.
    let container = AppContainer()
    let navigator = Navigator(window: window!)
    let builder = Builder(navigator: navigator, container: container)
    let router = Router(navigator: navigator, builder: builder)
    builder.router = router
    // 2.
    router.register(AppModule.self)
    router.open(AppModule.self)
    return true
}
  1. Instantiate all the top-level objects. The most important part is the Router, which is the gateway to each application Module
  2. Register and open the module.

Now that we have the main hierarchy setup, we can define an injection container for our Module in AppContainer.swift:

import UIKit
import Components

class AppContainer: ModuleContainer {
    // 1.
    private let storyboard = UIStoryboard(name: "Main", bundle: .main)

    override init() {
        super.init()
        // 2.
        addModuleInjection(AppModule.self) { [weak storyboard] module in
            module.controller = self.storyboard.instantiateInitialViewController() as? ViewController
        }
    }
}
  1. Instantiate the objects which you want to share between different modules.
  2. Add a closure which will perform property injection into an Module

And now, we need to actually create a concrete module. Let's add a file named AppModule.swift and put this there:

// 1.
import Components

class AppModule: ModuleInterface {

    static let route = "app"
    private let router: RouterInterface
    private let navigator: NavigatorInterface
    // 2.
    var controller: ViewController!

    init(router: RouterInterface, navigator: NavigatorInterface) {
        self.router = router
        self.navigator = navigator
    }

    // 3.
    func open(_ parameters: AnyDictionary?, callback: ((AnyDictionary?) -> Void)?) {
        // 4.
        navigator.present(as: .root, controller: UINavigationController(rootViewController: controller))
    }
}
  1. Implement the ModuleInterface in a class (or a struct)
  2. Add the required dependencies as properties
  3. The open method serves as the entry point to your module
  4. Replace the root controller with ours

So what did just happen above?

First of all, the Router has instantiated the AppModule. Then the Builder injected the dependency, which in this case is the AppViewController, using the closure you defined before in the ModuleContainer.

Once it was actually instantiated, the Router used the AppModule.open method to let you perform the application logic inside the AppModule.

Eventually, the Navigator replaced rootViewController of a UIWindow with the controller you just gave it.

Keep in mind AppModule didn't have to present a view at all. It could've been a request to your API over HTTP or any other piece of logic, which returns some result using the callback.

By default, Router dispatches the open call synchronously on an internal serial queue, which ends with an asynchronous dispatch on the main thread.

How do I present views in a different way?

Other presentation modes in the Navigator include presenting it in the stack of a top-level UINavigationController or a top-level presentedViewController. You can present a view from pretty much any point in the application.

How do I pass parameters or get a result?

Use the extended open method:

router.open(LoginModule.self, ["username": "username", "password": "abc123"]) { result in
    if let wasSuccessful = result["result"] as? Bool, wasSuccessful {
        happyPath()
    } else {
        errorPath()
    }
}

Why is it dynamically typed?

You can just use the ModuleInterface.route property to identify the module and you don't even need to import its definition in the file you open it from:

router.open("conversation")
router.open("account", ["id": "123456"])
router.open("login") { result in
    doSomethingBased(on: result)
}

This helps a lot when you're separating a framework and try to open a module which, due to tight coupling, has to be placed in the main bundle.

I'd like to contribute, how do I do that?

All contributions are welcome. If you have an idea for a feature, improvement, fix or want to test something in the existing solution - just add an issue.