Created
August 16, 2022 17:41
-
-
Save IanKeen/93a354aa54e5939968f931f94bc37dc3 to your computer and use it in GitHub Desktop.
TCA Scoping Abstraction
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: - TCAView | |
public protocol TCAView: View where Body == WithViewStore<ScopedState, ScopedAction, Content> { | |
associatedtype ViewState | |
associatedtype ViewAction | |
associatedtype ScopedState | |
associatedtype ScopedAction | |
associatedtype Content | |
var store: Store<ViewState, ViewAction> { get } | |
func isDuplicate(_ a: ScopedState, _ b: ScopedState) -> Bool | |
func scopeState(_ state: ViewState) -> ScopedState | |
func scopeAction(_ action: ScopedAction) -> ViewAction | |
@ViewBuilder func storeView(_ viewStore: ViewStore<ScopedState, ScopedAction>) -> Content | |
} | |
extension TCAView where ScopedState: Equatable { | |
public func isDuplicate(_ a: ScopedState, _ b: ScopedState) -> Bool { a == b } | |
} | |
extension TCAView where ScopedState == ViewState { | |
public func scopeState(_ state: ViewState) -> ScopedState { state } | |
} | |
extension TCAView where ViewAction == ScopedAction { | |
public func scopeAction(_ action: ScopedAction) -> ViewAction { action } | |
} | |
extension TCAView { | |
public var body: Body { | |
WithViewStore(store.scope(state: scopeState, action: scopeAction), removeDuplicates: isDuplicate, content: storeView) | |
} | |
} | |
// MARK: - ViewStateProvider | |
@dynamicMemberLookup | |
public protocol ViewStateProvider { | |
associatedtype ViewState | |
var viewState: ViewState { get set } | |
} | |
extension ViewStateProvider { | |
public subscript<T>(dynamicMember keyPath: KeyPath<ViewState, T>) -> T { | |
viewState[keyPath: keyPath] | |
} | |
public subscript<T>(dynamicMember keyPath: WritableKeyPath<ViewState, T>) -> T { | |
get { viewState[keyPath: keyPath] } | |
set { viewState[keyPath: keyPath] = newValue } | |
} | |
} | |
extension TCAView where ViewState: ViewStateProvider, ScopedState == ViewState.ViewState { | |
public func scopeState(_ state: ViewState) -> ScopedState { state.viewState } | |
} | |
// MARK: - ViewActionProvider | |
public protocol ViewActionProvider { | |
associatedtype ViewAction | |
static func view(_: ViewAction) -> Self | |
} | |
extension TCAView where ViewAction: ViewActionProvider, ScopedAction == ViewAction.ViewAction { | |
public func scopeAction(_ action: ScopedAction) -> ViewAction { ViewAction.view(action) } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum TodoAction: Equatable, ViewActionProvider { | |
enum ViewAction: Equatable { | |
case list | |
case toggle(Todo) | |
case dismissError | |
} | |
enum ReducerAction: Equatable { | |
case listResult(Result<[Todo], TodoError>) | |
case toggleResult(Result<Todo, TodoError>) | |
} | |
case view(ViewAction) | |
case reducer(ReducerAction) | |
} | |
struct TodoState: Equatable, ViewStateProvider { | |
struct ViewState: Equatable { | |
var error: TodoError? | |
var todos: IdentifiedArrayOf<Todo> | |
} | |
var viewState: ViewState | |
var user: User | |
} | |
let todoReducer = Reducer<TodoState, TodoAction, Void> { state, action, _ in | |
switch action { | |
case .view(.list): | |
let newTodos: [Todo] = [.init(name: "Wash Car", complete: false), .init(name: "Goto Gym", complete: true)] | |
return .task { .reducer(.listResult(.success(newTodos))) } | |
case .view(.toggle(let todo)): | |
return .task { .reducer(.toggleResult(.success(.init(id: todo.id, name: todo.name, complete: !todo.complete)))) } | |
case .view(.dismissError): | |
state.error = nil | |
return .none | |
case .reducer(.listResult(.success(let todos))): | |
state.todos = .init(uniqueElements: todos) | |
return .none | |
case .reducer(.toggleResult(.success(let todo))): | |
state.todos[id: todo.id] = todo | |
return .none | |
case .reducer(.listResult(.failure(let error))), .reducer(.toggleResult(.failure(let error))): | |
state.error = error | |
return .none | |
} | |
} | |
struct TodoList: TCAView { | |
var store: Store<TodoState, TodoAction> | |
func storeView(_ viewStore: ViewStore<TodoState.ViewState, TodoAction.ViewAction>) -> some View { | |
List(viewStore.todos) { todo in | |
HStack { | |
Text(todo.name).frame(maxWidth: .infinity, alignment: .leading) | |
if todo.complete { Image(systemName: "checkmark.square") } | |
} | |
.swipeActions { | |
Button(todo.complete ? "Mark Incomplete" : "Mark Complete") { | |
viewStore.send(.toggle(todo)) | |
} | |
} | |
} | |
.alert(item: viewStore.binding(get: \.error, send: .dismissError)) { error in | |
Alert.init(title: Text("Oh No"), message: Text(error.reason), dismissButton: .cancel { viewStore.send(.dismissError) }) | |
} | |
.onAppear { viewStore.send(.list) } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment