НейроАгент

Руководство по исправлению события valueChanged UISlider в iOS 26

Узнайте, как исправить сломанное событие valueChanged UISlider в iOS 26. Изучите надежные обходные пути для обнаружения фаз касания, таких как .began, .moved и .ended, когда event.allTouches возвращает nil.

Вопрос

В iOS 26 событие .valueChanged UISlider всегда пустое, а allTouches всегда равно nil, невозможно определить конец перетаскивания слайдера

У меня есть приложение, которое работало нормально до iOS 26. В моем приложении я использую событие .valueChanged UISlider для получения фазы и выполнения различных действий в зависимости от того, является ли она .began, .moved или .ended.

Это распространенное решение для определения фаз взаимодействия со слайдером. Ниже приведен мой простой пример:

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.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.

Как получить и эти фазы?

НейроАгент

Проблема с UISlider в iOS 26

Проблема, с которой вы сталкиваетесь с событием .valueChanged UISlider в iOS 26, является регрессией, при котором параметр события больше не правильно заполняется, что приводит к тому, что event.allTouches всегда равен nil. Это предотвращает обнаружение фаз взаимодействия с ползунком, таких как .began, .moved, .ended и .cancelled. Скорее всего, Apple изменил внутренний механизм обработки событий в iOS 26, нарушив предыдущий надежный метод использования event.allTouches для определения фаз взаимодействия.

Содержание

Понимание проблемы с UISlider в iOS 26

Регрессия в iOS 26 влияет на то, как обрабатываются события UISlider. Ранее параметр event в обратных вызовах .valueChanged содержал правильную информацию о касании, которую можно было использовать для определения фазы взаимодействия. В iOS 26 это событие кажется пустым или неправильно инициализированным.

Основные характеристики этой ошибки:

  • event.allTouches всегда равен nil
  • Объект события сам по себе кажется пустым при выводе
  • Событие .valueChanged все еще срабатывает, но не содержит информации о фазе касания
  • Фазы .ended и .cancelled особенно затронуты

Согласно обсуждениям разработчиков, это изменение, похоже, связано с тем, как Apple обрабатывает события касания внутренне в iOS 26, возможно, связанным с изменениями в конфиденциальности или безопасности обработки событий касания.

Доступные обходные пути

1. Использование .allEvents с дополнительной логикой

Самый надежный обходной путь - использовать .allEvents и реализовать пользовательскую логику для отслеживания состояния взаимодействия:

swift
class ViewController: UIViewController {
    
    let label = UILabel()
    let slider = UISlider()
    var isSliderBeingDragged = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Код настройки остается тем же...
        slider.addTarget(self, action: #selector(sliderValueChanged(sender:event:)), for: .allEvents)
    }

    @objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
        label.text = "Значение: \(sender.value)"
        
        guard let touches = event.allTouches else { return }
        guard let touch = touches.first else { return }
        
        switch touch.phase {
        case .began:
            print("начато")
            isSliderBeingDragged = true
        case .moved:
            print("перемещено")
        case .ended, .cancelled:
            print("завершено или отменено")
            isSliderBeingDragged = false
            // Обработка завершения взаимодействия
        default:
            break
        }
    }
}

Преимущества:

  • Надежно работает в iOS 26
  • Предоставляет все фазы взаимодействия
  • Позволяет настраивать отслеживание состояния

Недостатки:

  • Более многословно, чем предыдущий подход
  • Требует дополнительного управления состоянием

2. Использование UITapGestureRecognizer для обнаружения завершения

Для приложений, в которых в основном необходимо знать, когда взаимодействие с ползунком завершается, можно объединить .valueChanged с распознавателем жестов касания:

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)
        
        // Добавляем жест касания для обнаружения, когда пользователь перестает взаимодействовать
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(sliderInteractionEnded))
        slider.addGestureRecognizer(tapGesture)
    }

    @objc func sliderValueChanged(sender: UISlider) {
        label.text = "Значение: \(sender.value)"
        
        // Сбрасываем таймер при каждом изменении значения
        sliderInteractionTimer?.invalidate()
        sliderInteractionTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in
            self.sliderInteractionEnded()
        }
    }
    
    @objc func sliderInteractionEnded() {
        print("Взаимодействие с ползунком завершено")
        // Обработка завершения взаимодействия с ползунком
        sliderInteractionTimer?.invalidate()
    }
}

Преимущества:

  • Простая реализация
  • Работает с существующим подходом .valueChanged
  • Надежное обнаружение завершения

Недостатки:

  • Не предоставляет информацию о фазе
  • Может иметь небольшую задержку (300 мс в этом примере)

3. Переопределение подкласса UISlider

Для более сложных приложений создайте пользовательский подкласс UISlider, который переопределяет соответствующие методы:

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)
    }
}

Затем используйте его в вашем контроллере представления:

swift
let slider = CustomSlider()
slider.interactionCallback = { phase in
    switch phase {
    case .began:
        print("начато")
    case .moved:
        print("перемещено")
    case .ended:
        print("завершено")
    case .cancelled:
        print("отменено")
    default:
        break
    }
}

Полные примеры реализации

