Signal-iOS/SignalUI/ViewControllers/SheetViewController.swift
Igor Solomennikov 6dd2815fde
Unify backdropColor values.
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.
2025-10-24 14:43:08 -07:00

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
}
}