mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			224 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			224 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Swift
		
	
| //
 | |
| //  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| import Foundation
 | |
| 
 | |
| @objc(OWSSheetViewControllerDelegate)
 | |
| public protocol SheetViewControllerDelegate: class {
 | |
|     func sheetViewControllerRequestedDismiss(_ sheetViewController: SheetViewController)
 | |
| }
 | |
| 
 | |
| @objc(OWSSheetViewController)
 | |
| public class SheetViewController: UIViewController {
 | |
| 
 | |
|     @objc
 | |
|     weak var delegate: SheetViewControllerDelegate?
 | |
| 
 | |
|     @objc
 | |
|     public let contentView: UIView = UIView()
 | |
| 
 | |
|     private let sheetView: SheetView = SheetView()
 | |
|     private let handleView: UIView = UIView()
 | |
| 
 | |
|     deinit {
 | |
|         Logger.verbose("")
 | |
|     }
 | |
| 
 | |
|     @objc
 | |
|     public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
 | |
|         super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
 | |
|         self.transitioningDelegate = self
 | |
|         self.modalPresentationStyle = .overCurrentContext
 | |
|     }
 | |
| 
 | |
|     public required init?(coder aDecoder: NSCoder) {
 | |
|         notImplemented()
 | |
|     }
 | |
| 
 | |
|     // MARK: View LifeCycle
 | |
| 
 | |
|     var sheetViewVerticalConstraint: NSLayoutConstraint?
 | |
| 
 | |
|     override public func loadView() {
 | |
|         self.view = UIView()
 | |
| 
 | |
|         sheetView.preservesSuperviewLayoutMargins = true
 | |
| 
 | |
|         sheetView.addSubview(contentView)
 | |
|         contentView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
 | |
|         contentView.autoPinEdge(toSuperviewMargin: .bottom)
 | |
| 
 | |
|         view.addSubview(sheetView)
 | |
|         sheetView.autoPinWidthToSuperview()
 | |
|         sheetView.setContentHuggingVerticalHigh()
 | |
|         sheetView.setCompressionResistanceHigh()
 | |
|         self.sheetViewVerticalConstraint = sheetView.autoPinEdge(.top, to: .bottom, of: self.view)
 | |
| 
 | |
|         handleView.backgroundColor = Theme.isDarkThemeEnabled ? UIColor.ows_white : UIColor.ows_gray05
 | |
|         let kHandleViewHeight: CGFloat = 5
 | |
|         handleView.autoSetDimensions(to: CGSize(width: 40, height: kHandleViewHeight))
 | |
|         handleView.layer.cornerRadius = kHandleViewHeight / 2
 | |
|         view.addSubview(handleView)
 | |
|         handleView.autoAlignAxis(.vertical, toSameAxisOf: sheetView)
 | |
|         handleView.autoPinEdge(.bottom, to: .top, of: sheetView, withOffset: -6)
 | |
| 
 | |
|         // Gestures
 | |
|         let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapBackground))
 | |
|         self.view.addGestureRecognizer(tapGesture)
 | |
| 
 | |
|         let swipeDownGesture = UISwipeGestureRecognizer(target: self, action: #selector(didSwipeDown))
 | |
|         swipeDownGesture.direction = .down
 | |
|         self.view.addGestureRecognizer(swipeDownGesture)
 | |
|     }
 | |
| 
 | |
|     // MARK: Present / Dismiss animations
 | |
| 
 | |