Рекомендуемое решение: .allEvents с отслеживанием состояния

Вот полный, готовый к использованию пример:

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 = "Значение: 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 = "Значение: \(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("Взаимодействие с ползунком начато")
        sliderInteractionState = .interacting
        // Добавьте любую специфичную для начала логику здесь
    }
    
    private func handleSliderMoved() {
        print("Ползунок перемещен")
        // Добавьте любую специфичную для перемещения логику здесь
    }
    
    private func handleSliderEnded() {
        print("Взаимодействие с ползунком завершено")
        sliderInteractionState = .idle
        // Добавьте любую специфичную для завершения логику здесь
        // Здесь обычно сохраняется конечное значение
    }
}

Альтернатива: Обнаружение завершения на основе таймера

Для более простых случаев использования:

swift
class SimpleSliderViewController: UIViewController {
    
    let label = UILabel()
    let slider = UISlider()
    private var interactionTimer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupUI()
        setupSlider()
    }
    
    private func setupUI() {
        // ... (та же настройка UI, что и выше)
    }
    
    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 = "Значение: \(Int(sender.value))"
        
        // Сбрасываем таймер для обнаружения, когда пользователь перестает взаимодействовать
        interactionTimer?.invalidate()
        interactionTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in
            self.sliderInteractionEnded()
        }
    }
    
    @objc private func sliderInteractionEnded() {
        print("Взаимодействие с ползунком завершено")
        interactionTimer?.invalidate()
        // Обработка завершения взаимодействия
    }
}

Лучшие практики для обнаружения взаимодействия с ползунком

1. Выбор подхода в зависимости от ваших потребностей

Требование Рекомендуемый подход Сложность
Нужны все фазы .allEvents с отслеживанием состояния Средняя
Требуется только обнаружение завершения Подход на основе таймера Низкая
Требуется пользовательское поведение Подкласс UISlider Высокая
Совместимость с legacy Проверка версии iOS и использование соответствующего метода Средняя

2. Обработка крайних случаев

swift
@objc func sliderValueChanged(sender: UISlider, event: UIEvent) {
    // Всегда проверяйте на nil значения
    guard let touches = event.allTouches, let touch = touches.first else {
        // Резервное поведение для iOS 26 или других крайних случаев
        handleFallbackSliderInteraction(sender)
        return
    }
    
    // Обработка обычных фаз
    switch touch.phase {
    case .began:
        handleSliderBegan()
    case .moved:
        handleSliderMoved()
    case .ended, .cancelled:
        handleSliderEnded()
    default:
        break
    }
}

private func handleFallbackSliderInteraction(_ sender: UISlider) {
    // Для iOS 26 или когда event.allTouches равен nil
    // Здесь можно использовать таймер или другую логику
    print("Резервное взаимодействие с ползунком")
}

3. Соображения производительности

  • Избегайте трудоемких операций в обработчиках фазы .moved
  • Рассмотрите возможность дебаунсинга быстрых изменений значения для ресурсоемких операций
  • Используйте отслеживание состояния для предотвращения избыточной обработки

4. Стратегия тестирования

Тестируйте ваше решение на разных версиях iOS:

swift
// Специфичная для версии обработка
if #available(iOS 15.0, *) {
    // Поведение для iOS 15+
} else {
    // Устаревшее поведение для более старых версий iOS
}

Когда сообщать в Apple

Если ни один из этих обходных путей не удовлетворяет вашим потребностям, рассмотрите возможность сообщения об этой проблеме Apple через:

  1. Bug Reporter: Отправьте подробный отчет об ошибке с шагами воспроизведения
  2. Feedback Assistant: Используйте официальное приложение для обратной связи
  3. Developer Forums: Обсудите проблему с другими разработчиками

При сообщении включайте:

  • Ваш случай воспроизведения кода
  • Информацию о версии iOS
  • Информацию об устройстве
  • Ожидаемое поведение против фактического поведения
  • Обходные пути, которые вы пробовали

Заключение

Регрессия в обработке событий UISlider в iOS 26 влияет на разработчиков, которые полагаются на обнаружение фазы касания через свойство event.allTouches. Хотя это нарушает предыдущие шаблоны реализации, доступно несколько надежных обходных путей:

  1. Используйте .allEvents вместо .valueChanged - Это обеспечивает доступ к информации о фазе касания и надежно работает в iOS 26
  2. Реализуйте обнаружение завершения на основе таймера - Простой подход для приложений, которым нужно знать только когда взаимодействие завершается
  3. Создайте пользовательский подкласс UISlider - Наиболее гибкое решение для сложных требований к взаимодействию
  4. Комбинируйте несколько подходов - Используйте .allEvents для реальных фаз и таймеры для дополнительного обнаружения завершения

Для большинства приложений подход .allEvents обеспечивает лучший баланс надежности и функциональности. Как и в случае с любым регрессом iOS, важно тщательно тестировать и учитывать конкретные потребности вашего приложения при выборе обходного решения.

Помните, что следите за документацией Apple и примечаниями к релизам для потенциальных исправлений в будущих обновлениях iOS, так как это может быть исправлено в последующей версии.

Источники

  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