Skip to content

Commit 01e1d1e

Browse files
committed
Split voice memos and todos into separate files (#191)
* Put domain for voice memo row in a different file. * Put domain for todo row in a different file.
1 parent e5bec10 commit 01e1d1e

File tree

7 files changed

+240
-221
lines changed

7 files changed

+240
-221
lines changed

Examples/Todos/Todos.xcodeproj/project.pbxproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
CA93D060249BF4D000A6F65D /* Todo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA93D05F249BF4D000A6F65D /* Todo.swift */; };
1011
DC1394322469E57000EE1157 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = DC1394312469E57000EE1157 /* ComposableArchitecture */; };
1112
DCBCB77624290F6C00DE1F59 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBCB77524290F6C00DE1F59 /* SceneDelegate.swift */; };
1213
DCBCB77A24290F6D00DE1F59 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCBCB77924290F6D00DE1F59 /* Assets.xcassets */; };
@@ -48,6 +49,7 @@
4849
/* End PBXCopyFilesBuildPhase section */
4950

5051
/* Begin PBXFileReference section */
52+
CA93D05F249BF4D000A6F65D /* Todo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Todo.swift; sourceTree = "<group>"; };
5153
DC85B441242D0286009784B0 /* swift-composable-architecture */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "swift-composable-architecture"; path = ../..; sourceTree = "<group>"; };
5254
DCBCB77024290F6C00DE1F59 /* Todos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Todos.app; sourceTree = BUILT_PRODUCTS_DIR; };
5355
DCBCB77524290F6C00DE1F59 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
@@ -100,10 +102,11 @@
100102
DCBCB77224290F6C00DE1F59 /* Todos */ = {
101103
isa = PBXGroup;
102104
children = (
105+
DCBCB78124290F6D00DE1F59 /* Info.plist */,
103106
DCBCB77524290F6C00DE1F59 /* SceneDelegate.swift */,
107+
CA93D05F249BF4D000A6F65D /* Todo.swift */,
104108
DCBCB79A24290FEB00DE1F59 /* Todos.swift */,
105109
DCBCB77924290F6D00DE1F59 /* Assets.xcassets */,
106-
DCBCB78124290F6D00DE1F59 /* Info.plist */,
107110
);
108111
path = Todos;
109112
sourceTree = "<group>";
@@ -232,6 +235,7 @@
232235
buildActionMask = 2147483647;
233236
files = (
234237
DCBCB77624290F6C00DE1F59 /* SceneDelegate.swift in Sources */,
238+
CA93D060249BF4D000A6F65D /* Todo.swift in Sources */,
235239
DCBCB79B24290FEB00DE1F59 /* Todos.swift in Sources */,
236240
);
237241
runOnlyForDeploymentPostprocessing = 0;

Examples/Todos/Todos/Todo.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import ComposableArchitecture
2+
import Foundation
3+
import SwiftUI
4+
5+
struct Todo: Equatable, Identifiable {
6+
var description = ""
7+
let id: UUID
8+
var isComplete = false
9+
}
10+
11+
enum TodoAction: Equatable {
12+
case checkBoxToggled
13+
case textFieldChanged(String)
14+
}
15+
16+
struct TodoEnvironment {}
17+
18+
let todoReducer = Reducer<Todo, TodoAction, TodoEnvironment> { todo, action, _ in
19+
switch action {
20+
case .checkBoxToggled:
21+
todo.isComplete.toggle()
22+
return .none
23+
24+
case let .textFieldChanged(description):
25+
todo.description = description
26+
return .none
27+
}
28+
}
29+
30+
struct TodoView: View {
31+
let store: Store<Todo, TodoAction>
32+
33+
var body: some View {
34+
WithViewStore(self.store) { viewStore in
35+
HStack {
36+
Button(action: { viewStore.send(.checkBoxToggled) }) {
37+
Image(systemName: viewStore.isComplete ? "checkmark.square" : "square")
38+
}
39+
.buttonStyle(PlainButtonStyle())
40+
41+
TextField(
42+
"Untitled Todo",
43+
text: viewStore.binding(get: { $0.description }, send: TodoAction.textFieldChanged)
44+
)
45+
}
46+
.foregroundColor(viewStore.isComplete ? .gray : nil)
47+
}
48+
}
49+
}

Examples/Todos/Todos/Todos.swift

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,6 @@ import ComposableArchitecture
22
import ReactiveSwift
33
import SwiftUI
44

5-
struct Todo: Equatable, Identifiable {
6-
var description = ""
7-
let id: UUID
8-
var isComplete = false
9-
}
10-
11-
enum TodoAction: Equatable {
12-
case checkBoxToggled
13-
case textFieldChanged(String)
14-
}
15-
16-
struct TodoEnvironment {}
17-
18-
let todoReducer = Reducer<Todo, TodoAction, TodoEnvironment> { todo, action, _ in
19-
switch action {
20-
case .checkBoxToggled:
21-
todo.isComplete.toggle()
22-
return .none
23-
24-
case let .textFieldChanged(description):
25-
todo.description = description
26-
return .none
27-
}
28-
}
29-
30-
struct TodoView: View {
31-
let store: Store<Todo, TodoAction>
32-
33-
var body: some View {
34-
WithViewStore(self.store) { viewStore in
35-
HStack {
36-
Button(action: { viewStore.send(.checkBoxToggled) }) {
37-
Image(systemName: viewStore.isComplete ? "checkmark.square" : "square")
38-
}
39-
.buttonStyle(PlainButtonStyle())
40-
41-
TextField(
42-
"Untitled Todo",
43-
text: viewStore.binding(get: { $0.description }, send: TodoAction.textFieldChanged)
44-
)
45-
}
46-
.foregroundColor(viewStore.isComplete ? .gray : nil)
47-
}
48-
}
49-
}
50-
515
enum Filter: LocalizedStringKey, CaseIterable, Hashable {
526
case all = "All"
537
case active = "Active"

Examples/VoiceMemos/VoiceMemos.xcodeproj/project.pbxproj

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
CA93D05C249BF42500A6F65D /* VoiceMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA93D05B249BF42500A6F65D /* VoiceMemo.swift */; };
11+
CA93D05E249BF46E00A6F65D /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA93D05D249BF46E00A6F65D /* Helpers.swift */; };
1012
DC1394342469E59600EE1157 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = DC1394332469E59600EE1157 /* ComposableArchitecture */; };
1113
DC5BDCB024589177009C65A3 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5BDCAF24589177009C65A3 /* SceneDelegate.swift */; };
1214
DC5BDCB224589177009C65A3 /* VoiceMemos.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5BDCB124589177009C65A3 /* VoiceMemos.swift */; };
@@ -54,6 +56,8 @@
5456
/* End PBXCopyFilesBuildPhase section */
5557

