Skip to content

Instantly share code, notes, and snippets.

@oliverfoggin
Last active June 1, 2024 16:18
Show Gist options
  • Save oliverfoggin/0bcf83016b6ad6eee8e2d961a82a6a0c to your computer and use it in GitHub Desktop.
Save oliverfoggin/0bcf83016b6ad6eee8e2d961a82a6a0c to your computer and use it in GitHub Desktop.
I've removed a lot of the external code from this as I didn't have access to those. But this compiles now...
import SwiftUI
import ComposableArchitecture
enum ViewState {
case initial
case loading
case success
case failure(any Error)
}
struct ItemLight: Equatable, Identifiable {
let id: String
var name: String
}
struct MyError: Error {
}
@Reducer
struct ItemListReducer {
@ObservableState
struct State {
var status: ViewState = .initial
var items: [ItemLight] = []
var hasNextPage = false
}
enum Action: ViewAction {
case view(View)
case delegate(Delegate)
case fetchResponse(Result<[ItemLight], any Error>)
@CasePathable
enum View { // It is common practice to move view actions to their own enum like this... (And then TCA helps in the view also).
// case fetchFeed
case scrolledToLastItem // name the view action after what happened to trigger it.
case onAppear
case refresh
case itemButtonTapped(ItemLight)
}
@CasePathable
enum Delegate {
case showItem(ItemLight)
}
}
var body: some ReducerOf<Self> {
Reduce<State, Action> { state, action in
switch action {
case .view(.itemButtonTapped(let item)):
return .send(.delegate(.showItem(item)))
case .view(.onAppear):
state.status = .loading
return fetchFeedEffect
// Don't send actions back into the reducder to perform shared logic. Extract it to a function possibly.
// return .send(.fetchFeed)
// renamed action to reflect the view action that triggered it.
// case .fetchFeed:
case .view(.scrolledToLastItem):
return fetchFeedEffect
case .fetchResponse(.success(let items)):
state.status = .success
state.items.append(contentsOf: items)
return .none
case .fetchResponse(.failure(let error)):
print(error.localizedDescription)
return .none
case .view(.refresh):
return fetchFeedEffect
case .delegate:
return .none // delegate actions aren't used here. So return none for any of them
}
}
}
var fetchFeedEffect: Effect<Action> {
.run { send in
await send(.fetchResponse(.success([])))
}
}
}
@ViewAction(for: ItemListReducer.self) // this means you can just do `send(.someAction)`
struct ItemListView: View {
@Environment(\.colorScheme) var colorScheme
let store: StoreOf<ItemListReducer>
init(store: StoreOf<ItemListReducer>) {
self.store = store
}
var body: some View {
switch store.status { // don't need to use `store.state.xxx` just access `store.xxx`.
case .initial:
Color.clear.onAppear {
send(.onAppear)
}
case .loading:
ProgressView()
case .failure(let error):
Text(error.localizedDescription)
.refreshable {
await send(.refresh).finish()
}
case .success:
if !store.items.isEmpty {
ScrollView(.vertical) {
VStack {
LazyVStack {
ForEach(store.items) { item in // this is just one item. Call it `item` not `items`.
Button(action: {
send(.itemButtonTapped(item))
}, label: {
// ItemPreview(items: items) // Again... this preview displays a single `item`. Not multiple `items`.
Text(item.name)
.frame(height: 280)
.frame(minHeight: 280)
})
.onAppear {
if store.hasNextPage != false {
if item == store.items.last { // again... singular `item`
send(.scrolledToLastItem)
}
}
}
}
}
.refreshable {
await send(.refresh).finish()
}
}
}
} else {
Text("No items available")
.refreshable {
await send(.refresh).finish()
}
}
}
}
}
import SwiftUI
import ComposableArchitecture
@Reducer
struct ItemReducer {
@ObservableState
struct State {
var item: ItemLight
}
}
@Reducer
struct ParentReducer {
@Reducer
enum Path {
case itemList(ItemListReducer)
case item(ItemReducer)
}
@ObservableState
struct State {
var path = StackState<Path.State>()
var itemList = ItemListReducer.State()
}
enum Action {
case path(StackAction<Path.State, Path.Action>)
case itemList(ItemListReducer.Action)
}
init() {}
var body: some ReducerOf<Self> {
Scope(state: \.itemList, action: \.itemList) {
ItemListReducer()
}
Reduce<State, Action> { state, action in
switch action {
// ItemList
case .itemList(.delegate(.showItem(let item))): // Only deal with the delegate actions here.
state.path.append(.item(ItemReducer.State(item: item)))
return .none
case .itemList:
return .none
// Nested ItemList
case .path(.element(id: _, action: .itemList(.delegate(.showItem(let item))))): // again, only deal with the delegate actions
state.path.append(.item(ItemReducer.State(item: item)))
return .none
// Nested Item
// case .path(.element(id: _, action: .item(.delegate(.showList))): // Again, this should be a delegate action.
// state.path.append(.itemList(ItemListReducer.State()))
// return .none
case .path:
return .none
}
}
.forEach(\.path, action: \.path)
}
}
struct ParentReducerView: View {
@Bindable var store: StoreOf<ParentReducer>
public init(store: StoreOf<ParentReducer>) {
self.store = store
}
var body: some View {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
ItemListView(store: store.scope(state: \.itemList, action: \.itemList))
} destination: { store in
switch store.case {
case .itemList(let store):
ItemListView(store: store)
case .item(let store):
ItemView(store: store)
}
}
}
}
struct ItemView: View {
let store: StoreOf<ItemReducer>
var body: some View {
Text(store.item.name)
}
}
@oliverfoggin
Copy link
Author

Some of the changes I made...

  1. Move view actions into their own enum and use the ViewAction protocol available in TCA.
  2. Rename the view actions to reflect what causes them to be triggered. Not what they should do.
  3. Remove the use of store.state.xyz in favour of store.xyz.
  4. Make sure to use singular names when dealing with a singular item (not items).
  5. Extract shared logic into an effect that can be called. Sending actions back into a reducer is an anti pattern. (Except for delegate actions and responses from a .run effect.
  6. Rename the delegate action to reflect what you want the delegate to actually do.
  7. Rework the parent to only listen to delegate actions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment