Skip to content

Instantly share code, notes, and snippets.

@calebd
Created July 18, 2017 02:42
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save calebd/6f90eb96e524ca0665cac7b9c3204ca6 to your computer and use it in GitHub Desktop.
Save calebd/6f90eb96e524ca0665cac7b9c3204ca6 to your computer and use it in GitHub Desktop.
Action Sheet Presentation Controller

Action Sheet Presentation Controller

Use any view controller in an action sheet. This is not worthy of a dedicated pod or a framework. Please don't make it one.

Usage

Return an ActionSheetPresentationController from presentationController(forPresented:presenting:) and make sure the presented view controller's view has a measurable Auto Layout size.

To-Dos

  • Respect preferredContentSize instead of only Auto Layout size
  • Adapt to regular width by switching to a popover presentation
  • Switch to a normal UIButton instead of a custom dismiss button.
// Created by Caleb Davenport on 7/14/17.
import UIKit
final class ActionSheetPresentationController: UIPresentationController {
// MARK: - Properties
private var dimmingView: UIView!
private var customPresentedView: UIView!
override var presentedView: UIView? {
return customPresentedView
}
override var frameOfPresentedViewInContainerView: CGRect {
let size = customPresentedView.systemLayoutSizeFitting(
containerView!.bounds.size,
withHorizontalFittingPriority: UILayoutPriorityRequired,
verticalFittingPriority: UILayoutPriorityFittingSizeLevel)
let (slice, _) = containerView!.bounds.divided(atDistance: size.height, from: .maxYEdge)
return slice
}
// MARK: - UIPresentationController
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
if dimmingView == nil {
dimmingView = UIView()
dimmingView.backgroundColor = UIColor(white: 0, alpha: 0.4)
let cancelGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(cancel))
dimmingView.addGestureRecognizer(cancelGestureRecognizer)
dimmingView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
dimmingView.frame = containerView!.bounds
containerView!.addSubview(dimmingView)
}
if customPresentedView == nil {
let dismissButton = ActionSheetPresentationControllerDismissButton()
dismissButton.addTarget(self, action: #selector(cancel), for: .touchUpInside)
dismissButton.setContentHuggingPriority(UILayoutPriorityDefaultHigh, for: .vertical)
let dismissButtonSectionView = ActionSheetPresentationControllerSectionView()
dismissButton.translatesAutoresizingMaskIntoConstraints = false
dismissButtonSectionView.addSubview(dismissButton)
let presentedViewControllerSectionView = ActionSheetPresentationControllerSectionView()
presentedViewController.view.translatesAutoresizingMaskIntoConstraints = false
presentedViewControllerSectionView.addSubview(presentedViewController.view)
NSLayoutConstraint.activate([
dismissButton.leadingAnchor.constraint(equalTo: dismissButtonSectionView.leadingAnchor),
dismissButton.trailingAnchor.constraint(equalTo: dismissButtonSectionView.trailingAnchor),
dismissButton.topAnchor.constraint(equalTo: dismissButtonSectionView.topAnchor),
dismissButton.bottomAnchor.constraint(equalTo: dismissButtonSectionView.bottomAnchor),
presentedViewController.view.leadingAnchor.constraint(equalTo: presentedViewControllerSectionView.leadingAnchor),
presentedViewController.view.trailingAnchor.constraint(equalTo: presentedViewControllerSectionView.trailingAnchor),
presentedViewController.view.topAnchor.constraint(equalTo: presentedViewControllerSectionView.topAnchor),
presentedViewController.view.bottomAnchor.constraint(equalTo: presentedViewControllerSectionView.bottomAnchor)])
let stackView = UIStackView(arrangedSubviews: [presentedViewControllerSectionView, dismissButtonSectionView])
stackView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
stackView.axis = .vertical
stackView.isLayoutMarginsRelativeArrangement = true
stackView.spacing = 10
stackView.layoutMargins = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
customPresentedView = stackView
}
dimmingView.alpha = 0
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 1
})
}
override func dismissalTransitionWillBegin() {
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 0
})
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
self.customPresentedView.frame = self.frameOfPresentedViewInContainerView
})
}
// MARK: - Private
@objc private func cancel() {
presentedViewController.dismiss(animated: true, completion: nil)
}
}
// Created by Caleb Davenport on 7/14/17.
import UIKit
final class ActionSheetPresentationControllerDismissButton: UIControl {
// MARK: - Properties
private let textLabel: UILabel = {
let view = UILabel()
view.font = UIFont.systemFont(ofSize: 20, weight: UIFontWeightSemibold)
view.text = L10n.General.done.localizedCapitalized
view.textAlignment = .center
return view
}()
override var intrinsicContentSize: CGSize {
return CGSize(width: UIViewNoIntrinsicMetric, height: 57)
}
// MARK: - Initializers
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
// MARK: - UIView
override func layoutSubviews() {
super.layoutSubviews()
textLabel.frame = bounds
}
override func tintColorDidChange() {
super.tintColorDidChange()
textLabel.textColor = tintColor
}
// MARK: - Private
private func setup() {
textLabel.textColor = tintColor
addSubview(textLabel)
}
}
// Created by Caleb Davenport on 7/14/17.
import UIKit
final class ActionSheetPresentationControllerSectionView: UIView {
// MARK: - Properties
private let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight))
// MARK: - Initializers
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
visualEffectView.frame = bounds
}
// MARK: - Private
private func setup() {
layer.masksToBounds = true
layer.cornerRadius = 14
visualEffectView.isUserInteractionEnabled = false
addSubview(visualEffectView)
}
}
// Created by Caleb Davenport on 7/14/17.
import UIKit
final class DatePickerViewController: UIViewController {
// MARK: - Properties
let datePicker = UIDatePicker()
// MARK: - Initializers
init() {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
transitioningDelegate = self
}
@available(*, unavailable)
required init?(coder: NSCoder) {
unimplemented()
}
// MARK: - UIViewController
override func viewDidLoad() {
super.viewDidLoad()
datePicker.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(datePicker)
NSLayoutConstraint.activate([
datePicker.leadingAnchor.constraint(equalTo: view.leadingAnchor),
datePicker.trailingAnchor.constraint(equalTo: view.trailingAnchor),
datePicker.topAnchor.constraint(equalTo: view.topAnchor),
datePicker.bottomAnchor.constraint(equalTo: view.bottomAnchor)])
}
}
extension DatePickerViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return ActionSheetPresentationController(presentedViewController: presented, presenting: presenting)
}
}
@nckh
Copy link

nckh commented Oct 9, 2018

Thank you so much, this has been super helpful to me as I was having the same problem.
Two questions though.

  1. When comparing with UIAlertController, I noticed that the sheet's width stays the same in portrait and landscape, and seems to be equal to the layout margin frame in portrait orientation. In order to compute this value in frameOfPresentedViewInContainerView, is there a way I can get the layout margin frame of the portrait orientation even when the phone is actually in landscape?

  2. Originally I was trying to use auto layout constraints to size and place the presented view in the container, but never managed to get it work. Is that the actual recommended way to use frame calculation in frameOfPresentedViewInContainerView instead?

Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment