diff --git a/Example/Example.swift b/Example/Example.swift index 43196d9..0b30078 100644 --- a/Example/Example.swift +++ b/Example/Example.swift @@ -34,7 +34,7 @@ struct Example: App { Text("Photos (Original)") } - PhotoList(provider: photoProvider) + PhotoList(store: PhotoListViewStore(provider: photoProvider)) .tabItem { Image(systemName: "photo") Text("Photos") diff --git a/Example/MockItemProvider.swift b/Example/MockItemProvider.swift index 254907a..4c806a9 100644 --- a/Example/MockItemProvider.swift +++ b/Example/MockItemProvider.swift @@ -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 { - private let photos: [Photo] + let photos: [Photo] /// Creates a new `MockItemProvider` with the specified `Photo`s. /// - Parameter photos: the `Photo`s that the provider will "retrieve" synchronously. diff --git a/Example/Photos/PhotoList.swift b/Example/Photos/PhotoList.swift index 880dffd..833a674 100644 --- a/Example/Photos/PhotoList.swift +++ b/Example/Photos/PhotoList.swift @@ -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: 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) { + 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)) } } diff --git a/Example/Photos/PhotoListViewStore.swift b/Example/Photos/PhotoListViewStore.swift index 490a3ae..1bb223b 100644 --- a/Example/Photos/PhotoListViewStore.swift +++ b/Example/Photos/PhotoListViewStore.swift @@ -11,6 +11,8 @@ import Combine import SwiftUI import CasePaths +typealias PhotoListViewStoreType = ViewStore + /// 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() private let searchTextPublisher = PassthroughSubject() - var showsPhotoCount: Binding { -// -// return Binding { -// 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 { - 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 { + var showsPhotoCount: Binding { +// +// return Binding { +// 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 { + makeBinding(viewStateKeyPath: \.searchText, actionCasePath: /Action.search) + } +} + private extension Provider { func providePhotos() -> AnyPublisher, Never> { provideItems(request: APIRequest.photos, decoder: JSONDecoder(), providerBehaviors: [], requestBehaviors: [], allowExpiredItems: true) diff --git a/Sources/ViewStore/MockViewStore.swift b/Sources/ViewStore/MockViewStore.swift new file mode 100644 index 0000000..9fda923 --- /dev/null +++ b/Sources/ViewStore/MockViewStore.swift @@ -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: ViewStore { + public var viewState: ViewState + + public init(viewState: ViewState) { + self.viewState = viewState + } + + public func send(_ action: Action) {} + } diff --git a/ViewStore.xcodeproj/project.pbxproj b/ViewStore.xcodeproj/project.pbxproj index 50a5167..53171da 100644 --- a/ViewStore.xcodeproj/project.pbxproj +++ b/ViewStore.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 0B77E916287F23C400BC3595 /* Array+Filtering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B77E915287F23C400BC3595 /* Array+Filtering.swift */; }; 0B92D48328577B7700FF1BDF /* PhotoListOriginal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B92D48228577B7700FF1BDF /* PhotoListOriginal.swift */; }; 3A31E613287F47D800955C37 /* ViewStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A31E612287F47D800955C37 /* ViewStoreTests.swift */; }; + 3AF7CE6E2954BC500045B466 /* MockViewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF7CE6D2954BC500045B466 /* MockViewStore.swift */; }; F2B5F5B827C7EC0C00FD7831 /* Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B5F5B727C7EC0C00FD7831 /* Example.swift */; }; F2B5F5BC27C7EC0E00FD7831 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F2B5F5BB27C7EC0E00FD7831 /* Assets.xcassets */; }; F2B5F5BF27C7EC0E00FD7831 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F2B5F5BE27C7EC0E00FD7831 /* Preview Assets.xcassets */; }; @@ -52,6 +53,7 @@ 0B92D48228577B7700FF1BDF /* PhotoListOriginal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoListOriginal.swift; sourceTree = ""; }; 3A31E610287F47D700955C37 /* ViewStoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ViewStoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3A31E612287F47D800955C37 /* ViewStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewStoreTests.swift; sourceTree = ""; }; + 3AF7CE6D2954BC500045B466 /* MockViewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockViewStore.swift; sourceTree = ""; }; F2B5F5B427C7EC0C00FD7831 /* ViewStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ViewStore.app; sourceTree = BUILT_PRODUCTS_DIR; }; F2B5F5B727C7EC0C00FD7831 /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = ""; }; F2B5F5BB27C7EC0E00FD7831 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -97,6 +99,7 @@ 0B3201F428B3BF6B00AF8884 /* ViewStore+BindingAdditions.swift */, 0B3201F528B3BF6B00AF8884 /* MainQueueScheduler.swift */, 0B3201F628B3BF6B00AF8884 /* ViewStore.swift */, + 3AF7CE6D2954BC500045B466 /* MockViewStore.swift */, ); path = ViewStore; sourceTree = ""; @@ -303,6 +306,7 @@ F2B5F5D527C7FB0600FD7831 /* Photo.swift in Sources */, 0B3201FA28B3BF6B00AF8884 /* ViewStore.swift in Sources */, 0B3201F728B3BF6B00AF8884 /* Publisher+CombineLatest.swift in Sources */, + 3AF7CE6E2954BC500045B466 /* MockViewStore.swift in Sources */, F2B5F5D627C7FB0600FD7831 /* PhotoListViewStore.swift in Sources */, 0B3201F828B3BF6B00AF8884 /* ViewStore+BindingAdditions.swift in Sources */, F2B5F5D427C7FB0600FD7831 /* PhotoList.swift in Sources */,