5658
/* Begin PBXFileReference section */
59+
CA93D05B249BF42500A6F65D /* VoiceMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMemo.swift; sourceTree = "<group>"; };
60+
CA93D05D249BF46E00A6F65D /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
5761
DC5BDCAA24589177009C65A3 /* VoiceMemos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoiceMemos.app; sourceTree = BUILT_PRODUCTS_DIR; };
5862
DC5BDCAF24589177009C65A3 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
5963
DC5BDCB124589177009C65A3 /* VoiceMemos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMemos.swift; sourceTree = "<group>"; };
@@ -112,12 +116,14 @@
112116
DC5BDCAC24589177009C65A3 /* VoiceMemos */ = {
113117
isa = PBXGroup;
114118
children = (
119+
DC5BDCBB24589178009C65A3 /* Info.plist */,
120+
CA93D05D249BF46E00A6F65D /* Helpers.swift */,
115121
DC5BDCAF24589177009C65A3 /* SceneDelegate.swift */,
122+
CA93D05B249BF42500A6F65D /* VoiceMemo.swift */,
116123
DC5BDCB124589177009C65A3 /* VoiceMemos.swift */,
124+
DC5BDCB324589178009C65A3 /* Assets.xcassets */,
117125
DC5BDF3B245893DB009C65A3 /* AudioPlayerClient */,
118126
DC5BDF3424589389009C65A3 /* AudioRecorderClient */,
119-
DC5BDCB324589178009C65A3 /* Assets.xcassets */,
120-
DC5BDCBB24589178009C65A3 /* Info.plist */,
121127
);
122128
path = VoiceMemos;
123129
sourceTree = "<group>";
@@ -267,9 +273,11 @@
267273
DC5BDF362458939C009C65A3 /* AudioRecorderClient.swift in Sources */,
268274
DC94F0122458E0AA00082DE9 /* MockAudioRecorderClient.swift in Sources */,
269275
DC5BDF3D245893E6009C65A3 /* AudioPlayerClient.swift in Sources */,
276+
CA93D05E249BF46E00A6F65D /* Helpers.swift in Sources */,
270277
DC5BDF3A245893C1009C65A3 /* LiveAudioRecorderClient.swift in Sources */,
271278
DC5BDF3F24589406009C65A3 /* LiveAudioPlayerClient.swift in Sources */,
272279
DC94F0102458E09900082DE9 /* MockAudioPlayerClient.swift in Sources */,
280+
CA93D05C249BF42500A6F65D /* VoiceMemo.swift in Sources */,
273281
DC5BDCB024589177009C65A3 /* SceneDelegate.swift in Sources */,
274282
DC5BDCB224589177009C65A3 /* VoiceMemos.swift in Sources */,
275283
);
@@ -293,7 +301,6 @@
293301
};
294302
DCFC51462466F42900A0B8CF /* PBXTargetDependency */ = {
295303
isa = PBXTargetDependency;
296-
productRef = DCFC51452466F42900A0B8CF /* ComposableArchitecture */;
297304
};
298305
/* End PBXTargetDependency section */
299306

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Foundation
2+
3+
struct AlertData: Identifiable {
4+
var message: String
5+
var id: String { self.message }
6+
}
7+
8+
let dateFormatter: DateFormatter = {
9+
let formatter = DateFormatter()
10+
formatter.dateStyle = .short
11+
formatter.timeStyle = .medium
12+
return formatter
13+
}()
14+
15+
let dateComponentsFormatter: DateComponentsFormatter = {
16+
let formatter = DateComponentsFormatter()
17+
formatter.allowedUnits = [.minute, .second]
18+
formatter.zeroFormattingBehavior = .pad
19+
return formatter
20+
}()
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import ComposableArchitecture
2+
import Foundation
3+
import SwiftUI
4+
5+
struct VoiceMemo: Equatable {
6+
var date: Date
7+
var duration: TimeInterval
8+
var mode = Mode.notPlaying
9+
var title = ""
10+
var url: URL
11+
12+
enum Mode: Equatable {
13+
case notPlaying
14+
case playing(progress: Double)
15+
16+
var isPlaying: Bool {
17+
if case .playing = self { return true }
18+
return false
19+
}
20+
21+
var progress: Double? {
22+
if case let .playing(progress) = self { return progress }
23+
return nil
24+
}
25+
}
26+
}
27+
28+
enum VoiceMemoAction: Equatable {
29+
case audioPlayerClient(Result<AudioPlayerClient.Action, AudioPlayerClient.Failure>)
30+
case playButtonTapped
31+
case delete
32+
case timerUpdated(TimeInterval)
33+
case titleTextFieldChanged(String)
34+
}
35+
36+
struct VoiceMemoEnvironment {
37+
var audioPlayerClient: AudioPlayerClient
38+
var mainQueue: AnySchedulerOf<DispatchQueue>
39+
}
40+
41+
let voiceMemoReducer = Reducer<VoiceMemo, VoiceMemoAction, VoiceMemoEnvironment> {
42+
memo, action, environment in
43+
struct PlayerId: Hashable {}
44+
struct TimerId: Hashable {}
45+
46+
switch action {
47+
case .audioPlayerClient(.success(.didFinishPlaying)), .audioPlayerClient(.failure):
48+
memo.mode = .notPlaying
49+
return .cancel(id: TimerId())
50+
51+
case .delete:
52+
return .merge(
53+
.cancel(id: PlayerId()),
54+
.cancel(id: TimerId())
55+
)
56+
57+
case .playButtonTapped:
58+
switch memo.mode {
59+
case .notPlaying:
60+
memo.mode = .playing(progress: 0)
61+
let start = environment.mainQueue.now
62+
return .merge(
63+
environment.audioPlayerClient
64+
.play(PlayerId(), memo.url)
65+
.catchToEffect()
66+
.map(VoiceMemoAction.audioPlayerClient)
67+
.cancellable(id: PlayerId()),
68+
69+
Effect.timer(id: TimerId(), every: 0.5, on: environment.mainQueue)
70+
.map {
71+
.timerUpdated(
72+
TimeInterval($0.dispatchTime.uptimeNanoseconds - start.dispatchTime.uptimeNanoseconds)
73+
/ TimeInterval(NSEC_PER_SEC)
74+
)
75+
}
76+
)
77+
78+
case .playing:
79+
memo.mode = .notPlaying
80+
return .concatenate(
81+
.cancel(id: TimerId()),
82+
environment.audioPlayerClient
83+
.stop(PlayerId())
84+
.fireAndForget()
85+
)
86+
}
87+
88+
case let .timerUpdated(time):
89+
switch memo.mode {
90+
case .notPlaying:
91+
break
92+
case let .playing(progress: progress):
93+
memo.mode = .playing(progress: time / memo.duration)
94+
}
95+
return .none
96+
97+
case let .titleTextFieldChanged(text):
98+
memo.title = text
99+
return .none
100+
}
101+
}
102+
103+
struct VoiceMemoView: View {
104+
// NB: We are using an explicit `ObservedObject` for the view store here instead of
105+
// `WithViewStore` due to a SwiftUI bug where `GeometryReader`s inside `WithViewStore` will
106+
// not properly update.
107+
//
108+
// Feedback filed: https://gist.github.com/mbrandonw/cc5da3d487bcf7c4f21c27019a440d18
109+
@ObservedObject var viewStore: ViewStore<VoiceMemo, VoiceMemoAction>
110+
111+
init(store: Store<VoiceMemo, VoiceMemoAction>) {
112+
self.viewStore = ViewStore(store)
113+
}
114+
115+
var body: some View {
116+
GeometryReader { proxy in
117+
ZStack(alignment: .leading) {
118+
if self.viewStore.mode.isPlaying {
119+
Rectangle()
120+
.foregroundColor(Color(white: 0.9))
121+
.frame(width: proxy.size.width * CGFloat(self.viewStore.mode.progress ?? 0))
122+
.animation(.linear(duration: 0.5))
123+
}
124+
125+
HStack {
126+
TextField(
127+
"Untitled, \(dateFormatter.string(from: self.viewStore.date))",
128+
text: self.viewStore.binding(
129+
get: { $0.title }, send: VoiceMemoAction.titleTextFieldChanged)
130+
)
131+
132+
Spacer()
133+
134+
dateComponentsFormatter.string(from: self.currentTime).map {
135+
Text($0)
136+
.font(Font.footnote.monospacedDigit())
137+
.foregroundColor(.gray)
138+
}
139+
140+
Button(action: { self.viewStore.send(.playButtonTapped) }) {
141+
Image(systemName: self.viewStore.mode.isPlaying ? "stop.circle" : "play.circle")
142+
.font(Font.system(size: 22))
143+
}
144+
}
145+
.padding([.leading, .trailing])
146+
}
147+
}
148+
.buttonStyle(BorderlessButtonStyle())
149+
.listRowBackground(self.viewStore.mode.isPlaying ? Color(white: 0.97) : .clear)
150+
.listRowInsets(EdgeInsets())
151+
}
152+
153+
var currentTime: TimeInterval {
154+
self.viewStore.mode.progress.map { $0 * self.viewStore.duration } ?? self.viewStore.duration
155+
}
156+
}

0 commit comments

Comments
 (0)