-
Notifications
You must be signed in to change notification settings - Fork 0
[Proposal] Utilizing primary associated types to inject view stores to views #11
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,16 +9,15 @@ import SwiftUI | |
| import Provider | ||
|
|
||
| /// Displays a list of photos retrieved from an API. Uses a `ViewStore` for coordination with the data source. | ||
| struct PhotoList: View { | ||
| struct PhotoList<Store: PhotoListViewStoreType>: View { | ||
|
|
||
| @StateObject private var store: PhotoListViewStore | ||
| @StateObject private var store: Store | ||
|
|
||
| /// Creates a new `PhotoList`. | ||
| /// - Parameters: | ||
| /// - provider: The provider responsible for fetching photos. | ||
| /// - scheduler: Determines how state updates are scheduled to be delivered in the view store. Defaults to `default`, which asynchronously schedules updates on the main queue. | ||
| 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) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| self._store = StateObject(wrappedValue: store()) | ||
| } | ||
|
|
||
| // MARK: - View | ||
|
|
@@ -69,6 +68,8 @@ struct PhotoList: View { | |
|
|
||
| struct PhotoList_Previews: PreviewProvider { | ||
| static var previews: some View { | ||
| PhotoList(provider: MockItemProvider(photosCount: 3), scheduler: .init(type: .synchronous)) | ||
| let viewState = PhotoListViewStore.ViewState(status: .content(MockItemProvider(photosCount: 3).photos)) | ||
|
|
||
| PhotoList(store: MockViewStore(viewState: viewState)) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ import Combine | |
| import SwiftUI | ||
| import CasePaths | ||
|
|
||
| typealias PhotoListViewStoreType = ViewStore<PhotoListViewStore.ViewState, PhotoListViewStore.Action> | ||
|
|
||
| /// Coordinates state for use in `PhotoListView` | ||
| final class PhotoListViewStore: ViewStore { | ||
|
|
||
|
|
@@ -24,12 +26,19 @@ final class PhotoListViewStore: ViewStore { | |
| } | ||
|
|
||
| fileprivate static let defaultNavigationTitle = LocalizedStringKey("Photos") | ||
| fileprivate static let initial = ViewState(status: .loading, showsPhotoCount: false, navigationTitle: defaultNavigationTitle, searchText: "") | ||
| fileprivate static let initial = ViewState() | ||
|
|
||
| let status: Status | ||
| let showsPhotoCount: Bool | ||
| let navigationTitle: LocalizedStringKey | ||
| fileprivate let searchText: String | ||
|
|
||
| init(status: PhotoListViewStore.ViewState.Status = .loading, showsPhotoCount: Bool = false, navigationTitle: LocalizedStringKey = ViewState.defaultNavigationTitle, searchText: String = "") { | ||
| self.status = status | ||
| self.showsPhotoCount = showsPhotoCount | ||
| self.navigationTitle = navigationTitle | ||
| self.searchText = searchText | ||
| } | ||
| } | ||
|
|
||
| enum Action { | ||
|
|
@@ -45,22 +54,6 @@ final class PhotoListViewStore: ViewStore { | |
| private let showsPhotosCountPublisher = PassthroughSubject<Bool, Never>() | ||
| private let searchTextPublisher = PassthroughSubject<String, Never>() | ||
|
|
||
| var showsPhotoCount: Binding<Bool> { | ||
| // | ||
| // return Binding<Bool> { | ||
| // self.viewState.showsPhotoCount | ||
| // } set: { newValue in | ||
| // self.send(.toggleShowsPhotoCount(newValue)) | ||
| // } | ||
| // | ||
| // Note: This 👇 is just a shorthand version of this 👆 | ||
| makeBinding(viewStateKeyPath: \.showsPhotoCount, actionCasePath: /Action.toggleShowsPhotoCount) | ||
| } | ||
|
|
||
| var searchText: Binding<String> { | ||
| makeBinding(viewStateKeyPath: \.searchText, actionCasePath: /Action.search) | ||
| } | ||
|
|
||
| /// Creates a new `PhotoListViewStore` | ||
| /// - Parameters: | ||
| /// - provider: The provider responsible for fetching photos. | ||
|
|
@@ -98,6 +91,24 @@ final class PhotoListViewStore: ViewStore { | |
| } | ||
| } | ||
|
|
||
| extension PhotoListViewStoreType { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| var showsPhotoCount: Binding<Bool> { | ||
| // | ||
| // return Binding<Bool> { | ||
| // self.viewState.showsPhotoCount | ||
| // } set: { newValue in | ||
| // self.send(.toggleShowsPhotoCount(newValue)) | ||
| // } | ||
| // | ||
| // Note: This 👇 is just a shorthand version of this 👆 | ||
| makeBinding(viewStateKeyPath: \.showsPhotoCount, actionCasePath: /Action.toggleShowsPhotoCount) | ||
| } | ||
|
|
||
| var searchText: Binding<String> { | ||
| makeBinding(viewStateKeyPath: \.searchText, actionCasePath: /Action.search) | ||
| } | ||
| } | ||
|
|
||
| private extension Provider { | ||
| func providePhotos() -> AnyPublisher<Result<[Photo], ProviderError>, Never> { | ||
| provideItems(request: APIRequest.photos, decoder: JSONDecoder(), providerBehaviors: [], requestBehaviors: [], allowExpiredItems: true) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // | ||
| // MockViewStore.swift | ||
| // ViewStore | ||
| // | ||
| // Created by Kenneth Ackerson on 12/22/22. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| /// A generic object conforming to `ViewStore` that simply returns the passed-in view state. Useful in SwiftUI previews. | ||
| public final class MockViewStore<ViewState, Action>: ViewStore { | ||
| public var viewState: ViewState | ||
|
|
||
| public init(viewState: ViewState) { | ||
| self.viewState = viewState | ||
| } | ||
|
|
||
| public func send(_ action: Action) {} | ||
| } |
There was a problem hiding this comment.
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+SampleDataextension etc?There was a problem hiding this comment.
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.