// // Copyright 2020 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import UIKit /// Controller for custom sized presentations. By default, presented view controller will be sized as both half of the screen in width and height, and will be centered to the screen. Implement `CustomSizedPresentable` in presented view controller to change that if needed. This class can also be set as `transitioningDelegate` as presented view controller, as it's conforming `UIViewControllerTransitioningDelegate`. @objcMembers class CustomSizedPresentationController: UIPresentationController { // MARK: - Public Properties /// Corner radius for presented view controller's view. Default value is `8.0`. var cornerRadius: CGFloat = 8.0 /// Background color of dimming view, which is located behind the presented view controller's view. Default value is `white with 0.5 alpha`. var dimColor: UIColor = UIColor(white: 0.0, alpha: 0.5) /// Dismiss view controller when background tapped. Default value is `true`. var dismissOnBackgroundTap: Bool = true // MARK: - Private Properties /// Dim view private var dimmingView: UIView! /// Wrapper view for presentation. It's introduced to handle corner radius on presented view controller's view and it's superview of all other views. private var presentationWrappingView: UIView! // MARK: - Initializer override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) { super.init(presentedViewController: presentedViewController, presenting: presentingViewController) presentedViewController.modalPresentationStyle = .custom } // MARK: - Actions @objc private func dimmingViewTapped(_ sender: UITapGestureRecognizer) { if dismissOnBackgroundTap { presentedViewController.dismiss(animated: true, completion: nil) } } // MARK: - Presentation override func presentationTransitionWillBegin() { guard let presentedViewControllerView = super.presentedView else { return } // Wrap the presented view controller's view in an intermediate hierarchy // that applies a shadow and rounded corners to the top-left and top-right // edges. The final effect is built using three intermediate views. // // presentationWrapperView <- shadow // |- presentationRoundedCornerView <- rounded corners (masksToBounds) // |- presentedViewControllerWrapperView // |- presentedViewControllerView (presentedViewController.view) // // SEE ALSO: The note in AAPLCustomPresentationSecondViewController.m. do { let presentationWrapperView = UIView(frame: frameOfPresentedViewInContainerView) presentationWrapperView.layer.shadowOffset = CGSize(width: 0, height: -2) presentationWrapperView.layer.shadowRadius = 10 presentationWrapperView.layer.shadowColor = UIColor(white: 0, alpha: 0.5).cgColor presentationWrappingView = presentationWrapperView // presentationRoundedCornerView is CORNER_RADIUS points taller than the // height of the presented view controller's view. This is because // the cornerRadius is applied to all corners of the view. Since the // effect calls for only the top two corners to be rounded we size // the view such that the bottom CORNER_RADIUS points lie below // the bottom edge of the screen. let cornerViewRect = presentationWrapperView.bounds//.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: -cornerRadius, right: 0)) let presentationRoundedCornerView = UIView(frame: cornerViewRect) presentationRoundedCornerView.autoresizingMask = [.flexibleWidth, .flexibleHeight] presentationRoundedCornerView.layer.cornerRadius = cornerRadius presentationRoundedCornerView.layer.masksToBounds = true // To undo the extra height added to presentationRoundedCornerView, // presentedViewControllerWrapperView is inset by CORNER_RADIUS points. // This also matches the size of presentedViewControllerWrapperView's // bounds to the size of -frameOfPresentedViewInContainerView. let wrapperRect = presentationRoundedCornerView.bounds let presentedViewControllerWrapperView = UIView(frame: wrapperRect) presentedViewControllerWrapperView.autoresizingMask = [.flexibleWidth, .flexibleHeight] // Add presentedViewControllerView -> presentedViewControllerWrapperView. presentedViewControllerView.autoresizingMask = [.flexibleWidth, .flexibleHeight] presentedViewControllerView.frame = presentedViewControllerWrapperView.bounds presentedViewControllerWrapperView.addSubview(presentedViewControllerView) // Add presentedViewControllerWrapperView -> presentationRoundedCornerView. presentationRoundedCornerView.addSubview(presentedViewControllerWrapperView) // Add presentationRoundedCornerView -> presentationWrapperView. presentationWrapperView.addSubview(presentationRoundedCornerView) } // Add a dimming view behind presentationWrapperView. self.presentedView // is added later (by the animator) so any views added here will be // appear behind the -presentedView. do { let dimmingView = UIView(frame: containerView?.bounds ?? .zero) dimmingView.backgroundColor = dimColor dimmingView.isOpaque = false dimmingView.autoresizingMask = [.flexibleWidth, .flexibleHeight] let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dimmingViewTapped(_:))) dimmingView.addGestureRecognizer(tapGestureRecognizer) self.dimmingView = dimmingView containerView?.addSubview(dimmingView) // Get the transition coordinator for the presentation so we can // fade in the dimmingView alongside the presentation animation. let transitionCoordinator = self.presentingViewController.transitionCoordinator dimmingView.alpha = 0.0 transitionCoordinator?.animate(alongsideTransition: { _ in self.dimmingView?.alpha = 1.0 }, completion: nil) } } override func presentationTransitionDidEnd(_ completed: Bool) { if !completed { presentationWrappingView = nil dimmingView = nil } } // MARK: - Dismissal override func dismissalTransitionWillBegin() { guard let coordinator = presentingViewController.transitionCoordinator else { dimmingView.alpha = 0.0 return } coordinator.animate(alongsideTransition: { _ in self.dimmingView.alpha = 0.0 }) } override func dismissalTransitionDidEnd(_ completed: Bool) { if completed { presentationWrappingView = nil dimmingView = nil } } // MARK: - Overrides override var presentedView: UIView? { return presentationWrappingView } override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize { guard container === presentedViewController else { return super.size(forChildContentContainer: container, withParentContainerSize: parentSize) } // return value from presentable if implemented if let presentable = presentedViewController as? CustomSizedPresentable, let customSize = presentable.customSize?(withParentContainerSize: parentSize) { return customSize } // half of the width/height by default return CGSize(width: parentSize.width/2.0, height: parentSize.height/2.0) } override var frameOfPresentedViewInContainerView: CGRect { guard let containerView = containerView else { return super.frameOfPresentedViewInContainerView } let size = self.size(forChildContentContainer: presentedViewController, withParentContainerSize: containerView.bounds.size) // use origin value from presentable if implemented if let presentable = presentedViewController as? CustomSizedPresentable, let origin = presentable.position?(withParentContainerSize: containerView.bounds.size) { return CGRect(origin: origin, size: size) } // center presented view by default let origin = CGPoint(x: (containerView.bounds.width - size.width)/2, y: (containerView.bounds.height - size.height)/2) return CGRect(origin: origin, size: size) } override func containerViewWillLayoutSubviews() { super.containerViewWillLayoutSubviews() self.dimmingView?.frame = containerView?.bounds ?? .zero self.presentationWrappingView?.frame = frameOfPresentedViewInContainerView } override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { super.preferredContentSizeDidChange(forChildContentContainer: container) if container === presentedViewController { self.containerView?.setNeedsLayout() } } } // MARK: - UIViewControllerTransitioningDelegate extension CustomSizedPresentationController: UIViewControllerTransitioningDelegate { func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { return CustomSizedPresentationController(presentedViewController: presented, presenting: presenting) } }