Created
February 5, 2019 18:29
-
-
Save kylebshr/9966aad75590345b6cc209dbde9495ee to your computer and use it in GitHub Desktop.
When added to a scroll view, this control listens to the content offset and will trigger if it's pulled past a threshold then released. Inspired by Things 3.
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
import UIKit | |
class ScrollTriggeredControl: UIControl { | |
private let dragThreshold: CGFloat = 80 | |
private var previousFraction: CGFloat = 0 | |
private var shouldTrigger = false | |
private var offsetObservation: NSKeyValueObservation? | |
private let imageView = UIImageView() | |
private let impactGenerator = UIImpactFeedbackGenerator(style: .medium) | |
private lazy var widthConstraint: NSLayoutConstraint = widthAnchor.constraint(equalToConstant: 0) | |
init(image: UIImage?) { | |
super.init(frame: .zero) | |
translatesAutoresizingMaskIntoConstraints = false | |
clipsToBounds = false | |
addSubview(imageView) | |
imageView.translatesAutoresizingMaskIntoConstraints = false | |
imageView.image = image | |
let centerConstraint = imageView.centerXAnchor.constraint(equalTo: centerXAnchor) | |
centerConstraint.priority = .defaultHigh | |
NSLayoutConstraint.activate([ | |
imageView.heightAnchor.constraint(equalTo: heightAnchor), | |
imageView.centerYAnchor.constraint(equalTo: centerYAnchor), | |
imageView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -4), | |
centerConstraint, widthConstraint | |
]) | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func didMoveToSuperview() { | |
(superview as? UIScrollView).flatMap(observe) | |
} | |
private func observe(scrollView: UIScrollView) { | |
offsetObservation = scrollView.observe(\.contentOffset) { [weak self] scrollView, _ in | |
self?.updateOffset(for: scrollView) | |
} | |
} | |
private func updateOffset(for scrollView: UIScrollView) { | |
let offset = -scrollView.adjustedContentOffset.x | |
let fraction = min(offset / dragThreshold, 1) | |
widthConstraint.constant = max(offset, 0) | |
imageView.alpha = fraction == 1 ? 1 : 0.5 * fraction | |
if shouldTrigger, !scrollView.isTracking { | |
sendActions(for: .primaryActionTriggered) | |
shouldTrigger = false | |
} | |
if fraction < 1 { | |
impactGenerator.prepare() | |
shouldTrigger = false | |
} | |
if fraction == 1, previousFraction < 1, scrollView.isTracking { | |
impactGenerator.impactOccurred() | |
shouldTrigger = true | |
} | |
previousFraction = fraction | |
} | |
} | |
extension UIScrollView { | |
var adjustedContentOffset: CGPoint { | |
var offset = contentOffset | |
offset.x += adjustedContentInset.left | |
offset.y += adjustedContentInset.top | |
return offset | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment