Skip to content

Commit e5bec10

Browse files
authored
Merge pull request #9 from trading-point/michael/sync-upstream
Sync changes from the Pointfree repo
2 parents 6c272b1 + bba92dd commit e5bec10

File tree

11 files changed

+309
-75
lines changed

11 files changed

+309
-75
lines changed

Examples/Todos/Todos/Todos.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
131131
environment: { _ in TodoEnvironment() }
132132
)
133133
)
134-
.debug()
134+
135+
.debugActions(actionFormat: .labelsOnly)
135136

136137
struct AppView: View {
137138
struct ViewState: Equatable {

Examples/VoiceMemos/VoiceMemos/VoiceMemos.swift

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ struct VoiceMemo: Equatable {
2929
enum VoiceMemoAction: Equatable {
3030
case audioPlayerClient(Result<AudioPlayerClient.Action, AudioPlayerClient.Failure>)
3131
case playButtonTapped
32+
case delete
3233
case timerUpdated(TimeInterval)
3334
case titleTextFieldChanged(String)
3435
}
@@ -48,6 +49,12 @@ let voiceMemoReducer = Reducer<VoiceMemo, VoiceMemoAction, VoiceMemoEnvironment>
4849
memo.mode = .notPlaying
4950
return .cancel(id: TimerId())
5051

52+
case .delete:
53+
return .merge(
54+
Effect.cancel(id: PlayerId()),
55+
Effect.cancel(id: TimerId())
56+
)
57+
5158
case .playButtonTapped:
5259
switch memo.mode {
5360
case .notPlaying:
@@ -57,7 +64,8 @@ let voiceMemoReducer = Reducer<VoiceMemo, VoiceMemoAction, VoiceMemoEnvironment>
5764
environment.audioPlayerClient
5865
.play(PlayerId(), memo.url)
5966
.catchToEffect()
60-
.map(VoiceMemoAction.audioPlayerClient),
67+
.map(VoiceMemoAction.audioPlayerClient)
68+
.cancellable(id: PlayerId()),
6169
Effect.timer(id: TimerId(), every: .milliseconds(500), on: environment.mainQueue)
6270
.map { date -> VoiceMemoAction in
6371
.timerUpdated(
@@ -121,7 +129,6 @@ enum VoiceMemosAction: Equatable {
121129
case alertDismissed
122130
case audioRecorderClient(Result<AudioRecorderClient.Action, AudioRecorderClient.Failure>)
123131
case currentRecordingTimerUpdated
124-
case deleteVoiceMemo(IndexSet)
125132
case finalRecordingTime(TimeInterval)
126133
case openSettingsButtonTapped
127134
case recordButtonTapped
@@ -140,6 +147,12 @@ struct VoiceMemosEnvironment {
140147
}
141148

142149
let voiceMemosReducer = Reducer<VoiceMemosState, VoiceMemosAction, VoiceMemosEnvironment>.combine(
150+
voiceMemoReducer.forEach(
151+
state: \.voiceMemos,
152+
action: /VoiceMemosAction.voiceMemo(index:action:),
153+
environment: {
154+
VoiceMemoEnvironment(audioPlayerClient: $0.audioPlayerClient, mainQueue: $0.mainQueue)
155+
}),
143156
.init { state, action, environment in
144157
struct RecorderId: Hashable {}
145158
struct RecorderTimerId: Hashable {}
@@ -199,10 +212,6 @@ let voiceMemosReducer = Reducer<VoiceMemosState, VoiceMemosAction, VoiceMemosEnv
199212
state.currentRecording?.duration += 1
200213
return .none
201214

202-
case let .deleteVoiceMemo(indexSet):
203-
state.voiceMemos.remove(atOffsets: indexSet)
204-
return .none
205-
206215
case let .finalRecordingTime(duration):
207216
state.currentRecording?.duration = duration
208217
return .none
@@ -257,6 +266,10 @@ let voiceMemosReducer = Reducer<VoiceMemosState, VoiceMemosAction, VoiceMemosEnv
257266
state.alertMessage = "Voice memo playback failed."
258267
return .none
259268

269+
case let .voiceMemo(index: index, action: .delete):
270+
state.voiceMemos.remove(at: index)
271+
return .none
272+
260273
case let .voiceMemo(index: index, action: .playButtonTapped):
261274
for idx in state.voiceMemos.indices where idx != index {
262275
state.voiceMemos[idx].mode = .notPlaying
@@ -266,13 +279,7 @@ let voiceMemosReducer = Reducer<VoiceMemosState, VoiceMemosAction, VoiceMemosEnv
266279
case .voiceMemo:
267280
return .none
268281
}
269-
},
270-
voiceMemoReducer.forEach(
271-
state: \.voiceMemos,
272-
action: /VoiceMemosAction.voiceMemo(index:action:),
273-
environment: {
274-
VoiceMemoEnvironment(audioPlayerClient: $0.audioPlayerClient, mainQueue: $0.mainQueue)
275-
})
282+
}
276283
)
277284

278285
struct VoiceMemosView: View {
@@ -290,7 +297,11 @@ struct VoiceMemosView: View {
290297
id: \.url,
291298
content: VoiceMemoView.init(store:)
292299
)
293-
.onDelete { viewStore.send(.deleteVoiceMemo($0)) }
300+
.onDelete { indexSet in
301+
for index in indexSet {
302+
viewStore.send(.voiceMemo(index: index, action: .delete))
303+
}
304+
}
294305
}
295306
VStack {
296307
ZStack {

Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,12 +280,44 @@ class VoiceMemosTests: XCTestCase {
280280
)
281281

282282
store.assert(
283-
.send(.deleteVoiceMemo(IndexSet(integer: 1))),
284-
.send(.deleteVoiceMemo(IndexSet(integer: 0))) {
283+
.send(.voiceMemo(index: 0, action: .delete)) {
285284
$0.voiceMemos = []
286285
}
287286
)
288287
}
288+
289+
func testDeleteMemoWhilePlaying() {
290+
let store = TestStore(
291+
initialState: VoiceMemosState(
292+
voiceMemos: [
293+
VoiceMemo(
294+
date: Date(timeIntervalSinceNow: 0),
295+
duration: 10,
296+
mode: .notPlaying,
297+
title: "",
298+
url: URL(string: "https://www.pointfree.co/functions")!
299+
),
300+
]
301+
),
302+
reducer: voiceMemosReducer,
303+
environment: .mock(
304+
audioPlayerClient: .mock(
305+
play: { id, url in .future { _ in } }
306+
),
307+
mainQueue: self.scheduler
308+
)
309+
)
310+
311+
store.assert(
312+
.send(.voiceMemo(index: 0, action: .playButtonTapped)) {
313+
$0.voiceMemos[0].mode = .playing(progress: 0)
314+
},
315+
.send(.voiceMemo(index: 0, action: .delete)) {
316+
$0.voiceMemos = []
317+
},
318+
.do { self.scheduler.run() }
319+
)
320+
}
289321
}
290322

291323
extension VoiceMemosEnvironment {

Makefile

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,68 +15,52 @@ test-swift:
1515
test-workspace:
1616
xcodebuild test \
1717
-scheme ComposableArchitecture \
18-
-destination platform="$(PLATFORM_IOS)" \
19-
-quiet
18+
-destination platform="$(PLATFORM_IOS)"
2019
xcodebuild test \
2120
-scheme ComposableArchitecture \
22-
-destination platform="$(PLATFORM_MACOS)" \
23-
-quiet
21+
-destination platform="$(PLATFORM_MACOS)"
2422
xcodebuild test \
2523
-scheme ComposableArchitecture \
26-
-destination platform="$(PLATFORM_TVOS)" \
27-
-quiet
24+
-destination platform="$(PLATFORM_TVOS)"
2825
xcodebuild test \
2926
-scheme ComposableCoreLocation \
30-
-destination platform="$(PLATFORM_IOS)" \
31-
-quiet
27+
-destination platform="$(PLATFORM_IOS)"
3228
xcodebuild test \
3329
-scheme ComposableCoreLocation \
34-
-destination platform="$(PLATFORM_MACOS)" \
35-
-quiet
30+
-destination platform="$(PLATFORM_MACOS)"
3631
xcodebuild test \
3732
-scheme ComposableCoreLocation \
38-
-destination platform="$(PLATFORM_TVOS)" \
39-
-quiet
33+
-destination platform="$(PLATFORM_TVOS)"
4034
xcodebuild test \
4135
-scheme "CaseStudies (SwiftUI)" \
42-
-destination platform="$(PLATFORM_IOS)" \
43-
-quiet
36+
-destination platform="$(PLATFORM_IOS)"
4437
xcodebuild test \
4538
-scheme "CaseStudies (UIKit)" \
46-
-destination platform="$(PLATFORM_IOS)" \
47-
-quiet
39+
-destination platform="$(PLATFORM_IOS)"
4840
xcodebuild test \
4941
-scheme MotionManager \
50-
-destination platform="$(PLATFORM_IOS)" \
51-
-quiet
42+
-destination platform="$(PLATFORM_IOS)"
5243
xcodebuild test \
5344
-scheme LocationManagerDesktop \
54-
-destination platform="$(PLATFORM_MACOS)" \
55-
-quiet
45+
-destination platform="$(PLATFORM_MACOS)"
5646
xcodebuild test \
5747
-scheme LocationManagerMobile \
58-
-destination platform="$(PLATFORM_IOS)" \
59-
-quiet
48+
-destination platform="$(PLATFORM_IOS)"
6049
xcodebuild test \
6150
-scheme Search \
62-
-destination platform="$(PLATFORM_IOS)" \
63-
-quiet
51+
-destination platform="$(PLATFORM_IOS)"
6452
xcodebuild test \
6553
-scheme SpeechRecognition \
66-
-destination platform="$(PLATFORM_IOS)" \
67-
-quiet
54+
-destination platform="$(PLATFORM_IOS)"
6855
xcodebuild test \
6956
-scheme TicTacToe \
70-
-destination platform="$(PLATFORM_IOS)" \
71-
-quiet
57+
-destination platform="$(PLATFORM_IOS)"
7258
xcodebuild test \
7359
-scheme Todos \
74-
-destination platform="$(PLATFORM_IOS)" \
75-
-quiet
60+
-destination platform="$(PLATFORM_IOS)"
7661
xcodebuild test \
7762
-scheme VoiceMemos \
78-
-destination platform="$(PLATFORM_IOS)" \
79-
-quiet
63+
-destination platform="$(PLATFORM_IOS)"
8064

8165
format:
8266
swift format --in-place --recursive ./Package.swift ./Sources ./Tests

Sources/ComposableArchitecture/Debugging/ReducerDebugging.swift

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
import CasePaths
22
import Dispatch
33

4+
/// Determines how the string description of an action should be printed when using the `.debug()`
5+
/// higher-order reducer.
6+
public enum ActionFormat {
7+
/// Prints the action in a single line by only specifying the labels of the associated values:
8+
///
9+
/// Action.screenA(.row(index:, action: .textChanged(query:)))
10+
case labelsOnly
11+
/// Prints the action in a multiline, pretty-printed format, including all the labels of
12+
/// any associated values, as well as the data held in the associated values:
13+
///
14+
/// Action.screenA(
15+
/// ScreenA.row(
16+
/// index: 1,
17+
/// action: RowAction.textChanged(
18+
/// query: "Hi"
19+
/// )
20+
/// )
21+
/// )
22+
case prettyPrint
23+
}
24+
425
extension Reducer {
526
/// Prints debug messages describing all received actions and state mutations.
627
///
@@ -15,11 +36,18 @@ extension Reducer {
1536
/// - Returns: A reducer that prints debug messages for all received actions.
1637
public func debug(
1738
_ prefix: String = "",
39+
actionFormat: ActionFormat = .prettyPrint,
1840
environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
1941
DebugEnvironment()
2042
}
2143
) -> Reducer {
22-
self.debug(prefix, state: { $0 }, action: .self, environment: toDebugEnvironment)
44+
self.debug(
45+
prefix,
46+
state: { $0 },
47+
action: .self,
48+
actionFormat: actionFormat,
49+
environment: toDebugEnvironment
50+
)
2351
}
2452

2553
/// Prints debug messages describing all received actions.
@@ -35,11 +63,18 @@ extension Reducer {
3563
/// - Returns: A reducer that prints debug messages for all received actions.
3664
public func debugActions(
3765
_ prefix: String = "",
66+
actionFormat: ActionFormat = .prettyPrint,
3867
environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
3968
DebugEnvironment()
4069
}
4170
) -> Reducer {
42-
self.debug(prefix, state: { _ in () }, action: .self, environment: toDebugEnvironment)
71+
self.debug(
72+
prefix,
73+
state: { _ in () },
74+
action: .self,
75+
actionFormat: actionFormat,
76+
environment: toDebugEnvironment
77+
)
4378
}
4479

4580
/// Prints debug messages describing all received local actions and local state mutations.
@@ -59,6 +94,7 @@ extension Reducer {
5994
_ prefix: String = "",
6095
state toLocalState: @escaping (State) -> LocalState,
6196
action toLocalAction: CasePath<Action, LocalAction>,
97+
actionFormat: ActionFormat = .prettyPrint,
6298
environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in
6399
DebugEnvironment()
64100
}
@@ -73,9 +109,14 @@ extension Reducer {
73109
return .concatenate(
74110
.fireAndForget {
75111
debugEnvironment.queue.async {
76-
let actionOutput = debugOutput(localAction).indent(by: 2)
112+
let actionOutput =
113+
actionFormat == .prettyPrint
114+
? debugOutput(localAction).indent(by: 2)
115+
: debugCaseOutput(localAction).indent(by: 2)
77116
let stateOutput =
78-
debugDiff(previousState, nextState).map { "\($0)\n" } ?? " (No state changes)\n"
117+
LocalState.self == Void.self
118+
? ""
119+
: debugDiff(previousState, nextState).map { "\($0)\n" } ?? " (No state changes)\n"
79120
debugEnvironment.printer(
80121
"""
81122
\(prefix.isEmpty ? "" : "\(prefix): ")received action:

Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,31 @@ extension Effect where Error == Never {
8383
}
8484

8585
func debugCaseOutput(_ value: Any) -> String {
86-
let mirror = Mirror(reflecting: value)
87-
switch mirror.displayStyle {
88-
case .enum:
89-
guard let child = mirror.children.first else {
90-
let childOutput = "\(value)"
91-
return childOutput == "\(type(of: value))" ? "" : ".\(childOutput)"
92-
}
93-
let childOutput = debugCaseOutput(child.value)
94-
return ".\(child.label ?? "")\(childOutput.isEmpty ? "" : "(\(childOutput))")"
95-
case .tuple:
96-
return mirror.children.map { label, value in
97-
let childOutput = debugCaseOutput(value)
98-
return "\(label.map { "\($0):" } ?? "")\(childOutput.isEmpty ? "" : " \(childOutput)")"
86+
func debugCaseOutputHelp(_ value: Any) -> String {
87+
let mirror = Mirror(reflecting: value)
88+
switch mirror.displayStyle {
89+
case .enum:
90+
guard let child = mirror.children.first else {
91+
let childOutput = "\(value)"
92+
return childOutput == "\(type(of: value))" ? "" : ".\(childOutput)"
93+
}
94+
let childOutput = debugCaseOutputHelp(child.value)
95+
return ".\(child.label ?? "")\(childOutput.isEmpty ? "" : "(\(childOutput))")"
96+
case .tuple:
97+
return mirror.children.map { label, value in
98+
let childOutput = debugCaseOutputHelp(value)
99+
return
100+
"\(label.map { isUnlabeledArgument($0) ? "_:" : "\($0):" } ?? "")\(childOutput.isEmpty ? "" : " \(childOutput)")"
101+
}
102+
.joined(separator: ", ")
103+
default:
104+
return ""
99105
}
100-
.joined(separator: ", ")
101-
default:
102-
return ""
103106
}
107+
108+
return "\(type(of: value))\(debugCaseOutputHelp(value))"
109+
}
110+
111+
private func isUnlabeledArgument(_ label: String) -> Bool {
112+
label.firstIndex(where: { $0 != "." && !$0.isNumber }) == nil
104113
}

0 commit comments

Comments
 (0)