hmlongco / Factory

A new approach to Container-Based Dependency Injection for Swift and SwiftUI.
MIT License
1.85k stars 116 forks source link

[Question] How to use with a VIPER architecture? #14

Closed elprl closed 2 years ago

elprl commented 2 years ago

I love the simplicity of this DI package, but I'm finding the documentation a little confusing. My adoption of the package would depend on how it deals with a 'Clean Architecture' used in a professional workspace. Right now I'm struggling to understand how to use this package with a VIPER architecture. We use a similar structure to here: https://github.com/theswiftdev/tutorials/blob/master/VIPER/VIPERAndSwiftUI/VIPERAndSwiftUI/Sources/VIPER.swift

I began with this (below) but it doesn't compile (not even sure this is the correct approach):

import Foundation
import SwiftUI
import Factory

// MARK: - Router
typealias ListViewRouterInterface = any RouterInterface & ListViewRouterPresenterInterface

protocol ListViewRouterPresenterInterface: RouterPresenterInterface {
    func showDetails(for animal: Animal) -> any View
}

// MARK: - Presenter

typealias ListViewPresenterInterface = any PresenterInterface & ListViewPresenterRouterInterface & ListViewPresenterInteractorInterface & ListViewPresenterViewInterface

protocol ListViewPresenterRouterInterface: PresenterRouterInterface {

}

protocol ListViewPresenterInteractorInterface: PresenterInteractorInterface {
    func didLoad(animals: [Animal])
}

protocol ListViewPresenterViewInterface: PresenterViewInterface {
    var viewModel: ListViewModel? { get set }
    func onAppear()
    func onBtnPress(animal: String)
}

// MARK: - Interactor
typealias ListViewInteractorInterface = any InteractorInterface & ListViewInteractorPresenterInterface

protocol ListViewInteractorPresenterInterface: InteractorPresenterInterface {
    func fetchItems()
}

extension Container {
    static let apiService = Factory<APIServiceProtocol>(scope: .shared) { APIService() }
}

class ListViewContainer: SharedContainer {
    static let viewModel = Factory<ListViewModel>(scope: .shared) { ListViewModel() }
    static let presenter = Factory<ListViewPresenterInterface>(scope: .shared) { ListViewPresenter() }
    static let interactor = Factory<ListViewInteractorInterface>(scope: .shared) { ListViewInteractor() }
    static let router = Factory<ListViewRouterInterface>(scope: .shared) { ListViewRouter() }
}

final class ListViewModule {
    @Injected(ListViewContainer.viewModel) private var viewModel
    @Injected(ListViewContainer.presenter) private var presenter
    @Injected(ListViewContainer.interactor) private var interactor
    @Injected(ListViewContainer.router) private var router

    func build() -> some View {
        presenter.viewModel = self.viewModel
        let view = ListView(presenter: presenter)
        presenter.interactor = self.interactor
        presenter.router = self.router
        interactor.presenter = self.presenter
        router.presenter = self.presenter
        return view
    }

Do you have any advice on how to do this in a VIPER architecture? I think this would be a great example to add to your samples folder. The fact that VIPER has various Input and Output contracts make it difficult.

hmlongco commented 2 years ago

Not sure where to start here. You're right that the code doesn't compile. Give me me a working version that compiles w/o DI and I might be able to help.

Part of the problem is that you're trying to make everything a protocol, and I think you're getting into trouble with Swift's famous associated type requirements on the presenter that needs to return "some" view.

Not everything needs or wants to be a protocol. See: Using View Model Protocols in SwiftUI? You’re Doing it Wrong. for a discussion of how to swap out backend services.

That said, I'm going to go on the record as saying that VIPER is a fairly decent methodology... for UIKit.

The entire system is designed to wire together components in an old-school, imperative, direct reference delegate mechanism. And does that in order to provide control and update and segregation mechanisms that in more modern architectures would be done with RxSwift or Combine, or in the case of SwiftUI, with state observation and lightweight view composition.

Further, VIPER is a somewhat verbose and complex architectural system designed to slim down and manage heavyweight UIViewControllers and the choices it makes are, IMHO, is extremely ill-suited to SwiftUI. I've written about it somewhat extensively in SwiftUI: Choosing an Application Architecture.

elprl commented 2 years ago

Hi Michael, Allow me to offer a starting point for discussion. I've forked, created a branch called viper-spike, and added sample code here: https://github.com/elprl/Factory/tree/viper-spike It compiles but errors when ran, for the moment. I not going to assert VIPER is a good fit for SwiftUI, but if someone wanted to adapt UIKit VIPER architecture with Factory, I'm sure they would come across similar challenges to what I experienced. These include:

elprl commented 2 years ago

I've added a second commit that runs without error at least.

class ListViewContainer: SharedContainer {
    static let viewModel = Factory<ListViewModel>(scope: .shared) { ListViewModel() }
    static let presenter = Factory<ListViewPresenter>(scope: .shared) { ListViewPresenter() }
    static let interactor = Factory<ListViewInteractor>(scope: .shared) { ListViewInteractor() }
    static let router = Factory<ListViewRouter>(scope: .shared) { ListViewRouter() }
}

final class ListViewModule {
    @Injected(ListViewContainer.viewModel) private var viewModel
    @Injected(ListViewContainer.presenter) private var presenter
    @Injected(ListViewContainer.interactor) private var interactor
    @Injected(ListViewContainer.router) private var router

