Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Created August 16, 2022 17:41
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save IanKeen/93a354aa54e5939968f931f94bc37dc3 to your computer and use it in GitHub Desktop.
Save IanKeen/93a354aa54e5939968f931f94bc37dc3 to your computer and use it in GitHub Desktop.
TCA Scoping Abstraction
// 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) }
}
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