Skip to content

[Proposal] Utilizing primary associated types to inject view stores to views #11

Merged
Cordavi merged 2 commits intomainfrom
kpa/inject-viewstores
Feb 2, 2023
Merged

[Proposal] Utilizing primary associated types to inject view stores to views #11
Cordavi merged 2 commits intomainfrom
kpa/inject-viewstores

Conversation

@Pearapps
Copy link
Copy Markdown
Contributor

@Pearapps Pearapps commented Dec 22, 2022

What it Does

Shows example of a proposed new best practice of injecting view stores into views.

Dependent on the PR to add primary associated types to the ViewStore protocol.

This injection has several advantages:

  • Timing: We now have MockViewStore, which can be injected by SwiftUI previews and always immediately returns the data. We no longer rely on each individual view store to send an initial value with correct timing for previews and (theoretical) snapshot tests – we can now guarantee it. Control over timing in this manner is a prerequisite for using async/await & AsyncAlgorithms in view stores in place of Combine.
  • Mocks: We only have to mock each store's view state now, which contains a subset of the data we were mocking before. For example, we don't need the whole provider for SwiftUI previews, we only use the Photos (even though we are, for now, just using the provider to generate those photos).
  • Extensibility: Since we are injecting an object conforming to the ViewStore protocol, we can now conditionally return different view stores, compose them together, or do any number of combinations and pass in the result to a view. This means that if we wanted to add automatic analytics logging, for example, we could write a small AnalyticsViewStore (see below example) wrapper and inject a view store, passing that to a view.
AnalyticsViewStore example
final class AnalyticsViewStore<V, A: Loggable>: ViewStore {
    let otherViewStore: any ViewStore<V, A>

    init(otherViewStore: any ViewStore<V, A>) {
        self.otherViewStore = otherViewStore
    }

    var viewState: V {
        return otherViewStore.viewState
    }

    func send(_ action: A) {

        let log = action.log
        // log this to crashlytics or whatever ^
        
        otherViewStore.send(action)
    }
}

For something like this, you could transparently pass this view store instead of the PhotoListViewStore in this project!

let viewStore/*: PhotoListViewStoreType*/ = AnalyticsViewStore(otherViewStore: PhotoListViewStore(provider: /* ....*/))

How I Tested

  • Made sure the app built
  • Made sure the tests pass

Notes

Plan is to discuss this in the next engineering meeting!

init(provider: Provider, scheduler: MainQueueScheduler = .init(type: .default)) {
self._store = StateObject(wrappedValue: PhotoListViewStore(provider: provider, scheduler: scheduler))
/// - store: The `ViewStore` that drives
init(store: @autoclosure @escaping () -> Store) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@autoclosure to avoid any problems with having stores being incorrectly created multiple times. See the initializer on StateObject as well

@Cordavi Cordavi requested review from Cordavi and Twigz January 18, 2023 18:07
@Pearapps Pearapps changed the base branch from kpa/primary-associated-types to main January 18, 2023 18:15
@Pearapps Pearapps marked this pull request as ready for review January 18, 2023 18:17
}
}

extension PhotoListViewStoreType {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, why did you move these onto the type over the store?

Copy link
Copy Markdown
Contributor Author

@Pearapps Pearapps Feb 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have to, since its generic in the view you can't talk to the concrete store type directly - its also an unintended advantage because it is a bit more strict with what you can do and protects the ViewStore concept with extra Published properties and the like

@@ -13,7 +13,7 @@ import Networking

/// A provider meant to be usable by SwiftUI previews and unit tests to provide mocked, successful `Photo`s synchronously.
final class MockItemProvider: Provider {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, does this really even need to be a provider? Couldn't you just pass the photos from say Photo+SampleData extension etc?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used for the unit tests, still -- in the future we will probably move this into a test target or a test helper target, to your point, though.

@Cordavi Cordavi merged commit 720c299 into main Feb 2, 2023
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

Successfully merging this pull request may close these issues.

2 participants