diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0001-previous.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0001-previous.swift index 7ff9088..fb5e5b1 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0001-previous.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0001-previous.swift @@ -7,19 +7,21 @@ import GitHubAPIClient import IdentifiedCollections import SwiftUI -public struct RepositoryList: Reducer { +@Reducer +public struct RepositoryList { + @ObservableState public struct State: Equatable { var repositoryRows: IdentifiedArrayOf = [] var isLoading: Bool = false - @BindingState var query: String = "" + var query: String = "" public init() {} } - public enum Action: Equatable, BindableAction { + public enum Action: BindableAction { case onAppear - case searchRepositoriesResponse(TaskResult<[Repository]>) - case repositoryRow(id: RepositoryRow.State.ID, action: RepositoryRow.Action) + case searchRepositoriesResponse(Result<[Repository]>) + case repositoryRows(IdentifiedActionOf) case queryChangeDebounced case binding(BindingAction) } @@ -55,9 +57,9 @@ public struct RepositoryList: Reducer { // TODO: Handling error return .none } - case .repositoryRow: + case .repositoryRows: return .none - case .binding(\.$query): + case .binding(\.query): return .run { send in await send(.queryChangeDebounced) } @@ -78,7 +80,7 @@ public struct RepositoryList: Reducer { return .none } } - .forEach(\.repositoryRows, action: /Action.repositoryRow(id:action:)) { + .forEach(\.repositoryRows, action: \.repositoryRows) { RepositoryRow() } } @@ -87,7 +89,7 @@ public struct RepositoryList: Reducer { .run { send in await send( .searchRepositoriesResponse( - TaskResult { + Result { try await gitHubAPIClient.searchRepositories(query) } ) diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0001.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0001.swift index f0535b4..f165d1f 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0001.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0001.swift @@ -8,20 +8,22 @@ import IdentifiedCollections import SwiftUI import SwiftUINavigationCore -public struct RepositoryList: Reducer { +@Reducer +public struct RepositoryList { + @ObservableState public struct State: Equatable { var repositoryRows: IdentifiedArrayOf = [] var isLoading: Bool = false - @BindingState var query: String = "" - @PresentationState var alert: AlertState? + var query: String = "" + @Presents var alert: AlertState? public init() {} } - public enum Action: Equatable, BindableAction { + public enum Action: BindableAction { case onAppear - case searchRepositoriesResponse(TaskResult<[Repository]>) - case repositoryRow(id: RepositoryRow.State.ID, action: RepositoryRow.Action) + case searchRepositoriesResponse(Result<[Repository]>) + case repositoryRows(IdentifiedActionOf) case queryChangeDebounced case binding(BindingAction) case alert(PresentationAction) @@ -60,9 +62,9 @@ public struct RepositoryList: Reducer { // TODO: Handling error return .none } - case .repositoryRow: + case .repositoryRows: return .none - case .binding(\.$query): + case .binding(\.query): return .run { send in await send(.queryChangeDebounced) } @@ -83,7 +85,7 @@ public struct RepositoryList: Reducer { return .none } } - .forEach(\.repositoryRows, action: /Action.repositoryRow(id:action:)) { + .forEach(\.repositoryRows, action: \.repositoryRows) { RepositoryRow() } } @@ -92,7 +94,7 @@ public struct RepositoryList: Reducer { .run { send in await send( .searchRepositoriesResponse( - TaskResult { + Result { try await gitHubAPIClient.searchRepositories(query) } ) diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0002.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0002.swift index 4275c7f..f207169 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0002.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0002.swift @@ -8,20 +8,22 @@ import IdentifiedCollections import SwiftUI import SwiftUINavigationCore -public struct RepositoryList: Reducer { +@Reducer +public struct RepositoryList { + @ObservableState public struct State: Equatable { var repositoryRows: IdentifiedArrayOf = [] var isLoading: Bool = false - @BindingState var query: String = "" - @PresentationState var alert: AlertState? + var query: String = "" + @Presents var alert: AlertState? public init() {} } - public enum Action: Equatable, BindableAction { + public enum Action: BindableAction { case onAppear - case searchRepositoriesResponse(TaskResult<[Repository]>) - case repositoryRow(id: RepositoryRow.State.ID, action: RepositoryRow.Action) + case searchRepositoriesResponse(Result<[Repository]>) + case repositoryRows(IdentifiedActionOf) case queryChangeDebounced case binding(BindingAction) case alert(PresentationAction) @@ -64,9 +66,9 @@ public struct RepositoryList: Reducer { } return .none } - case .repositoryRow: + case .repositoryRows: return .none - case .binding(\.$query): + case .binding(\.query): return .run { send in await send(.queryChangeDebounced) } @@ -89,7 +91,7 @@ public struct RepositoryList: Reducer { return .none } } - .forEach(\.repositoryRows, action: /Action.repositoryRow(id:action:)) { + .forEach(\.repositoryRows, action: \.repositoryRows) { RepositoryRow() } } @@ -98,7 +100,7 @@ public struct RepositoryList: Reducer { .run { send in await send( .searchRepositoriesResponse( - TaskResult { + Result { try await gitHubAPIClient.searchRepositories(query) } ) diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0003.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0003.swift index d1162fd..489a071 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0003.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0003.swift @@ -8,20 +8,22 @@ import IdentifiedCollections import SwiftUI import SwiftUINavigationCore -public struct RepositoryList: Reducer { +@Reducer +public struct RepositoryList { + @ObservableState public struct State: Equatable { var repositoryRows: IdentifiedArrayOf = [] var isLoading: Bool = false - @BindingState var query: String = "" - @PresentationState var alert: AlertState? + var query: String = "" + @Presents var alert: AlertState? public init() {} } - public enum Action: Equatable, BindableAction { + public enum Action: BindableAction { case onAppear - case searchRepositoriesResponse(TaskResult<[Repository]>) - case repositoryRow(id: RepositoryRow.State.ID, action: RepositoryRow.Action) + case searchRepositoriesResponse(Result<[Repository]>) + case repositoryRows(IdentifiedActionOf) case queryChangeDebounced case binding(BindingAction) case alert(PresentationAction) @@ -60,9 +62,9 @@ public struct RepositoryList: Reducer { state.alert = .networkError return .none } - case .repositoryRow: + case .repositoryRows: return .none - case .binding(\.$query): + case .binding(\.query): return .run { send in await send(.queryChangeDebounced) } @@ -85,7 +87,7 @@ public struct RepositoryList: Reducer { return .none } } - .forEach(\.repositoryRows, action: /Action.repositoryRow(id:action:)) { + .forEach(\.repositoryRows, action: \.repositoryRows) { RepositoryRow() } } @@ -94,7 +96,7 @@ public struct RepositoryList: Reducer { .run { send in await send( .searchRepositoriesResponse( - TaskResult { + Result { try await gitHubAPIClient.searchRepositories(query) } ) diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0004-previous.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0004-previous.swift index 2da9214..ebd3636 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0004-previous.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0004-previous.swift @@ -9,7 +9,7 @@ import SwiftUI import SwiftUINavigationCore public struct RepositoryListView: View { - let store: StoreOf + @Bindable var store: StoreOf public init(store: StoreOf) { self.store = store @@ -17,32 +17,30 @@ public struct RepositoryListView: View { public var body: some View { NavigationStack { - WithViewStore(store, observe: { $0 }) { viewStore in - Group { - if viewStore.isLoading { - ProgressView() - } else { - List { - ForEachStore( - store.scope( - state: \.repositoryRows, - action: { .repositoryRow(id: $0, action: $1) } - ), - content: RepositoryRowView.init(store:) - ) - } + Group { + if store.isLoading { + ProgressView() + } else { + List { + ForEach( + store.scope( + state: \.repositoryRows, + action: \.repositoryRows + ), + content: RepositoryRowView.init(store:) + ) } } - .onAppear { - viewStore.send(.onAppear) - } - .navigationTitle("Repositories") - .searchable( - text: viewStore.$query, - placement: .navigationBarDrawer, - prompt: "Input query" - ) } + .onAppear { + store.send(.onAppear) + } + .navigationTitle("Repositories") + .searchable( + text: $store.query, + placement: .navigationBarDrawer, + prompt: "Input query" + ) } } } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0004.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0004.swift index 6025e8e..e688aa8 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0004.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0004.swift @@ -9,7 +9,7 @@ import SwiftUI import SwiftUINavigationCore public struct RepositoryListView: View { - let store: StoreOf + @Bindable var store: StoreOf public init(store: StoreOf) { self.store = store @@ -17,38 +17,36 @@ public struct RepositoryListView: View { public var body: some View { NavigationStack { - WithViewStore(store, observe: { $0 }) { viewStore in - Group { - if viewStore.isLoading { - ProgressView() - } else { - List { - ForEachStore( - store.scope( - state: \.repositoryRows, - action: { .repositoryRow(id: $0, action: $1) } - ), - content: RepositoryRowView.init(store:) - ) - } + Group { + if store.isLoading { + ProgressView() + } else { + List { + ForEach( + store.scope( + state: \.repositoryRows, + action: \.repositoryRows + ), + content: RepositoryRowView.init(store:) + ) } } - .onAppear { - viewStore.send(.onAppear) - } - .navigationTitle("Repositories") - .searchable( - text: viewStore.$query, - placement: .navigationBarDrawer, - prompt: "Input query" - ) - .alert( - store: store.scope( - state: \.$alert, - action: { .alert($0) } - ) - ) } + .onAppear { + store.send(.onAppear) + } + .navigationTitle("Repositories") + .searchable( + text: $store.query, + placement: .navigationBarDrawer, + prompt: "Input query" + ) + .alert( + $store.scope( + state: \.alert, + action: \.alert + ) + ) } } } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0005.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0005.swift index 927fd60..107875a 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0005.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-code-0005.swift @@ -9,7 +9,7 @@ import SwiftUI import SwiftUINavigationCore public struct RepositoryListView: View { - let store: StoreOf + @Bindable var store: StoreOf public init(store: StoreOf) { self.store = store @@ -17,38 +17,36 @@ public struct RepositoryListView: View { public var body: some View { NavigationStack { - WithViewStore(store, observe: { $0 }) { viewStore in - Group { - if viewStore.isLoading { - ProgressView() - } else { - List { - ForEachStore( - store.scope( - state: \.repositoryRows, - action: { .repositoryRow(id: $0, action: $1) } - ), - content: RepositoryRowView.init(store:) - ) - } + Group { + if store.isLoading { + ProgressView() + } else { + List { + ForEach( + store.scope( + state: \.repositoryRows, + action: \.repositoryRows + ), + content: RepositoryRowView.init(store:) + ) } } - .onAppear { - viewStore.send(.onAppear) - } - .navigationTitle("Repositories") - .searchable( - text: viewStore.$query, - placement: .navigationBarDrawer, - prompt: "Input query" - ) - .alert( - store: store.scope( - state: \.$alert, - action: { .alert($0) } - ) - ) } + .onAppear { + store.send(.onAppear) + } + .navigationTitle("Repositories") + .searchable( + text: $store.query, + placement: .navigationBarDrawer, + prompt: "Input query" + ) + .alert( + $store.scope( + state: \.alert, + action: \.alert + ) + ) } } } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-image-0005.png b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-image-0006.png similarity index 100% rename from Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-image-0005.png rename to Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-01-image-0006.png diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0001.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0001.swift index 7bf552f..96b981d 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0001.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0001.swift @@ -8,20 +8,22 @@ import IdentifiedCollections import SwiftUI import SwiftUINavigationCore -public struct RepositoryList: Reducer { +@Reducer +public struct RepositoryList { + @ObservableState public struct State: Equatable { var repositoryRows: IdentifiedArrayOf = [] var isLoading: Bool = false - @BindingState var query: String = "" - @PresentationState var alert: AlertState? + var query: String = "" + @Presents var alert: AlertState? public init() {} } - public enum Action: Equatable, BindableAction { + public enum Action: BindableAction { case onAppear - case searchRepositoriesResponse(TaskResult<[Repository]>) - case repositoryRow(id: RepositoryRow.State.ID, action: RepositoryRow.Action) + case searchRepositoriesResponse(Result<[Repository]>) + case repositoryRows(IdentifiedActionOf) case queryChangeDebounced case binding(BindingAction) case alert(PresentationAction) @@ -60,9 +62,9 @@ public struct RepositoryList: Reducer { state.alert = .networkError return .none } - case .repositoryRow: + case .repositoryRows: return .none - case .binding(\.$query): + case .binding(\.query): return .run { send in await send(.queryChangeDebounced) } @@ -85,7 +87,7 @@ public struct RepositoryList: Reducer { return .none } } - .forEach(\.repositoryRows, action: /Action.repositoryRow(id:action:)) { + .forEach(\.repositoryRows, action: \.repositoryRows) { RepositoryRow() } } @@ -94,7 +96,7 @@ public struct RepositoryList: Reducer { .run { send in await send( .searchRepositoriesResponse( - TaskResult { + Result { try await gitHubAPIClient.searchRepositories(query) } ) @@ -112,7 +114,8 @@ extension AlertState where Action == RepositoryList.Action.Alert { } extension RepositoryList { - public struct Destination: Reducer { - + @Reducer + public enum Destination { + } } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0002.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0002.swift index bce5f94..ebb7080 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0002.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0002.swift @@ -8,20 +8,22 @@ import IdentifiedCollections import SwiftUI import SwiftUINavigationCore -public struct RepositoryList: Reducer { +@Reducer +public struct RepositoryList { + @ObservableState public struct State: Equatable { var repositoryRows: IdentifiedArrayOf = [] var isLoading: Bool = false - @BindingState var query: String = "" - @PresentationState var alert: AlertState? + var query: String = "" + @Presents var alert: AlertState? public init() {} } - public enum Action: Equatable, BindableAction { + public enum Action: BindableAction { case onAppear - case searchRepositoriesResponse(TaskResult<[Repository]>) - case repositoryRow(id: RepositoryRow.State.ID, action: RepositoryRow.Action) + case searchRepositoriesResponse(Result<[Repository]>) + case repositoryRows(IdentifiedActionOf) case queryChangeDebounced case binding(BindingAction) case alert(PresentationAction) @@ -60,9 +62,9 @@ public struct RepositoryList: Reducer { state.alert = .networkError return .none } - case .repositoryRow: + case .repositoryRows: return .none - case .binding(\.$query): + case .binding(\.query): return .run { send in await send(.queryChangeDebounced) } @@ -85,7 +87,7 @@ public struct RepositoryList: Reducer { return .none } } - .forEach(\.repositoryRows, action: /Action.repositoryRow(id:action:)) { + .forEach(\.repositoryRows, action: \.repositoryRows) { RepositoryRow() } } @@ -94,7 +96,7 @@ public struct RepositoryList: Reducer { .run { send in await send( .searchRepositoriesResponse( - TaskResult { + Result { try await gitHubAPIClient.searchRepositories(query) } ) @@ -112,9 +114,10 @@ extension AlertState where Action == RepositoryList.Action.Alert { } extension RepositoryList { - public struct Destination: Reducer { - public enum State: Equatable { - case alert(AlertState) - } + @Reducer + public enum Destination { + case alert(AlertState) + + public enum Alert: Equatable {} } } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0003.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0003.swift index 416f820..a350506 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0003.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0003.swift @@ -8,23 +8,25 @@ import IdentifiedCollections import SwiftUI import SwiftUINavigationCore -public struct RepositoryList: Reducer { +@Reducer +public struct RepositoryList { + @ObservableState public struct State: Equatable { var repositoryRows: IdentifiedArrayOf = [] var isLoading: Bool = false - @BindingState var query: String = "" - @PresentationState var alert: AlertState? + var query: String = "" + @Presents var destination: Destination.State? public init() {} } - public enum Action: Equatable, BindableAction { + public enum Action: BindableAction { case onAppear - case searchRepositoriesResponse(TaskResult<[Repository]>) - case repositoryRow(id: RepositoryRow.State.ID, action: RepositoryRow.Action) + case searchRepositoriesResponse(Result<[Repository]>) + case repositoryRows(IdentifiedActionOf) case queryChangeDebounced case binding(BindingAction) - case alert(PresentationAction) + case destination(PresentationAction) } public init() {} @@ -55,12 +57,12 @@ public struct RepositoryList: Reducer { ) return .none case .failure: - state.alert = .networkError + state.destination = .alert(.networkError) return .none } - case .repositoryRow: + case .repositoryRows: return .none - case .binding(\.$query): + case .binding(\.query): return .run { send in await send(.queryChangeDebounced) } @@ -79,11 +81,11 @@ public struct RepositoryList: Reducer { return searchRepositories(by: state.query) case .binding: return .none - case .alert: + case .destination: return .none } } - .forEach(\.repositoryRows, action: /Action.repositoryRow(id:action:)) { + .forEach(\.repositoryRows, action: \.repositoryRows) { RepositoryRow() } } @@ -92,7 +94,7 @@ public struct RepositoryList: Reducer { .run { send in await send( .searchRepositoriesResponse( - TaskResult { + Result { try await gitHubAPIClient.searchRepositories(query) } ) @@ -101,7 +103,7 @@ public struct RepositoryList: Reducer { } } -extension AlertState where Action == RepositoryList.Action.Alert { +extension AlertState where Action == RepositoryList.Destination.Alert { static let networkError = Self { TextState("Network Error") } message: { @@ -110,19 +112,10 @@ extension AlertState where Action == RepositoryList.Action.Alert { } extension RepositoryList { - public struct Destination: Reducer { - public enum State: Equatable { - case alert(AlertState) - } - - public enum Action: Equatable { - case alert(Alert) - - public enum Alert: Equatable {} - } + @Reducer + public enum Destination { + case alert(AlertState) - public var body: some ReducerOf { - EmptyReducer() - } + public enum Alert: Equatable {} } } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0004.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0004.swift index 75313eb..c62876e 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0004.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0004.swift @@ -8,121 +8,77 @@ import IdentifiedCollections import SwiftUI import SwiftUINavigationCore -public struct RepositoryList: Reducer { - public struct State: Equatable { - var repositoryRows: IdentifiedArrayOf = [] - var isLoading: Bool = false - @BindingState var query: String = "" - @PresentationState var destination: Destination.State? +public struct RepositoryListView: View { + @Bindable var store: StoreOf - public init() {} + public init(store: StoreOf) { + self.store = store } - public enum Action: Equatable, BindableAction { - case onAppear - case searchRepositoriesResponse(TaskResult<[Repository]>) - case repositoryRow(id: RepositoryRow.State.ID, action: RepositoryRow.Action) - case queryChangeDebounced - case binding(BindingAction) - case destination(PresentationAction) - } - - public init() {} - - private enum CancelID { - case response - } - - @Dependency(\.gitHubAPIClient) var gitHubAPIClient - @Dependency(\.mainQueue) var mainQueue - - public var body: some ReducerOf { - BindingReducer() - Reduce { state, action in - switch action { - case .onAppear: - state.isLoading = true - return searchRepositories(by: "composable") - case let .searchRepositoriesResponse(result): - state.isLoading = false - - switch result { - case let .success(response): - state.repositoryRows = .init( - uniqueElements: response.map { - .init(repository: $0) - } - ) - return .none - case .failure: - state.destination = .alert(.networkError) - return .none - } - case .repositoryRow: - return .none - case .binding(\.$query): - return .run { send in - await send(.queryChangeDebounced) - } - .debounce( - id: CancelID.response, - for: .seconds(0.3), - scheduler: mainQueue - ) - case .queryChangeDebounced: - guard !state.query.isEmpty else { - return .none + public var body: some View { + NavigationStack { + Group { + if store.isLoading { + ProgressView() + } else { + List { + ForEach( + store.scope( + state: \.repositoryRows, + action: \.repositoryRows + ), + content: RepositoryRowView.init(store:) + ) + } } - - state.isLoading = true - - return searchRepositories(by: state.query) - case .binding: - return .none - case .destination: - return .none } - } - .forEach(\.repositoryRows, action: /Action.repositoryRow(id:action:)) { - RepositoryRow() - } - } - - func searchRepositories(by query: String) -> Effect { - .run { send in - await send( - .searchRepositoriesResponse( - TaskResult { - try await gitHubAPIClient.searchRepositories(query) - } + .onAppear { + store.send(.onAppear) + } + .navigationTitle("Repositories") + .searchable( + text: $store.query, + placement: .navigationBarDrawer, + prompt: "Input query" + ) + .alert( + $store.scope( + state: \.destination?.alert, + action: \.destination.alert ) ) } } } -extension AlertState where Action == RepositoryList.Destination.Action.Alert { - static let networkError = Self { - TextState("Network Error") - } message: { - TextState("Failed to fetch data.") - } -} - -extension RepositoryList { - public struct Destination: Reducer { - public enum State: Equatable { - case alert(AlertState) - } - - public enum Action: Equatable { - case alert(Alert) - - public enum Alert: Equatable {} +#Preview("API Succeeded") { + RepositoryListView( + store: .init( + initialState: RepositoryList.State() + ) { + RepositoryList() + } withDependencies: { + $0.gitHubAPIClient.searchRepositories = { _ in + try await Task.sleep(for: .seconds(0.3)) + return (1...20).map { .mock(id: $0) } + } } + ) +} - public var body: some ReducerOf { - EmptyReducer() - } +#Preview("API Failed") { + enum PreviewError: Error { + case fetchFailed } + return RepositoryListView( + store: .init( + initialState: RepositoryList.State() + ) { + RepositoryList() + } withDependencies: { + $0.gitHubAPIClient.searchRepositories = { _ in + throw PreviewError.fetchFailed + } + } + ) } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0005.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0005.swift index 9a5a3e9..452d9af 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0005.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0005.swift @@ -5,84 +5,119 @@ import Entity import Foundation import GitHubAPIClient import IdentifiedCollections +import RepositoryDetailFeature import SwiftUI import SwiftUINavigationCore -public struct RepositoryListView: View { - let store: StoreOf +@Reducer +public struct RepositoryList { + @ObservableState + public struct State: Equatable { + var repositoryRows: IdentifiedArrayOf = [] + var isLoading: Bool = false + var query: String = "" + @Presents var destination: Destination.State? - public init(store: StoreOf) { - self.store = store + public init() {} } - public var body: some View { - NavigationStack { - WithViewStore(store, observe: { $0 }) { viewStore in - Group { - if viewStore.isLoading { - ProgressView() - } else { - List { - ForEachStore( - store.scope( - state: \.repositoryRows, - action: { .repositoryRow(id: $0, action: $1) } - ), - content: RepositoryRowView.init(store:) - ) + public enum Action: BindableAction { + case onAppear + case searchRepositoriesResponse(Result<[Repository]>) + case repositoryRows(IdentifiedActionOf) + case queryChangeDebounced + case binding(BindingAction) + case destination(PresentationAction) + } + + public init() {} + + private enum CancelID { + case response + } + + @Dependency(\.gitHubAPIClient) var gitHubAPIClient + @Dependency(\.mainQueue) var mainQueue + + public var body: some ReducerOf { + BindingReducer() + Reduce { state, action in + switch action { + case .onAppear: + state.isLoading = true + return searchRepositories(by: "composable") + case let .searchRepositoriesResponse(result): + state.isLoading = false + + switch result { + case let .success(response): + state.repositoryRows = .init( + uniqueElements: response.map { + .init(repository: $0) } - } + ) + return .none + case .failure: + state.destination = .alert(.networkError) + return .none } - .onAppear { - viewStore.send(.onAppear) + case .repositoryRows: + return .none + case .binding(\.query): + return .run { send in + await send(.queryChangeDebounced) } - .navigationTitle("Repositories") - .searchable( - text: viewStore.$query, - placement: .navigationBarDrawer, - prompt: "Input query" - ) - .alert( - store: store.scope( - state: \.$destination, - action: { .destination($0) } - ), - state: /RepositoryList.Destination.State.alert, - action: RepositoryList.Destination.Action.alert + .debounce( + id: CancelID.response, + for: .seconds(0.3), + scheduler: mainQueue ) + case .queryChangeDebounced: + guard !state.query.isEmpty else { + return .none + } + + state.isLoading = true + + return searchRepositories(by: state.query) + case .binding: + return .none + case .destination: + return .none } } + .forEach(\.repositoryRows, action: \.repositoryRows) { + RepositoryRow() + } } -} -#Preview("API Succeeded") { - RepositoryListView( - store: .init( - initialState: RepositoryList.State() - ) { - RepositoryList() - } withDependencies: { - $0.gitHubAPIClient.searchRepositories = { _ in - try await Task.sleep(for: .seconds(0.3)) - return (1...20).map { .mock(id: $0) } - } + func searchRepositories(by query: String) -> Effect { + .run { send in + await send( + .searchRepositoriesResponse( + Result { + try await gitHubAPIClient.searchRepositories(query) + } + ) + ) } - ) + } } -#Preview("API Failed") { - enum PreviewError: Error { - case fetchFailed +extension AlertState where Action == RepositoryList.Destination.Alert { + static let networkError = Self { + TextState("Network Error") + } message: { + TextState("Failed to fetch data.") + } +} + +extension RepositoryList { + @Reducer + public enum Destination { + case alert(AlertState) + case repositoryDetail(RepositoryDetail) + + public enum Alert: Equatable {} } - return RepositoryListView( - store: .init( - initialState: RepositoryList.State() - ) { - RepositoryList() - } withDependencies: { - $0.gitHubAPIClient.searchRepositories = { _ in - throw PreviewError.fetchFailed - } - } - ) } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0006.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0006.swift index bf69ab4..eeb19e6 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0006.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0006.swift @@ -9,20 +9,22 @@ import RepositoryDetailFeature import SwiftUI import SwiftUINavigationCore -public struct RepositoryList: Reducer { +@Reducer +public struct RepositoryList { + @ObservableState public struct State: Equatable { var repositoryRows: IdentifiedArrayOf = [] var isLoading: Bool = false - @BindingState var query: String = "" - @PresentationState var destination: Destination.State? + var query: String = "" + @Presents var destination: Destination.State? public init() {} } - public enum Action: Equatable, BindableAction { + public enum Action: BindableAction { case onAppear - case searchRepositoriesResponse(TaskResult<[Repository]>) - case repositoryRow(id: RepositoryRow.State.ID, action: RepositoryRow.Action) + case searchRepositoriesResponse(Result<[Repository]>) + case repositoryRows(IdentifiedActionOf) case queryChangeDebounced case binding(BindingAction) case destination(PresentationAction) @@ -59,9 +61,9 @@ public struct RepositoryList: Reducer { state.destination = .alert(.networkError) return .none } - case .repositoryRow: + case .repositoryRows: return .none - case .binding(\.$query): + case .binding(\.query): return .run { send in await send(.queryChangeDebounced) } @@ -84,9 +86,10 @@ public struct RepositoryList: Reducer { return .none } } - .forEach(\.repositoryRows, action: /Action.repositoryRow(id:action:)) { + .forEach(\.repositoryRows, action: \.repositoryRows) { RepositoryRow() } + .ifLet(\.$destination, action: \.destination) } func searchRepositories(by query: String) -> Effect { @@ -102,7 +105,7 @@ public struct RepositoryList: Reducer { } } -extension AlertState where Action == RepositoryList.Destination.Action.Alert { +extension AlertState where Action == RepositoryList.Destination.Alert { static let networkError = Self { TextState("Network Error") } message: { @@ -111,23 +114,11 @@ extension AlertState where Action == RepositoryList.Destination.Action.Alert { } extension RepositoryList { - public struct Destination: Reducer { - public enum State: Equatable { - case alert(AlertState) - case repositoryDetail(RepositoryDetail.State) - } - - public enum Action: Equatable { - case alert(Alert) - case repositoryDetail(RepositoryDetail.Action) - - public enum Alert: Equatable {} - } + @Reducer + public enum Destination { + case alert(AlertState) + case repositoryDetail(RepositoryDetail) - public var body: some ReducerOf { - Scope(state: /State.repositoryDetail, action: /Action.repositoryDetail) { - RepositoryDetail() - } - } + public enum Alert: Equatable {} } } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0008-previous.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0007-previous.swift similarity index 81% rename from Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0008-previous.swift rename to Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0007-previous.swift index cf2e2bc..d809f26 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0008-previous.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0007-previous.swift @@ -1,13 +1,15 @@ import ComposableArchitecture import Entity -public struct RepositoryRow: Reducer { +@Reducer +public struct RepositoryRow { + @ObservableState public struct State: Equatable, Identifiable { public var id: Int { repository.id } let repository: Repository } - public enum Action: Equatable { + public enum Action { case rowTapped } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0007.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0007.swift index 386a2b5..dbf1c30 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0007.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0007.swift @@ -1,135 +1,30 @@ -import CasePaths import ComposableArchitecture -import Dependencies import Entity -import Foundation -import GitHubAPIClient -import IdentifiedCollections -import RepositoryDetailFeature -import SwiftUI -import SwiftUINavigationCore -public struct RepositoryList: Reducer { - public struct State: Equatable { - var repositoryRows: IdentifiedArrayOf = [] - var isLoading: Bool = false - @BindingState var query: String = "" - @PresentationState var destination: Destination.State? - - public init() {} - } - - public enum Action: Equatable, BindableAction { - case onAppear - case searchRepositoriesResponse(TaskResult<[Repository]>) - case repositoryRow(id: RepositoryRow.State.ID, action: RepositoryRow.Action) - case queryChangeDebounced - case binding(BindingAction) - case destination(PresentationAction) +@Reducer +public struct RepositoryRow { + @ObservableState + public struct State: Equatable, Identifiable { + public var id: Int { repository.id } + let repository: Repository } - public init() {} - - private enum CancelID { - case response + public enum Action { + case rowTapped + case delegate(Delegate) + + public enum Delegate { + case rowTapped + } } - @Dependency(\.gitHubAPIClient) var gitHubAPIClient - @Dependency(\.mainQueue) var mainQueue - public var body: some ReducerOf { - BindingReducer() Reduce { state, action in switch action { - case .onAppear: - state.isLoading = true - return searchRepositories(by: "composable") - case let .searchRepositoriesResponse(result): - state.isLoading = false - - switch result { - case let .success(response): - state.repositoryRows = .init( - uniqueElements: response.map { - .init(repository: $0) - } - ) - return .none - case .failure: - state.destination = .alert(.networkError) - return .none - } - case .repositoryRow: + case .rowTapped: + return .send(.delegate(.rowTapped)) + case .delegate: return .none - case .binding(\.$query): - return .run { send in - await send(.queryChangeDebounced) - } - .debounce( - id: CancelID.response, - for: .seconds(0.3), - scheduler: mainQueue - ) - case .queryChangeDebounced: - guard !state.query.isEmpty else { - return .none - } - - state.isLoading = true - - return searchRepositories(by: state.query) - case .binding: - return .none - case .destination: - return .none - } - } - .forEach(\.repositoryRows, action: /Action.repositoryRow(id:action:)) { - RepositoryRow() - } - .ifLet(\.$destination, action: /Action.destination) { - Destination() - } - } - - func searchRepositories(by query: String) -> Effect { - .run { send in - await send( - .searchRepositoriesResponse( - TaskResult { - try await gitHubAPIClient.searchRepositories(query) - } - ) - ) - } - } -} - -extension AlertState where Action == RepositoryList.Destination.Action.Alert { - static let networkError = Self { - TextState("Network Error") - } message: { - TextState("Failed to fetch data.") - } -} - -extension RepositoryList { - public struct Destination: Reducer { - public enum State: Equatable { - case alert(AlertState) - case repositoryDetail(RepositoryDetail.State) - } - - public enum Action: Equatable { - case alert(Alert) - case repositoryDetail(RepositoryDetail.Action) - - public enum Alert: Equatable {} - } - - public var body: some ReducerOf { - Scope(state: /State.repositoryDetail, action: /Action.repositoryDetail) { - RepositoryDetail() } } } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0008.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0008.swift index 1f4cff8..3d95c02 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0008.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0008.swift @@ -1,29 +1,132 @@ +import CasePaths import ComposableArchitecture +import Dependencies import Entity +import Foundation +import GitHubAPIClient +import IdentifiedCollections +import RepositoryDetailFeature +import SwiftUI +import SwiftUINavigationCore -public struct RepositoryRow: Reducer { - public struct State: Equatable, Identifiable { - public var id: Int { repository.id } - let repository: Repository +@Reducer +public struct RepositoryList { + @ObservableState + public struct State: Equatable { + var repositoryRows: IdentifiedArrayOf = [] + var isLoading: Bool = false + var query: String = "" + @Presents var destination: Destination.State? + + public init() {} } - public enum Action: Equatable { - case rowTapped - case delegate(Delegate) - - public enum Delegate: Equatable { - case rowTapped - } + public enum Action: BindableAction { + case onAppear + case searchRepositoriesResponse(Result<[Repository]>) + case repositoryRows(IdentifiedActionOf) + case queryChangeDebounced + case binding(BindingAction) + case destination(PresentationAction) + } + + public init() {} + + private enum CancelID { + case response } + @Dependency(\.gitHubAPIClient) var gitHubAPIClient + @Dependency(\.mainQueue) var mainQueue + public var body: some ReducerOf { + BindingReducer() Reduce { state, action in switch action { - case .rowTapped: - return .send(.delegate(.rowTapped)) - case .delegate: + case .onAppear: + state.isLoading = true + return searchRepositories(by: "composable") + case let .searchRepositoriesResponse(result): + state.isLoading = false + + switch result { + case let .success(response): + state.repositoryRows = .init( + uniqueElements: response.map { + .init(repository: $0) + } + ) + return .none + case .failure: + state.destination = .alert(.networkError) + return .none + } + case let .repositoryRows(.element(id, .delegate(.rowTapped))): + guard let repository = state.repositoryRows[id: id]?.repository + else { return .none } + + state.destination = .repositoryDetail( + .init(repository: repository) + ) + return .none + case .repositoryRows: + return .none + case .binding(\.query): + return .run { send in + await send(.queryChangeDebounced) + } + .debounce( + id: CancelID.response, + for: .seconds(0.3), + scheduler: mainQueue + ) + case .queryChangeDebounced: + guard !state.query.isEmpty else { + return .none + } + + state.isLoading = true + + return searchRepositories(by: state.query) + case .binding: + return .none + case .destination: return .none } } + .forEach(\.repositoryRows, action: \.repositoryRows) { + RepositoryRow() + } + .ifLet(\.$destination, action: \.destination) + } + + func searchRepositories(by query: String) -> Effect { + .run { send in + await send( + .searchRepositoriesResponse( + Result { + try await gitHubAPIClient.searchRepositories(query) + } + ) + ) + } + } +} + +extension AlertState where Action == RepositoryList.Destination.Alert { + static let networkError = Self { + TextState("Network Error") + } message: { + TextState("Failed to fetch data.") + } +} + +extension RepositoryList { + @Reducer + public enum Destination { + case alert(AlertState) + case repositoryDetail(RepositoryDetail) + + public enum Alert {} } } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0010-previous.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0009-previous.swift similarity index 53% rename from Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0010-previous.swift rename to Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0009-previous.swift index cd10a42..c1d7950 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0010-previous.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0009-previous.swift @@ -10,7 +10,7 @@ import SwiftUI import SwiftUINavigationCore public struct RepositoryListView: View { - let store: StoreOf + @Bindable var store: StoreOf public init(store: StoreOf) { self.store = store @@ -18,40 +18,36 @@ public struct RepositoryListView: View { public var body: some View { NavigationStack { - WithViewStore(store, observe: { $0 }) { viewStore in - Group { - if viewStore.isLoading { - ProgressView() - } else { - List { - ForEachStore( - store.scope( - state: \.repositoryRows, - action: { .repositoryRow(id: $0, action: $1) } - ), - content: RepositoryRowView.init(store:) - ) - } + Group { + if store.isLoading { + ProgressView() + } else { + List { + ForEach( + store.scope( + state: \.repositoryRows, + action: \.repositoryRows + ), + content: RepositoryRowView.init(store:) + ) } } - .onAppear { - viewStore.send(.onAppear) - } - .navigationTitle("Repositories") - .searchable( - text: viewStore.$query, - placement: .navigationBarDrawer, - prompt: "Input query" - ) - .alert( - store: store.scope( - state: \.$destination, - action: { .destination($0) } - ), - state: /RepositoryList.Destination.State.alert, - action: RepositoryList.Destination.Action.alert - ) } + .onAppear { + store.send(.onAppear) + } + .navigationTitle("Repositories") + .searchable( + text: $store.query, + placement: .navigationBarDrawer, + prompt: "Input query" + ) + .alert( + $store.scope( + state: \.destination?.alert, + action: \.destination.alert + ) + ) } } } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0009.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0009.swift index ba000e3..c0f71fc 100644 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0009.swift +++ b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0009.swift @@ -9,136 +9,84 @@ import RepositoryDetailFeature import SwiftUI import SwiftUINavigationCore -public struct RepositoryList: Reducer { - public struct State: Equatable { - var repositoryRows: IdentifiedArrayOf = [] - var isLoading: Bool = false - @BindingState var query: String = "" - @PresentationState var destination: Destination.State? +public struct RepositoryListView: View { + @Bindable var store: StoreOf - public init() {} + public init(store: StoreOf) { + self.store = store } - public enum Action: Equatable, BindableAction { - case onAppear - case searchRepositoriesResponse(TaskResult<[Repository]>) - case repositoryRow(id: RepositoryRow.State.ID, action: RepositoryRow.Action) - case queryChangeDebounced - case binding(BindingAction) - case destination(PresentationAction) - } - - public init() {} - - private enum CancelID { - case response - } - - @Dependency(\.gitHubAPIClient) var gitHubAPIClient - @Dependency(\.mainQueue) var mainQueue - - public var body: some ReducerOf { - BindingReducer() - Reduce { state, action in - switch action { - case .onAppear: - state.isLoading = true - return searchRepositories(by: "composable") - case let .searchRepositoriesResponse(result): - state.isLoading = false - - switch result { - case let .success(response): - state.repositoryRows = .init( - uniqueElements: response.map { - .init(repository: $0) - } - ) - return .none - case .failure: - state.destination = .alert(.networkError) - return .none - } - case let .repositoryRow(id, .delegate(.rowTapped)): - guard let repository = state.repositoryRows[id: id]?.repository - else { return .none } - - state.destination = .repositoryDetail( - .init(repository: repository) - ) - return .none - case .repositoryRow: - return .none - case .binding(\.$query): - return .run { send in - await send(.queryChangeDebounced) - } - .debounce( - id: CancelID.response, - for: .seconds(0.3), - scheduler: mainQueue - ) - case .queryChangeDebounced: - guard !state.query.isEmpty else { - return .none + public var body: some View { + NavigationStack { + Group { + if store.isLoading { + ProgressView() + } else { + List { + ForEach( + store.scope( + state: \.repositoryRows, + action: \.repositoryRows + ), + content: RepositoryRowView.init(store:) + ) + } } - - state.isLoading = true - - return searchRepositories(by: state.query) - case .binding: - return .none - case .destination: - return .none } - } - .forEach(\.repositoryRows, action: /Action.repositoryRow(id:action:)) { - RepositoryRow() - } - .ifLet(\.$destination, action: /Action.destination) { - Destination() - } - } - - func searchRepositories(by query: String) -> Effect { - .run { send in - await send( - .searchRepositoriesResponse( - TaskResult { - try await gitHubAPIClient.searchRepositories(query) - } + .onAppear { + store.send(.onAppear) + } + .navigationTitle("Repositories") + .searchable( + text: $store.query, + placement: .navigationBarDrawer, + prompt: "Input query" + ) + .alert( + $store.scope( + state: \.destination?.alert, + action: \.destination.alert ) ) + .navigationDestination( + item: $store.scope( + state: \.destination?.repositoryDetail, + action: \.destination.repositoryDetail + ), + destination: RepositoryDetailView.init(store:) + ) } } } -extension AlertState where Action == RepositoryList.Destination.Action.Alert { - static let networkError = Self { - TextState("Network Error") - } message: { - TextState("Failed to fetch data.") - } -} - -extension RepositoryList { - public struct Destination: Reducer { - public enum State: Equatable { - case alert(AlertState) - case repositoryDetail(RepositoryDetail.State) - } - - public enum Action: Equatable { - case alert(Alert) - case repositoryDetail(RepositoryDetail.Action) - - public enum Alert: Equatable {} +#Preview("API Succeeded") { + RepositoryListView( + store: .init( + initialState: RepositoryList.State() + ) { + RepositoryList() + } withDependencies: { + $0.gitHubAPIClient.searchRepositories = { _ in + try await Task.sleep(for: .seconds(0.3)) + return (1...20).map { .mock(id: $0) } + } } + ) +} - public var body: some ReducerOf { - Scope(state: /State.repositoryDetail, action: /Action.repositoryDetail) { - RepositoryDetail() +#Preview("API Failed") { + enum PreviewError: Error { + case fetchFailed + } + return RepositoryListView( + store: .init( + initialState: RepositoryList.State() + ) { + RepositoryList() + } withDependencies: { + $0.gitHubAPIClient.searchRepositories = { _ in + throw PreviewError.fetchFailed } } - } + ) } diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0010.swift b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0010.swift deleted file mode 100644 index 133c736..0000000 --- a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-code-0010.swift +++ /dev/null @@ -1,98 +0,0 @@ -import CasePaths -import ComposableArchitecture -import Dependencies -import Entity -import Foundation -import GitHubAPIClient -import IdentifiedCollections -import RepositoryDetailFeature -import SwiftUI -import SwiftUINavigationCore - -public struct RepositoryListView: View { - let store: StoreOf - - public init(store: StoreOf) { - self.store = store - } - - public var body: some View { - NavigationStack { - WithViewStore(store, observe: { $0 }) { viewStore in - Group { - if viewStore.isLoading { - ProgressView() - } else { - List { - ForEachStore( - store.scope( - state: \.repositoryRows, - action: { .repositoryRow(id: $0, action: $1) } - ), - content: RepositoryRowView.init(store:) - ) - } - } - } - .onAppear { - viewStore.send(.onAppear) - } - .navigationTitle("Repositories") - .searchable( - text: viewStore.$query, - placement: .navigationBarDrawer, - prompt: "Input query" - ) - .alert( - store: store.scope( - state: \.$destination, - action: { .destination($0) } - ), - state: /RepositoryList.Destination.State.alert, - action: RepositoryList.Destination.Action.alert - ) - .navigationDestination( - store: store.scope( - state: \.$destination, - action: { .destination($0) } - ), - state: /RepositoryList.Destination.State.repositoryDetail, - action: RepositoryList.Destination.Action.repositoryDetail, - destination: RepositoryDetailView.init(store:) - ) - } - } - } -} - -#Preview("API Succeeded") { - RepositoryListView( - store: .init( - initialState: RepositoryList.State() - ) { - RepositoryList() - } withDependencies: { - $0.gitHubAPIClient.searchRepositories = { _ in - try await Task.sleep(for: .seconds(0.3)) - return (1...20).map { .mock(id: $0) } - } - } - ) -} - -#Preview("API Failed") { - enum PreviewError: Error { - case fetchFailed - } - return RepositoryListView( - store: .init( - initialState: RepositoryList.State() - ) { - RepositoryList() - } withDependencies: { - $0.gitHubAPIClient.searchRepositories = { _ in - throw PreviewError.fetchFailed - } - } - ) -} diff --git a/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-image-0011.gif b/Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-image-0010.gif similarity index 100% rename from Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-image-0011.gif rename to Sources/Docs/Documentation.docc/Resources/06-TreeBasedNavigation/06-02-image-0010.gif diff --git a/Sources/Docs/Documentation.docc/Tutorials/06-TreeBasedNavigation.tutorial b/Sources/Docs/Documentation.docc/Tutorials/06-TreeBasedNavigation.tutorial index 29d545a..45001c5 100644 --- a/Sources/Docs/Documentation.docc/Tutorials/06-TreeBasedNavigation.tutorial +++ b/Sources/Docs/Documentation.docc/Tutorials/06-TreeBasedNavigation.tutorial @@ -15,7 +15,7 @@ @Steps { @Step { TCA で Alert を表現するために、まずは必要な `State` と `Action` を定義する必要があります。 - それぞれ `@PresentationState` Property Wrapper と `PresentationAction` という型を利用して定義していきましょう。 + それぞれ `@Presents` macro と `PresentationAction` という型を利用して定義していきましょう。 `State` は swiftui-navigation というライブラリに用意されている `AlertState` も利用して定義します。 `Action` は `Alert` 用の blank な enum を定義しつつ、新しい `alert` case を追加します。 @@ -25,7 +25,7 @@ } @Step { 次に `body` で Alert を表示するための実装を行います。 - `AlertState` は `TextState` という独自の API によって表現できます。 + `AlertState` は `TextState` という API によって表現できます。 ここでは、API 通信が失敗したことを簡単にユーザーに伝えられれば良いため、適当な title と message を持ったシンプルな Alert として実装しましょう。 また、先ほど `alert` Action を新たに追加したため、特に何も処理しない case として追加だけしておきます。 @@ -40,10 +40,9 @@ } @Step { 次に Alert を View で表示するために View の実装を追加します。 - 先ほど `@PresentationState` と `PresentationAction` を利用して `State`, `Action` を定義しましたが、これを View で利用するための `alert` function が用意されています。 - `ForEachStore` を利用した時と同様に、`store.scope` を利用して `alert` function に必要な引数を提供します。 - ただ、`alert(store:)` に提供する引数は `PresentationState` 型を必要とします。 - `@PresentationState` は `wrappedValue` として `AlertState` を取得できるようになっていますが、`projectValue` で `PresentationState>` を取得できるようにもなっているため、`\.$alert` とすることで、`PresentationState` 型を `alert` function に提供できます。 + TCA には `Store` を受け付ける `alert` function が用意されており、これを利用することで Alert を表示できます。 + `Store` を受け付ける点以外は、Pure SwiftUI の `alert` function とほとんど使い勝手は変わりません。 + ここでは、`ForEach` を利用した時と同様に、`store.scope` を利用して `alert` function に必要な引数を提供します。 @Code(name: "RepositoryListView.swift", file: 06-01-code-0004.swift, previousFile: 06-01-code-0004-previous.swift) } @@ -68,52 +67,45 @@ @Steps { @Step { リポジトリ詳細画面に遷移できるようにする前に、先ほどの Alert のための実装を拡張性高いものに変更していきます。 - 現在のままだと Alert, Confirmation Dialog, Sheet など、Navigation の対象が増える度に `State`, `Action` それぞれに `@PresentationState`, `PresentationAction` を利用したものを増やしていかなければいけません。 + 現在のままだと Alert, Confirmation Dialog, Sheet など、Navigation の対象が増える度に `State`, `Action` それぞれに `@Presents`, `PresentationAction` を利用したものを増やしていかなければいけません。 Navigation を管理するための新しい Reducer を定義し、この問題を解決します。 - まずは、そのための `Destination` Reducer を作成します。 + まずは、そのための `Destination` Reducer を作成します。 + この Reducer は struct ではなく enum で表現します。Sheet, Alert などの Navigation は複数の種類が同時に発生することはあり得ないため、enum で表現することが適切です。 @Code(name: "RepositoryListView.swift", file: 06-02-code-0001.swift, previousFile: 06-01-code-0003.swift) } @Step { - 次に `Destination` Reducer に `State` を追加します。 - 今まで `State` は struct で表現してきましたが、enum を使って表現することもできます。 - そして、複数種類の Navigation は基本的に同時に発生することはあり得ないため、Navigation の状態は enum で表現することが適切です。 - それでは、`alert` case を持った enum の `State` を定義しましょう。 - この方法では、case の associated value で各 Navigation が必要とする状態を保持することができるため、よりシンプルに安全な形で Navigation の状態を表現できるというメリットもあります。 + 次に `Destination` Reducer に case を追加します。 + 今まで Reducer には `State`, `Action`, `body` を定義してきましたが、Navigation 用の Reducer ではそれらを定義する必要はなく、Reducer enum に直接 case を定義していけば良い形になっています。 + それでは、先ほど実装した Alert を移植するために `alert` case を追加しましょう。 @Code(name: "RepositoryListView.swift", file: 06-02-code-0002.swift) - } - @Step { - `Destination` Reducer の実装に必要な `Action`, `body` をまとめて実装してしまいましょう。 - `alert` Action に必要な `Alert` enum は、先ほど `RepositoryList.Action.Alert` として定義していたものをそのまま持ってきます。 - また、今のところ Alert のために Reducer で特定の処理を実行したいわけではないため、`body` は何も実行しない `EmptyReducer` API を使って実装しておきます。 - - @Code(name: "RepositoryListView.swift", file: 06-02-code-0003.swift) + + > Navigation を enum で表現すると、case の associated value で各 Navigation が必要とする状態を保持することができるため、よりシンプルに安全な形で Navigation の状態を表現できるというメリットもあります。 } @Step { 元々 `RepositoryList` Reducer に直接定義していた Alert 関係の処理を `Destination` Reducer にまとめたため、`RepositoryList` Reducer の諸々を修正します。 - @Code(name: "RepositoryListView.swift", file: 06-02-code-0004.swift) + @Code(name: "RepositoryListView.swift", file: 06-02-code-0003.swift) } @Step { - View も少し修正する必要があるため、`destination` を利用するように書き換えていきましょう。 - 元々の `alert(store:)` をそのまま利用しても良いのですが、`Destination` に新しい Navigation のための case が追加された際に対応できなくなってしまうため、case を区別できる `alert(store:state:action:)` を利用するように修正します。 + View も少し修正していきます。 + `RepositoryList` Reducer の `State`, `Action` に `destination` を追加したため、`alert` function の引数でも `destination` を利用するように書き換えます。 - @Code(name: "RepositoryListView.swift", file: 06-02-code-0005.swift, previousFile: 06-01-code-0005.swift) + @Code(name: "RepositoryListView.swift", file: 06-02-code-0004.swift, previousFile: 06-01-code-0005.swift) } @Step { ここまでで、リポジトリ詳細画面への遷移を表現するための下準備が整いました。 ここからは、リポジトリ詳細画面への Push 遷移を実装していきましょう。 - リポジトリ詳細画面は、`RepositoryDetailView.swift` というファイル名でプロジェクト内に用意されています。基本的な実装は一通り済ませてあるので、`RepositoryDetailFeature` を import しつつ、`RepositoryDetail.State`, `RepositoryDetail.Action`, `RepositoryDetail` の実装を `Destination` Reducer で利用します。 - `RepositoryDetail` Reducer を `Destination` Reducer に組み込むためには、`Scope` という API を利用します。使い方は `RepositoryRow` Reducer を組み込む際に利用した `forEach` と大きくは変わりませんが、`Destination.State` は enum で表現されているため、`state` 引数にも CasePath を利用していることに注意しましょう。 + リポジトリ詳細画面は、`RepositoryDetailView.swift` というファイル名でプロジェクト内に用意されています。基本的な実装は一通り済ませてあるので、`RepositoryDetailFeature` を import しつつ、`RepositoryDetail`の実装を `Destination` Reducer で利用するようにします。 - @Code(name: "RepositoryListView.swift", file: 06-02-code-0006.swift, previousFile: 06-02-code-0004.swift) + @Code(name: "RepositoryListView.swift", file: 06-02-code-0005.swift, previousFile: 06-02-code-0003.swift) } @Step { - Alert の場合は、Reducer で何らかの処理を実行する必要がなかったため考慮しませんでしたが、`Destination` Reducer で `RepositoryDetail` Reducer の処理を実行する必要が出てきたため、`Reducer.ifLet(_:action:destination)` を利用して `Destination` Reducer と `RepositoryList` Reducer を接続する必要があります。 + Alert の場合は、Reducer で何らかの処理を実行する必要がなかったため考慮しませんでしたが、`Destination` Reducer で `RepositoryDetail` Reducer の処理を実行する必要が出てきたため、`Reducer.ifLet(_:action:)` を利用して `Destination` Reducer と `RepositoryList` Reducer を接続する必要があります。 そのためのコードを `RepositoryList` Reducer に追加しましょう。 - @Code(name: "RepositoryListView.swift", file: 06-02-code-0007.swift) + @Code(name: "RepositoryListView.swift", file: 06-02-code-0006.swift) > `destination` が Optional な状態として保持されており、その Optional な状態と接続するために Swift の Optional binding のような名前の `ifLet` という API を利用する必要があります。 > Collection に対しては `forEach`, Optional に対しては `ifLet` で接続すると覚えておくと理解しやすいかもしれません。 @@ -123,7 +115,7 @@ `RepositoryList` Reducer は `RepositoryRow` Reducer を管理しているため、`RepositoryRow` のどんな Action もハンドリングできますが、Parent Reducer に処理を委譲することを目的とした `Delegate` という namespace を切って新しい Action を作ります。 そして、その Action を `rowTapped` のタイミングで送るようにします。 - @Code(name: "RepositoryRowView.swift", file: 06-02-code-0008.swift, previousFile: 06-02-code-0008-previous.swift) + @Code(name: "RepositoryRowView.swift", file: 06-02-code-0007.swift, previousFile: 06-02-code-0007-previous.swift) > TCA は Reducer を容易に分割し結合することを強みとしていますが、分割された親子間の Reducer で何も考えずに Action をやり取りすると、コードはすぐに複雑になってしまいます。 > この問題を解決するための手段として、例えば今回のように `Delegate` という namespace を切るというものがあります。 @@ -133,17 +125,17 @@ 送信した Delegate Action を Parent Reducer である `RepositoryList` Reducer 側で処理しましょう。 `RepositoryRow` がタップされたタイミングでリポジトリ詳細画面に遷移するようにしたいため、`State` に保持してある `destination` に `repositoryDetail` を格納します。 - @Code(name: "RepositoryListView.swift", file: 06-02-code-0009.swift, previousFile: 06-02-code-0007.swift) + @Code(name: "RepositoryListView.swift", file: 06-02-code-0008.swift, previousFile: 06-02-code-0006.swift) } @Step { - 最後に、`state.destination` の状態が変わったタイミングで Push 遷移が実行されるようにするために、iOS 16 から利用できる `navigationDestination` の TCA 版 API である `navigationDestination(store:state:action:destination:)` を利用して実装を追加します。 + 最後に、`state.destination` の状態が変わったタイミングで Push 遷移が実行されるようにするために、iOS 16 から利用できる `navigationDestination` を利用して実装を追加します。 - @Code(name: "RepositoryListView.swift", file: 06-02-code-0010.swift, previousFile: 06-02-code-0010-previous.swift) + @Code(name: "RepositoryListView.swift", file: 06-02-code-0009.swift, previousFile: 06-02-code-0009-previous.swift) } @Step { この状態でアプリを実行してみれば、リポジトリ一覧画面の Row から各リポジトリ詳細画面に遷移できるようになっているはずです。確認してみましょう。 - @Image(source: "06-02-image-0011.gif", alt: "リポジトリ一覧画面からリポジトリ詳細画面に遷移している図") + @Image(source: "06-02-image-0010.gif", alt: "リポジトリ一覧画面からリポジトリ詳細画面に遷移している図") } } }