Skip to content

Commit ef5201a

Browse files
authored
Clean up Voice Memos demo (#915)
* Clean up Voice Memos demo * Tests
1 parent 87f388b commit ef5201a

File tree

10 files changed

+165
-169
lines changed

10 files changed

+165
-169
lines changed

Examples/VoiceMemos/VoiceMemos/AudioPlayerClient/AudioPlayerClient.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import ComposableArchitecture
22
import Foundation
33

44
struct AudioPlayerClient {
5-
var play: (AnyHashable, URL) -> Effect<Action, Failure>
6-
var stop: (AnyHashable) -> Effect<Never, Never>
5+
var play: (URL) -> Effect<Action, Failure>
6+
var stop: () -> Effect<Never, Never>
77

88
enum Action: Equatable {
99
case didFinishPlaying(successfully: Bool)

Examples/VoiceMemos/VoiceMemos/AudioPlayerClient/FailingAudioPlayerClient.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import Foundation
44
#if DEBUG
55
extension AudioPlayerClient {
66
static let failing = Self(
7-
play: { _, _ in .failing("AudioPlayerClient.play") },
8-
stop: { _ in .failing("AudioPlayerClient.stop") }
7+
play: { _ in .failing("AudioPlayerClient.play") },
8+
stop: { .failing("AudioPlayerClient.stop") }
99
)
1010
}
1111
#endif

Examples/VoiceMemos/VoiceMemos/AudioPlayerClient/LiveAudioPlayerClient.swift

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,42 @@ import AVFoundation
22
import ComposableArchitecture
33

44
extension AudioPlayerClient {
5-
static let live = AudioPlayerClient(
6-
play: { id, url in
7-
.future { callback in
8-
do {
9-
let delegate = try AudioPlayerClientDelegate(
10-
url: url,
11-
didFinishPlaying: { flag in
12-
callback(.success(.didFinishPlaying(successfully: flag)))
13-
dependencies[id] = nil
14-
},
15-
decodeErrorDidOccur: { _ in
16-
callback(.failure(.decodeErrorDidOccur))
17-
dependencies[id] = nil
18-
}
19-
)
5+
static var live: Self {
6+
var delegate: AudioPlayerClientDelegate?
7+
return Self(
8+
play: { url in
9+
.future { callback in
10+
delegate?.player.stop()
11+
delegate = nil
12+
do {
13+
delegate = try AudioPlayerClientDelegate(
14+
url: url,
15+
didFinishPlaying: { flag in
16+
callback(.success(.didFinishPlaying(successfully: flag)))
17+
delegate = nil
18+
},
19+
decodeErrorDidOccur: { _ in
20+
callback(.failure(.decodeErrorDidOccur))
21+
delegate = nil
22+
}
23+
)
2024

21-
delegate.player.play()
22-
dependencies[id] = delegate
23-
} catch {
24-
callback(.failure(.couldntCreateAudioPlayer))
25+
delegate?.player.play()
26+
} catch {
27+
callback(.failure(.couldntCreateAudioPlayer))
28+
}
29+
}
30+
},
31+
stop: {
32+
.fireAndForget {
33+
delegate?.player.stop()
34+
delegate = nil
2535
}
2636
}
27-
},
28-
stop: { id in
29-
.fireAndForget {
30-
dependencies[id]?.player.stop()
31-
dependencies[id] = nil
32-
}
33-
}
34-
)
37+
)
38+
}
3539
}
3640

37-
private var dependencies: [AnyHashable: AudioPlayerClientDelegate] = [:]
38-
3941
private class AudioPlayerClientDelegate: NSObject, AVAudioPlayerDelegate {
4042
let didFinishPlaying: (Bool) -> Void
4143
let decodeErrorDidOccur: (Error?) -> Void

Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/AudioRecorderClient.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import ComposableArchitecture
22
import Foundation
33

44
struct AudioRecorderClient {
5-
var currentTime: (AnyHashable) -> Effect<TimeInterval?, Never>
5+
var currentTime: () -> Effect<TimeInterval?, Never>
66
var requestRecordPermission: () -> Effect<Bool, Never>
7-
var startRecording: (AnyHashable, URL) -> Effect<Action, Failure>
8-
var stopRecording: (AnyHashable) -> Effect<Never, Never>
7+
var startRecording: (URL) -> Effect<Action, Failure>
8+
var stopRecording: () -> Effect<Never, Never>
99

1010
enum Action: Equatable {
1111
case didFinishRecording(successfully: Bool)

Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/FailingAudioRecorderClient.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import Foundation
44
#if DEBUG
55
extension AudioRecorderClient {
66
static let failing = Self(
7-
currentTime: { _ in .failing("AudioRecorderClient.currentTime") },
7+
currentTime: { .failing("AudioRecorderClient.currentTime") },
88
requestRecordPermission: { .failing("AudioRecorderClient.requestRecordPermission") },
9-
startRecording: { _, _ in .failing("AudioRecorderClient.startRecording") },
10-
stopRecording: { _ in .failing("AudioRecorderClient.stopRecording") }
9+
startRecording: { _ in .failing("AudioRecorderClient.startRecording") },
10+
stopRecording: { .failing("AudioRecorderClient.stopRecording") }
1111
)
1212
}
1313
#endif

Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/LiveAudioRecorderClient.swift

Lines changed: 62 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,74 @@ import AVFoundation
22
import ComposableArchitecture
33

44
extension AudioRecorderClient {
5-
static let live = AudioRecorderClient(
6-
currentTime: { id in
7-
Effect.result {
8-
guard
9-
let recorder = dependencies[id]?.recorder,
10-
recorder.isRecording
11-
else { return .success(nil) }
12-
return .success(recorder.currentTime)
13-
}
14-
},
15-
requestRecordPermission: {
16-
.future { callback in
17-
AVAudioSession.sharedInstance().requestRecordPermission { granted in
18-
callback(.success(granted))
5+
static var live: Self {
6+
var delegate: AudioRecorderClientDelegate?
7+
8+
return Self(
9+
currentTime: {
10+
.result {
11+
guard
12+
let recorder = delegate?.recorder,
13+
recorder.isRecording
14+
else { return .success(nil) }
15+
return .success(recorder.currentTime)
1916
}
20-
}
21-
},
22-
startRecording: { id, url in
23-
.future { callback in
24-
guard
25-
let delegate = try? AudioRecorderClientDelegate(
26-
url: url,
27-
didFinishRecording: { flag in
28-
callback(.success(.didFinishRecording(successfully: flag)))
29-
dependencies[id] = nil
30-
},
31-
encodeErrorDidOccur: { _ in
32-
callback(.failure(.encodeErrorDidOccur))
33-
dependencies[id] = nil
34-
}
35-
)
36-
else {
37-
callback(.failure(.couldntCreateAudioRecorder))
38-
return
17+
},
18+
requestRecordPermission: {
19+
.future { callback in
20+
AVAudioSession.sharedInstance().requestRecordPermission { granted in
21+
callback(.success(granted))
22+
}
3923
}
24+
},
25+
startRecording: { url in
26+
.future { callback in
27+
delegate?.recorder.stop()
28+
delegate = nil
29+
do {
30+
delegate = try AudioRecorderClientDelegate(
31+
url: url,
32+
didFinishRecording: { flag in
33+
callback(.success(.didFinishRecording(successfully: flag)))
34+
delegate = nil
35+
try? AVAudioSession.sharedInstance().setActive(false)
36+
},
37+
encodeErrorDidOccur: { _ in
38+
callback(.failure(.encodeErrorDidOccur))
39+
delegate = nil
40+
try? AVAudioSession.sharedInstance().setActive(false)
41+
}
42+
)
43+
} catch {
44+
callback(.failure(.couldntCreateAudioRecorder))
45+
return
46+
}
4047

41-
do {
42-
try AVAudioSession.sharedInstance().setCategory(.record, mode: .default)
43-
} catch {
44-
callback(.failure(.couldntActivateAudioSession))
45-
return
46-
}
48+
do {
49+
try AVAudioSession.sharedInstance().setCategory(.record, mode: .default)
50+
} catch {
51+
callback(.failure(.couldntActivateAudioSession))
52+
return
53+
}
4754

48-
do {
49-
try AVAudioSession.sharedInstance().setActive(true, options: [])
50-
} catch {
51-
callback(.failure(.couldntSetAudioSessionCategory))
52-
return
53-
}
55+
do {
56+
try AVAudioSession.sharedInstance().setActive(true)
57+
} catch {
58+
callback(.failure(.couldntSetAudioSessionCategory))
59+
return
60+
}
5461

55-
dependencies[id] = delegate
56-
delegate.recorder.record()
57-
}
58-
},
59-
stopRecording: { id in
60-
.fireAndForget {
61-
dependencies[id]?.recorder.stop()
62-
try? AVAudioSession.sharedInstance().setActive(false, options: [])
62+
delegate?.recorder.record()
63+
}
64+
},
65+
stopRecording: {
66+
.fireAndForget {
67+
delegate?.recorder.stop()
68+
try? AVAudioSession.sharedInstance().setActive(false)
69+
}
6370
}
64-
}
65-
)
71+
)
72+
}
6673
}
6774

6875
private class AudioRecorderClientDelegate: NSObject, AVAudioRecorderDelegate {
@@ -97,5 +104,3 @@ private class AudioRecorderClientDelegate: NSObject, AVAudioRecorderDelegate {
97104
self.encodeErrorDidOccur(error)
98105
}
99106
}
100-
101-
private var dependencies: [AnyHashable: AudioRecorderClientDelegate] = [:]

Examples/VoiceMemos/VoiceMemos/Helpers.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
import Foundation
22

3-
let dateFormatter: DateFormatter = {
4-
let formatter = DateFormatter()
5-
formatter.dateStyle = .short
6-
formatter.timeStyle = .medium
7-
return formatter
8-
}()
9-
103
let dateComponentsFormatter: DateComponentsFormatter = {
114
let formatter = DateComponentsFormatter()
125
formatter.allowedUnits = [.minute, .second]

Examples/VoiceMemos/VoiceMemos/VoiceMemo.swift

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ struct VoiceMemoEnvironment {
4040
var mainRunLoop: AnySchedulerOf<RunLoop>
4141
}
4242

43-
let voiceMemoReducer = Reducer<VoiceMemo, VoiceMemoAction, VoiceMemoEnvironment> {
44-
memo, action, environment in
45-
struct PlayerId: Hashable {}
43+
let voiceMemoReducer = Reducer<
44+
VoiceMemo, VoiceMemoAction, VoiceMemoEnvironment
45+
> { memo, action, environment in
4646
struct TimerId: Hashable {}
4747

4848
switch action {
@@ -52,35 +52,31 @@ let voiceMemoReducer = Reducer<VoiceMemo, VoiceMemoAction, VoiceMemoEnvironment>
5252

5353
case .delete:
5454
return .merge(
55-
environment.audioPlayerClient
56-
.stop(PlayerId())
57-
.fireAndForget(),
58-
.cancel(id: PlayerId()),
55+
environment.audioPlayerClient.stop().fireAndForget(),
5956
.cancel(id: TimerId())
6057
)
6158

6259
case .playButtonTapped:
6360
switch memo.mode {
6461
case .notPlaying:
6562
memo.mode = .playing(progress: 0)
63+
6664
let start = environment.mainRunLoop.now
6765
return .merge(
6866
Effect.timer(id: TimerId(), every: 0.5, on: environment.mainRunLoop)
6967
.map { .timerUpdated($0.date.timeIntervalSince1970 - start.date.timeIntervalSince1970) },
7068

7169
environment.audioPlayerClient
72-
.play(PlayerId(), memo.url)
70+
.play(memo.url)
7371
.catchToEffect(VoiceMemoAction.audioPlayerClient)
74-
.cancellable(id: PlayerId())
7572
)
7673

7774
case .playing:
7875
memo.mode = .notPlaying
76+
7977
return .concatenate(
8078
.cancel(id: TimerId()),
81-
environment.audioPlayerClient
82-
.stop(PlayerId())
83-
.fireAndForget()
79+
environment.audioPlayerClient.stop().fireAndForget()
8480
)
8581
}
8682

@@ -112,42 +108,40 @@ struct VoiceMemoView: View {
112108
}
113109

114110
var body: some View {
115-
GeometryReader { proxy in
116-
ZStack(alignment: .leading) {
117-
if self.viewStore.mode.isPlaying {
118-
Rectangle()
119-
.foregroundColor(Color(.systemGray5))
120-
.frame(width: proxy.size.width * CGFloat(self.viewStore.mode.progress ?? 0))
121-
.animation(.linear(duration: 0.5), value: self.viewStore.mode.progress)
122-
}
123-
124-
HStack {
125-
TextField(
126-
"Untitled, \(dateFormatter.string(from: self.viewStore.date))",
127-
text: self.viewStore.binding(
128-
get: \.title, send: VoiceMemoAction.titleTextFieldChanged)
129-
)
130-
131-
Spacer()
132-
133-
dateComponentsFormatter.string(from: self.currentTime).map {
134-
Text($0)
135-
.font(.footnote.monospacedDigit())
136-
.foregroundColor(Color(.systemGray))
137-
}
138-
139-
Button(action: { self.viewStore.send(.playButtonTapped) }) {
140-
Image(systemName: self.viewStore.mode.isPlaying ? "stop.circle" : "play.circle")
141-
.font(.system(size: 22))
142-
}
143-
}
144-
.frame(maxHeight: .infinity, alignment: .center)
145-
.padding(.horizontal)
111+
HStack {
112+
TextField(
113+
"Untitled, \(self.viewStore.date.formatted(date: .numeric, time: .shortened))",
114+
text: self.viewStore.binding(
115+
get: \.title, send: VoiceMemoAction.titleTextFieldChanged)
116+
)
117+
118+
Spacer()
119+
120+
dateComponentsFormatter.string(from: self.currentTime).map {
121+
Text($0)
122+
.font(.footnote.monospacedDigit())
123+
.foregroundColor(Color(.systemGray))
124+
}
125+
126+
Button(action: { self.viewStore.send(.playButtonTapped) }) {
127+
Image(systemName: self.viewStore.mode.isPlaying ? "stop.circle" : "play.circle")
128+
.font(.system(size: 22))
146129
}
147130
}
148131
.buttonStyle(.borderless)
132+
.frame(maxHeight: .infinity, alignment: .center)
133+
.padding(.horizontal)
149134
.listRowBackground(self.viewStore.mode.isPlaying ? Color(.systemGray6) : .clear)
150135
.listRowInsets(EdgeInsets())
136+
.background(
137+
Color(.systemGray5)
138+
.frame(maxWidth: self.viewStore.mode.isPlaying ? .infinity : 0)
139+
.animation(
140+
self.viewStore.mode.isPlaying ? .linear(duration: self.viewStore.duration) : nil,
141+
value: self.viewStore.mode.isPlaying
142+
),
143+
alignment: .leading
144+
)
151145
}
152146

153147
var currentTime: TimeInterval {

0 commit comments

Comments
 (0)