Skip to content

Commit b2e1f07

Browse files
mbrandonwmluisbrown
authored andcommitted
Voice memo record feature (#1256)
* Refactor voice memos to have a recording domain. * alert * fixes * wip * fix tests * clean up * revert sendable stuff for now Co-authored-by: Stephen Celis <[email protected]> (cherry picked from commit 407844787c1a4d0232416123d1ad42cf1f8452b9) # Conflicts: # Examples/VoiceMemos/VoiceMemos/VoiceMemos.swift # Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift
1 parent 5d2e58c commit b2e1f07

File tree

4 files changed

+226
-132
lines changed

4 files changed

+226
-132
lines changed

Examples/VoiceMemos/VoiceMemos.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/* Begin PBXBuildFile section */
1010
CA93D05C249BF42500A6F65D /* VoiceMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA93D05B249BF42500A6F65D /* VoiceMemo.swift */; };
1111
CA93D05E249BF46E00A6F65D /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA93D05D249BF46E00A6F65D /* Helpers.swift */; };
12+
CABAB49028A2B5F900122307 /* RecordingMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABAB48F28A2B5F900122307 /* RecordingMemo.swift */; };
1213
DC1394342469E59600EE1157 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = DC1394332469E59600EE1157 /* ComposableArchitecture */; };
1314
DC5BDCB024589177009C65A3 /* VoiceMemosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5BDCAF24589177009C65A3 /* VoiceMemosApp.swift */; };
1415
DC5BDCB224589177009C65A3 /* VoiceMemos.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5BDCB124589177009C65A3 /* VoiceMemos.swift */; };
@@ -59,6 +60,7 @@
5960
23EDBE6B271CD8DD004F7430 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
6061
CA93D05B249BF42500A6F65D /* VoiceMemo.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = VoiceMemo.swift; sourceTree = "<group>"; };
6162
CA93D05D249BF46E00A6F65D /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
63+
CABAB48F28A2B5F900122307 /* RecordingMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingMemo.swift; sourceTree = "<group>"; };
6264
DC5BDCAA24589177009C65A3 /* VoiceMemos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoiceMemos.app; sourceTree = BUILT_PRODUCTS_DIR; };
6365
DC5BDCAF24589177009C65A3 /* VoiceMemosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMemosApp.swift; sourceTree = "<group>"; };
6466
DC5BDCB124589177009C65A3 /* VoiceMemos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMemos.swift; sourceTree = "<group>"; };
@@ -120,6 +122,7 @@
120122
children = (
121123
DC5BDCBB24589178009C65A3 /* Info.plist */,
122124
CA93D05D249BF46E00A6F65D /* Helpers.swift */,
125+
CABAB48F28A2B5F900122307 /* RecordingMemo.swift */,
123126
CA93D05B249BF42500A6F65D /* VoiceMemo.swift */,
124127
DC5BDCB124589177009C65A3 /* VoiceMemos.swift */,
125128
DC5BDCAF24589177009C65A3 /* VoiceMemosApp.swift */,
@@ -274,6 +277,7 @@
274277
DC94F0102458E09900082DE9 /* UnimplementedAudioPlayerClient.swift in Sources */,
275278
CA93D05C249BF42500A6F65D /* VoiceMemo.swift in Sources */,
276279
DC5BDCB024589177009C65A3 /* VoiceMemosApp.swift in Sources */,
280+
CABAB49028A2B5F900122307 /* RecordingMemo.swift in Sources */,
277281
DC5BDCB224589177009C65A3 /* VoiceMemos.swift in Sources */,
278282
);
279283
runOnlyForDeploymentPostprocessing = 0;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
struct RecordingMemoState: Equatable {
5+
var date: Date
6+
var duration: TimeInterval = 0
7+
var mode: Mode = .recording
8+
var url: URL
9+
10+
enum Mode {
11+
case recording
12+
case encoding
13+
}
14+
15+
struct Failed: Equatable, Error {}
16+
}
17+
18+
enum RecordingMemoAction: Equatable {
19+
case audioRecorderDidFinish(TaskResult<Bool>)
20+
case delegate(DelegateAction)
21+
case finalRecordingTime(TimeInterval)
22+
case task
23+
case timerUpdated
24+
case stopButtonTapped
25+
26+
enum DelegateAction: Equatable {
27+
case didFinish(TaskResult<RecordingMemoState>)
28+
}
29+
}
30+
31+
struct RecordingMemoEnvironment {
32+
var audioRecorder: AudioRecorderClient
33+
var mainRunLoop: AnySchedulerOf<RunLoop>
34+
}
35+
36+
let recordingMemoReducer = Reducer<
37+
RecordingMemoState,
38+
RecordingMemoAction,
39+
RecordingMemoEnvironment
40+
> { state, action, environment in
41+
switch action {
42+
case .audioRecorderDidFinish(.success(true)):
43+
return .task { [state] in .delegate(.didFinish(.success(state))) }
44+
45+
case .audioRecorderDidFinish(.success(false)):
46+
return .task { .delegate(.didFinish(.failure(RecordingMemoState.Failed()))) }
47+
48+
case let .audioRecorderDidFinish(.failure(error)):
49+
return .task { .delegate(.didFinish(.failure(error))) }
50+
51+
case .delegate:
52+
return .none
53+
54+
case let .finalRecordingTime(duration):
55+
state.duration = duration
56+
return .none
57+
58+
case .stopButtonTapped:
59+
state.mode = .encoding
60+
return .run { send in
61+
if let currentTime = await environment.audioRecorder.currentTime() {
62+
await send(.finalRecordingTime(currentTime))
63+
}
64+
await environment.audioRecorder.stopRecording()
65+
}
66+
67+
case .task:
68+
return .run { [url = state.url] send in
69+
async let startRecording: Void = await send(
70+
.audioRecorderDidFinish(
71+
TaskResult { try await environment.audioRecorder.startRecording(url) }
72+
)
73+
)
74+
75+
for await _ in environment.mainRunLoop.timer(interval: .seconds(1)) {
76+
await send(.timerUpdated)
77+
}
78+
}
79+
80+
case .timerUpdated:
81+
state.duration += 1
82+
return .none
83+
}
84+
}
85+
86+
struct RecordingMemoView: View {
87+
let store: Store<RecordingMemoState, RecordingMemoAction>
88+
89+
var body: some View {
90+
WithViewStore(self.store) { viewStore in
91+
VStack(spacing: 12) {
92+
Text("Recording")
93+
.font(.title)
94+
.colorMultiply(Color(Int(viewStore.duration).isMultiple(of: 2) ? .systemRed : .label))
95+
.animation(.easeInOut(duration: 0.5), value: viewStore.duration)
96+
97+
if let formattedDuration = dateComponentsFormatter.string(from: viewStore.duration) {
98+
Text(formattedDuration)
99+
.font(.body.monospacedDigit().bold())
100+
.foregroundColor(.black)
101+
}
102+
103+
ZStack {
104+
Circle()
105+
.foregroundColor(Color(.label))
106+
.frame(width: 74, height: 74)
107+
108+
Button(action: { viewStore.send(.stopButtonTapped, animation: .default) }) {
109+
RoundedRectangle(cornerRadius: 4)
110+
.foregroundColor(Color(.systemRed))
111+
.padding(17)
112+
}
113+
.frame(width: 70, height: 70)
114+
}
115+
}
116+
.task {
117+
await viewStore.send(.task).finish()
118+
}
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)