Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
thomverbeek committed May 10, 2020
2 parents 7e6541e + 6fe5b31 commit 1ba809f
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 50 deletions.
144 changes: 102 additions & 42 deletions Sources/VIPER/VIPER.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,88 +4,120 @@ import Foundation
/**
A VIPER View represents the UI logic of your screen.
It must define:
- an Interactor (which handles all user interaction logic on its behalf);
- a View Model (which contains its view state information).
Messages are passed between VIPER components via entities. Views receive View Models to update their
view state information.
Views receive view models to update their view state information, and notify the interactor of any user
interaction.
Examples of Views include View Controllers, Windows and other UI-based components.
Examples of Views include View Controllers, Windows and other UI-interface components.
*/
public protocol VIPERView {

associatedtype Interactor
associatedtype ViewModel

/**
Initialise a view with an interactor and view model.
- Parameters:
- interactor: An object to notify regarding any user interaction.
- viewModel: An object that conveys view state information.
*/
init(interactor: Interactor, viewModel: ViewModel)

/**
Updates the view with a view model.
- Parameters:
- viewModel:An object that conveys view state information.
*/
func update(with viewModel: ViewModel)

}

/**
A `VIPERInteractor` represents the business logic of your screen module.
It must define:
- `associatedtype` Dependencies: it relies on (micro-services and other information used to configure the screen);
- a Router (which handles navigation logic on its behalf);
- a Presenter Model (which contains presentation state information).
A VIPER Interactor represents the business logic of your screen module.
Interactors respond to user interaction events and communicate with entities to determine the state of the
module. They broadcast state information to Presenters to consume, and notify routers when it's time to
navigate.
*/
public protocol VIPERInteractor {

associatedtype Entities
associatedtype Router
associatedtype PresenterModel

/// Used to broadcast state information for Presenters to consume.
var output: CurrentValueSubject<PresenterModel, Never> { get }

/**
- Parameters:
- Entities: Services and repositories for the Interactor to depend on.
- Router: An object to handle navigation logic.
- PresenterModel: An object that conveys state information.
*/
init(entities: Entities, router: Router)

}

/**
A VIPER Presenter represents the presentation logic of your screen module.
It must define:
- A PresenterModel (which contains state information received from the Interactor);
- A ViewModel (which is a mapping of state information suitable to be displayed in the view).
Presenters take business logic processed by the Interactor and map it into presentation logic for the view.
*/
public protocol VIPERPresenter {

associatedtype PresenterModel
associatedtype ViewModel

/**
Maps business logic from the Interactor into presentation logic for the View to consume.
- Parameters:
- presenterModel: an object that conveys state information.
- Returns: A view model that conveys view state information.
*/
static func map(presenterModel: PresenterModel) -> ViewModel

}

