Skip to content

Commit 720c299

Browse files
authored
Merge pull request #11 from Lickability/kpa/inject-viewstores
[Proposal] Utilizing primary associated types to inject view stores to views
2 parents a8417c9 + 88e5d1b commit 720c299

6 files changed

Lines changed: 61 additions & 26 deletions

File tree

Example/Example.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ struct Example: App {
3434
Text("Photos (Original)")
3535
}
3636

37-
PhotoList(provider: photoProvider)
37+
PhotoList(store: PhotoListViewStore(provider: photoProvider))
3838
.tabItem {
3939
Image(systemName: "photo")
4040
Text("Photos")

Example/MockItemProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Networking
1313

1414
/// A provider meant to be usable by SwiftUI previews and unit tests to provide mocked, successful `Photo`s synchronously.
1515
final class MockItemProvider: Provider {
16-
private let photos: [Photo]
16+
let photos: [Photo]
1717

1818
/// Creates a new `MockItemProvider` with the specified `Photo`s.
1919
/// - Parameter photos: the `Photo`s that the provider will "retrieve" synchronously.

Example/Photos/PhotoList.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,15 @@ import SwiftUI
99
import Provider
1010

1111
/// Displays a list of photos retrieved from an API. Uses a `ViewStore` for coordination with the data source.
12-
struct PhotoList: View {
12+
struct PhotoList<Store: PhotoListViewStoreType>: View {
1313

14-
@StateObject private var store: PhotoListViewStore
14+
@StateObject private var store: Store
1515

1616
/// Creates a new `PhotoList`.
1717
/// - Parameters:
18-
/// - provider: The provider responsible for fetching photos.
19-
/// - 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.
20-
init(provider: Provider, scheduler: MainQueueScheduler = .init(type: .default)) {
21-
self._store = StateObject(wrappedValue: PhotoListViewStore(provider: provider, scheduler: scheduler))
18+
/// - store: The `ViewStore` that drives
19+
init(store: @autoclosure @escaping () -> Store) {
20+
self._store = StateObject(wrappedValue: store())
2221
}
2322

2423
// MARK: - View
@@ -69,6 +68,8 @@ struct PhotoList: View {
6968

7069
struct PhotoList_Previews: PreviewProvider {
7170
static var previews: some View {
72-
PhotoList(provider: MockItemProvider(photosCount: 3), scheduler: .init(type: .synchronous))
71+
let viewState = PhotoListViewStore.ViewState(status: .content(MockItemProvider(photosCount: 3).photos))
72+
73+
PhotoList(store: MockViewStore(viewState: viewState))
7374
}
7475
}

Example/Photos/PhotoListViewStore.swift

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import Combine
1111
import SwiftUI
1212
import CasePaths
1313

14+
typealias PhotoListViewStoreType = ViewStore<PhotoListViewStore.ViewState, PhotoListViewStore.Action>
15+
1416
/// Coordinates state for use in `PhotoListView`
1517
final class PhotoListViewStore: ViewStore {
1618

@@ -24,12 +26,19 @@ final class PhotoListViewStore: ViewStore {
2426
}
2527

2628
fileprivate static let defaultNavigationTitle = LocalizedStringKey("Photos")
27-
fileprivate static let initial = ViewState(status: .loading, showsPhotoCount: false, navigationTitle: defaultNavigationTitle, searchText: "")
29+
fileprivate static let initial = ViewState()
2830

2931
let status: Status
3032
let showsPhotoCount: Bool
3133
let navigationTitle: LocalizedStringKey
3234
fileprivate let searchText: String
35+
36+
init(status: PhotoListViewStore.ViewState.Status = .loading, showsPhotoCount: Bool = false, navigationTitle: LocalizedStringKey = ViewState.defaultNavigationTitle, searchText: String = "") {
37+
self.status = status
38+
self.showsPhotoCount = showsPhotoCount
39+
self.navigationTitle = navigationTitle
40+
self.searchText = searchText
41+
}
3342
}
3443

3544
enum Action {
@@ -45,22 +54,6 @@ final class PhotoListViewStore: ViewStore {
4554
private let showsPhotosCountPublisher = PassthroughSubject<Bool, Never>()
4655
private let searchTextPublisher = PassthroughSubject<String, Never>()
4756

48-
var showsPhotoCount: Binding<Bool> {
49-
//
50-
// return Binding<Bool> {
51-
// self.viewState.showsPhotoCount
52-
// } set: { newValue in
53-
// self.send(.toggleShowsPhotoCount(newValue))
54-
// }
55-
//
56-
// Note: This 👇 is just a shorthand version of this 👆
57-
makeBinding(viewStateKeyPath: \.showsPhotoCount, actionCasePath: /Action.toggleShowsPhotoCount)
58-
}
59-
60-
var searchText: Binding<String> {
61-
makeBinding(viewStateKeyPath: \.searchText, actionCasePath: /Action.search)
62-
}
63-
6457
/// Creates a new `PhotoListViewStore`
6558
/// - Parameters:
6659
/// - provider: The provider responsible for fetching photos.
@@ -98,6 +91,24 @@ final class PhotoListViewStore: ViewStore {
9891
}
9992
}
10093

94+
extension PhotoListViewStoreType {
95+
var showsPhotoCount: Binding<Bool> {
96+
//
97+
// return Binding<Bool> {
98+
// self.viewState.showsPhotoCount
99+
// } set: { newValue in
100+
// self.send(.toggleShowsPhotoCount(newValue))
101+
// }
102+
//
103+
// Note: This 👇 is just a shorthand version of this 👆
104+
makeBinding(viewStateKeyPath: \.showsPhotoCount, actionCasePath: /Action.toggleShowsPhotoCount)
105+
}
106+
107+
var searchText: Binding<String> {
108+
makeBinding(viewStateKeyPath: \.searchText, actionCasePath: /Action.search)
109+
}
110+
}
111+
101112
private extension Provider {
102113
func providePhotos() -> AnyPublisher<Result<[Photo], ProviderError>, Never> {
103114
provideItems(request: APIRequest.photos, decoder: JSONDecoder(), providerBehaviors: [], requestBehaviors: [], allowExpiredItems: true)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// MockViewStore.swift
3+
// ViewStore
4+
//
5+
// Created by Kenneth Ackerson on 12/22/22.
6+
//
7+
8+
import Foundation
9+
10+
/// A generic object conforming to `ViewStore` that simply returns the passed-in view state. Useful in SwiftUI previews.
11+
public final class MockViewStore<ViewState, Action>: ViewStore {
12+
public var viewState: ViewState
13+
14+
public init(viewState: ViewState) {
15+
self.viewState = viewState
16+
}
17+
18+
public func send(_ action: Action) {}
19+
}

ViewStore.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
0B77E916287F23C400BC3595 /* Array+Filtering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B77E915287F23C400BC3595 /* Array+Filtering.swift */; };
2020
0B92D48328577B7700FF1BDF /* PhotoListOriginal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B92D48228577B7700FF1BDF /* PhotoListOriginal.swift */; };
2121
3A31E613287F47D800955C37 /* ViewStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A31E612287F47D800955C37 /* ViewStoreTests.swift */; };
22+
3AF7CE6E2954BC500045B466 /* MockViewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF7CE6D2954BC500045B466 /* MockViewStore.swift */; };
2223
F2B5F5B827C7EC0C00FD7831 /* Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B5F5B727C7EC0C00FD7831 /* Example.swift */; };
2324
F2B5F5BC27C7EC0E00FD7831 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F2B5F5BB27C7EC0E00FD7831 /* Assets.xcassets */; };
2425
F2B5F5BF27C7EC0E00FD7831 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F2B5F5BE27C7EC0E00FD7831 /* Preview Assets.xcassets */; };
@@ -52,6 +53,7 @@
5253
0B92D48228577B7700FF1BDF /* PhotoListOriginal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoListOriginal.swift; sourceTree = "<group>"; };
5354
3A31E610287F47D700955C37 /* ViewStoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ViewStoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
5455
3A31E612287F47D800955C37 /* ViewStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewStoreTests.swift; sourceTree = "<group>"; };
56+
3AF7CE6D2954BC500045B466 /* MockViewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockViewStore.swift; sourceTree = "<group>"; };
5557
F2B5F5B427C7EC0C00FD7831 /* ViewStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ViewStore.app; sourceTree = BUILT_PRODUCTS_DIR; };
5658
F2B5F5B727C7EC0C00FD7831 /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = "<group>"; };
5759
F2B5F5BB27C7EC0E00FD7831 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -97,6 +99,7 @@
9799
0B3201F428B3BF6B00AF8884 /* ViewStore+BindingAdditions.swift */,
98100
0B3201F528B3BF6B00AF8884 /* MainQueueScheduler.swift */,
99101
0B3201F628B3BF6B00AF8884 /* ViewStore.swift */,
102+
3AF7CE6D2954BC500045B466 /* MockViewStore.swift */,
100103
);
101104
path = ViewStore;
102105
sourceTree = "<group>";
@@ -303,6 +306,7 @@
303306
F2B5F5D527C7FB0600FD7831 /* Photo.swift in Sources */,
304307
0B3201FA28B3BF6B00AF8884 /* ViewStore.swift in Sources */,
305308
0B3201F728B3BF6B00AF8884 /* Publisher+CombineLatest.swift in Sources */,
309+
3AF7CE6E2954BC500045B466 /* MockViewStore.swift in Sources */,
306310
F2B5F5D627C7FB0600FD7831 /* PhotoListViewStore.swift in Sources */,
307311
0B3201F828B3BF6B00AF8884 /* ViewStore+BindingAdditions.swift in Sources */,
308312
F2B5F5D427C7FB0600FD7831 /* PhotoList.swift in Sources */,

0 commit comments

Comments
 (0)