diff --git a/DefaultSheetAnimator.swift b/DefaultSheetAnimator.swift index a253aeb..b21b0be 100644 --- a/DefaultSheetAnimator.swift +++ b/DefaultSheetAnimator.swift @@ -8,8 +8,8 @@ import UIKit -class DefaultSheetAnimator: Animatable { - func animate(animations: @escaping () -> Void, completion: ((Bool) -> Void)?){ +public class DefaultSheetAnimator: Animatable { + public func animate(animations: @escaping () -> Void, completion: ((Bool) -> Void)?){ UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [.curveEaseInOut, .allowUserInteraction], animations: animations, completion: completion) } } diff --git a/Example/UBottomSheet.xcodeproj/project.pbxproj b/Example/UBottomSheet.xcodeproj/project.pbxproj index c6808a5..21dd675 100644 --- a/Example/UBottomSheet.xcodeproj/project.pbxproj +++ b/Example/UBottomSheet.xcodeproj/project.pbxproj @@ -14,6 +14,10 @@ 0CC171452334257C002A02FD /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC171422334257C002A02FD /* ViewController.swift */; }; 0CC1714723342585002A02FD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0CC1714623342585002A02FD /* Main.storyboard */; }; 0CC17149233425AB002A02FD /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CC17148233425AA002A02FD /* Images.xcassets */; }; + 0CDF2F67245638B100AAE66E /* SimpleCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDF2F66245638B100AAE66E /* SimpleCollectionCell.swift */; }; + 0CDF2F69245638C700AAE66E /* EmbeddedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDF2F68245638C700AAE66E /* EmbeddedCell.swift */; }; + 0CDF2F6C245647ED00AAE66E /* LabelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDF2F6A245647ED00AAE66E /* LabelViewController.swift */; }; + 0CDF2F6D245647ED00AAE66E /* LabelViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0CDF2F6B245647ED00AAE66E /* LabelViewController.xib */; }; 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; }; @@ -37,6 +41,10 @@ 0CC171422334257C002A02FD /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 0CC1714623342585002A02FD /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 0CC17148233425AA002A02FD /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 0CDF2F66245638B100AAE66E /* SimpleCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleCollectionCell.swift; sourceTree = ""; }; + 0CDF2F68245638C700AAE66E /* EmbeddedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedCell.swift; sourceTree = ""; }; + 0CDF2F6A245647ED00AAE66E /* LabelViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelViewController.swift; sourceTree = ""; }; + 0CDF2F6B245647ED00AAE66E /* LabelViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LabelViewController.xib; sourceTree = ""; }; 4E980B1CC97C3CFCFF0A9FB0 /* Pods-UBottomSheet_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UBottomSheet_Example.release.xcconfig"; path = "Target Support Files/Pods-UBottomSheet_Example/Pods-UBottomSheet_Example.release.xcconfig"; sourceTree = ""; }; 5D316F3B7AE3074480477D40 /* UBottomSheet.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = UBottomSheet.podspec; path = ../UBottomSheet.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 607FACD01AFB9204008FA782 /* UBottomSheet_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UBottomSheet_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -103,9 +111,13 @@ 607FACD51AFB9204008FA782 /* AppDelegate.swift */, 0CC171422334257C002A02FD /* ViewController.swift */, 0CC171412334257C002A02FD /* SimpleTableCell.swift */, + 0CDF2F68245638C700AAE66E /* EmbeddedCell.swift */, + 0CDF2F66245638B100AAE66E /* SimpleCollectionCell.swift */, 0CA1808B245377CF00C6CBDE /* MapsDemoBottomSheetController.swift */, 0CC17148233425AA002A02FD /* Images.xcassets */, 0CC1714623342585002A02FD /* Main.storyboard */, + 0CDF2F6A245647ED00AAE66E /* LabelViewController.swift */, + 0CDF2F6B245647ED00AAE66E /* LabelViewController.xib */, 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 607FACD31AFB9204008FA782 /* Supporting Files */, ); @@ -260,6 +272,7 @@ 0CC17149233425AB002A02FD /* Images.xcassets in Resources */, 0CC1714723342585002A02FD /* Main.storyboard in Resources */, 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, + 0CDF2F6D245647ED00AAE66E /* LabelViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -346,6 +359,9 @@ 0CC171452334257C002A02FD /* ViewController.swift in Sources */, 0CA1808C245377CF00C6CBDE /* MapsDemoBottomSheetController.swift in Sources */, 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, + 0CDF2F6C245647ED00AAE66E /* LabelViewController.swift in Sources */, + 0CDF2F67245638B100AAE66E /* SimpleCollectionCell.swift in Sources */, + 0CDF2F69245638C700AAE66E /* EmbeddedCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/UBottomSheet/EmbeddedCell.swift b/Example/UBottomSheet/EmbeddedCell.swift new file mode 100644 index 0000000..a08270a --- /dev/null +++ b/Example/UBottomSheet/EmbeddedCell.swift @@ -0,0 +1,51 @@ +// +// EmbeddedCell.swift +// UBottomSheet_Example +// +// Created by ugur on 27.04.2020. +// Copyright © 2020 CocoaPods. All rights reserved. +// + +import UIKit + +class EmbeddedCell: UITableViewCell, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + @IBOutlet weak var collectionView: UICollectionView! + + var items: [CollectionModel] = [] + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + + collectionView.contentInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 0) + collectionView.dataSource = self + collectionView.delegate = self + collectionView.alwaysBounceHorizontal = true + collectionView.showsHorizontalScrollIndicator = false + + if #available(iOS 13.0, *) { + items.append(CollectionModel(image: UIImage(systemName: "house.fill"), title: "Home")) + items.append(CollectionModel(image: UIImage(systemName: "briefcase.fill"), title: "Work")) + items.append(CollectionModel(image: UIImage(systemName: "plus"), title: "Add")) + } else { + items.append(CollectionModel(image: #imageLiteral(resourceName: "heart_icon"), title: "Home")) + items.append(CollectionModel(image: #imageLiteral(resourceName: "heart_icon"), title: "Work")) + items.append(CollectionModel(image: #imageLiteral(resourceName: "heart_icon"), title: "Add")) + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: collectionView.frame.width/5, height: collectionView.frame.height) + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return items.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SimpleCollectionCell", for: indexPath) as! SimpleCollectionCell + cell.configure(with: items[indexPath.item]) + return cell + } + +} diff --git a/Example/UBottomSheet/LabelViewController.swift b/Example/UBottomSheet/LabelViewController.swift new file mode 100644 index 0000000..bc538a5 --- /dev/null +++ b/Example/UBottomSheet/LabelViewController.swift @@ -0,0 +1,27 @@ +// +// LabelViewController.swift +// UBottomSheet_Example +// +// Created by ugur on 27.04.2020. +// Copyright © 2020 CocoaPods. All rights reserved. +// + +import UIKit +import UBottomSheet + +class LabelViewController: UIViewController, Draggable { + var sheetCoordinator: UBottomSheetCoordinator? + + override func viewDidLoad() { + super.viewDidLoad() + + sheetCoordinator?.startTracking(item: self) + + // Do any additional setup after loading the view. + } + + @IBAction func dismissAction(){ + sheetCoordinator?.removeSheetChild(item: self) + } + +} diff --git a/Example/UBottomSheet/LabelViewController.xib b/Example/UBottomSheet/LabelViewController.xib new file mode 100644 index 0000000..c16e630 --- /dev/null +++ b/Example/UBottomSheet/LabelViewController.xib @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/UBottomSheet/Main.storyboard b/Example/UBottomSheet/Main.storyboard index 9066880..de7ba66 100644 --- a/Example/UBottomSheet/Main.storyboard +++ b/Example/UBottomSheet/Main.storyboard @@ -5,6 +5,7 @@ + @@ -23,8 +24,8 @@ - - + + @@ -62,43 +63,122 @@ - + - + Title Title - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + + + - + - + + diff --git a/Example/UBottomSheet/MapsDemoBottomSheetController.swift b/Example/UBottomSheet/MapsDemoBottomSheetController.swift index 178fd68..a1265fd 100644 --- a/Example/UBottomSheet/MapsDemoBottomSheetController.swift +++ b/Example/UBottomSheet/MapsDemoBottomSheetController.swift @@ -18,6 +18,7 @@ class MapsDemoBottomSheetController: UIViewController{ super.viewDidLoad() sheetCoordinator?.startTracking(item: self) + tableView.delegate = self tableView.dataSource = self } @@ -32,18 +33,54 @@ extension MapsDemoBottomSheetController: Draggable{ extension MapsDemoBottomSheetController: UITableViewDelegate, UITableViewDataSource{ + func numberOfSections(in tableView: UITableView) -> Int { + return 2 + } + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 100 + switch section { + case 0: + return 1 + default: + return 100 + } + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch section { + case 0: + return "Favorites" + default: + return "Recently Viewed" + } + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + return .leastNormalMagnitude + } + + func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + return nil } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "SimpleTableCell", for: indexPath) as! SimpleTableCell - let model = SimpleTableCellViewModel(image: nil, title: "Title \(indexPath.row)", subtitle: "Subtitle \(indexPath.row)") - cell.configure(model: model) - return cell + switch indexPath.section { + case 0: + let cell = tableView.dequeueReusableCell(withIdentifier: "EmbeddedCell", for: indexPath) as! EmbeddedCell + return cell + default: + let cell = tableView.dequeueReusableCell(withIdentifier: "SimpleTableCell", for: indexPath) as! SimpleTableCell + let model = SimpleTableCellViewModel(image: nil, title: "Title \(indexPath.row)", subtitle: "Subtitle \(indexPath.row)") + cell.configure(model: model) + return cell + } + } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - print(indexPath.row) + let sc = UBottomSheetCoordinator(parent: sheetCoordinator!.parent) + let vc = LabelViewController() + vc.sheetCoordinator = sc + sc.addSheet(vc, to: sheetCoordinator!.parent) } } diff --git a/Example/UBottomSheet/SimpleCollectionCell.swift b/Example/UBottomSheet/SimpleCollectionCell.swift new file mode 100644 index 0000000..f51a61a --- /dev/null +++ b/Example/UBottomSheet/SimpleCollectionCell.swift @@ -0,0 +1,30 @@ +// +// SimpleCollectionCell.swift +// UBottomSheet_Example +// +// Created by ugur on 27.04.2020. +// Copyright © 2020 CocoaPods. All rights reserved. +// + +import UIKit + +struct CollectionModel { + let image: UIImage? + let title: String +} + +class SimpleCollectionCell: UICollectionViewCell { + @IBOutlet weak var button: UIButton! + @IBOutlet weak var lbl: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + button.layer.cornerRadius = button.frame.height/2 + button.layer.masksToBounds = true + } + + func configure(with model: CollectionModel){ + button.setImage(model.image, for: .normal) + lbl.text = model.title + } +} diff --git a/Example/UBottomSheet/ViewController.swift b/Example/UBottomSheet/ViewController.swift index 304fe65..a984ae3 100644 --- a/Example/UBottomSheet/ViewController.swift +++ b/Example/UBottomSheet/ViewController.swift @@ -15,38 +15,52 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() sheetCoordinator = UBottomSheetCoordinator(parent: self, - delegate: self, - showBackDimmingView: false) + delegate: self) let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MapsDemoBottomSheetController") as! MapsDemoBottomSheetController vc.sheetCoordinator = sheetCoordinator sheetCoordinator.addSheet(vc, to: self, didContainerCreate: { container in + self.addBackDimmingBackView(below: container) let f = self.view.frame - let rect = CGRect(x: f.minX, y: f.minY, width: f.width, height: f.height*0.8) -// container.roundCorners(corners: [.topLeft, .topRight], radius: 20, rect: rect) + let rect = CGRect(x: f.minX, y: f.minY, width: f.width, height: f.height) + container.roundCorners(corners: [.topLeft, .topRight], radius: 10, rect: rect) }) - sheetCoordinator.setCornerRadius(20) + sheetCoordinator.setCornerRadius(10) } + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + sheetCoordinator.addDropShadowIfNotExist() + } + + private func addBackDimmingBackView(below container: UIView){ + let backView = PassThroughView() + self.view.insertSubview(backView, belowSubview: container) + backView.translatesAutoresizingMaskIntoConstraints = false + backView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true + backView.bottomAnchor.constraint(equalTo: container.topAnchor, constant: 0).isActive = true + backView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true + backView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true + } } extension ViewController: UBottomSheetCoordinatorDelegate{ func bottomSheet(_ container: UIView?, didChange state: SheetTranslationState) { - switch state { - case .changing(_, let percent): - self.sheetCoordinator.backView?.backgroundColor = UIColor.black.withAlphaComponent(max(0.2, percent/100 * 0.8)) - case .finished(_, let percent): - self.sheetCoordinator.backView?.backgroundColor = UIColor.black.withAlphaComponent(max(0.2, percent/100 * 0.8)) - default: - break - } +// switch state { +// case .progressing(_, let percent): +// self.backView?.backgroundColor = UIColor.black.withAlphaComponent(max(0.2, percent/100 * 0.8)) +// case .finished(_, let percent): +// self.backView?.backgroundColor = UIColor.black.withAlphaComponent(max(0.2, percent/100 * 0.8)) +// default: +// break +// } } func bottomSheet(_ container: UIView?, finishTranslateWith extraAnimation: @escaping ((CGFloat) -> Void) -> Void) { extraAnimation({ percent in - self.sheetCoordinator.backView?.backgroundColor = UIColor.black.withAlphaComponent(max(0.2, percent/100 * 0.8)) +// self.backView?.backgroundColor = UIColor.black.withAlphaComponent(max(0.2, percent/100 * 0.8)) }) } diff --git a/README.md b/README.md index fd36061..bb699f6 100644 --- a/README.md +++ b/README.md @@ -14,52 +14,43 @@ To run the example project, clone the repo, and run `pod install` from the Example directory first. -Create a view controller that inherits BottomSheetController. Configure the following parameters according to your needs. +Bottom sheet chiild view controllers must conform to the Draggable protocol. ```swift -class MapsDemoBottomSheetController: BottomSheetController{ +class MapsDemoBottomSheetController: UIViewController, Draggable{ + @IBOutlet weak var tableView: UITableView! - //MARK: BottomSheetController configurations - // override var initialPosition: SheetPosition { - // return .middle - // } - - // override var topYPercentage: CGFloat + var sheetCoordinator: UBottomSheetCoordinator? + + override func viewDidLoad() { + super.viewDidLoad() - // override var bottomYPercentage: CGFloat + //adds pan gesture recognizer to draggableView() + sheetCoordinator?.startTracking(item: self) - // override var middleYPercentage: CGFloat - - // override var bottomInset: CGFloat - - // override var topInset: CGFloat - - // Don't override if not necessary as it is auto-detected - // override var scrollView: UIScrollView?{ - // return put_your_tableView, collectionView, etc. - // } - - // //Override this to apply custom animations - // override func animate(animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) { - // UIView.animate(withDuration: 0.3, animations: animations) - // } - - // To change sheet position manually - // call ´changePosition(to: .top)´ anywhere in the code + tableView.delegate = self + tableView.dataSource = self + } + + func draggableView() -> UIScrollView? { + return tableView + } } + ``` -Attach to the parent view controller +Create a UBottomSheetCoordinator from the main view controller. Use the UBottomSheetCoordinator to add and configure the sheet. ```swift -let vc = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MapsDemoBottomSheetController") as! MapsDemoBottomSheetController - - //Add bottom sheet to the current viewcontroller - vc.attach(to: self) -// //Remove sheet from the current viewcontroller -// vc.detach() +// parentViewController: main view controller that presents the bottom sheet +let sheetCoordinator = UBottomSheetCoordinator(parent: parentViewController) + +let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MapsDemoBottomSheetController") as! MapsDemoBottomSheetController + +vc.sheetCoordinator = sheetCoordinator +sheetCoordinator.addSheet(vc, to: parentViewController) ``` diff --git a/UBottomSheet.podspec b/UBottomSheet.podspec index db47232..e9f0bc6 100644 --- a/UBottomSheet.podspec +++ b/UBottomSheet.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'UBottomSheet' - s.version = '0.2.0' + s.version = '1.0.0' s.summary = 'Mimics the iPhone Maps App bottom sheet' s.swift_version = '5.0' @@ -19,7 +19,7 @@ Pod::Spec.new do |s| # * Finally, don't worry about the indent, CocoaPods strips it! s.description = <<-DESC - 'Bottom sheet that mimics the behaviours and UI of iPhone Maps app.' + 'Bottom sheet that mimics the behaviours of iPhone Maps app.' DESC s.homepage = 'https://github.com/OfTheWolf/UBottomSheet' diff --git a/UBottomSheet/Classes/Extensions.swift b/UBottomSheet/Classes/Extensions.swift index 7f52d3e..ea81544 100644 --- a/UBottomSheet/Classes/Extensions.swift +++ b/UBottomSheet/Classes/Extensions.swift @@ -13,15 +13,19 @@ import UIKit addChild(child) container.addSubview(child.view) child.didMove(toParent: self) - child.view.pinToEdges(to: container) -// if animated{ -// child.view.frame = container.bounds.offsetBy(dx: 0, dy: container.bounds.height) -// UIView.animate(withDuration: 0.3) { -// child.view.frame = container.bounds -// } -// }else{ -// child.view.frame = container.bounds -// } + let minY = sheetPositions(view.frame.height).min() ?? 0 + let f = CGRect(x: view.frame.minX, y: view.frame.minY, width: view.frame.width, height: view.frame.maxY - minY) + if animated{ + container.frame = f.offsetBy(dx: 0, dy: f.height) + child.view.frame = container.bounds + UIView.animate(withDuration: 0.3, animations: { + container.frame = f + }) { (_) in + self.view.layoutIfNeeded() + } + }else{ + child.view.pinToEdges(to: container) + } } @@ -34,12 +38,12 @@ import UIKit } extension UIView{ - func pinToEdges(to view: UIView){ + func pinToEdges(to view: UIView, insets: UIEdgeInsets = .zero){ self.translatesAutoresizingMaskIntoConstraints = false - self.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - self.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - self.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - self.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + self.topAnchor.constraint(equalTo: view.topAnchor, constant: insets.top).isActive = true + self.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: insets.bottom).isActive = true + self.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: insets.left).isActive = true + self.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: insets.right).isActive = true } func constraint(_ parent: UIViewController, for attribute: NSLayoutConstraint.Attribute) -> NSLayoutConstraint?{ diff --git a/UBottomSheetCoordinator.swift b/UBottomSheetCoordinator.swift index f69d318..a3a2a8d 100644 --- a/UBottomSheetCoordinator.swift +++ b/UBottomSheetCoordinator.swift @@ -9,38 +9,44 @@ import UIKit public enum SheetTranslationState{ - case changing(_ minYPosition: CGFloat, _ percent: CGFloat) //currently updating + case progressing(_ minYPosition: CGFloat, _ percent: CGFloat) //currently updating case willFinish(_ minYPosition: CGFloat, _ percent: CGFloat) //animatiion start case finished(_ minYPosition: CGFloat, _ percent: CGFloat) //animation end } public class UBottomSheetCoordinator { - private weak var parent: UIViewController! + public weak var parent: UIViewController! private var container: UIView? - public var backView: PassThroughView? private weak var dataSource: UBottomSheetCoordinatorDataSource! private weak var delegate: UBottomSheetCoordinatorDelegate? - private var showBackDimmingView = false private var minSheetPosition: CGFloat? private var maxSheetPosition: CGFloat? + ///View controllers which conform to Draggable protocol private var draggables: [DraggableItem] = [] + ///Drop shadow view behind container. + private var dropShadowView: PassThroughView? public var availableHeight: CGFloat{ return parent.view.frame.height } + + private var cornerRadius: CGFloat = 0{ + didSet{ + applyDefaultShadowParams() + clearShadowBackground() + } + } /** Creates UBottomSheetCoordinator object. - parameter parent: UIViewController - parameter delegate: UBottomSheetCoordinatorDelegate - - parameter showBackDimmingView: pass true to show dimming background view; otherwise pass false. Default is false. */ - public init(parent: UIViewController, delegate: UBottomSheetCoordinatorDelegate? = nil, showBackDimmingView: Bool = false) { + public init(parent: UIViewController, delegate: UBottomSheetCoordinatorDelegate? = nil) { self.parent = parent self.dataSource = parent self.delegate = delegate - self.showBackDimmingView = showBackDimmingView minSheetPosition = dataSource.sheetPositions(availableHeight).min() maxSheetPosition = dataSource.sheetPositions(availableHeight).max() @@ -84,7 +90,6 @@ public class UBottomSheetCoordinator { config(view) container?.pinToEdges(to: parent.view) container?.constraint(parent, for: .top)?.constant = dataSource.sheetPositions(availableHeight)[0] - addBackDimmingViewIfNeeded() setPosition(dataSource.initialPosition(availableHeight), animated: false) } @@ -112,15 +117,14 @@ public class UBottomSheetCoordinator { public func addSheet(_ item: DraggableItem, to parent: UIViewController, didContainerCreate: ((UIView)->Void)? = nil){ let container = PassThroughView() self.container = container - didContainerCreate?(container) parent.view.addSubview(container) parent.ub_add(item, in: container) + didContainerCreate?(container) container.translatesAutoresizingMaskIntoConstraints = true let y = dataSource.sheetPositions(availableHeight)[0] container.frame = CGRect(x: 0, y: y, width: parent.view.frame.width, height: parent.view.frame.height - y) // container.pinToEdges(to: parent.view) // container.constraint(parent, for: .top)?.constant = dataSource.sheetPositions(availableHeight)[0] - addBackDimmingViewIfNeeded() setPosition(dataSource.initialPosition(availableHeight), animated: false) } @@ -133,30 +137,45 @@ public class UBottomSheetCoordinator { parent?.ub_add(item, in: container!) } + public func addDropShadowIfNotExist(_ config: ((UIView)->Void)? = nil){ + guard self.dropShadowView == nil else {return} + self.dropShadowView = PassThroughView() + parent.view.insertSubview(dropShadowView!, belowSubview: container!) + self.dropShadowView?.pinToEdges(to: container!, insets: UIEdgeInsets(top: -container!.frame.minY, left: 0, bottom: 0, right: 0)) + self.dropShadowView?.layer.masksToBounds = false + if config == nil{ + applyDefaultShadowParams() + clearShadowBackground() + }else{ + config?(dropShadowView!) + } + } + + private func applyDefaultShadowParams(){ + dropShadowView?.layer.shadowPath = UIBezierPath(roundedRect: container!.frame, cornerRadius: cornerRadius).cgPath + dropShadowView?.layer.shadowColor = UIColor.black.cgColor + dropShadowView?.layer.shadowRadius = CGFloat.init(10) + dropShadowView?.layer.shadowOpacity = Float.init(0.5) + dropShadowView?.layer.shadowOffset = CGSize.init(width: 0.0, height: 4.0) + } + + private func clearShadowBackground(){ + let p = CGMutablePath() + p.addRect(parent.view.bounds) + p.addPath(UIBezierPath(roundedRect: container!.frame, cornerRadius: cornerRadius).cgPath) + let mask = CAShapeLayer() + mask.path = p + mask.fillRule = .evenOdd + dropShadowView?.layer.mask = mask + } + /** - Adjust backView and container vertical offset according to the cornerradius + Adjust drop shadow corner radius if exists. - parameter radius: corner radius */ public func setCornerRadius(_ radius: CGFloat){ - backView?.constraint(parent, for: .bottom)?.constant = radius - } - - /** - Adds dimming background view to the parent if showBackDimmingView sets true - */ - public func addBackDimmingViewIfNeeded(){ - guard showBackDimmingView else { - return - } - backView = PassThroughView() - parent.view.insertSubview(backView!, belowSubview: container!) - backView!.translatesAutoresizingMaskIntoConstraints = false - backView!.topAnchor.constraint(equalTo: parent.view.topAnchor).isActive = true - backView!.bottomAnchor.constraint(equalTo: container!.topAnchor, constant: 0).isActive = true - backView!.leadingAnchor.constraint(equalTo: parent.view.leadingAnchor).isActive = true - backView!.trailingAnchor.constraint(equalTo: parent.view.trailingAnchor).isActive = true - delegate?.bottomSheet(container, didCreateBackView: backView!) + self.cornerRadius = radius } /** @@ -199,18 +218,16 @@ public class UBottomSheetCoordinator { - parameter block: use this closure to apply custom sheet dismissal animation */ - public func removeSheet(_ block: ((_ container: UIView?, _ backView: UIView?)->Void)? = nil){ + public func removeSheet(_ block: ((_ container: UIView?)->Void)? = nil){ self.draggables.removeAll() if block != nil{ - block?(container, backView) + block?(container) }else{ UIView.animate(withDuration: 0.3, animations: {[weak self] in guard let sSelf = self else {return} - sSelf.backView?.alpha = 0 sSelf.container!.frame = sSelf.container!.frame.offsetBy(dx: 0, dy: sSelf.parent.view.frame.height) }) {[weak self] (finished) in - self?.backView?.removeFromSuperview() self?.container?.removeFromSuperview() } } @@ -276,30 +293,41 @@ public class UBottomSheetCoordinator { totalTranslationMaxY = maxSheetPosition! case .changed: if let scroll = scrollView{ - if vel.y < 0 { //dragging up - if container!.frame.minY - minSheetPosition! > 0.001{ - translate(dy: dy - lastY) - scroll.contentOffset.y = lastContentOffset.y - } - }else{ - if scroll.contentOffset.y <= 0 && !scroll.isDecelerating{ - translate(dy: dy - lastY) - scroll.contentOffset.y = 0 - } + switch dragDirection(vel) { + case .up where (container!.frame.minY - minSheetPosition! > 0.001): + translate(dy: dy - lastY) + scroll.contentOffset.y = lastContentOffset.y + case .down where scroll.contentOffset.y <= 0 && !scroll.isDecelerating: + translate(dy: dy - lastY) + scroll.contentOffset.y = 0 + default: + break } +// if vel.y < 0 /*dragging up*/ && (container!.frame.minY - minSheetPosition! > 0.001){ +// translate(dy: dy - lastY) +// scroll.contentOffset.y = lastContentOffset.y +// }else if vel.y > 0 /*dragging down*/ && scroll.contentOffset.y <= 0 && !scroll.isDecelerating{ +// translate(dy: dy - lastY) +// scroll.contentOffset.y = 0 +// } }else{ translate(dy: dy) } case .ended, .cancelled, .failed: - if let scroll = scrollView, let minY = container?.frame.minY{ - if minY - minSheetPosition! > 0.001 && minY - maxSheetPosition! < -0.001{ + if let scroll = scrollView{ + let minY = container!.frame.minY + switch dragDirection(vel) { + case .up where minY - minSheetPosition! > 0.001: scroll.setContentOffset(lastContentOffset, animated: false) self.finishDragging(with: vel) + default: + if !isSheetPosition(minY){ + self.finishDragging(with: vel) + } } }else{ self.finishDragging(with: vel) } - default: break } @@ -311,6 +339,37 @@ public class UBottomSheetCoordinator { } } + /** + Check if current top y position is one of the UBottomSheetCoordinatorDataSource#sheetPositions(availableHeight) + + - parameter point: current top y position + */ + private func isSheetPosition(_ point: CGFloat) -> Bool{ + return dataSource.sheetPositions(availableHeight).first(where: { (p) -> Bool in + abs(p - point) < 0.001 + }) != nil + } + + /// Scroll view pan direction state: up, down or idle + private enum DraggingState{ + case up, down, idle + } + + /** + Find the scroll pan direction; dragging up or down. + + - parameter velocity: draging velocity of scroll view recognizer + */ + private func dragDirection(_ velocity: CGPoint) -> DraggingState{ + if velocity.y < 0 { + return .up + }else if velocity.y > 0{ + return .down + }else{ + return .idle + } + } + /** It helps when finishing dragging to the nearest sheet position in the direction of movement. @@ -377,7 +436,7 @@ public class UBottomSheetCoordinator { let f = CGRect(x: 0, y: newY, width: oldFrame.width, height: height) container?.frame = f - self.delegate?.bottomSheet(self.container, didChange: .changing(f.minY, calculatePercent(at: f.minY))) + self.delegate?.bottomSheet(self.container, didChange: .progressing(f.minY, calculatePercent(at: f.minY))) } /** @@ -419,6 +478,9 @@ public class UBottomSheetCoordinator { }, completion: { finished in if self.lastAnimatedValue != position {return} self.delegate?.bottomSheet(self.container, didChange: .finished(position, self.calculatePercent(at: position))) + if position >= self.availableHeight{ + self.removeSheet() + } }) }else{ self.container!.frame = f diff --git a/UBottomSheetCoordinatorDataSource.swift b/UBottomSheetCoordinatorDataSource.swift index 98c4f86..93b5ed8 100644 --- a/UBottomSheetCoordinatorDataSource.swift +++ b/UBottomSheetCoordinatorDataSource.swift @@ -25,7 +25,7 @@ public protocol UBottomSheetCoordinatorDataSource: class { ///Default data source implementation extension UBottomSheetCoordinatorDataSource{ public func sheetPositions(_ availableHeight: CGFloat) -> [CGFloat]{ - return [availableHeight*0.2, availableHeight*0.8] + return [0.2, 0.7].map{$0*availableHeight} } public var animator: Animatable?{ diff --git a/UBottomSheetCoordinatorDelegate.swift b/UBottomSheetCoordinatorDelegate.swift index e32e01f..0ccf617 100644 --- a/UBottomSheetCoordinatorDelegate.swift +++ b/UBottomSheetCoordinatorDelegate.swift @@ -12,12 +12,10 @@ import UIKit public protocol UBottomSheetCoordinatorDelegate: class { func bottomSheet(_ container: UIView?, finishTranslateWith extraAnimation: @escaping ((_ percent: CGFloat)->Void)->Void) func bottomSheet(_ container: UIView?, didChange state: SheetTranslationState) - func bottomSheet(_ container: UIView?, didCreateBackView view: UIView) } ///Default empty implementations extension UBottomSheetCoordinatorDelegate{ public func bottomSheet(_ container: UIView?, finishTranslateWith extraAnimation: @escaping ((_ percent: CGFloat)->Void)->Void){ } public func bottomSheet(_ container: UIView?, didChange state: SheetTranslationState){ } - public func bottomSheet(_ container: UIView?, didCreateBackView view: UIView){ } }