Closed elprl closed 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.
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:
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.
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.
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.
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.
Any luck?
Also, I put together a few thoughts on SwiftUI and VIPER here.... https://michaellong.medium.com/viper-for-swiftui-please-no-ee61ce99694c
I think you can also conform your protocols to AnyObject.
They already conform to AnyObject. It needs to be a class as the error states.
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.
There are some updates in 1.2.7.
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?
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!
}
Seems to be working well. I added some sample unit tests and all seemed to work well.
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):
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.