Sheet/Sheet/Bottom Sheet/Presentation/SheetPresenter.swift

143 lines
5.3 KiB
Swift

//
// SheetPresenter.swift
// Sheet
//
// Created by Guilherme Rambo on 05/08/19.
// Copyright © 2019 Guilherme Rambo. All rights reserved.
//
import UIKit
/// Allows a controller to present another controller as a sheet that can be
/// snapped to different positions.
public final class SheetPresenter: NSObject {
private var window: SheetPresentationWindow?
private var container: SheetContainerViewController?
private weak var presenter: UIViewController?
private var presenterWindow: UIWindow? {
return presenter?.view.window
}
/// Whether the sheet is currently being presented.
private(set) var isPresentingSheet = false
/// Starts the presentation of a controller as a sheet.
/// - Parameter presenter: The view controller that's presenting the sheet.
/// - Parameter content: The view controller that will be inside the sheet.
/// - Parameter initialDetent: The initial position of the sheet (defaults to `.middle`)
/// - Parameter allowedDetents: The allowed snapping positions for the sheet (defaults to all positions).
/// - Parameter dismissWhenFlungDown: Whether the sheet can be dismissed when flung down by the user.
/// - Parameter metrics: Metrics defining the look of the sheet (can be ommited to use default metrics).
public func presentSheet(from presenter: UIViewController,
with content: UIViewController,
initialDetent: SheetDetent = .middle,
allowedDetents: [SheetDetent] = SheetDetent.allCases,
dismissWhenFlungDown: Bool = false,
metrics: SheetMetrics = .default)
{
guard !isPresentingSheet else { return }
assert(presenter.view.window != nil, "Tried to present a sheet from a view controller that's not currently on screen!")
guard window == nil else { return }
self.presenter = presenter
presenterWindow?.clipsToBounds = true
let w = SheetPresentationWindow(frame: presenter.view.bounds)
let c = SheetContainerViewController(
sheetContentController: content,
presentingSheetPresenter: self,
initialDetent: initialDetent,
allowedDetents: allowedDetents,
metrics: metrics,
dismissWhenFlungDown: dismissWhenFlungDown
)
w.rootViewController = c
w.windowLevel = .alert
w.makeKeyAndVisible()
self.window = w
self.container = c
c.performSnapCompanionAnimations = { [weak self] detent in
guard let self = self else { return }
switch detent {
case .maximum:
self.animateToMaximumDetent()
default:
self.animateToNonMaximumDetent()
}
}
c.transitionToMaximumDetentProgressDidChange = { [weak self] progress in
self?.updateSheetAnimationStateToMaximumDetent(with: progress)
}
isPresentingSheet = true
}
/// Dismisses the sheet.
/// - Parameter coordinator: Perform the dismissal together with an animated transition.
/// - Parameter completion: Called when the dismissal animation has completed.
public func dismiss(with coordinator: UIViewControllerTransitionCoordinator? = nil, completion: (() -> Void)? = nil) {
container?.dismissSheet(duration: 0.4) { [weak self] in
completion?()
self?.window?.resignKey()
self?.window?.isHidden = true
self?.window?.removeFromSuperview()
self?.container = nil
self?.presenter = nil
self?.window = nil
self?.isPresentingSheet = false
}
}
private var presenterTranslationWhenAtMaximumDetent: CGFloat {
let safeAreaTop = container?.view.safeAreaInsets.top ?? 0
return safeAreaTop <= 20 ? safeAreaTop + 22 : safeAreaTop + 8
}
private let presenterHorizontalScaleWhenAtMaximumDetent: CGFloat = 0.914
private let presenterCornerRadiusWhenAtMaximumDetent: CGFloat = 10
private let presenterScaleWhenAtMaximumDetent: CGFloat = 0.9
private func updateSheetAnimationStateToMaximumDetent(with progress: CGFloat) {
let translation = presenterTranslationWhenAtMaximumDetent * progress
let radius = presenterCornerRadiusWhenAtMaximumDetent * progress
let scale = min(1, 1 - progress + presenterScaleWhenAtMaximumDetent)
let translationTransform = CATransform3DMakeTranslation(0, translation, 0)
let scaleTransform = CATransform3DMakeScale(scale, 1, 1)
presenterWindow?.layer.transform = CATransform3DConcat(translationTransform, scaleTransform)
presenterWindow?.layer.cornerRadius = radius
}
private func animateToMaximumDetent() {
let translationTransform = CATransform3DMakeTranslation(0, presenterTranslationWhenAtMaximumDetent, 0)
let scaleTransform = CATransform3DMakeScale(presenterScaleWhenAtMaximumDetent, 1, 1)
presenterWindow?.layer.transform = CATransform3DConcat(translationTransform, scaleTransform)
presenterWindow?.layer.cornerRadius = presenterCornerRadiusWhenAtMaximumDetent
}
private func animateToNonMaximumDetent() {
presenterWindow?.layer.transform = CATransform3DIdentity
presenterWindow?.layer.cornerRadius = 0
}
}