1

I want to implement the presentation of controllers as in the standard applications Mail, Music and as in the UIActivityViewController. This controller shows on half by default and if you swipe, the controller will open completely.

How I can implement this presentation with my custom controller?

I can’t find references to the implementation of such a presentation in the documentation.

Screenshots:

Example from Apple Music

Dmitry Kuzin
  • 656
  • 1
  • 6
  • 15

2 Answers2

1

Do not use a custom presentation controller, because you would lose the visuals and physics provided by Apple.

Present normally, then use systemLayoutSizeFitting in viewDidLayoutSubviews to adjust the frame to the minimum required size. Assuming your custom controller is CustomController, replace it with this subclass and you are done.

import UIKit

final class ResizedViewController: CustomController {

    private let cornerRadius: CGFloat = 22
    private var containerView: UIView? { view.superview }
    private var presentedView: UIView? { view }

    private lazy var tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(resign))

    override func viewDidLoad() {

        super.viewDidLoad()
        tapRecognizer.cancelsTouchesInView = true
        view.layer.cornerRadius = cornerRadius
        view.layer.cornerCurve = .continuous
    }

    override func viewDidAppear(_ animated: Bool) {

        super.viewDidAppear(animated)
        containerView?.addGestureRecognizer(tapRecognizer)
    }

    override func viewWillDisappear(_ animated: Bool) {

        super.viewWillDisappear(animated)
        if let recognizer = containerView?.gestureRecognizers?.first(where: { $0 == tapRecognizer }) {
            containerView?.removeGestureRecognizer(recognizer)
        }
    }

    override func viewDidLayoutSubviews() {

        super.viewDidLayoutSubviews()
        view.frame = frameOfPresentedViewInContainerView
    }

    private var frameOfPresentedViewInContainerView: CGRect {
        guard let containerView = containerView else {
            return CGRect.zero
        }
        guard let presentedView = presentedView else {
            return CGRect.zero
        }
        let safeBounds = containerView.bounds.inset(by: containerView.safeAreaInsets)
        let newSize = CGSize(
            width: safeBounds.width,
            height: UIView.layoutFittingCompressedSize.height
        )
        let newHeight = view.safeAreaInsets.bottom +
            presentedView.systemLayoutSizeFitting(
                newSize,
                withHorizontalFittingPriority: .required,
                verticalFittingPriority: .defaultLow
            ).height
        return CGRect(
            x: 0,
            y: containerView.frame.height - newHeight,
            width: safeBounds.width,
            height: newHeight
        )
    }

    @objc
    private func resign() {

        dismiss(animated: true, completion: nil)
    }
}

The tap recognizer on the superview is needed to dismiss the controller tapping outside. The cornerRadius from Apple is lost after altering the frame so I set it again.

Jano
  • 62,815
  • 21
  • 164
  • 192
0

You will need to subclass the UIPresentationController to achieve that.

Then, overwrite its method to show the view only in the bottom part of the page - frameOfPresentedViewInContainerView.

It's fairly straightforward if you have some experience with UIKit. Otherwise, I advise skimming through a couple of tutorials about implementing custom transitions using UIPresentationController. It should get you started.

Rafał Sroka
  • 39,540
  • 23
  • 113
  • 143