Created
March 22, 2021 22:18
-
-
Save gromwel/e0ef6bcbddf61039783762de921dac2a to your computer and use it in GitHub Desktop.
Interactive Popup View
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ViewController: UIViewController { | |
// MARK: - Сабвью | |
private lazy var popup: UIView = { | |
let view = UIView() | |
view.backgroundColor = .systemIndigo | |
view.frame.size.height = self.state == .open ? 400.0 : 50.0 | |
return view | |
}() | |
private lazy var panGesture: UIPanGestureRecognizer = { | |
let gr = InstantPanGestureRecognizer() | |
gr.addTarget(self, action: #selector(self.pan)) | |
return gr | |
}() | |
// MARK: - Цикл жизни | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
self.view.backgroundColor = .systemGray6 | |
self.view.addSubview(self.popup) | |
self.popup.addGestureRecognizer(self.panGesture) | |
} | |
override func viewWillLayoutSubviews() { | |
super.viewWillLayoutSubviews() | |
let frame: CGRect = self.view.frame | |
let height: CGFloat = self.popup.frame.height | |
self.popup.frame = CGRect(x: frame.minX, y: frame.maxY - height, width: frame.width, height: height) | |
} | |
// MARK: - Состояние | |
private enum State { | |
case open | |
case close | |
var opposite: State { | |
switch self { | |
case .open: return .close | |
case .close: return .open | |
} | |
} | |
} | |
private var state: State = .close | |
// MARK: - Логика | |
private lazy var animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) | |
private var createAnimation: (()-> Void) { | |
return { () -> Void in | |
// Новое значение которое собираемся анимировать | |
self.popup.frame.size.height = self.state.opposite == .open ? 400.0 : 50.0 | |
// Переверстка | |
self.view.setNeedsLayout() | |
self.view.layoutIfNeeded() | |
} | |
} | |
private var createCompletion: ((UIViewAnimatingPosition) -> Void) { | |
return { (completion: UIViewAnimatingPosition) -> Void in | |
// Выходим не продолжая если анимация не завершена | |
switch completion { | |
case .end: break | |
case .start: break | |
case .current: return | |
@unknown default: break | |
} | |
// По завершении изменим состояние (смотрим инвертирована ли анимация) | |
if self.animator.isReversed { return } | |
self.state = self.state.opposite | |
} | |
} | |
// Прогресс анимации (для прерывания анимации) | |
private var interruptedProgress: CGFloat = 0 | |
@objc private func pan(sender: UIPanGestureRecognizer) { | |
switch sender.state { | |
case .began: | |
// При перехвате во время выполнения анимации запоминаем процент прерванной анимации | |
self.interruptedProgress = self.animator.fractionComplete | |
// Выходим ставя аниматор на паузу если анимация еще выполняется | |
if self.animator.isRunning { | |
self.animator.pauseAnimation() | |
return | |
} | |
// Ставим новые анимации и блоки завершения и ставим на паузу | |
self.animator.addAnimations(self.createAnimation) | |
self.animator.addCompletion(self.createCompletion) | |
self.animator.pauseAnimation() | |
case .changed: | |
// Умножители для правильного расчета процента выполнения | |
// 1. При открытии умножаем на -1 для перевода в движения в положительные координаты | |
let m1: CGFloat = self.state == .open ? 1 : -1 | |
// 2. При изменении направления анимации умножаем на -1 для перевода в положительные координаты | |
let m2: CGFloat = self.animator.isReversed ? -1 : 1 | |
// Дельта | |
let delta: CGFloat = 350.0 | |
// Движение по вьюхе пальцем от точки старта (вверх - отрицательное, вниз - положительное) | |
let translation = sender.translation(in: self.popup).y * m1 * m2 | |
// Рассчитываем процент завершения анимации | |
let fraction = translation / delta | |
// Ставим процент завершения (+ процент выполнения от прерванной анимации) | |
self.animator.fractionComplete = fraction + self.interruptedProgress | |
case .ended: | |
// Скорость движения пальца по вьюхе (вверх - отрицательная, вниз - положителья) | |
let velocity = sender.velocity(in: self.popup).y | |
// Если скорость нулевая (тап по вьюхе) то просто завершаем анимацию | |
guard velocity != 0 else { | |
self.animator.continueAnimation(withTimingParameters: nil, durationFactor: 0.0) | |
return | |
} | |
// Закрывается ли вьюха | |
let isClosed = velocity > 0 | |
// Вычисляем было ли изменено направлене движения пальца по сравнению со стартом | |
let isReversed: Bool = { | |
switch (self.state, isClosed, self.animator.isReversed) { | |
case (.open, false, false): return true | |
case (.open, true, true): return true | |
case (.close, false, true): return true | |
case (.close, true, false): return true | |
default: return false | |
} | |
}() | |
// Если изменено - меняем состояние анимации | |
if isReversed { self.animator.isReversed = !self.animator.isReversed } | |
// Завершаем анимацию | |
self.animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) | |
case .possible: | |
print("possible") | |
case .cancelled: | |
print("candelled") | |
self.animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) | |
case .failed: | |
print("failed") | |
self.animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) | |
@unknown default: | |
break | |
} | |
} | |
} | |
class InstantPanGestureRecognizer: UIPanGestureRecognizer { | |
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { | |
if self.state == .began { return } | |
super.touchesBegan(touches, with: event) | |
self.state = .began | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment