Skip to content

Instantly share code, notes, and snippets.

@gromwel
Created March 22, 2021 22:18
Show Gist options
  • Save gromwel/e0ef6bcbddf61039783762de921dac2a to your computer and use it in GitHub Desktop.
Save gromwel/e0ef6bcbddf61039783762de921dac2a to your computer and use it in GitHub Desktop.
Interactive Popup View
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