Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SwiftUI Previews only working in the main app of multi framework application #114

Closed
theblaggy opened this issue Jun 17, 2021 · 8 comments
Closed

Comments

@theblaggy
Copy link

I'm developing a SwiftUI app with a framework structure like in this picture. The Presentation, Domain and Data Layers are frameworks and the App Layer is the main app.

I'm registering all dependencies in the main app and it's working fine in simulator or real devices but in preview it gets tricky.
When previewing views of the main app (where all the dependencies get registered) everything is fine, too.

But when trying to preview views of the Presentation layer, the preview crashes because no dependencies are registered.
I think this happens because in preview not the complete app runs but only the framework (and it's sub frameworks) of the view your previewing gets executed. Therefore the ResolverRegistering extension doesn't run which registers the dependencies. (At least this would explain why it's only working in the preview of views implemented in the main app.)

I also tried to register all dependencies in the presentation layer but this only works for the dependencies implemented in this framework and not for these which are implemented in data framework, which the presentation framework has no access to.

So my question is how to make Resolver notice the dependencies registered in the main app from sub frameworks like presentation layer if running in preview mode?

@hmlongco
Copy link
Owner

I can't give you a good answer to that question because I don't have a good idea as to how the dependencies run.

That said, my first idea would be to register each frameworks objects in its own container, then wire together the containers in the main application.

Look at the following issue that shows using containers in SwiftUI previews and see if that helps.

#72

@theblaggy
Copy link
Author

Thank you for your fast answer!
I already saw the issue you're referencing and I think that this isn't really my problem.

But I saw an answer from you which sounds interesting but I don't understand it completely.
A "shared" package would definitely eliminate my issue but I'm not sure how you would register dependencies in a package which the other frameworks depend on. Since it has no access to the frameworks as this would result in a dependency cycle. Avoiding this with dependency inversion wouldn't work either for previews, at least that what I think.

A more simple project setup where the same error occurs is the following:
You have an application which depends on a framework.
The framework depends via dependency inversion using a protocol of a class in the application.
You can register the class of the application conforming to the protocol of the framework in the application.
No problems there. Everything works fine while you're running the application or previewing only views located in the application.

As soon as you're previewing one of the views in the framework Resolver won't find the registrations done in the application because only the framework gets executed.

While writing this I noticed that even without using Resolver and instead constructor dependency injection this would never work, because I can't access the class of the application from the framework. There I can only access the protocol but I can make the class only conform to the protocol from the application but this won't be possible if running a preview inside the framework.

Now the only way I can think of to make this work is to make the framework which I want the previews to work the highest in the dependency chain. In my setup the easiest way is to make the Presentation layer depend on the Data layer. This isn't a nice approach as this destroys kind of the complete project architecture. But if this is the only working way, I will go it.

Furthermore I then will use the approaches from issue 72 to mock my view models.

Maybe you could explain the approach with the "shared" package, otherwise the above will be my final solution.

@hmlongco
Copy link
Owner

So this may not be everything, but it may help. The following project repository demonstrates using Resolver across frameworks and in the main application.

https://github.com/hmlongco/ResolverFrameworksTest

Functionally, the main trick is to place Resolver into its own, dedicated framework that's imported into the main application and into each individual framework. This is needed because If you simply include Resolver in each framework, then each framework has its own "copy" of Resolver that's a different type that all of the other copies of Resolver.

I called that framework ResolverFramework in the example, but it could also be a common "SharedFramework" that contained a copy of Resolver plus other common code shared by the application.

Either way, once that's done the main application would be responsible for calling the registration blocks for each framework the application wants to use.

extension Resolver: ResolverRegistering {
    public static func registerAllServices() {
        registerAppServices()
        registerPresentationServices()
    }
}

The example also shows defining a delegate protocol in the framework, and then fulfilling that protocol with a class registered in the main application.

Hope this helps.

@theblaggy
Copy link
Author

Thank you for your help. Now I understand what you meant by using Resolver in a shared framework.

I'm not sure if you didn't upload the *.xcodeproj and the *.xcworkspace file or if you constructed your project in a way that all frameworks are sharing the same XcodeProject but in my setup every framework is in it own XcodeProject and they are combined in one XcodeWorkspace.

To see if your approaches are applicable to my setup I recreated your work using multiple XcodeProjects in one Workspace and than the Preview feature for the PresentationView.swift is crashing.
Checking the crashlog I can see that it's crashing while trying to resolve the injected object.

0   libswiftCore.dylib            	0x000000010641b90c _assertionFailure(_:_:file:line:flags:) + 1472
1   libswiftCore.dylib            	0x000000010641b90c _assertionFailure(_:_:file:line:flags:) + 1472
2   com.blaggy.ResolverFramework  	0x0000000127f9c0e8 static Resolver.resolve<A>(_:name:args:) + 1952 (Resolver.swift:230)
3   com.blaggy.ResolverFramework  	0x0000000127facb54 InjectedObject.init() + 228 (Resolver.swift:901)
4   com.blaggy.PresentationFramework	0x0000000127f712dc PresentationView.init() + 108 (PresentationView.swift:32)
5   PresentationView.2.preview-thunk.dylib	0x000000012800a6a0 static PresentationView_Previews.__preview__previews.getter + 44

That's the same crash I always get when trying to preview a view that's not part of the framework in which the Resolver extension conforming to ResolverRegistering is implemented.

@hmlongco
Copy link
Owner

hmlongco commented Jun 21, 2021

Um.... if you run the main app you're going to get something to to automatically call registerAllServices, which according to my demo will call each frameworks individual registerSomeServices function, right?

So if you run a preview for a view in a framework, there's no registerAllServices to call, which means that nothing is registered, which means that nothing is found, which means... crash. As such, what I suspect that you need to do is explicitly call registerPresentationServices() (or whatever) in your preview function. I would try something like...

struct PresentationView_Previews: PreviewProvider {
    static var previews: some View {
        Resolver.registerPreviewServices()
        return PresentationView()
    }
}

Where registerPreviewServices() is something like...

extension Resolver {
    public static func registerPresentationServices() {
        register { PresentationViewModel() }
            .scope(.shared)
    }
    static func registerPreviewServices() {
        registerPresentationServices()
        // registerDataFramework()
        // registerAnotherFramework()
    }
}

Note that the registerPreviewServices function is also calling the registration functions for any dependent frameworks.

See if that gets us anywhere. (Note I tried being cute with an earlier version of this.)

@theblaggy
Copy link
Author

So if you run a preview for a view in a framework, there's no registerAllServices to call, which means that nothing is registered, which means that nothing is found, which means... crash

That's what I meant in my first post when writing

But when trying to preview views of the Presentation layer, the preview crashes because no dependencies are registered.
I think this happens because in preview not the complete app runs but only the framework (and it's sub frameworks) of the view your previewing gets executed. Therefore the ResolverRegistering extension doesn't run which registers the dependencies.

Your approach is like what I tried but the problem there is that I won't be able to register the PresentationViewModelDelegate protocol of our example because the class (MyPresentationViewModelDelegate) that should conform to it and that I want to register accordingly is implemented in the ResolverFrameworksTest. Which means I can't register it from PresentationFramework. (At least I think so)

@hmlongco
Copy link
Owner

hmlongco commented Jun 21, 2021

So make it a protocol, mock it, and register that in registerPreviewServices as well?

extension Resolver {
    static func registerPreviewServices() {
        registerPresentationServices()
        register { MockPresentationViewModelDelegate() as PresentationViewModelDelegate }
    }
}

public protocol PresentationViewModelDelegate {
    func test()
}

internal class MockPresentationViewModelDelegate: PresentationViewModelDelegate {
    func test() {}
}

Bottom line is that I really don't think you have any other choice, since you're in a framework and the delegate is going to come from outside of the framework.

I mean, Resolver isn't magic. Even if you weren't using it and you were using, say, environment objects, you'd still have the same issue: How do I provide some instance of the data to the preview when I'm working in the standalone framework?

@theblaggy
Copy link
Author

Yes your totally right.
Now I'm feeling dumb. Thank you for pointing out the use of mocks.
I created a custom container to register them :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants