RunLoop – это бесконечный цикл обработки событий в потоке. Он:
- Обрабатывает события (тачи, таймеры, источники данных)
- Управляет входящими событиями
- Переводит поток в режим ожидания, когда событий нет (экономия CPU)
Главный поток (main thread) всегда имеет RunLoop, который:
- Обрабатывает UI-события
- Запускает таймеры
- Обрабатывает асинхронные callbacks
- Управляет анимациями
// Псевдокод работы RunLoop
while (true) {
// 1. Получить события из источников
let event = waitForEvent()
// 2. Обработать событие
handleEvent(event)
// 3. Если нет событий - спать
if noEvents {
sleep()
}
}RunLoop работает в разных режимах:
// Основные режимы
RunLoop.Mode.default // Обычный режим (большинство событий)
RunLoop.Mode.tracking // Режим трекинга (когда скроллишь ScrollView)
RunLoop.Mode.common // Общие события для обоих режимовПроблема: Таймер, добавленный в .default, не будет работать во время скролла (.tracking).
Решение:
// ❌ Плохо: не будет работать при скролле
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
print("Tick")
}
// ✅ Хорошо: работает всегда
let timer = Timer(timeInterval: 1.0, repeats: true) { _ in
print("Tick")
}
RunLoop.current.add(timer, forMode: .common)class TimerViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
// Создаём таймер, который работает даже при скролле
timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateUI()
}
RunLoop.current.add(timer!, forMode: .common)
}
func updateUI() {
print("Timer tick")
// Обновление UI
}
deinit {
timer?.invalidate()
}
}// Получить RunLoop главного потока
let mainRunLoop = RunLoop.main
// Получить RunLoop текущего потока
let currentRunLoop = RunLoop.current
// Запустить RunLoop (обычно не нужно для main thread)
RunLoop.current.run()First Responder – это объект в Responder Chain, который первым получает право обработать событие (тач, клавиатура, shake и т.д.).
События передаются по цепочке:
UIButton → UIView → UIViewController.view → UIViewController → UIWindow → UIApplication → AppDelegate
Если объект не обработал событие, оно передаётся следующему в цепочке.
┌─────────────┐
│ UIButton │ ← First Responder (если он активен)
└──────┬──────┘
│ nextResponder
▼
┌─────────────┐
│ UIView │
└──────┬──────┘
│ nextResponder
▼
┌─────────────┐
│UIViewController│
└──────┬──────┘
│ nextResponder
▼
┌─────────────┐
│ UIWindow │
└──────┬──────┘
│ nextResponder
▼
┌─────────────┐
│UIApplication│
└──────┬──────┘
│ nextResponder
▼
┌─────────────┐
│ AppDelegate │
└─────────────┘
class MyTextField: UITextField {
override func becomeFirstResponder() -> Bool {
print("Стал first responder - клавиатура появится")
return super.becomeFirstResponder()
}
override func resignFirstResponder() -> Bool {
print("Перестал быть first responder - клавиатура скроется")
return super.resignFirstResponder()
}
}
// Использование
textField.becomeFirstResponder() // Показать клавиатуру
textField.resignFirstResponder() // Скрыть клавиатуру
// Проверка
if textField.isFirstResponder {
print("TextField is first responder")
}class ShakeDetectorView: UIView {
override var canBecomeFirstResponder: Bool {
return true
}
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
print("Device shaken!")
// Показать alert, undo и т.п.
}
}
}
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Стать first responder для получения shake events
becomeFirstResponder()
}
override var canBecomeFirstResponder: Bool {
return true
}
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
showUndoAlert()
}
}
}class DocumentViewController: UIViewController {
override var canBecomeFirstResponder: Bool {
return true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
becomeFirstResponder()
}
// Кастомное действие
@objc func deleteDocument() {
print("Delete document")
}
@objc func shareDocument() {
print("Share document")
}
// Определяем, какие действия доступны
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(deleteDocument) || action == #selector(shareDocument) {
return true
}
return super.canPerformAction(action, withSender: sender)
}
}class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Добавляем gesture recognizer для скрытия клавиатуры
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
tapGesture.cancelsTouchesInView = false
view.addGestureRecognizer(tapGesture)
}
@objc func dismissKeyboard() {
view.endEditing(true)
}
}class CustomView: UIView {
// 1. Тач начался
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
print("Touch began at: \(location)")
}
// 2. Тач движется
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
print("Touch moved to: \(location)")
}
// 3. Тач закончился
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
print("Touch ended at: \(location)")
}
// 4. Тач отменён (например, звонок, системный alert)
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Touch cancelled")
}
}Как система определяет, какая view должна обработать тач:
class CustomView: UIView {
// 1. Проверка, попадает ли точка в bounds
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// Кастомная логика: например, расширить область касания
let expandedBounds = bounds.insetBy(dx: -20, dy: -20)
return expandedBounds.contains(point)
}
// 2. Поиск самой глубокой view, которая может обработать тач
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Проверка: видима ли view
if isHidden || alpha < 0.01 || !isUserInteractionEnabled {
return nil
}
// Проверка: попадает ли точка
if !self.point(inside: point, with: event) {
return nil
}
// Проверка всех subviews (в обратном порядке - сверху вниз)
for subview in subviews.reversed() {
let convertedPoint = subview.convert(point, from: self)
if let hitView = subview.hitTest(convertedPoint, with: event) {
return hitView
}
}
// Если ни одна subview не обработала - возвращаем себя
return self
}
}class ExpandedButton: UIButton {
var touchAreaInsets = UIEdgeInsets(top: -20, left: -20, bottom: -20, right: -20)
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let expandedBounds = bounds.inset(by: touchAreaInsets)
return expandedBounds.contains(point)
}
}class MultiTouchView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
isMultipleTouchEnabled = true // Включить множественные касания
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Number of touches: \(touches.count)")
for touch in touches {
let location = touch.location(in: self)
print("Touch at: \(location)")
}
}
}- Hit Testing - система определяет, какая view должна получить событие
- Gesture Recognizers - распознаватели жестов могут перехватить событие
- Touch Events - если gesture не перехватил, событие попадает в
touchesBegan/Moved/Ended - Responder Chain - если view не обработала, событие идёт вверх по цепочке
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Tap
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tapGesture.numberOfTapsRequired = 1
view.addGestureRecognizer(tapGesture)
// Double Tap
let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap))
doubleTapGesture.numberOfTapsRequired = 2
view.addGestureRecognizer(doubleTapGesture)
// Tap должен ждать double tap
tapGesture.require(toFail: doubleTapGesture)
// Pan (drag)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
view.addGestureRecognizer(panGesture)
// Pinch
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch))
view.addGestureRecognizer(pinchGesture)
// Long Press
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
longPressGesture.minimumPressDuration = 0.5
view.addGestureRecognizer(longPressGesture)
}
@objc func handleTap(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: view)
print("Tapped at: \(location)")
}
@objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
print("Double tapped")
}
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: view)
let velocity = gesture.velocity(in: view)
switch gesture.state {
case .began:
print("Pan began")
case .changed:
print("Panned: \(translation), velocity: \(velocity)")
case .ended:
print("Pan ended")
default:
break
}
// Сбросить translation после обработки
gesture.setTranslation(.zero, in: view)
}
@objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
print("Pinch scale: \(gesture.scale), velocity: \(gesture.velocity)")
if gesture.state == .ended {
gesture.scale = 1.0 // Сброс
}
}
@objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
if gesture.state == .began {
print("Long press started")
}
}
}class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
view.addSubview(button)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(viewTapped))
tapGesture.delegate = self
view.addGestureRecognizer(tapGesture)
}
@objc func buttonTapped() {
print("Button tapped")
}
@objc func viewTapped() {
print("View tapped")
}
}
extension ViewController: UIGestureRecognizerDelegate {
// Должен ли gesture начать распознавание
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
// Должен ли gesture получить тач
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
// Не обрабатывать gesture, если тач по кнопке
return !(touch.view is UIButton)
}
// Могут ли два gesture работать одновременно
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true // Оба gesture могут распознаваться одновременно
}
}// Gesture A должен провалиться, чтобы Gesture B начал работу
gestureB.require(toFail: gestureA)
// Пример: single tap ждёт double tap
singleTapGesture.require(toFail: doubleTapGesture)class AnimationViewController: UIViewController {
var displayLink: CADisplayLink?
var startTime: CFTimeInterval = 0
override func viewDidLoad() {
super.viewDidLoad()
// Вызывается каждый кадр (60 FPS или 120 FPS на ProMotion)
displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink?.add(to: .current, forMode: .default)
}
@objc func update(displayLink: CADisplayLink) {
if startTime == 0 {
startTime = displayLink.timestamp
}
let elapsed = displayLink.timestamp - startTime
print("Frame update at: \(elapsed)s")
// Обновление анимации
updateAnimation(progress: elapsed)
}
func updateAnimation(progress: CFTimeInterval) {
// Кастомная анимация
}
deinit {
displayLink?.invalidate()
}
}class FileMonitor {
private var source: DispatchSourceFileSystemObject?
func monitorFile(at url: URL) {
let fileHandle = try! FileHandle(forReadingFrom: url)
source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: fileHandle.fileDescriptor,
eventMask: .write,
queue: .main
)
source?.setEventHandler { [weak self] in
print("File changed")
self?.handleFileChange()
}
source?.resume()
}
func handleFileChange() {
// Обработка изменения файла
}
}class NotificationManager {
func setup() {
// Регистрация на уведомление
NotificationCenter.default.addObserver(
self,
selector: #selector(dataLoaded),
name: NSNotification.Name("DataLoaded"),
object: nil
)
// Keyboard notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
}
@objc func dataLoaded(notification: Notification) {
print("Data loaded notification received")
if let data = notification.userInfo?["data"] as? String {
print("Data: \(data)")
}
}
@objc func keyboardWillShow(notification: Notification) {
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
print("Keyboard will show with height: \(keyboardFrame.height)")
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
// Отправка уведомления
NotificationCenter.default.post(
name: NSNotification.Name("DataLoaded"),
object: nil,
userInfo: ["data": "Hello"]
)Q: Что такое RunLoop?
A: Бесконечный цикл обработки событий в потоке. Обрабатывает UI-события, таймеры, источники данных. Главный поток всегда имеет RunLoop для UI.
Q: Почему таймер перестаёт работать при скролле UIScrollView?
A: RunLoop переключается из .default в .tracking режим во время скролла. Нужно добавить таймер в .common режим с помощью RunLoop.current.add(timer, forMode: .common).
Q: Что такое First Responder?
A: Объект в Responder Chain, который первым получает право обработать событие (например, UITextField становится first responder при показе клавиатуры).
Q: Как работает Responder Chain?
A: События передаются по цепочке: View → ViewController → Window → Application → AppDelegate, пока кто-то не обработает событие или цепочка не закончится.
Q: Что такое Hit Testing?
A: Процесс определения, какая view должна обработать тач. Система вызывает hitTest(_:with:) и point(inside:with:), проходя по иерархии view сверху вниз (от самой верхней к самой глубокой).
Q: В каком порядке обрабатываются touch события?
A: touchesBegan → touchesMoved (может вызываться много раз) → touchesEnded или touchesCancelled (при прерывании, например звонком).
Q: Как gesture recognizer взаимодействует с touch events?
A: Gesture recognizer перехватывает события перед touchesBegan/Moved/Ended. Можно контролировать поведение через UIGestureRecognizerDelegate (например, отменить gesture для определённых view).
Q: Для чего используется CADisplayLink?
A: Для плавных анимаций, синхронизированных с частотой обновления экрана (60 FPS или 120 FPS на ProMotion). Вызывается каждый кадр.
Q: Как скрыть клавиатуру по тапу вне TextField?
A: Добавить UITapGestureRecognizer на view и вызвать view.endEditing(true) или textField.resignFirstResponder().
Q: Как обработать shake событие?
A: Переопределить motionEnded(_:with:) в view или view controller, который стал first responder через becomeFirstResponder(). Проверить motion == .motionShake.
Q: В каком порядке обрабатываются события?
A: 1) Hit Testing определяет целевую view, 2) Gesture Recognizers пытаются перехватить, 3) Touch Events (touchesBegan и т.д.), 4) Responder Chain, если событие не обработано.
Q: Как расширить область касания кнопки?
A: Переопределить point(inside:with:) и вернуть true для расширенной области:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let expandedBounds = bounds.insetBy(dx: -20, dy: -20)
return expandedBounds.contains(point)
}Q: Как сделать, чтобы два gesture recognizer работали одновременно?
A: Реализовать UIGestureRecognizerDelegate и вернуть true в методе gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:).
Q: Что делает require(toFail:) для gesture recognizers?
A: Задаёт приоритет: один gesture должен провалиться, чтобы другой начал работу. Например, single tap ждёт, пока не станет ясно, что это не double tap.
Q: Какие есть RunLoop modes?
A:
.default– обычный режим (большинство событий).tracking– режим трекинга (скролл).common– псевдо-режим, включающий оба
- Понимаю, что такое RunLoop и зачем он нужен
- Знаю RunLoop modes (
.default,.tracking,.common) - Понимаю, как работает First Responder
- Знаю структуру Responder Chain
- Понимаю процесс Hit Testing
- Знаю жизненный цикл touch events
- Умею работать с Gesture Recognizers
- Понимаю приоритеты gesture recognizers
- Знаю, как обработать shake событие
- Понимаю, как работает CADisplayLink
- RunLoop – цикл обработки событий; главный поток всегда имеет RunLoop
- RunLoop Modes –
.default,.tracking,.common - First Responder – объект, который первым получает событие
- Responder Chain – цепочка передачи событий вверх по иерархии
- Hit Testing – определение, какая view обработает тач
- Touch Events –
touchesBegan/Moved/Ended/Cancelled - Gesture Recognizers – высокоуровневая обработка жестов
- CADisplayLink – синхронизация с частотой обновления экрана
Помни: RunLoop и Responder Chain – это фундаментальные механизмы iOS для обработки событий!