Skip to content

Instantly share code, notes, and snippets.

@valeriomazzeo
Last active July 2, 2018 08:44
Show Gist options
  • Save valeriomazzeo/48e09cbed4ab4e09b381f595c6b4711c to your computer and use it in GitHub Desktop.
Save valeriomazzeo/48e09cbed4ab4e09b381f595c6b4711c to your computer and use it in GitHub Desktop.
Add parallax effect to any scrollview subview
//
// UIScrollView+Parallax.swift
// Parallax
//
// Created by Valerio Mazzeo on 20/05/2016.
// Copyright © 2016 Valerio Mazzeo. All rights reserved.
//
import UIKit
// https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-ID12
private var parallaxContext = 0
private var parallaxViewTag: Int = 789456
extension UIScrollView {
private struct ConstraintsIdentifiers {
static let parallaxTopConstraint = "parallaxTopConstraint"
static let parallaxTopLessConstraint = "parallaxTopLessConstraint"
static let parallaxHeightConstraint = "parallaxHeightConstraint"
}
/*
Activate the parallax effect for a scroll view subview.
- Try call this method only once (standard usage: `viewDidLoad`).
- Balance any call to this with `deactivateParallax` (standard usage: `dealloc` / `deinit`).
- Do NOT change the view's tag as this extension uses it to understand which one is the parallax view
*/
func activateParallaxForSubview(view: UIView) -> Bool {
guard view.superview == self else { return false }
guard let superview = self.superview else { return false }
self.alwaysBounceVertical = true
// Constraint to allow the view to stick to the top of the scroll when we pull the scroll view
let topConstraint = NSLayoutConstraint(
item: view,
attribute: .Top,
relatedBy: .Equal,
toItem: self,
attribute: .Top,
multiplier: 1.0,
constant: 0.0
)
topConstraint.identifier = ConstraintsIdentifiers.parallaxTopConstraint
topConstraint.priority = UILayoutPriorityDefaultLow
// Constraint to allow the view to scroll up and disappear when we start dragging
let topLessConstraint = NSLayoutConstraint(
item: view,
attribute: .Top,
relatedBy: .LessThanOrEqual,
toItem: superview,
attribute: .Top,
multiplier: 1.0,
constant: 0.0
)
topLessConstraint.identifier = ConstraintsIdentifiers.parallaxTopLessConstraint
// Constraint to allow the view to grow in size when we pull the scroll view
let heightConstraint = NSLayoutConstraint(
item: view,
attribute: .Height,
relatedBy: .GreaterThanOrEqual,
toItem: nil,
attribute: .NotAnAttribute,
multiplier: 0.0,
constant: UIViewNoIntrinsicMetric
)
heightConstraint.identifier = ConstraintsIdentifiers.parallaxHeightConstraint
NSLayoutConstraint.activateConstraints([topConstraint, topLessConstraint, heightConstraint])
view.tag = parallaxViewTag
self.addObserver(self, forKeyPath: "contentOffset", options: [.New], context: &parallaxContext)
return true
}
/*
Deactivates the parallax effect.
This method is not safe to call if `activateParallaxForSubview:` hasn't been called first.
*/
func deactivateParallax() {
NSLayoutConstraint.deactivateConstraints(self.parallaxConstraints())
self.removeObserver(self, forKeyPath: "contentOffset", context: &parallaxContext)
}
override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard context == &parallaxContext else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
return
}
guard let scrollView = object as? UIScrollView else { return }
guard let view = scrollView.viewWithTag(parallaxViewTag) else { return }
guard let index = (view.constraints.indexOf { return $0.identifier == ConstraintsIdentifiers.parallaxHeightConstraint }) else { return }
let constraint = view.constraints[index]
var height = view.intrinsicContentSize().height
if height == UIViewNoIntrinsicMetric {
// Reset the height constraint so that `systemLayoutSizeFittingSize` can return the correct height
constraint.constant = 0.0
height = view.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
}
if scrollView.contentOffset.y < 0.0 {
height -= scrollView.contentOffset.y
}
constraint.constant = height
}
private var identifiers: Set<String> {
get {
return Set<String>([
ConstraintsIdentifiers.parallaxTopConstraint,
ConstraintsIdentifiers.parallaxTopLessConstraint,
ConstraintsIdentifiers.parallaxHeightConstraint
])
}
}
private func parallaxConstraints() -> [NSLayoutConstraint] {
let identifiers = self.identifiers
guard let view = self.viewWithTag(parallaxViewTag) else { return [] }
return view.constraints.filter({ constraint in
guard let identifier = constraint.identifier else { return false }
return identifiers.contains(identifier)
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment