From c34938a03d62fbea05b0522fd0496353fe36230e Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 31 Dec 2019 18:18:24 -0500 Subject: [PATCH] Add scroll view support --- SheetImagePicker.xcodeproj/project.pbxproj | 12 +- .../SheetContainerViewController.swift | 111 +++++++++++++----- .../Base.lproj/Main.storyboard | 41 +++++-- .../ContentTableViewController.swift | 43 +++++++ SheetImagePickerTest/ViewController.swift | 20 +++- 5 files changed, 178 insertions(+), 49 deletions(-) create mode 100644 SheetImagePickerTest/ContentTableViewController.swift diff --git a/SheetImagePicker.xcodeproj/project.pbxproj b/SheetImagePicker.xcodeproj/project.pbxproj index 3bc7a47..3585e9e 100644 --- a/SheetImagePicker.xcodeproj/project.pbxproj +++ b/SheetImagePicker.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + D6055E46234D1B31007BEF52 /* ContentTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6055E45234D1B31007BEF52 /* ContentTableViewController.swift */; }; D610D2D6233945AF009EB06A /* SheetImagePicker.h in Headers */ = {isa = PBXBuildFile; fileRef = D610D2D4233945AF009EB06A /* SheetImagePicker.h */; settings = {ATTRIBUTES = (Public, ); }; }; D610D2E3233945B9009EB06A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D610D2E2233945B9009EB06A /* AppDelegate.swift */; }; D610D2E5233945B9009EB06A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D610D2E4233945B9009EB06A /* SceneDelegate.swift */; }; @@ -48,6 +49,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + D6055E45234D1B31007BEF52 /* ContentTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTableViewController.swift; sourceTree = ""; }; D610D2D1233945AF009EB06A /* SheetImagePicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SheetImagePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D610D2D4233945AF009EB06A /* SheetImagePicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SheetImagePicker.h; sourceTree = ""; }; D610D2D5233945AF009EB06A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -124,6 +126,7 @@ D610D2E2233945B9009EB06A /* AppDelegate.swift */, D610D2E4233945B9009EB06A /* SceneDelegate.swift */, D610D2E6233945B9009EB06A /* ViewController.swift */, + D6055E45234D1B31007BEF52 /* ContentTableViewController.swift */, D610D2E8233945B9009EB06A /* Main.storyboard */, D610D2EB233945BB009EB06A /* Assets.xcassets */, D610D2ED233945BB009EB06A /* LaunchScreen.storyboard */, @@ -267,6 +270,7 @@ files = ( D610D2E7233945B9009EB06A /* ViewController.swift in Sources */, D610D2E3233945B9009EB06A /* AppDelegate.swift in Sources */, + D6055E46234D1B31007BEF52 /* ContentTableViewController.swift in Sources */, D610D2E5233945B9009EB06A /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -478,13 +482,13 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = HGYVAQA9FW; + DEVELOPMENT_TEAM = 46G5674823; INFOPLIST_FILE = SheetImagePickerTest/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.SheetImagePickerTest; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.dev.SheetImagePickerTest; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -497,13 +501,13 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = HGYVAQA9FW; + DEVELOPMENT_TEAM = 46G5674823; INFOPLIST_FILE = SheetImagePickerTest/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.SheetImagePickerTest; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.dev.SheetImagePickerTest; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/SheetImagePicker/SheetContainerViewController.swift b/SheetImagePicker/SheetContainerViewController.swift index cb16ca3..a6fd8c1 100644 --- a/SheetImagePicker/SheetContainerViewController.swift +++ b/SheetImagePicker/SheetContainerViewController.swift @@ -44,6 +44,7 @@ public class SheetContainerViewController: UIViewController { var dimmingView: UIView! public var minimumDimmingAlpha: CGFloat = 0 public var maximumDimmingAlpha: CGFloat = 0.75 + var initialScrollViewContentOffset = CGPoint.zero public init(content: UIViewController) { self.content = content @@ -84,8 +85,12 @@ public class SheetContainerViewController: UIViewController { dimmingView.bottomAnchor.constraint(equalTo: content.view.topAnchor, constant: content.view.layer.cornerRadius) ]) - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized(_:))) - content.view.addGestureRecognizer(panGesture) + if let scrollView = content.view as? UIScrollView { + scrollView.panGestureRecognizer.addTarget(self, action: #selector(scrollViewPanGestureRecognized)) + } else { + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized(_:))) + content.view.addGestureRecognizer(panGesture) + } } @objc func panGestureRecognized(_ recognizer: UIPanGestureRecognizer) { @@ -95,44 +100,90 @@ public class SheetContainerViewController: UIViewController { case .changed: let translation = recognizer.translation(in: content.view) - var realOffset = initialConstant + translation.y - let topOffset = topDetent.offset - if realOffset < topOffset { - let smoothed = smoothstep(value: realOffset, from: topOffset, to: 0) - realOffset = topOffset - smoothed * maximumStretchDistance - - } - topConstraint.constant = realOffset - dimmingView.alpha = lerp(realOffset, min: topOffset, max: bottomDetent.offset, from: maximumDimmingAlpha, to: minimumDimmingAlpha) + let realOffset = initialConstant + translation.y + setTopOffset(realOffset) case .ended: let velocity = recognizer.velocity(in: view) + springToNearestDetent(verticalVelocity: velocity.y) - let springToDetent: (Detent, CGFloat) - if abs(velocity.y) > minimumDetentJumpVelocity, - let detentInVelocityDirection = nearestDetentOffset(currentOffset: topConstraint.constant, direction: velocity.y) { - springToDetent = detentInVelocityDirection - } else if let nearestDetent = nearestDetentOffset(currentOffset: topConstraint.constant) { - springToDetent = nearestDetent - } else { - return + default: + break + } + } + + @objc func scrollViewPanGestureRecognized(_ recognizer: UIPanGestureRecognizer) { + guard let scrollView = recognizer.view as? UIScrollView else { return } + + let translation = recognizer.translation(in: scrollView) + let velocity = recognizer.velocity(in: scrollView) + + let shouldMoveSheetDown = scrollView.contentOffset.y <= 0 && velocity.y > 0 // scrolled to top and dragging down + let shouldMoveSheetUp = topConstraint.constant > topDetent.offset && velocity.y < 0 // not fully expanded and dragging up + + let shouldMoveSheet = shouldMoveSheetDown || shouldMoveSheetUp + if shouldMoveSheet { + scrollView.bounces = false + scrollView.setContentOffset(.zero, animated: false) + } + + switch recognizer.state { + case .began: + initialScrollViewContentOffset = scrollView.contentOffset + + case .changed: + if shouldMoveSheet { + setTopOffset(topConstraint.constant + translation.y) + recognizer.setTranslation(initialScrollViewContentOffset, in: scrollView) } - if delegate?.sheetContainer(self, willSnapToDetent: springToDetent.0) ?? true { - let springDistance = abs(topConstraint.constant - springToDetent.1) - self.topConstraint.constant = springToDetent.1 - let springVelocity = velocity.y / springDistance - - UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 0.75, initialSpringVelocity: springVelocity, animations: { - self.view.layoutIfNeeded() - self.dimmingView.alpha = lerp(springToDetent.1, min: self.topDetent.offset, max: self.bottomDetent.offset, from: self.maximumDimmingAlpha, to: self.minimumDimmingAlpha) - }, completion: { (finished) in - self.delegate?.sheetContainer(self, didSnapToDetent: springToDetent.0) - }) + case .ended: + scrollView.bounces = true + if shouldMoveSheet { + springToNearestDetent(verticalVelocity: velocity.y) } + default: + break + } + } + + func setTopOffset(_ offset: CGFloat) { + var offset = offset + + let topOffset = topDetent.offset + if offset < topOffset { + let smoothed = smoothstep(value: offset, from: topOffset, to: 0) + offset = topOffset - smoothed * maximumStretchDistance + + } + topConstraint.constant = offset + dimmingView.alpha = lerp(offset, min: topOffset, max: bottomDetent.offset, from: maximumDimmingAlpha, to: minimumDimmingAlpha) + } + + func springToNearestDetent(verticalVelocity velocity: CGFloat) { + let springToDetent: (Detent, CGFloat) + if abs(velocity) > minimumDetentJumpVelocity, + let detentInVelocityDirection = nearestDetentOffset(currentOffset: topConstraint.constant, direction: velocity) { + springToDetent = detentInVelocityDirection + } else if let nearestDetent = nearestDetentOffset(currentOffset: topConstraint.constant) { + springToDetent = nearestDetent + } else { return } + + if delegate?.sheetContainer(self, willSnapToDetent: springToDetent.0) ?? true { + let springDistance = abs(topConstraint.constant - springToDetent.1) + self.topConstraint.constant = springToDetent.1 + let springVelocity = velocity / springDistance + + UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 0.75, initialSpringVelocity: springVelocity, animations: { + self.view.layoutIfNeeded() + self.dimmingView.alpha = lerp(springToDetent.1, min: self.topDetent.offset, max: self.bottomDetent.offset, from: self.maximumDimmingAlpha, to: self.minimumDimmingAlpha) + }, completion: { (finished) in + self.delegate?.sheetContainer(self, didSnapToDetent: springToDetent.0) + }) + } } func nearestDetentOffset(currentOffset: CGFloat) -> (Detent, CGFloat)? { diff --git a/SheetImagePickerTest/Base.lproj/Main.storyboard b/SheetImagePickerTest/Base.lproj/Main.storyboard index b9471fd..78619f3 100644 --- a/SheetImagePickerTest/Base.lproj/Main.storyboard +++ b/SheetImagePickerTest/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -15,18 +15,37 @@ - + + + + + + + + - - + + diff --git a/SheetImagePickerTest/ContentTableViewController.swift b/SheetImagePickerTest/ContentTableViewController.swift new file mode 100644 index 0000000..e1c60cb --- /dev/null +++ b/SheetImagePickerTest/ContentTableViewController.swift @@ -0,0 +1,43 @@ +// +// ContentTableViewController.swift +// SheetImagePickerTest +// +// Created by Shadowfacts on 10/8/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit + +class ContentTableViewController: UITableViewController { + init() { + super.init(style: .plain) + } + + required init?(coder: NSCoder) { + fatalError() + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "testCell") + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 40 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "testCell", for: indexPath) + + cell.textLabel!.text = "\(indexPath.row)" + + return cell + } +} diff --git a/SheetImagePickerTest/ViewController.swift b/SheetImagePickerTest/ViewController.swift index bce9acb..69e8044 100644 --- a/SheetImagePickerTest/ViewController.swift +++ b/SheetImagePickerTest/ViewController.swift @@ -17,12 +17,9 @@ class ViewController: UIViewController { view.backgroundColor = .green } - @IBAction func buttonPressed(_ sender: Any) { + @IBAction func plainPressed(_ sender: Any) { let content = UIViewController() content.view.translatesAutoresizingMaskIntoConstraints = false - content.view.layer.masksToBounds = true - content.view.layer.cornerRadius = view.bounds.width * 0.02 - let blurEffect = UIBlurEffect(style: .systemChromeMaterial) let blurView = UIVisualEffectView(effect: blurEffect) blurView.translatesAutoresizingMaskIntoConstraints = false @@ -39,6 +36,19 @@ class ViewController: UIViewController { label.centerXAnchor.constraint(equalTo: blurView.contentView.centerXAnchor), label.centerYAnchor.constraint(equalTo: blurView.contentView.centerYAnchor) ]) + + let sheet = SheetContainerViewController(content: content) + sheet.delegate = self + sheet.detents = [.bottom, .middle, .top] + + present(sheet, animated: true) + } + + @IBAction func tablePressed(_ sender: Any) { + let content = ContentTableViewController() + content.view.translatesAutoresizingMaskIntoConstraints = false + content.view.layer.masksToBounds = true + content.view.layer.cornerRadius = view.bounds.width * 0.02 let sheet = SheetContainerViewController(content: content) sheet.delegate = self @@ -47,6 +57,8 @@ class ViewController: UIViewController { present(sheet, animated: true) } + @IBAction func navPressed(_ sender: Any) { + } } extension ViewController: SheetContainerViewControllerDelegate {