iOS 26: UISlider .valueChanged — allTouches nil
В iOS 26 событие .valueChanged UISlider приходит пустым, allTouches == nil. Причины проблемы, рабочие обходы: touch-события, подкласс UISlider, тестирование. Обновление ios 26 и фиксы для разработчиков.
В iOS 26 событие .valueChanged UISlider всегда пустое, а allTouches всегда равно nil, невозможно определить конец перетаскивания слайдера
У меня есть приложение, которое работало нормально до iOS 26. В моем приложении я использую событие .valueChanged UISlider для получения фазы и выполнения различных действий в зависимости от того, является ли она .began, .moved или .ended.
Это распространенное решение для определения фаз взаимодействия со слайдером. Ниже приведен мой простой пример:
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.value), (event)")
label.text = "Значение: (sender.value)"
switch event.allTouches?.first?.phase {
case .began:
print("начато")
case .moved:
print("перемещено")
case .ended:
print("завершено")
case .cancelled:
print("отменено")
default:
break
}
}
}
Ранее это работало нормально. Но в iOS 26 событие, передаваемое в мою функцию sliderValueChanged, проблематично. Оно не равно nil, но также не является корректным событием. Например, операция print просто выводит пустой текст для события. А установка точки останова показывает его как пустой объект события.
Из-за этого в iOS 26 event.allTouches всегда равно nil. Поэтому я не могу определить, какая фаза события.
Это ошибка? Как можно обойти эту проблему?
Я обнаружил, что если я переключаюсь с .valueChanged на .allEvents, то получаю событие для фаз began и moved. Однако я не получаю фазы ended или cancelled.
Как получить и эти фазы?
В iOS 26 действительно наблюдаются изменения: в обработчике UISlider для события .valueChanged объект UIEvent часто приходит без прикосновений — event.allTouches == nil, и на его основе нельзя надёжно определять фазы .began/.moved/.ended. Рабочие обходы — не полагаться на event.allTouches в .valueChanged, а использовать явные UIControl.Event (.touchDown, .touchUpInside/.touchUpOutside/.touchCancel, .touchDragInside) или подклассировать UISlider и переопределить beginTracking/continueTracking/endTracking. Также стоит воспроизвести баг на нескольких устройствах и, при подтверждении, отправить отчёт в Apple (Feedback Assistant).
Содержание
- Причина: iOS 26 и UISlider .valueChanged — почему event.allTouches == nil
- Быстрые обходы: подписка на touch-события и проверка isTracking
- Надёжный подход: подкласс UISlider с переопределением tracking-методов
- Ещё варианты: UIGestureRecognizer и комбинации методов
- Тестирование и отчёт об ошибке (как воспроизвести и что приложить)
- Пример полного кода: addTarget + subclass — готовые паттерны
- Источники
- Заключение
Причина: iOS 26 и UISlider .valueChanged — почему event.allTouches == nil
Коротко — поведение выглядит как изменение в том, как iOS 26 формирует/передаёт UIEvent в action-методы для .valueChanged. Пара причин, которые надо учитывать одновременно:
.valueChanged— это контрольное событие UIControl; оно может вызываться не только в ответ на физическое прикосновение (touch), но и программно или через Accessibility/клавиатуру. В таких случаяхevent.allTouchesбудет пустым.- В iOS 26, судя по репортам, система иногда передаёт
UIEventбез ссылок на touches даже во время трекинга; поэтому ваш код, основанный наevent.allTouches?.first?.phase, перестаёт работать как раньше. - Пока нет подтверждения из релиз-нот Apple (нужен отдельный поиск/bug report), логично считать это либо регрессией, либо намеренным поведением (оптимизация/унификация генерации
UIEvent). Для оценки популярности ситуации см. данные по iOS 26 в поисковой аналитике (Yandex Wordstat — ios 26).
В итоге: полагаться на event.allTouches в handler’e .valueChanged больше нельзя считать надёжным кросс‑версийным решением.
Быстрые обходы: подписка на touch-события и проверка isTracking
Если нужно быстро вернуть контроль фаз (began/moved/ended/cancelled), используйте стандартные UIControl-события и свойство isTracking. Это максимально просто и не требует подклассов.
Пример:
slider.isContinuous = true
slider.addTarget(self, action: #selector(sliderTouchDown(_:)), for: .touchDown)
slider.addTarget(self, action: #selector(sliderValueChanged(_:event:)), for: .valueChanged)
slider.addTarget(self, action: #selector(sliderTouchUp(_:)), for: [.touchUpInside, .touchUpOutside])
slider.addTarget(self, action: #selector(sliderTouchCancel(_:)), for: .touchCancel)
И реализация:
@objc func sliderTouchDown(_ sender: UISlider) {
print("начато")
}
@objc func sliderValueChanged(_ sender: UISlider, event: UIEvent?) {
if sender.isTracking {
print("перемещено — текущие значения: (sender.value)")
} else {
// значение изменилось без трекинга (программно или через A11Y)
print("valueChanged без трекинга")
}
}
@objc func sliderTouchUp(_ sender: UISlider) {
print("завершено — финальное значение: (sender.value)")
}
@objc func sliderTouchCancel(_ sender: UISlider) {
print("отменено")
}
Почему это работает:
.touchDown/.touchUpInside/.touchUpOutside/.touchCancel— именно те события, которые отражают начало и конец взаимодействия с контролом.sender.isTrackingдаёт простую проверку: если true — пользователь сейчас тащит ползунок; если false — изменение пришло не от текущего трека.
Недостатки: если система в iOS 26/устройстве ведёт себя нестандартно, потребуются дополнительные проверки (например, когда трекинг прерывается системно). Но в большинстве случаев этот набор покрывает сценарии начала/перемещения/окончания.
Надёжный подход: подкласс UISlider с переопределением tracking-методов
Если нужно максимально надёжно получать фазы от самого контрола, лучше подклассировать UISlider и обрабатывать beginTracking/continueTracking/endTracking/cancelTracking. Эти методы получают UITouch (и UIEvent?) напрямую — и их поведение стабильнее при системных изменениях.
Пример класса-обёртки:
class TrackingSlider: UISlider {
var onDragBegan: (() -> Void)?
var onDragChanged: ((Float) -> Void)?
var onDragEnded: ((Float) -> Void)?
var onDragCancelled: (() -> Void)?
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let result = super.beginTracking(touch, with: event)
onDragBegan?()
return result
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let result = super.continueTracking(touch, with: event)
onDragChanged?(self.value)
return result
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)
onDragEnded?(self.value)
}
override func cancelTracking(with event: UIEvent?) {
super.cancelTracking(with: event)
onDragCancelled?()
}
}
Применение:
let slider = TrackingSlider()
slider.onDragBegan = { print("начато") }
slider.onDragChanged = { value in print("перемещено: (value)") }
slider.onDragEnded = { value in print("завершено: (value)") }
slider.onDragCancelled = { print("отменено") }
Плюсы:
- Вы получаете точки входа именно в жизненный цикл трекинга контрола.
- Менее зависимо от того, какой
UIEventOS вкладывает в action-метод. - Удобно для передачи в архитектуру MVVM/closure-коллбэков.
Учтите: endTracking(_:, with:) может получать touch == nil в редких сценариях, поэтому проверяйте значение и/или используйте внутреннюю переменную состояния.
Ещё варианты: UIGestureRecognizer и комбинации методов
Если подклассировать нельзя (например, вы используете готовую библиотеку), есть другие подходы:
- UIPanGestureRecognizer на слайдере:
- Добавьте
UIPanGestureRecognizer(cancelsTouchesInView: false)к слайдеру и отслеживайте.began/.changed/.ended/.cancelled. Минус — возможные конфликты с внутренней обработкойUISlider. - Наблюдение за
isTrackingциклически (таймер) — не рекомендую (хрупко). Лучше react на события. - Перехват
touchesBegan/...в родительском view — работает, но сложнее покрыть все случаи и аккуратно не ломать другие элементы.
Если кратко: сначала пробуйте комбинацию addTarget + isTracking, затем — подкласс. Gesture‑подходы — запасной вариант.
Тестирование и отчёт об ошибке (как воспроизвести и что приложить)
Чтобы понять, баг это или изменение поведения, сделайте так:
- Минимальный проект: новый Single View App, разместите
UISliderи скопируйте ваш минимальный handler (тот, что в вопросе). - Прогоните на реальных устройствах с iOS 25.x и iOS 26.x (симулятор не всегда отражает нюансы ввода).
- Повторите сценарии: тащение, быстрый свайп, отпускание внутри/снаружи, программное изменение
slider.value = ..., включение Accessibility (VoiceOver), использование мыши/трекпада. - Соберите лог, скриншоты, небольшой sample project и шаги воспроизведения.
- Если поведение повторилось — отправьте баг в Apple через Feedback Assistant (описание + sample project + логи). Укажите точные версии iOS 26 (26.0 / 26.x), модель устройства и шаги.
Дополнительно: мониторьте обсуждения и отчёты сообщества — сейчас тема iOS 26 активно ищется пользователями (см. Yandex Wordstat — ios 26). По точным терминам разработки (например, allTouches, UISlider) поисковая активность мала, но это проблема именно разработчиков (Yandex Wordstat — allTouches, Yandex Wordstat — uislider).
Пример полного кода: addTarget + subclass — готовые паттерны
Полный рабочий пример, комбинирующий быстрый обход (touch events) и подкласс:
import UIKit
class TrackingSlider: UISlider {
var onDragBegan: (() -> Void)?
var onDragChanged: ((Float) -> Void)?
var onDragEnded: ((Float) -> Void)?
var onDragCancelled: (() -> Void)?
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let started = super.beginTracking(touch, with: event)
onDragBegan?()
return started
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let cont = super.continueTracking(touch, with: event)
onDragChanged?(self.value)
return cont
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)
onDragEnded?(self.value)
}
override func cancelTracking(with event: UIEvent?) {
super.cancelTracking(with: event)
onDragCancelled?()
}
}
class ViewController: UIViewController {
let slider = TrackingSlider()
let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
slider.minimumValue = 0
slider.maximumValue = 100
slider.isContinuous = true
slider.onDragBegan = { [weak self] in
self?.label.text = "Начато"
}
slider.onDragChanged = { [weak self] value in
self?.label.text = "Перемещено: (value)"
}
slider.onDragEnded = { [weak self] value in
self?.label.text = "Завершено: (value)"
}
slider.onDragCancelled = { [weak self] in
self?.label.text = "Отменено"
}
slider.addTarget(self, action: #selector(valueChanged(_:event:)), for: .valueChanged)
// layout omitted
}
@objc func valueChanged(_ sender: UISlider, event: UIEvent?) {
// fallback: если вдруг трекинг не используется, проверяем isTracking
if sender.isTracking {
// уже обрабатывается через callbacks
return
} else {
// программное или другое изменение
label.text = "Изменено программно: (sender.value)"
}
}
}
Этот паттерн даёт устойчивую обработку фаз и при этом сохраняет совместимость с программной установкой значения.
Источники
- User Report — репродуцируемый кейс (предоставлен автором) — URL: N/A
- Yandex Wordstat — ios 26 (поисковая аналитика): https://wordstat.yandex.ru/
- Yandex Wordstat — allTouches: https://wordstat.yandex.ru/
- Yandex Wordstat — uislider: https://wordstat.yandex.ru/
Заключение
Кратко: в iOS 26 не стоит полагаться на event.allTouches внутри обработчика .valueChanged у UISlider — вместо этого используйте подписку на явные UIControl.Event (.touchDown, .touchUpInside/.touchUpOutside, .touchCancel, .touchDragInside) и/или подклассируйте UISlider, переопределяя beginTracking/continueTracking/endTracking. Это вернёт вам надёжное обнаружение фаз began/moved/ended независимо от того, какие поля UIEvent заполняет система в конкретной сборке iOS 26. Параллельно воспроизведите баг на реальных устройствах и, если воспроизводится — отправьте отчёт в Apple.