iOS 26 UISlider valueChanged allTouches Nil Fix
iOS 26 UISlider .valueChanged event is blank, event.allTouches nil preventing phase detection (.began, .moved, .ended). Fixes: use touch events (.touchDown, .touchUpInside) or subclass UISlider overriding tracking methods.
iOS 26 UISlider’s .valueChanged event is always blank and allTouches is always nil, unable to detect end of slider drag
I have an app that was working fine until iOS 26. In my app, I’m using the .valueChanged event of UISlider to get the phase and perform different actions based on whether it’s .began, .moved, or .ended.
This is a common solution for detecting slider interaction phases. Below is my simple example:
import UIKit
import SnapKit
class ViewController: UIViewController {
let label = UILabel()
let slider = UISlider()
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView(arrangedSubviews: [label, slider])
stackView.axis = .vertical
stackView.spacing = 20
view.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.horizontalEdges.equalToSuperview().inset(30)
}
label.textAlignment = .center
slider.isContinuous = true
slider.minimumValue = 0
slider.maximumValue = 100
slider.addTarget(self, action: #selector(sliderValueChanged(sender:event:)), for: .valueChanged)
}
@objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
print("Slider: (slider.value), (event)")
label.text = "Value: (sender.value)"
switch event.allTouches?.first?.phase {
case .began:
print("began")
case .moved:
print("moved")
case .ended:
print("ended")
case .cancelled:
print("cancelled")
default:
break
}
}
}
This had been working fine before. But in iOS 26, the event being passed to my sliderValueChanged function is problematic. It’s not nil but it’s also not a proper event. For example, the print statement just prints empty text for the event. And setting a breakpoint shows it as an empty event object.
Because of this, in iOS 26, event.allTouches is always nil. So, I’m unable to determine what event phase it is.
Is this a bug? How can I work around it?
I found that if I switch from .valueChanged to .allEvents, then I get the event for began and moved phase. However, I don’t get the ended or cancelled phase.
How can I get those phases too?
iOS 26 changed UISlider internals so the UIEvent you used to get from a .valueChanged action no longer carries touch data — event.allTouches will be nil or empty. Quick fixes: attach explicit touch control targets (.touchDown, .touchUpInside/.touchUpOutside/.touchCancel) for begin/end and keep using .valueChanged for live updates; robust fix: subclass UISlider and override beginTracking(:with:), continueTracking(:with:), endTracking(_:with:), cancelTracking(with:) to observe phases reliably.
Contents
- Problem: .valueChanged event and event.allTouches behavior on iOS 26
- Why this changed in recent iOS versions
- Quick fixes: use touch control events (.touchDown, .touchUpInside, .touchUpOutside, .touchCancel)
- Robust fix: Subclass UISlider and override tracking methods
- Integration notes and fallbacks
- Sources
- Conclusion
Problem: .valueChanged event and event.allTouches behavior on iOS 26
Until iOS 26 many developers relied on the UIEvent forwarded into a .valueChanged target-action to inspect event.allTouches and read touch.phase (.began, .moved, .ended). Several people have reported that on iOS 26 the UIEvent printed as “empty” and event.allTouches is nil, so that pattern no longer works (see the community report here: https://stackoverflow.com/questions/79793891/ios-26-uisliders-valuechanged-event-is-always-blank-and-alltouches-is-always-n).
So: it’s not your code — it’s a behavior change in how UISlider forwards events to valueChanged handlers. That makes relying on event.allTouches inside your valueChanged selector unreliable on iOS 26.
Why this changed in recent iOS versions
Apple has been evolving UISlider internals (the UI you see in Music/Podcasts and the new fluid slider styles), and that can change what UIControl forwards to target-actions. A developer writeup about UIKit changes points at internal slider changes in recent releases (see https://sebvidal.com/blog/whats-new-in-uikit-26/). There’s no public Apple doc that says “we removed UIEvent forwarding for .valueChanged”, so treat this as an API‑behavior change that you should work around or file a bug for.
If you want Apple to address it, file feedback via Feedback Assistant / Apple bug reporting. Meanwhile pick a workaround.
Quick fixes: use touch control events (.touchDown, .touchUpInside, .touchUpOutside, .touchCancel)
Short answer: stop depending on UIEvent from .valueChanged. Use control events that are meant to signal touch boundaries. They’re reliable and don’t require inspecting event.allTouches.
Minimal changes to your existing code:
// Add these targets in viewDidLoad (or where you configure the slider)
slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged) // live updates
slider.addTarget(self, action: #selector(sliderTouchDown(_:)), for: .touchDown) // began
slider.addTarget(self, action: #selector(sliderTouchUp(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel]) // ended/cancelled
Handlers:
@objc func sliderTouchDown(_ sender: UISlider) {
print("began")
// set a flag if you need to know "user is interacting"
isUserSliding = true
}
@objc func sliderTouchUp(_ sender: UISlider) {
print("ended/cancelled")
isUserSliding = false
}
@objc func sliderValueChanged(_ sender: UISlider) {
label.text = "Value: (sender.value)"
// if you want to distinguish user-driven vs programmatic changes:
if isUserSliding {
print("moved")
} else {
// programmatic change
}
}
Why this works: .touchDown and .touchUpInside/.touchUpOutside/.touchCancel are explicit control events — they tell you when the finger goes down and when it’s lifted or the system cancels the interaction. This is the same pattern used in multiple community answers (for example, see guidance on detecting slider drag end: https://stackoverflow.com/questions/9390298/iphone-how-to-detect-the-end-of-slider-drag and more notes on using touch events: https://stackoverflow.com/questions/4664506/uislider-events).
Note: you observed .allEvents sometimes delivering began/moved but not ended — that’s exactly why explicit touch events are preferable. .allEvents is convenient for debugging but not a stable contract for end/cancel detection.
Robust fix: Subclass UISlider and override tracking methods
If you need absolute control (and want the actual UITouch / UIEvent data), subclass UISlider and override the tracking lifecycle. These methods are called by the control itself and receive the real touch and event parameters:
- beginTracking(_:with:)
- continueTracking(_:with:)
- endTracking(_:with:)
- cancelTracking(with:)
A small, practical TrackingSlider:
import UIKit
final class TrackingSlider: UISlider {
enum Phase { case began, moved, ended, cancelled }
/// Closure callback — could be a delegate instead.
var onPhase: ((Phase) -> Void)?
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let handled = super.beginTracking(touch, with: event)
onPhase?(.began)
return handled
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let handled = super.continueTracking(touch, with: event)
onPhase?(.moved)
return handled
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)
onPhase?(.ended)
}
override func cancelTracking(with event: UIEvent?) {
super.cancelTracking(with: event)
onPhase?(.cancelled)
}
}
Usage in your view controller:
let slider = TrackingSlider()
slider.isContinuous = true
slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
slider.onPhase = { [weak self] phase in
switch phase {
case .began: print("began")
case .moved: print("moved")
case .ended: print("ended")
case .cancelled:print("cancelled")
}
}
Why subclassing? Because begin/continue/end/cancelTracking are the lowest-level, control-provided hooks for touch lifecycle — they will run even if a higher-level action’s UIEvent is no longer populated. The accepted Stack Overflow answers recommend this pattern when control-event forwarding isn’t sufficient (see the iOS 26 report and the suggested subclass approach: https://stackoverflow.com/questions/79793891/ios-26-uisliders-valuechanged-event-is-always-blank-and-alltouches-is-always-n).
Do remember to call super so you preserve UISlider’s built-in behavior.
Integration notes and fallbacks
- continuous vs. discrete: slider.isContinuous = true gives continuous .valueChanged events as the thumb moves. If you only care about final value, set isContinuous = false and handle the final value on touch-up events (or endTracking).
- Programmatic changes: programmatic value assignments (slider.value = x) won’t trigger touch events; use your own logic to distinguish user vs programmatic changes (an isUserSliding flag set on touchDown/touchUp is common).
- Accessibility & keyboard: keyboard/assistive interactions may not generate touch events. If accessibility support matters, also handle value change events and check whether user interaction was via touch or accessibility APIs.
- Gesture recognizers: adding a UIPanGestureRecognizer on top of a UISlider is messy and usually unnecessary; prefer control events or subclassing.
- Testing: test on devices running the iOS versions you support. Changes like this can differ between simulator and device.
- Reporting: if this behavior breaks important workflows in your app and you want Apple to change it, file a Feedback Assistant/bug report with a small sample project that reproduces the empty UIEvent forwarding.
Sources
- https://stackoverflow.com/questions/79793891/ios-26-uisliders-valuechanged-event-is-always-blank-and-alltouches-is-always-n
- https://stackoverflow.com/questions/9390298/iphone-how-to-detect-the-end-of-slider-drag
- https://stackoverflow.com/questions/43014244/uislider-value-changed-not-firing
- https://stackoverflow.com/questions/10971154/detecting-touches-on-a-uislider
- https://stackoverflow.com/questions/4664506/uislider-events
- https://sebvidal.com/blog/whats-new-in-uikit-26/
Conclusion
Don’t rely on the UIEvent forwarded into .valueChanged on iOS 26 — event.allTouches may be nil. For a quick, reliable fix add explicit touch control events (.touchDown and .touchUpInside/.touchUpOutside/.touchCancel) and keep .valueChanged for live value updates. If you want guaranteed phase information (and access to the actual UITouch/UIEvent), subclass UISlider and override beginTracking/continueTracking/endTracking/cancelTracking. If you’d like, I can convert your ViewController example into a drop-in TrackingSlider implementation (with SnapKit integration) so you can paste it into your project.