Last active
July 2, 2018 08:44
-
-
Save valeriomazzeo/48e09cbed4ab4e09b381f595c6b4711c to your computer and use it in GitHub Desktop.
Add parallax effect to any scrollview subview
This file contains 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
// | |
// 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: ¶llaxContext) | |
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: ¶llaxContext) | |
} | |
override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) { | |
guard context == ¶llaxContext 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