Skip to content

Latest commit

 

History

History
105 lines (83 loc) · 5.01 KB

Implementing-view-builders.md

File metadata and controls

105 lines (83 loc) · 5.01 KB

This guide gives several examples of how ViewBuilder objects may be implemented, as well as extended.

Constructing ViewBuilder

Lets start with very common example of UI - showing alerts using UIAlertController. While being very easy to setup with a few lines of code, default setup for UIAlertController lacks dependency injection and is tricky to test. Which is why test for this example would be to not only show how UIAlertController can be built with Ariadne, but also to become testable and reusable.

To do that, lets start decompositing alert's logic by implementing simple model for UIAlertAction:

class AlertActionModel {
    var isPreferredAction : Bool = false
    let title : String?
    let style : UIAlertAction.Style
    let handler : ((UIAlertAction) -> ())?

    init(title: String? = nil, style: UIAlertAction.Style = .default, handler: ((UIAlertAction) -> ())? = nil) {
        self.title = title
        self.style = style
        self.handler = handler
    }
}

Next, lets define similar model for UIAlertController.

class AlertModel {
    let title: String?
    let message: String?
    let style: UIAlertController.Style

    var actions: [AlertActionModel] = []

    init(title: String? = nil, message: String? = nil, style: UIAlertController.Style = .alert) {
        self.title = title
        self.message = message
        self.style = style
    }
}

Note that, to simplify example, not all functionality of UIAlertController is supported - such as adding a textfield with configuration handler, but this can easily be added.

Ok, so now when we have models for UIAlertAction and UIAlertController, we can implement our AlertBuilder to build instances of them:

struct AlertBuilder: ViewBuilder {
    func build(with context: AlertModel) -> UIAlertController {
        let alert = UIAlertController(title: context.title, message: context.message, preferredStyle: context.style)
        for action in context.actions {
            let alertAction = UIAlertAction(title: action.title, style: action.style, handler: action.handler)
            alert.addAction(alertAction)
            if action.isPreferredAction, alert.preferredAction == nil {
                alert.preferredAction = alertAction
            }
        }
        return alert
    }
}

On this stage, we are ready to finally use our abstractions. Let's imagine user of the app tapped a button to leave some screen - for example in a game, and you want to warn him, that if he leaves, his progress wont be saved. Here's how we might show this alert to the user:

let alert = AlertModel(title: "Are you sure you want to leave?", message: "Any unsaved progress will be lost!")
alert.actions.append(.init(title: "No", style: .cancel))
alert.actions.append(.init(title: "Leave", style: .destructive, handler: { [unowned router] _ in
    router.navigate(to: Router.popToRootRoute())
}))
router.navigate(to: AlertBuilder().presentRoute(), with: alert)

Code looks very similar to direct UIAlertController initialization, however there are several key differences. First of all, models for alert and actions are completely testable without creating actual controller. Secondly, alert presentation logic is actually completely separated from current view controller, and those models can be created in any environment, like view model in MVVM or presenter in MVP. Last but not least, models coupling with actual UI is not very tight, so in future versions of your app replacing UIAlertController with custom built control will be much easier, since actual UIAlertController and UIAlertAction creation happens in AlertBuilder which can be easily replaced.

Extending ViewBuilder

Now, let's imagine we are working on an iPad app, that sometimes requires alerts and pickers to be shown in popover. Lets extend ViewBuilder protocol to achieve this:

extension ViewBuilder {
    func popoverRoute(from sourceView: UIView,
                           permittedArrowDirections : UIPopoverArrowDirection = .up) -> Route<Self, PresentationTransition> {
        let route = presentRoute()
        route.prepareForShowTransition = { view, _, _ in
            view.modalPresentationStyle = .popover
            view.popoverPresentationController?.sourceView = sourceView
            view.popoverPresentationController?.sourceRect = sourceView.bounds
            view.popoverPresentationController?.permittedArrowDirections = permittedArrowDirections
        }
        return route
    }
}

Once this is done, we can actually replace our presentation code with popover presentation code easily:

router.navigate(to: AlertBuilder().popoverRoute(from: button), with: alert)

By leveraging protocol extensions and extending ViewBuilder instead of AlertBuilder directly we achieved ability to reuse popover presentations for any builders. For example, if some picker requires popover presentation, we can just call it like so:

router.navigate(to: PickerBuilder().popoverRoute(from: button), with: pickerConfiguration)