Skip to content

Instantly share code, notes, and snippets.

@cameroncooke
Last active March 4, 2022 09:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cameroncooke/fea9126060ec6ad3661ff711e66926ce to your computer and use it in GitHub Desktop.
Save cameroncooke/fea9126060ec6ad3661ff711e66926ce to your computer and use it in GitHub Desktop.
import ComposableArchitecture
import SwiftUI
// Main.swift -------------------------------------------------------------------------------------------------------------
@main
struct testApp: App {
var body: some Scene {
WindowGroup {
GroupsView(
store: Store(
initialState: GroupsViewState(items: .mock),
reducer: groupsViewReducer,
environment: AppEnvironment(
uuid: UUID.init
)
)
)
}
}
}
struct AppEnvironment {
var uuid: () -> UUID
}
// EditableItemView.swift -----------------------------------------------------------------------------------------------
protocol EditableItemConformable: Equatable, Identifiable {
var id: UUID { get }
var title: String { get set }
}
enum EditableItemAction: Equatable {
case textFieldChanged(String)
}
func makeItemReducer<T>() -> Reducer<T, EditableItemAction, AppEnvironment> where T: EditableItemConformable {
return Reducer { item, action, environment in
switch action {
case .textFieldChanged(let string):
item.title = string
return .none
}
}
}
struct EditableItemView<T>: View where T: EditableItemConformable {
let store: Store<T, EditableItemAction>
var body: some View {
WithViewStore(store) { viewStore in
TextField(
viewStore.title,
text: viewStore.binding(get: \.title, send: EditableItemAction.textFieldChanged)
)
}
}
}
// GroupsView.swift -----------------------------------------------------------------------
enum GroupItemAction: Equatable {
case details(GroupDetailsAction)
case edit(EditableItemAction)
}
let groupItemReducer = Reducer<GroupModel, GroupItemAction, AppEnvironment>.combine(
groupDetailsReducer
.pullback(state: \.self,
action: /GroupItemAction.details,
environment: { $0 }
),
groupReducer.pullback(
state: \.self,
action: /GroupItemAction.edit,
environment: { $0 }
)
)
enum GroupsViewAction: Equatable {
case addGroup
case delete(IndexSet)
case move(IndexSet, Int)
case editModeChanged(EditMode)
case item(id: GroupModel.ID, action: GroupItemAction)
}
struct GroupsViewState: Equatable {
var items: IdentifiedArrayOf<GroupModel> = []
var editMode: EditMode = .inactive
}
/// How can I use this reducer for a single view (EditableItemView) below?
let groupReducer: Reducer<GroupModel, EditableItemAction, AppEnvironment> = makeItemReducer()
let groupsViewReducer = Reducer<GroupsViewState, GroupsViewAction, AppEnvironment>.combine(
groupItemReducer.forEach(
state: \.items,
action: /GroupsViewAction.item(id:action:),
environment: { $0 }
),
Reducer { state, action, environment in
switch action {
case .addGroup:
let item = GroupModel(
id: environment.uuid(),
title: "Test",
items: []
)
state.items.append(item)
return .none
case let .editModeChanged(editMode):
state.editMode = editMode
return .none
case .item:
return .none
case let .delete(indexSet):
state.items.remove(atOffsets: indexSet)
return .none
case let .move(source, destination):
state.items.move(fromOffsets: source, toOffset: destination)
return .none
}
}
)
.debugActions(actionFormat: .prettyPrint)
struct GroupsView: View {
let store: Store<GroupsViewState, GroupsViewAction>
var body: some View {
WithViewStore(store) { viewStore in
NavigationView {
List {
ForEachStore(
self.store.scope(
state: \.items,
action: GroupsViewAction.item(id:action:)
)
) { itemStore in
NavigationLink {
GroupDetailsView(store: itemStore
.scope(state: { $0 }, action: GroupItemAction.details)
)
} label: {
EditableItemView<GroupModel>(
store: itemStore
.scope(state: { $0 }, action: GroupItemAction.edit)
)
}
}
.onDelete { viewStore.send(.delete($0)) }
.onMove { viewStore.send(.move($0, $1)) }
}
.navigationTitle("Items")
.toolbar {
HStack {
EditButton()
Button {
viewStore.send(.addGroup)
} label: {
Image(systemName: "plus")
}
}
}
}
.environment(
\.editMode,
viewStore.binding(get: \.editMode, send: GroupsViewAction.editModeChanged)
)
}
}
}
// Group details ------------------------------------------------------------------------------------
struct ChildModel: EditableItemConformable {
let id: UUID
var title: String
// other unrelated fields
var isPopular: Bool
}
struct GroupModel: EditableItemConformable {
let id: UUID
var title: String
var items: IdentifiedArrayOf<ChildModel> = []
}
enum GroupDetailsAction: Equatable {
case addItem
case delete(IndexSet)
case move(IndexSet, Int)
case item(id: ChildModel.ID, action: EditableItemAction)
}
let childReducer: Reducer<ChildModel, EditableItemAction, AppEnvironment> = makeItemReducer()
let groupDetailsReducer = Reducer<GroupModel, GroupDetailsAction, AppEnvironment>.combine(
childReducer.forEach(
state: \.items,
action: /GroupDetailsAction.item(id:action:),
environment: { $0 }
),
Reducer { state, action, environment in
switch action {
case .addItem:
let item = ChildModel(
id: environment.uuid(),
title: "Test",
isPopular: false
)
state.items.append(item)
return .none
case .item:
return .none
case let .delete(indexSet):
state.items.remove(atOffsets: indexSet)
return .none
case let .move(source, destination):
state.items.move(fromOffsets: source, toOffset: destination)
return .none
}
}
)
.debugActions(actionFormat: .prettyPrint)
struct GroupDetailsView: View {
let store: Store<GroupModel, GroupDetailsAction>
var body: some View {
WithViewStore(store) { viewStore in
List {
ForEachStore(
self.store.scope(
state: \.items,
action: GroupDetailsAction.item(id:action:)
)
) { groupDetails in
HStack {
EditableItemView<ChildModel>(store: groupDetails)
WithViewStore(groupDetails) { viewStore in
if viewStore.isPopular {
Text("Popular!")
}
}
}
}
.onDelete { viewStore.send(.delete($0)) }
.onMove { viewStore.send(.move($0, $1)) }
}
.toolbar {
HStack {
EditButton()
Button {
viewStore.send(.addItem)
} label: {
Image(systemName: "plus")
}
}
}
.navigationTitle(viewStore.title)
}
}
}
// Preview ------------------------------------------------------------------------------------
struct GroupsView_Previews: PreviewProvider {
static var previews: some View {
Group {
GroupsView(
store: Store(
initialState: GroupsViewState(items: .mock),
reducer: groupsViewReducer,
environment: AppEnvironment(
uuid: UUID.init
)
)
)
}
}
}
extension IdentifiedArray where ID == GroupModel.ID, Element == GroupModel {
static let mock: Self = [
GroupModel(
id: UUID(uuidString: "DEADBEEF-DEAD-BEEF-DEAD-BEEDDEADBEEF")!,
title: "These items are not editable",
items: .mock
),
GroupModel(
id: UUID(uuidString: "CAFEBEEF-CAFE-BEEF-CAFE-BEEFCAFEBEEF")!,
title: "They should be editable",
items: .mock
)
]
}
extension IdentifiedArray where ID == ChildModel.ID, Element == ChildModel {
static let mock: Self = [
ChildModel(
id: UUID(uuidString: "2A543D97-844A-4BAA-B648-86B6478D475A")!,
title: "Item 1",
isPopular: false
),
ChildModel(
id: UUID(uuidString: "9C4436EF-6D5A-4CE0-9321-08FA8C7DD0D7")!,
title: "Item 2",
isPopular: true
),
ChildModel(
id: UUID(uuidString: "FC604136-38E0-4306-BFF2-3CCBE8FB5E34")!,
title: "Item 3",
isPopular: false
),
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment