// // 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 } if let navController = presentedViewController as? UINavigationController, let presentable = navController.viewControllers.first(where: { $0 is CustomSizedPresentable }) 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) } if let navController = presentedViewController as? UINavigationController, let presentable = navController.viewControllers.first(where: { $0 is CustomSizedPresentable }) 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? { let controller = CustomSizedPresentationController(presentedViewController: presented, presenting: presenting) controller.cornerRadius = cornerRadius controller.dimColor = dimColor controller.dismissOnBackgroundTap = dismissOnBackgroundTap return controller } }