-
-
Save cameroncooke/fea9126060ec6ad3661ff711e66926ce to your computer and use it in GitHub Desktop.
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
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