    func build() -> some View {
        presenter.viewModel = self.viewModel
        presenter.router = (self.router as ListViewRouterPresenterInterface)
        presenter.interactor = (self.interactor as ListViewInteractorPresenterInterface)
        let view = ListView(presenter: (self.presenter as ListViewPresenterViewInterface))
        interactor.presenter = (self.presenter as ListViewPresenterInteractorInterface)
        router.presenter = (self.presenter as ListViewPresenterRouterInterface)
        return view
    }
}

You'll see that, in order to get it to work, I've used concrete classes in the Container. The downside to this is that it becomes harder to create mock classes (see the SwiftUI Preview class). One has to create mocks using inheritance which means we have to add @objc keyword and override functions.

hmlongco commented 2 years ago

It looks as if you solved most of the issues in the current branch I just pulled.

I discussed this a bit in the Resolver documentation. (Factory isn't as complete yet.) But basically at some point with cyclic dependencies you have to step outside of the DI system and do some wiring.

This is especially true for VIPER, which just loves having objects that have references to all of the other objects.

On reading the Resolver docs I noticed a reference to a WeakLazyInjected property wrapper which comes in handy with parent-child relationships. That's now in Factory 1.2.5.

elprl commented 2 years ago

I've just seen those 1.2.5 changes - nice one. That makes it interesting, as I've been wondering whether to put the Injections in the individual classes, versus in the module builder as it currently is. In fact, I'll just try it in a new branch.

elprl commented 2 years ago

I tried on a new branch here: https://github.com/elprl/Factory/tree/viper-spike-weak-inject

It doesn't compile because WeakLazyInjected requires a class type rather than my protocols.

final class ListViewInteractor: ListViewInteractorInterface {
    @WeakLazyInjected(ListViewContainer.presenterInteractor) var presenter // Error: Generic struct 'WeakLazyInjected' requires that 'any ListViewPresenterInteractorInterface' be a class type
    @Injected(Container.apiService) private var apiService
}

So looks like it can't be done in the way I'd hoped.

hmlongco commented 2 years ago

Any luck?

Also, I put together a few thoughts on SwiftUI and VIPER here.... https://michaellong.medium.com/viper-for-swiftui-please-no-ee61ce99694c

hmlongco commented 2 years ago

I think you can also conform your protocols to AnyObject.

elprl commented 2 years ago

They already conform to AnyObject. It needs to be a class as the error states.

hmlongco commented 2 years ago

I may need to file a radar on that as Swift should be able to figure out the conformance.

This, for example, works:

weak var presenter: ListViewPresenterInteractorInterface? = ListViewContainer.presenterInteractor()

Without the AnyObject conformance on the protocol the assignment fails complaining about assigning a non-class-based protocol to weak variable.

hmlongco commented 2 years ago

There are some updates in 1.2.7.

elprl commented 2 years ago

Nice one, I checked-in a working version. The only caveat is that I'm forced to use Optionals in some of the protocols. Why is that?

protocol ListViewRouterInterface: ListViewRouterPresenterInterface {
    /*weak*/ var presenter: ListViewPresenterRouterInterface? { get set }
}

final class ListViewRouter: ListViewRouterInterface {
    @WeakLazyInjected(ListViewContainer.presenterRouter) var presenter
}

Any way around this?

hmlongco commented 2 years ago

Because the wrapped value returned by WeakLazyInjected is optional? It is weak, after all.

You can start explicitly unwrapping things, but as I wrote in the section on optionals, doing so violates the core premise on which Factory was built in the first place: Your code is guaranteed to be safe.

protocol ListViewRouterInterface {
    /*weak*/ var presenter: ListViewPresenterRouterInterface! { get set }
}

final class ListViewRouter: ListViewRouterInterface {
    @WeakLazyInjected(ListViewContainer.presenterRouter) var presenter: ListViewPresenterRouterInterface!
}
elprl commented 2 years ago

Seems to be working well. I added some sample unit tests and all seemed to work well.