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?
The issue you’re experiencing with UISlider’s .valueChanged event in iOS 26 appears to be a regression where the event parameter is no longer properly populated, resulting in event.allTouches being always nil. This prevents detection of slider interaction phases like .began, .moved, .ended, and .cancelled. Apple likely changed the internal event handling mechanism in iOS 26, breaking the previous reliable method of using event.allTouches to determine interaction phases.
Contents
- Understanding the iOS 26 UISlider Issue
- Available Workarounds
- Complete Implementation Examples
- Best Practices for Slider Interaction Detection
- When to Report to Apple
Understanding the iOS 26 UISlider Issue
The regression in iOS 26 affects how UISlider events are processed. Previously, the event parameter in .valueChanged callbacks contained proper touch information that could be used to determine the phase of the interaction. In iOS 26, this event appears to be empty or improperly initialized.
Key characteristics of this bug:
event.allTouchesis alwaysnil- The event object itself appears empty when printed
.valueChangedstill fires but lacks the touch phase information- The
.endedand.cancelledphases are particularly affected
According to developer discussions, this seems to be a change in how Apple handles touch events internally in iOS 26, possibly related to privacy or security changes in touch event handling.
Available Workarounds
1. Use .allEvents with Additional Logic
The most reliable workaround is to use .allEvents and implement custom logic to track the interaction state:
class ViewController: UIViewController {
let label = UILabel()
let slider = UISlider()
var isSliderBeingDragged = false
override func viewDidLoad() {
super.viewDidLoad()
// Setup code remains the same...
slider.addTarget(self, action: #selector(sliderValueChanged(sender:event:)), for: .allEvents)
}
@objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
label.text = "Value: \(sender.value)"
guard let touches = event.allTouches else { return }
guard let touch = touches.first else { return }
switch touch.phase {
case .began:
print("began")
isSliderBeingDragged = true
case .moved:
print("moved")
case .ended, .cancelled:
print("ended or cancelled")
isSliderBeingDragged = false
// Handle end of interaction
default:
break
}
}
}
Advantages:
- Works reliably in iOS 26
- Provides all interaction phases
- Allows custom state tracking
Disadvantages:
- More verbose than the previous approach
- Requires additional state management
2. Use UITapGestureRecognizer for End Detection
For applications that primarily need to know when the slider interaction ends, you can combine .valueChanged with a tap gesture recognizer:
class ViewController: UIViewController {
let label = UILabel()
let slider = UISlider()
var sliderInteractionTimer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
// Add tap gesture to detect when user stops interacting
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(sliderInteractionEnded))
slider.addGestureRecognizer(tapGesture)
}
@objc func sliderValueChanged(sender: UISlider) {
label.text = "Value: \(sender.value)"
// Reset timer each time value changes
sliderInteractionTimer?.invalidate()
sliderInteractionTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in
self.sliderInteractionEnded()
}
}
@objc func sliderInteractionEnded() {
print("Slider interaction ended")
// Handle end of slider interaction
sliderInteractionTimer?.invalidate()
}
}
Advantages:
- Simple implementation
- Works with existing .valueChanged approach
- Reliable end detection
Disadvantages:
- Doesn’t provide phase information
- May have slight delay (300ms in this example)
3. Override UISlider Subclass
For more complex applications, create a custom UISlider subclass that overrides the appropriate methods:
class CustomSlider: UISlider {
var touchPhase: UITouch.Phase = .ended
var interactionCallback: ((UITouch.Phase) -> Void)?
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
touchPhase = .began
interactionCallback?(touchPhase)
return super.beginTracking(touch, with: event)
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
touchPhase = .moved
interactionCallback?(touchPhase)
return super.continueTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
touchPhase = .ended
interactionCallback?(touchPhase)
super.endTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
touchPhase = .cancelled
interactionCallback?(touchPhase)
super.cancelTracking(with: event)
}
}
Then use it in your view controller:
let slider = CustomSlider()
slider.interactionCallback = { phase in
switch phase {
case .began:
print("began")
case .moved:
print("moved")
case .ended:
print("ended")
case .cancelled:
print("cancelled")
default:
break
}
}
Complete Implementation Examples
Recommended Solution: .allEvents with State Tracking
Here’s a complete, production-ready implementation:
import UIKit
import SnapKit
class ViewController: UIViewController {
let label = UILabel()
let slider = UISlider()
private var sliderInteractionState: SliderInteractionState = .idle
enum SliderInteractionState {
case idle
case interacting
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupSlider()
}
private func setupUI() {
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
label.text = "Value: 0"
}
private func setupSlider() {
slider.isContinuous = true
slider.minimumValue = 0
slider.maximumValue = 100
slider.addTarget(self, action: #selector(sliderValueChanged(sender:event:)), for: .allEvents)
}
@objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
label.text = "Value: \(Int(sender.value))"
guard let touches = event.allTouches else { return }
guard let touch = touches.first else { return }
switch touch.phase {
case .began:
handleSliderBegan()
case .moved:
handleSliderMoved()
case .ended, .cancelled:
handleSliderEnded()
default:
break
}
}
private func handleSliderBegan() {
print("Slider interaction began")
sliderInteractionState = .interacting
// Add any began-specific logic here
}
private func handleSliderMoved() {
print("Slider moved")
// Add any moved-specific logic here
}
private func handleSliderEnded() {
print("Slider interaction ended")
sliderInteractionState = .idle
// Add any ended-specific logic here
// This is where you'd typically save the final value
}
}
Alternative: Timer-Based End Detection
For simpler use cases:
class SimpleSliderViewController: UIViewController {
let label = UILabel()
let slider = UISlider()
private var interactionTimer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupSlider()
}
private func setupUI() {
// ... (same UI setup as above)
}
private func setupSlider() {
slider.isContinuous = true
slider.minimumValue = 0
slider.maximumValue = 100
slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
}
@objc func sliderValueChanged(sender: UISlider) {
label.text = "Value: \(Int(sender.value))"
// Reset timer to detect when user stops interacting
interactionTimer?.invalidate()
interactionTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in
self.sliderInteractionEnded()
}
}
@objc private func sliderInteractionEnded() {
print("Slider interaction ended")
interactionTimer?.invalidate()
// Handle end of interaction
}
}
Best Practices for Slider Interaction Detection
1. Choose the Right Approach Based on Your Needs
| Requirement | Recommended Approach | Complexity |
|---|---|---|
| Need all phases | .allEvents with state tracking | Medium |
| Only need end detection | Timer-based approach | Low |
| Custom behavior needed | UISlider subclass | High |
| Legacy compatibility | Check iOS version and use appropriate method | Medium |
2. Handle Edge Cases
@objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
// Always check for nil values
guard let touches = event.allTouches, let touch = touches.first else {
// Fallback behavior for iOS 26 or other edge cases
handleFallbackSliderInteraction(sender)
return
}
// Normal phase handling
switch touch.phase {
case .began:
handleSliderBegan()
case .moved:
handleSliderMoved()
case .ended, .cancelled:
handleSliderEnded()
default:
break
}
}
private func handleFallbackSliderInteraction(_ sender: UISlider) {
// For iOS 26 or when event.allTouches is nil
// You could use a timer or other logic here
print("Fallback slider interaction handling")
}
3. Performance Considerations
- Avoid heavy operations in
.movedphase handlers - Consider debouncing rapid value changes for performance-intensive operations
- Use state tracking to prevent redundant processing
4. Testing Strategy
Test your solution across different iOS versions:
// Version-specific handling
if #available(iOS 15.0, *) {
// iOS 15+ specific behavior
} else {
// Legacy behavior for older iOS versions
}
When to Report to Apple
If none of these workarounds meet your needs, consider reporting this issue to Apple through:
- Bug Reporter: Submit a detailed bug report with your reproduction steps
- Feedback Assistant: Use the official feedback app
- Developer Forums: Discuss the issue with other developers
When reporting, include:
- Your code reproduction case
- iOS version information
- Device information
- Expected behavior vs. actual behavior
- The workarounds you’ve tried
Conclusion
The iOS 26 UISlider event handling regression affects developers who rely on touch phase detection through the event.allTouches property. While this breaks previous implementation patterns, several reliable workarounds are available:
- Use .allEvents instead of .valueChanged - This provides access to touch phase information and works reliably in iOS 26
- Implement timer-based end detection - Simple approach for applications that only need to know when interaction ends
- Create a custom UISlider subclass - Most flexible solution for complex interaction requirements
- Combine multiple approaches - Use .allEvents for real-time phases and timers for additional end detection
For most applications, the .allEvents approach provides the best balance of reliability and functionality. As with any iOS regression, it’s important to test thoroughly and consider the specific needs of your application when choosing a workaround solution.
Remember to monitor Apple’s documentation and release notes for potential fixes in future iOS updates, as this may be addressed in a subsequent version.
Sources
- iOS 26 UISlider’s .valueChanged event is always blank and allTouches is always nil - Stack Overflow
- UISlider value changed not firing - Stack Overflow
- UISlider not updating values - Stack Overflow
- How to detect the end of slider drag - Stack Overflow
- What gets called when a UISlider value changes - Stack Overflow
- UISlider events - Stack Overflow
- UISlider TouchUpInside Not firing at ends of slider - Stack Overflow