Skip to content

Commit

Permalink
added lots of comments to explain the code
Browse files Browse the repository at this point in the history
  • Loading branch information
Nathan Gitter committed Sep 4, 2017
1 parent ea61739 commit 6c06e71
Showing 1 changed file with 66 additions and 0 deletions.
66 changes: 66 additions & 0 deletions InteractiveAnimations/InteractiveAnimations/ViewController.swift
Expand Up @@ -9,6 +9,8 @@
import UIKit
import UIKit.UIGestureRecognizerSubclass

// MARK: - State

private enum State {
case closed
case open
Expand All @@ -23,10 +25,16 @@ extension State {
}
}

// MARK: - View Controller

class ViewController: UIViewController {

// MARK: - Constants

private let popupOffset: CGFloat = 440

// MARK: - Views

private lazy var contentImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = #imageLiteral(resourceName: "content")
Expand Down Expand Up @@ -76,6 +84,8 @@ class ViewController: UIViewController {
return imageView
}()

// MARK: - View Controller Lifecycle

override func viewDidLoad() {
super.viewDidLoad()
layout()
Expand All @@ -86,6 +96,8 @@ class ViewController: UIViewController {
return true
}

// MARK: - Layout

private var bottomConstraint = NSLayoutConstraint()

private func layout() {
Expand Down Expand Up @@ -133,10 +145,15 @@ class ViewController: UIViewController {

}

// MARK: - Animation

/// The current state of the animation. This variable is changed only when an animation completes.
private var currentState: State = .closed

/// All of the currently running animators.
private var runningAnimators = [UIViewPropertyAnimator]()

/// The progress of each animator. This array is parallel to the `runningAnimators` array.
private var animationProgress = [CGFloat]()

private lazy var panRecognizer: InstantPanGestureRecognizer = {
Expand All @@ -145,8 +162,13 @@ class ViewController: UIViewController {
return recognizer
}()

/// Animates the transition, if the animation is not already running.
private func animateTransitionIfNeeded(to state: State, duration: TimeInterval) {

// ensure that the animators array is empty (which implies new animations need to be created)
guard runningAnimators.isEmpty else { return }

// an animator for the transition
let transitionAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1, animations: {
switch state {
case .open:
Expand All @@ -164,7 +186,11 @@ class ViewController: UIViewController {
}
self.view.layoutIfNeeded()
})

// the transition completion block
transitionAnimator.addCompletion { position in

// update the state
switch position {
case .start:
self.currentState = state.opposite
Expand All @@ -173,14 +199,21 @@ class ViewController: UIViewController {
case .current:
()
}

// manually reset the constraint positions
switch self.currentState {
case .open:
self.bottomConstraint.constant = 0
case .closed:
self.bottomConstraint.constant = self.popupOffset
}

// remove all running animators
self.runningAnimators.removeAll()

}

// an animator for the title that is transitioning into view
let inTitleAnimator = UIViewPropertyAnimator(duration: duration, curve: .easeIn, animations: {
switch state {
case .open:
Expand All @@ -190,6 +223,8 @@ class ViewController: UIViewController {
}
})
inTitleAnimator.scrubsLinearly = false

// an animator for the title that is transitioning out of view
let outTitleAnimator = UIViewPropertyAnimator(duration: duration, curve: .easeOut, animations: {
switch state {
case .open:
Expand All @@ -199,35 +234,60 @@ class ViewController: UIViewController {
}
})
outTitleAnimator.scrubsLinearly = false

// start all animators
transitionAnimator.startAnimation()
inTitleAnimator.startAnimation()
outTitleAnimator.startAnimation()

// keep track of all running animators
runningAnimators.append(transitionAnimator)
runningAnimators.append(inTitleAnimator)
runningAnimators.append(outTitleAnimator)

}

@objc private func popupViewPanned(recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:

// start the animations
animateTransitionIfNeeded(to: currentState.opposite, duration: 1)

// pause all animations, since the next event may be a pan changed
runningAnimators.forEach { $0.pauseAnimation() }

// keep track of each animator's progress
animationProgress = runningAnimators.map { $0.fractionComplete }

case .changed:

// variable setup
let translation = recognizer.translation(in: popupView)
var fraction = -translation.y / popupOffset

// adjust the fraction for the current state and reversed state
if currentState == .open { fraction *= -1 }
if runningAnimators[0].isReversed { fraction *= -1 }

// apply the new fraction
for (index, animator) in runningAnimators.enumerated() {
animator.fractionComplete = fraction + animationProgress[index]
}

case .ended:

// variable setup
let yVelocity = recognizer.velocity(in: popupView).y
let shouldClose = yVelocity > 0

// if there is no motion, continue all animations and exit early
if yVelocity == 0 {
runningAnimators.forEach { $0.continueAnimation(withTimingParameters: nil, durationFactor: 0) }
break
}

// reverse the animations based on their current state and pan motion
switch currentState {
case .open:
if !shouldClose && !runningAnimators[0].isReversed { runningAnimators.forEach { $0.isReversed = !$0.isReversed } }
Expand All @@ -236,14 +296,20 @@ class ViewController: UIViewController {
if shouldClose && !runningAnimators[0].isReversed { runningAnimators.forEach { $0.isReversed = !$0.isReversed } }
if !shouldClose && runningAnimators[0].isReversed { runningAnimators.forEach { $0.isReversed = !$0.isReversed } }
}

// continue all animations
runningAnimators.forEach { $0.continueAnimation(withTimingParameters: nil, durationFactor: 0) }

default:
()
}
}

}

// MARK: - InstantPanGestureRecognizer

/// A pan gesture that enters into the `began` state on touch down instead of waiting for a touches moved event.
class InstantPanGestureRecognizer: UIPanGestureRecognizer {

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
Expand Down

0 comments on commit 6c06e71

Please sign in to comment.