Skip to content

Commit 7d9650f

Browse files
stephencelismbrandonw
authored andcommitted
Add runtime warnings for unused binding actions (#1159)
* Add runtime warnings for unused binding actions An alternative approach to #1158. * wip * finesse * finesse * Update Sources/ComposableArchitecture/SwiftUI/Binding.swift Co-authored-by: Brandon Williams <[email protected]> * Update Binding.swift Co-authored-by: Brandon Williams <[email protected]>
1 parent 89f76b5 commit 7d9650f

File tree

2 files changed

+72
-7
lines changed

2 files changed

+72
-7
lines changed

Sources/ComposableArchitecture/SwiftUI/Binding.swift

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,15 @@ import SwiftUI
282282
/// - Parameter keyPath: A key path to a specific bindable state.
283283
/// - Returns: A new binding.
284284
public func binding<Value: Equatable>(
285-
_ keyPath: WritableKeyPath<State, BindableState<Value>>
285+
_ keyPath: WritableKeyPath<State, BindableState<Value>>,
286+
file: StaticString = #fileID,
287+
line: UInt = #line
286288
) -> Binding<Value> {
287289
self.binding(
288290
get: { $0[keyPath: keyPath].wrappedValue },
289-
send: { .binding(.set(keyPath, $0)) }
291+
send: {
292+
.binding(.set(keyPath, $0, bindableActionType: Action.self, file: file, line: line))
293+
}
290294
)
291295
}
292296
}
@@ -325,11 +329,25 @@ public struct BindingAction<Root>: Equatable {
325329
/// path.
326330
public static func set<Value: Equatable>(
327331
_ keyPath: WritableKeyPath<Root, BindableState<Value>>,
328-
_ value: Value
332+
_ value: Value,
333+
bindableActionType: Any.Type? = nil,
334+
file: StaticString = #fileID,
335+
line: UInt = #line
329336
) -> Self {
330-
.init(
337+
#if DEBUG
338+
let debugger = Debugger(
339+
value: value, bindableActionType: bindableActionType, file: file, line: line
340+
)
341+
let set: (inout Root) -> Void = {
342+
$0[keyPath: keyPath].wrappedValue = value
343+
debugger.wasCalled = true
344+
}
345+
#else
346+
let set: (inout Root) -> Void = { $0[keyPath: keyPath].wrappedValue = value }
347+
#endif
348+
return .init(
331349
keyPath: keyPath,
332-
set: { $0[keyPath: keyPath].wrappedValue = value },
350+
set: set,
333351
value: value,
334352
valueIsEqualTo: { $0 as? Value == value }
335353
)
@@ -353,6 +371,49 @@ public struct BindingAction<Root>: Equatable {
353371
) -> Bool {
354372
keyPath == bindingAction.keyPath
355373
}
374+
375+
#if DEBUG
376+
private class Debugger<Value> {
377+
let value: Value
378+
let bindableActionType: Any.Type?
379+
let file: StaticString
380+
let line: UInt
381+
var wasCalled = false
382+
383+
init(value: Value, bindableActionType: Any.Type?, file: StaticString, line: UInt) {
384+
self.value = value
385+
self.bindableActionType = bindableActionType
386+
self.file = file
387+
self.line = line
388+
}
389+
390+
deinit {
391+
guard self.wasCalled else {
392+
let action = """
393+
\(bindableActionType.map { "\($0).binding(" } ?? "\(BindingAction.self)")\
394+
.set(_, \(self.value))\
395+
\(bindableActionType != nil ? ")" : "")
396+
"""
397+
runtimeWarning(
398+
"""
399+
A binding action created at "%@:%d" was not handled:
400+
401+
Action:
402+
%@
403+
404+
To fix this, invoke the "binding()" method on your feature's reducer.
405+
""",
406+
[
407+
"\(self.file)",
408+
self.line,
409+
action,
410+
]
411+
)
412+
return
413+
}
414+
}
415+
}
416+
#endif
356417
}
357418
#endif
358419

Tests/ComposableArchitectureTests/DebugTests.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ final class DebugTests: XCTestCase {
189189
struct State {
190190
@BindableState var width = 0
191191
}
192-
192+
let action = BindingAction.set(\State.$width, 50)
193193
var dump = ""
194-
customDump(BindingAction.set(\State.$width, 50), to: &dump)
194+
customDump(action, to: &dump)
195195
XCTAssertNoDifference(
196196
dump,
197197
#"""
@@ -201,6 +201,10 @@ final class DebugTests: XCTestCase {
201201
)
202202
"""#
203203
)
204+
205+
// NB: Call setter to avoid runtime warning
206+
var state = State()
207+
action.set(&state)
204208
}
205209
#endif
206210
}

0 commit comments

Comments
 (0)