Skip to content

Instantly share code, notes, and snippets.

@kylebshr
Last active December 18, 2023 02:33
Show Gist options
  • Save kylebshr/61ae171ad29d2ed54e9dfcaf6b5f0288 to your computer and use it in GitHub Desktop.
Save kylebshr/61ae171ad29d2ed54e9dfcaf6b5f0288 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.
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),
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
}
}
@kylebshr
Copy link
Author

kylebshr commented Feb 5, 2019

An example of using this control:

let menuControl = ScrollTriggeredControl(image: UIImage(named: "Large Chevron"))
menuControl.addTarget(self, action: #selector(showMenu), for: .primaryActionTriggered)
menuControl.tintColor = .white
scrollView.addSubview(menuControl)
NSLayoutConstraint.activate([
    menuControl.centerYAnchor.constraint(equalTo: scrollView.contentLayoutGuide.centerYAnchor),
    menuControl.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
])

In action in Cypher:

ezgif-4-0e8d86d65726

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment