I have a stepper button which is in a higher level view that's hidden based on the adjusted value hitting a threshold.
The bug is that hiding the control doesn't cancel the repeat behaviour even though the gesture which triggers it has (obviously) ended.
Hitting the Reset Count button makes the vanished content reappear and you can see the count decrease until it vanishes again.
The nested button specifies buttonRepeatBehavior(.enabled)
Note that the problem occurs on both iOS 17+ and macOS 14+.
fileprivate func stepperLabel(isDown: Bool) -> some View {
if isDown {
Text("-")
.font(.title2)
.foregroundColor(.primary)
.frame(maxWidth: .infinity, minHeight: 40, maxHeight: .infinity,alignment: .leading)
.padding(.leading, 16)
} else {
Text("+")
.font(.title2)
.foregroundColor(.primary)
.frame(maxWidth: .infinity, minHeight: 40, maxHeight: .infinity,alignment: .trailing)
.padding(.trailing, 16)
}
}
struct StepperButtonView: View {
let isDown: Bool
let action: ()->Void
var body: some View {
Button(action: action) {
stepperLabel(isDown: isDown)
}
.buttonStyle(.plain)
.buttonRepeatBehavior(.enabled)
}
}
This is grouped into a higher-level control, eg:
struct StepperNumView<T: Numeric & Comparable>: View {
@Binding var value: T
let step: T
let zeroBased = T.self == UInt.self
var body: some View {
VStack {
HStack(spacing: 0) {
StepperButtonView(isDown: true) {
if zeroBased {
if value == 0 {
// message will keep appearing in console & if make visible again, keeps repeating until vanishes
print("skipping adjustment from repeating button")
} else {
value = max(0, value-step)
}
} else {
value -= step
}
}
.disabled(zeroBased && value == 0)
...
The parent view controls visibility of a larger section based on the count that's adjusted by the repeating button.
struct ContentView: View {
@State var count: UInt = 10
var body: some View {
VStack {
Button("Reset Count") {
count = 10
}
.buttonStyle(.borderedProminent)
Spacer()
if count > 0 {
Text("Button vanishes when hits zero, holding down on\n+/- to repeat should trigger weird things")
Spacer()
StepperNumView<Int>(title: "Count, tap centre to edit", value: $count, step: 1)
.padding(.horizontal)
} else {
Text("Button vanished with zero count")
}
Full code on GitHub
Lodged with Apple as FB15477204 and as rdar://FB15477204
Whilst the original behaviour remains an outstanding Apple bug, I had an insight whilst writing this question.
Use Animation to delay removal of the view, so the button.disabled clause is evaluated - my theory being that the right way to cancel auto-repeat is for the button to become disabled.
Simple changes:
stepperShouldAppear
which depends on countstepperShouldAppear
in the conditional view (more for neatness).animation(... value: stepperShouldAppear)
at the end of the ContentView
struct StepperNumView<T: Numeric & Comparable>: View {
...
StepperButtonView(isDown: true) {
if zeroBased {
if value == 0 {
print("skipping adjustment from repeating button")
} else {
value = max(0, value-step)
}
} else {
value -= step
}
}
.disabled(zeroBased && value == 0)
..
struct ContentView: View {
@State var count: UInt = 10
var stepperShouldAppear: Bool {get{ // 1
count > 0
}}
var body: some View {
VStack {
Button("Reset Count") {
count = 10
}
.buttonStyle(.borderedProminent)
Spacer()
if stepperShouldAppear { // 2
Text("Button vanishes when hits zero, holding down on\n+/- to repeat should trigger weird things")
Spacer()
StepperNumView<Int>(title: "Count, tap centre to edit", value: $count, step: 1)
.padding(.horizontal)
} else {
Text("Button vanished with zero count")
}
...
}
.animation(.easeOut(duration: 1.0), value: stepperShouldAppear) // 3
Branch AnimatingFixes shows it, now merged into main.