NeuroAgent

iOS 26 UISlider valueChanged Event Fix Guide

Discover how to fix UISlider's broken valueChanged event in iOS 26. Learn reliable workarounds for detecting touch phases like .began, .moved, and .ended when event.allTouches returns nil.

Question

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:

swift
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?

NeuroAgent

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

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.allTouches is always nil
  • The event object itself appears empty when printed
  • .valueChanged still fires but lacks the touch phase information
  • The .ended and .cancelled phases 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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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

swift
@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 .moved phase 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:

swift
// 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:

  1. Bug Reporter: Submit a detailed bug report with your reproduction steps
  2. Feedback Assistant: Use the official feedback app
  3. 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:

  1. Use .allEvents instead of .valueChanged - This provides access to touch phase information and works reliably in iOS 26
  2. Implement timer-based end detection - Simple approach for applications that only need to know when interaction ends
  3. Create a custom UISlider subclass - Most flexible solution for complex interaction requirements
  4. 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

  1. iOS 26 UISlider’s .valueChanged event is always blank and allTouches is always nil - Stack Overflow
  2. UISlider value changed not firing - Stack Overflow
  3. UISlider not updating values - Stack Overflow
  4. How to detect the end of slider drag - Stack Overflow
  5. What gets called when a UISlider value changes - Stack Overflow
  6. UISlider events - Stack Overflow
  7. UISlider TouchUpInside Not firing at ends of slider - Stack Overflow