/**
A VIPER Router handles the navigation logic of your screen module.
The Router defines:
- Modules (which is used to instantiate other modules);
- A View (that is weakly held by the router, a context for configuration and navigation).
As Routers hold a weak reference to the view, they're also designated to hold the reference to the cancellable
subscription.
As the Router lives in the same realm as the View, it is empowered to configure the view if needed. It also
uses the view as a context to present new modules.
Routers need to hold a weak reference to the view. They are therefore a class as opposed to a protocol.
Consequentially, they're also designated to hold the reference to the module's data flow subscription.
*/
open class VIPERRouter<Resolver, View: AnyObject>: NSObject {
open class VIPERRouter<Builder, View: AnyObject>: NSObject {

/// A weak reference to the view of the module. The View provides a UI context for the Router to
/// configure and navigate from.
public weak var view: View? {
didSet {
viewDidChange()
}
}

fileprivate var subscription: AnyCancellable?
public let resolver: Resolver

/**
An object that can construct dependencies on behalf of the Router. The builder is
used by the router to instantiate other modules; typically a dependency injection
container.
*/
public let builder: Builder

required public init(resolver: Resolver) {
self.resolver = resolver
/**
Initialises the Router with a Builder.
- Parameters:
- builder: An object that can construct dependencies on behalf of the Router. The builder is
used by the router to instantiate other modules; typically a dependency injection
container.
*/
required public init(builder: Builder) {
self.builder = builder
}

/**
Expand All @@ -105,16 +137,17 @@ open class VIPERRouter<Resolver, View: AnyObject>: NSObject {
`VIPERModule` is a composable assembler that constructs VIPER components based on constraints.
Particularly, it assembles the following components:
- View: Strongly retains the VIPER components.
- Interactor: Communicates with the specified Router.
- Presenter: Maps the Interactor's PresenterModel into the View's ViewModel.
- Router: Retains the subscription of the communication loop between components.
It defines strict guidelines that VIPER configurations must adhere to and ensures that
communication between components is set up. This guarantees that any vended VIPER assembly will
communicate in identical manner, regardless of whether the assembly is for production or testing. It is
therefore a final class.
- Parameters:
- View: Strongly retains the VIPER components
- Interactor: Communicates with the specified Router
- Presenter: Maps the Interactor's PresenterModel into the View's ViewModel
- Router: Retains the subscription of the communication loop between components
*/
public final class VIPERModule<View: VIPERView & AnyObject, Interactor: VIPERInteractor, Presenter: VIPERPresenter, Router>
where
Expand All @@ -124,22 +157,49 @@ public final class VIPERModule<View: VIPERView & AnyObject, Interactor: VIPERInt
Presenter.PresenterModel == Interactor.PresenterModel
{

/// For testing purposes, you can use this method to both assemble and access components.
internal static func components<Resolver>(entities: Interactor.Entities, resolver: Resolver) -> (view: View, interactor: Interactor, router: Router) where Router: VIPERRouter<Resolver, View> {
let router = Router(resolver: resolver)
/**
Assembles a VIPER module and returns individual components for testing purposes.
A VIPER module can assemble when the Router is a `VIPERRouter` that defines a particular type of
Builder.
- Parameters:
- entities: Any repositories or services that the Interactor should depend on, defined by the
Interactor. These repositories and services should be abstract.
- builder: An object that can construct dependencies on behalf of the Router. The builder is
used by the router to instantiate other modules; it's typically a dependency injection
container.
- Returns: VIPER components configured according to the VIPER assembly criteria.
*/
internal static func components<Builder>(entities: Interactor.Entities, builder: Builder) -> (view: View, interactor: Interactor, router: Router) where Router: VIPERRouter<Builder, View> {
let router = Router(builder: builder)
let interactor = Interactor(entities: entities, router: router)
let view = View(interactor: interactor, viewModel: Presenter.map(presenterModel: interactor.output.value))

router.view = view
router.subscription = interactor.output.sink { [weak view] presenterModel in
view?.update(with: Presenter.map(presenterModel: presenterModel))
}
router.view = view

return (view: view, interactor: interactor, router: router)
}

public static func assemble<Resolver>(entities: Interactor.Entities, resolver: Resolver) -> View where Router: VIPERRouter<Resolver, View> {
return components(entities: entities, resolver: resolver).view
/**
Assembles a VIPER module.
A VIPER module can assemble when the Router is a `VIPERRouter` that defines a particular type of
Builder.
- Parameters:
- entities: Any repositories or services that the Interactor should depend on, defined by the
Interactor. These repositories and services should be abstract.
- builder: An object that can construct dependencies on behalf of the Router. The builder is
used by the router to instantiate other modules; typically a dependency injection
container.
- Returns: A view configured according to the VIPER assembly criteria.
*/
public static func assemble<Builder>(entities: Interactor.Entities, builder: Builder) -> View where Router: VIPERRouter<Builder, View> {
return components(entities: entities, builder: builder).view
}

}
6 changes: 3 additions & 3 deletions Sources/VIPERCommandLine/Templates/Module.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ import \(framework)
import VIPER
public protocol Dependencies {
public protocol Builder {
}
public enum \(moduleName) {
public func assemble(entities: Entities, resolver: Resolver) -> \(view) {
return VIPERModule<View, Interactor<Router>, Presenter, Router>.assemble(entities: entities, resolver: Resolver)
public func assemble(entities: Entities, builder: Builder) -> \(view) {
return VIPERModule<View, Interactor<Router>, Presenter, Router>.assemble(entities: entities, builder: builder)
}
public struct Entities {
Expand Down
2 changes: 1 addition & 1 deletion Sources/VIPERCommandLine/Templates/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import VIPER
extension \(moduleName) {
class Router: VIPERRouter<Dependencies, View> {
class Router: VIPERRouter<Builder, View> {
}
Expand Down
8 changes: 4 additions & 4 deletions Tests/VIPERTests/VIPERTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Combine

class VIPERTests: XCTestCase {

struct Dependencies {}
struct Builder {}

struct Entities {
var values: [String] = ["one", "two", "three"]
Expand Down Expand Up @@ -70,7 +70,7 @@ class VIPERTests: XCTestCase {

}

class Router: VIPERRouter<Dependencies, View> {
class Router: VIPERRouter<Builder, View> {

var expectation: XCTestExpectation?

Expand All @@ -90,7 +90,7 @@ extension VIPERTests {

func testAssembly() {
// arrange
var components = Optional(VIPERModule<View, Interactor, Presenter, Router>.components(entities: .init(), resolver: .init()))
var components = Optional(VIPERModule<View, Interactor, Presenter, Router>.components(entities: .init(), builder: .init()))

weak var view = components?.view
weak var interactor = components?.interactor
Expand Down Expand Up @@ -118,7 +118,7 @@ extension VIPERTests {

func testDataFlow() {
// arrange
let view = VIPERModule<View, Interactor, Presenter, Router>.assemble(entities: .init(), resolver: .init())
let view = VIPERModule<View, Interactor, Presenter, Router>.assemble(entities: .init(), builder: .init())
XCTAssertEqual(view.viewModel.title, "3")

view.interactor.select(string: "four")
Expand Down

0 comments on commit 1ba809f

Please sign in to comment.