Last active
December 7, 2022 09:14
-
-
Save zwaldowski/0cce893245986a519c09c4be19ee3709 to your computer and use it in GitHub Desktop.
iOS presentation controller for bottom-focused cards using Auto Layout - https://www.icloud.com/iclouddrive/0wJzCDOwwXTRF53bM4xWLbYag#card-magic-ii
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
private class CardPresenter: UIPresentationController { | |
private let dimmingView = UIView() | |
private let roundingView = UIView() | |
// MARK: - | |
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) { | |
super.init(presentedViewController: presentedViewController, presenting: presentingViewController) | |
dimmingView.translatesAutoresizingMaskIntoConstraints = false | |
dimmingView.backgroundColor = UIColor(white: 0, alpha: 0.5) | |
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onTapShroud)) | |
tapGesture.cancelsTouchesInView = false | |
dimmingView.addGestureRecognizer(tapGesture) | |
roundingView.translatesAutoresizingMaskIntoConstraints = false | |
roundingView.backgroundColor = .systemBackground | |
roundingView.clipsToBounds = true | |
roundingView.layer.cornerRadius = 42 | |
roundingView.layer.cornerCurve = .continuous | |
} | |
override func containerViewWillLayoutSubviews() { | |
super.containerViewWillLayoutSubviews() | |
// Double-checking this here in case nested modals stteal our presented | |
// view controller's view, since that breaks all of our constraints. | |
installPresentedViewInCustomViews() | |
} | |
override var presentedView: UIView? { | |
return roundingView | |
} | |
override func presentationTransitionWillBegin() { | |
super.presentationTransitionWillBegin() | |
installCustomViews() | |
installPresentedViewInCustomViews() | |
animateDimmingViewIn() | |
} | |
override func presentationTransitionDidEnd(_ completed: Bool) { | |
// Remove views if transition was aborted. | |
// | |
// If transition completed normally, nothing to do. | |
if !completed { | |
removeCustomViews() | |
} | |
} | |
override func dismissalTransitionWillBegin() { | |
super.dismissalTransitionWillBegin() | |
animateDimmingViewOut() | |
} | |
override func dismissalTransitionDidEnd(_ completed: Bool) { | |
// Remove views if transition completed. | |
// | |
// If transition was aborted, nothing to do. | |
if completed { | |
removeCustomViews() | |
} | |
} | |
// MARK: - | |
@objc private func onTapShroud(_ sender: UIControl) { | |
presentingViewController.dismiss(animated: true, completion: nil) | |
} | |
private func installCustomViews() { | |
guard let containerView = containerView else { | |
assertionFailure("Can't set up custom views without a container view. Transition must not be started yet.") | |
return | |
} | |
containerView.addSubview(dimmingView) | |
containerView.addSubview(roundingView) | |
NSLayoutConstraint.activate([ | |
// Block the content. | |
dimmingView.topAnchor.constraint(equalTo: containerView.topAnchor), | |
dimmingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), | |
containerView.bottomAnchor.constraint(equalTo: dimmingView.bottomAnchor), | |
containerView.trailingAnchor.constraint(equalTo: dimmingView.trailingAnchor), | |
// Fit the card to the bottom of the screen within the readable width. | |
roundingView.topAnchor.constraint(greaterThanOrEqualToSystemSpacingBelow: containerView.readableContentGuide.topAnchor, multiplier: 1), | |
roundingView.leadingAnchor.constraint(equalTo: containerView.readableContentGuide.leadingAnchor), | |
containerView.readableContentGuide.bottomAnchor.constraint(equalTo: roundingView.bottomAnchor), | |
containerView.readableContentGuide.trailingAnchor.constraint(equalTo: roundingView.trailingAnchor), { | |
// Weakly squeeze the content toward the bottom. This functions | |
// just like the `verticalFittingPriority` in | |
// `UIView.systemLayoutSizeFitting` to get the card to try | |
// and fit its content while meeting the other constrainnts. | |
let minimizingHeight = roundingView.heightAnchor.constraint(equalToConstant: 0) | |
minimizingHeight.priority = .fittingSizeLevel | |
return minimizingHeight | |
}() | |
]) | |
} | |
private func installPresentedViewInCustomViews() { | |
guard !presentedViewController.view.isDescendant(of: roundingView) else { return } | |
presentedViewController.view.translatesAutoresizingMaskIntoConstraints = false | |
roundingView.addSubview(presentedViewController.view) | |
NSLayoutConstraint.activate([ | |
presentedViewController.view.topAnchor.constraint(equalTo: roundingView.topAnchor), | |
presentedViewController.view.leadingAnchor.constraint(equalTo: roundingView.leadingAnchor), | |
roundingView.bottomAnchor.constraint(equalTo: presentedViewController.view.bottomAnchor), | |
roundingView.trailingAnchor.constraint(equalTo: presentedViewController.view.trailingAnchor), | |
]) | |
} | |
private func animateDimmingViewIn() { | |
dimmingView.alpha = 0 | |
presentingViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in | |
self.dimmingView.alpha = 1 | |
}, completion: nil) | |
} | |
private func animateDimmingViewOut() { | |
presentingViewController.transitionCoordinator?.animate(alongsideTransition: { _ in | |
self.dimmingView.alpha = 0 | |
}, completion: nil) | |
} | |
private func removeCustomViews() { | |
roundingView.removeFromSuperview() | |
dimmingView.removeFromSuperview() | |
} | |
} | |
final class CardPresenting: NSObject, UIViewControllerTransitioningDelegate { | |
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { | |
return CardPresenter(presentedViewController: presented, presenting: presenting) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for sharing. However when I tried to use it, it just dims the view and nothing else. Can you explain how to properly use it?