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
Modulewith aContainer - Functional modules can be implemented using any sort of architectural pattern, be it
MVC,MVVM+C,VIPERorRedux, just use theModuleas 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/componentsOnce it's there:
- Drag the
.xcodeprojfile into your workspace. - Edit the application scheme settings and add
Components-iOSbefore your application target. - Open the general pane in project settings and add the framework to either
Embedded binariesorLinked 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
ModuleInterfacein aclass(or astruct) - Add the required dependencies as properties
- The
openmethod 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.