Components
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 openedNavigator
- Allows for view hierarchy agnostic presentation of view controllersModule
- Fully encapsulates a specific piece of functionalityBuilder
- Instantiates modules and prepares them for useModuleContainer/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 aContainer
- Functional modules can be implemented using any sort of architectural pattern, be it
MVC
,MVVM+C
,VIPER
orRedux
, just use theModule
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:
- Drag the
.xcodeproj
file into your workspace. - Edit the application scheme settings and add
Components-iOS
before your application target. - Open the general pane in project settings and add the framework to either
Embedded binaries
orLinked 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
}
- Instantiate all the top-level objects. The most important part is the
Router
, which is the gateway to each applicationModule
- 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
}
}
}
- Instantiate the objects which you want to share between different modules.
- 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))
}
}
- Implement the
ModuleInterface
in aclass
(or astruct
) - Add the required dependencies as properties
- The
open
method serves as the entry point to your module - 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.