|     fileprivate func animatePresentation(completion: @escaping (Bool) -> Void) {
 | |
|         guard let sheetViewVerticalConstraint = self.sheetViewVerticalConstraint else {
 | |
|             owsFailDebug("sheetViewVerticalConstraint was unexpectedly nil")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         let backgroundDuration: TimeInterval = 0.1
 | |
|         UIView.animate(withDuration: backgroundDuration) {
 | |
|             let alpha: CGFloat = Theme.isDarkThemeEnabled ? 0.7 : 0.6
 | |
|             self.view.backgroundColor = UIColor.black.withAlphaComponent(alpha)
 | |
|         }
 | |
| 
 | |
|         self.sheetView.superview?.layoutIfNeeded()
 | |
| 
 | |
|         NSLayoutConstraint.deactivate([sheetViewVerticalConstraint])
 | |
|         self.sheetViewVerticalConstraint = self.sheetView.autoPinEdge(toSuperviewEdge: .bottom)
 | |
|         UIView.animate(withDuration: 0.2,
 | |
|                        delay: backgroundDuration,
 | |
|                        options: .curveEaseOut,
 | |
|                        animations: {
 | |
|                         self.sheetView.superview?.layoutIfNeeded()
 | |
|         },
 | |
|                        completion: completion)
 | |
|     }
 | |
| 
 | |
|     fileprivate func animateDismiss(completion: @escaping (Bool) -> Void) {
 | |
|         guard let sheetViewVerticalConstraint = self.sheetViewVerticalConstraint else {
 | |
|             owsFailDebug("sheetVerticalConstraint was unexpectedly nil")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         self.sheetView.superview?.layoutIfNeeded()
 | |
|         NSLayoutConstraint.deactivate([sheetViewVerticalConstraint])
 | |
| 
 | |
|         let dismissDuration: TimeInterval = 0.2
 | |
|         self.sheetViewVerticalConstraint = self.sheetView.autoPinEdge(.top, to: .bottom, of: self.view)
 | |
|         UIView.animate(withDuration: dismissDuration,
 | |
|                        delay: 0,
 | |
|                        options: .curveEaseOut,
 | |
|                        animations: {
 | |
|                         self.view.backgroundColor = UIColor.clear
 | |
|                         self.sheetView.superview?.layoutIfNeeded()
 | |
|         },
 | |
|                        completion: completion)
 | |
|     }
 | |
| 
 | |
|     // MARK: Actions
 | |
| 
 | |
|     @objc
 | |
|     func didTapBackground() {
 | |
|         // inform delegate to
 | |
|         delegate?.sheetViewControllerRequestedDismiss(self)
 | |
|     }
 | |
| 
 | |
|     @objc
 | |
|     func didSwipeDown() {
 | |
|         // inform delegate to
 | |
|         delegate?.sheetViewControllerRequestedDismiss(self)
 | |
|     }
 | |
| }
 | |
| 
 | |
| extension SheetViewController: UIViewControllerTransitioningDelegate {
 | |
|     public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
 | |
|         return SheetViewPresentationController(sheetViewController: self)
 | |
|     }
 | |
| 
 | |
|     public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
 | |
|         return SheetViewDismissalController(sheetViewController: self)
 | |
|     }
 | |
| }
 | |
| 
 | |
| private class SheetViewPresentationController: NSObject, UIViewControllerAnimatedTransitioning {
 | |
| 
 | |
|     let sheetViewController: SheetViewController
 | |
|     init(sheetViewController: SheetViewController) {
 | |
|         self.sheetViewController = sheetViewController
 | |
|     }
 | |
| 
 | |
|     // This is used for percent driven interactive transitions, as well as for
 | |
|     // container controllers that have companion animations that might need to
 | |
|     // synchronize with the main animation.
 | |
|     public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
 | |
|         return 0.3
 | |
|     }
 | |
| 
 | |
|     // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition.
 | |
|     public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
 | |
|         Logger.debug("")
 | |
|         transitionContext.containerView.addSubview(sheetViewController.view)
 | |
|         sheetViewController.view.autoPinEdgesToSuperviewEdges()
 | |
|         sheetViewController.animatePresentation { didComplete in
 | |
|             Logger.debug("completed: \(didComplete)")
 | |
|             transitionContext.completeTransition(didComplete)
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| private class SheetViewDismissalController: NSObject, UIViewControllerAnimatedTransitioning {
 | |
| 
 | |
|     let sheetViewController: SheetViewController
 | |
|     init(sheetViewController: SheetViewController) {
 | |
|         self.sheetViewController = sheetViewController
 | |
|     }
 | |
| 
 | |
|     // This is used for percent driven interactive transitions, as well as for
 | |
|     // container controllers that have companion animations that might need to
 | |
|     // synchronize with the main animation.
 | |
|     public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
 | |
|         return 0.3
 | |
|     }
 | |
| 
 | |
|     // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition.
 | |
|     public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
 | |
|         Logger.debug("")
 | |
|         sheetViewController.animateDismiss { didComplete in
 | |
|             Logger.debug("completed: \(didComplete)")
 | |
|             transitionContext.completeTransition(didComplete)
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| private class SheetView: UIView {
 | |
| 
 | |
|     override init(frame: CGRect) {
 | |
|         super.init(frame: frame)
 | |
|         self.backgroundColor = Theme.isDarkThemeEnabled ? UIColor.ows_gray90
 | |
|             : UIColor.ows_gray05
 | |
|     }
 | |
| 
 | |
|     required init?(coder aDecoder: NSCoder) {
 | |
|         notImplemented()
 | |
|     }
 | |
| 
 | |
|     override var bounds: CGRect {
 | |
|         didSet {
 | |
|             updateMask()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private func updateMask() {
 | |
|         let cornerRadius: CGFloat = 16
 | |
|         let path: UIBezierPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
 | |
|         let mask = CAShapeLayer()
 | |
|         mask.path = path.cgPath
 | |
|         self.layer.mask = mask
 | |
|     }
 | |
| }
 |