This is the color of UI layer that dims content underneath when presenting something modally (eg action sheet). Values were taken from UIDimmingView that UIKit uses to obscure content under modally presented view controller.
220 lines
8.0 KiB
Swift
220 lines
8.0 KiB
Swift
//
|
|
// Copyright 2018 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
|
|
open class SheetViewController: UIViewController {
|
|
|
|
public var dismissHandler: ((SheetViewController) -> Void)?
|
|
|
|
public let contentView: UIView = UIView()
|
|
|
|
private let sheetView: SheetView = SheetView()
|
|
private let handleView: UIView = UIView()
|
|
|
|
public var isHandleHidden: Bool {
|
|
get { handleView.isHidden }
|
|
set { handleView.isHidden = newValue }
|
|
}
|
|
|
|
deinit {
|
|
Logger.verbose("")
|
|
}
|
|
|
|
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) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// 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) {
|
|
self.view.backgroundColor = .Signal.backdrop
|
|
}
|
|
|
|
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
|
|
private func didTapBackground() {
|
|
dismissHandler?(self)
|
|
}
|
|
|
|
@objc
|
|
private func didSwipeDown() {
|
|
dismissHandler?(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) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override var bounds: CGRect {
|
|
didSet {
|
|
updateMask()
|
|
}
|
|
}
|
|
|
|
private func updateMask() {
|
|
let cornerRadius: CGFloat = 16
|
|
let path: UIBezierPath = UIBezierPath(roundedRect: bounds,
|
|
byRoundingCorners: [.topLeft, .topRight],
|
|
cornerRadii: CGSize(square: cornerRadius))
|
|
let mask = CAShapeLayer()
|
|
mask.path = path.cgPath
|
|
self.layer.mask = mask
|
|
}
|
|
}
|