Skip to content

Commit 3ff6150

Browse files
committed
Clean up Voice Memos demo (#915)
* Clean up Voice Memos demo * Tests
1 parent 63260c6 commit 3ff6150

File tree

11 files changed

+195
-169
lines changed

11 files changed

+195
-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
@@ -3,67 +3,74 @@ import ComposableArchitecture
33
import ReactiveSwift
44

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

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

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

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

6976
private class AudioRecorderClientDelegate: NSObject, AVAudioRecorderDelegate {
@@ -98,5 +105,3 @@ private class AudioRecorderClientDelegate: NSObject, AVAudioRecorderDelegate {
98105
self.encodeErrorDidOccur(error)
99106
}
100107
}
101-
102-
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
@@ -41,9 +41,9 @@ struct VoiceMemoEnvironment {
4141
var mainRunLoop: DateScheduler
4242
}
4343

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

4949
switch action {
@@ -53,35 +53,31 @@ let voiceMemoReducer = Reducer<VoiceMemo, VoiceMemoAction, VoiceMemoEnvironment>
5353

5454
case .delete:
5555
return .merge(
56-
environment.audioPlayerClient
57-
.stop(PlayerId())
58-
.fireAndForget(),
59-
Effect.cancel(id: PlayerId()),
56+
environment.audioPlayerClient.stop().fireAndForget(),
6057
Effect.cancel(id: TimerId())
6158
)
6259

6360
case .playButtonTapped:
6461
switch memo.mode {
6562
case .notPlaying:
6663
memo.mode = .playing(progress: 0)
64+
6765
let start = environment.mainRunLoop.currentDate
6866
return .merge(
6967
Effect.timer(id: TimerId(), every: .milliseconds(500), on: environment.mainRunLoop)
7068
.map { .timerUpdated($0.timeIntervalSince1970 - start.timeIntervalSince1970) },
7169

7270
environment.audioPlayerClient
73-
.play(PlayerId(), memo.url)
71+
.play(memo.url)
7472
.catchToEffect(VoiceMemoAction.audioPlayerClient)
75-
.cancellable(id: PlayerId())
7673
)
7774

7875
case .playing:
7976
memo.mode = .notPlaying
77+
8078
return .concatenate(
8179
.cancel(id: TimerId()),
82-
environment.audioPlayerClient
83-
.stop(PlayerId())
84-
.fireAndForget()
80+
environment.audioPlayerClient.stop().fireAndForget()
8581
)
8682
}
8783

@@ -113,42 +109,40 @@ struct VoiceMemoView: View {
113109
}
114110

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

154148
var currentTime: TimeInterval {

0 commit comments

Comments
 (0)