-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathSyncUpForm.swift
More file actions
156 lines (137 loc) · 3.88 KB
/
SyncUpForm.swift
File metadata and controls
156 lines (137 loc) · 3.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import ComposableArchitecture
import SwiftUI
import SwiftUINavigation
@Reducer
struct SyncUpForm {
@ObservableState
struct State: Equatable, Sendable {
var focus: Field? = .title
var syncUp: SyncUp
init(focus: Field? = .title, syncUp: SyncUp) {
self.focus = focus
self.syncUp = syncUp
if self.syncUp.attendees.isEmpty {
@Dependency(\.uuid) var uuid
self.syncUp.attendees.append(Attendee(id: Attendee.ID(uuid())))
}
}
enum Field: Hashable {
case attendee(Attendee.ID)
case title
}
}
enum Action: BindableAction, Equatable, Sendable {
case addAttendeeButtonTapped
case binding(BindingAction<State>)
case deleteAttendees(atOffsets: IndexSet)
}
@Dependency(\.uuid) var uuid
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .addAttendeeButtonTapped:
let attendee = Attendee(id: Attendee.ID(uuid()))
state.syncUp.attendees.append(attendee)
state.focus = .attendee(attendee.id)
return .none
case .binding:
return .none
case .deleteAttendees(atOffsets: let indices):
state.syncUp.attendees.remove(atOffsets: indices)
if state.syncUp.attendees.isEmpty {
state.syncUp.attendees.append(Attendee(id: Attendee.ID(uuid())))
}
guard let firstIndex = indices.first
else { return .none }
let index = min(firstIndex, state.syncUp.attendees.count - 1)
state.focus = .attendee(state.syncUp.attendees[index].id)
return .none
}
}
}
}
struct SyncUpFormView: View {
@Bindable var store: StoreOf<SyncUpForm>
@FocusState var focus: SyncUpForm.State.Field?
var body: some View {
SyncUpFormContents(store: store, focus: $focus)
.bind($store.focus, to: $focus)
}
}
private struct SyncUpFormPreviewView: View {
@Bindable var store: StoreOf<SyncUpForm>
@FocusState var focus: SyncUpForm.State.Field?
var body: some View {
SyncUpFormContents(store: store, focus: $focus)
}
}
private struct SyncUpFormContents: View {
@Bindable var store: StoreOf<SyncUpForm>
@FocusState.Binding var focus: SyncUpForm.State.Field?
var body: some View {
Form {
Section {
TextField("Title", text: $store.syncUp.title)
.focused($focus, equals: .title)
HStack {
Slider(value: $store.syncUp.duration.minutes, in: 5...30, step: 1) {
Text("Length")
}
Spacer()
Text(store.syncUp.duration.formatted(.units()))
}
ThemePicker(selection: $store.syncUp.theme)
} header: {
Text("Sync-up Info")
}
Section {
ForEach($store.syncUp.attendees) { $attendee in
TextField("Name", text: $attendee.name)
.focused($focus, equals: .attendee(attendee.id))
}
.onDelete { indices in
store.send(.deleteAttendees(atOffsets: indices))
}
Button("New attendee") {
store.send(.addAttendeeButtonTapped)
}
} header: {
Text("Attendees")
}
}
}
}
struct ThemePicker: View {
@Binding var selection: Theme
var body: some View {
Picker("Theme", selection: $selection) {
ForEach(Theme.allCases) { theme in
ZStack {
RoundedRectangle(cornerRadius: 4)
.fill(theme.mainColor)
Label(theme.name, systemImage: "paintpalette")
.padding(4)
}
.foregroundColor(theme.accentColor)
.fixedSize(horizontal: false, vertical: true)
.tag(theme)
}
}
}
}
extension Duration {
fileprivate var minutes: Double {
get { Double(components.seconds / 60) }
set { self = .seconds(newValue * 60) }
}
}
#Preview {
NavigationStack {
SyncUpFormPreviewView(
store: Store(initialState: SyncUpForm.State(syncUp: .mock)) {
SyncUpForm()
}
)
}
}