UIKit框架(二十二) —— 基于UIPresentationController的自定义viewController的转场和展示(一)

版本记录

版本号 时间
V1.0 2019.07.25 星期四

前言

iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)

UIPresentationController API

首先看下UIPresentationController的API文档

NS_ASSUME_NONNULL_BEGIN

@class UIPresentationController;

@protocol UIAdaptivePresentationControllerDelegate <NSObject>

@optional

/* For iOS8.0, the only supported adaptive presentation styles are UIModalPresentationFullScreen and UIModalPresentationOverFullScreen. */
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller;

// Returning UIModalPresentationNone will indicate that an adaptation should not happen.
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection NS_AVAILABLE_IOS(8_3);

/* If this method is not implemented, or returns nil, then the originally presented view controller is used. */
- (nullable UIViewController *)presentationController:(UIPresentationController *)controller viewControllerForAdaptivePresentationStyle:(UIModalPresentationStyle)style;

// If there is no adaptation happening and an original style is used UIModalPresentationNone will be passed as an argument.
- (void)presentationController:(UIPresentationController *)presentationController willPresentWithAdaptiveStyle:(UIModalPresentationStyle)style transitionCoordinator:(nullable id <UIViewControllerTransitionCoordinator>)transitionCoordinator NS_AVAILABLE_IOS(8_3);


@end

NS_CLASS_AVAILABLE_IOS(8_0) @interface UIPresentationController : NSObject <UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>

@property(nonatomic, strong, readonly) UIViewController *presentingViewController;
@property(nonatomic, strong, readonly) UIViewController *presentedViewController;

@property(nonatomic, readonly) UIModalPresentationStyle presentationStyle;

// The view in which a presentation occurs. It is an ancestor of both the presenting and presented view controller's views.
// This view is being passed to the animation controller.
@property(nullable, nonatomic, readonly, strong) UIView *containerView;

@property(nullable, nonatomic, weak) id <UIAdaptivePresentationControllerDelegate> delegate;

- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(nullable UIViewController *)presentingViewController NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;

// By default this implementation defers to the delegate, if one exists, or returns the current presentation style. UIFormSheetPresentationController, and
// UIPopoverPresentationController override this implementation to return UIModalPresentationStyleFullscreen if the delegate does not provide an
// implementation for adaptivePresentationStyleForPresentationController:
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) UIModalPresentationStyle adaptivePresentationStyle;
#else
- (UIModalPresentationStyle)adaptivePresentationStyle;
#endif
- (UIModalPresentationStyle)adaptivePresentationStyleForTraitCollection:(UITraitCollection *)traitCollection NS_AVAILABLE_IOS(8_3);

- (void)containerViewWillLayoutSubviews;
- (void)containerViewDidLayoutSubviews;

// A view that's going to be animated during the presentation. Must be an ancestor of a presented view controller's view
// or a presented view controller's view itself.
// (Default: presented view controller's view)
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIView *presentedView;

// Position of the presented view in the container view by the end of the presentation transition.
// (Default: container view bounds)
@property(nonatomic, readonly) CGRect frameOfPresentedViewInContainerView;

// By default each new presentation is full screen.
// This behavior can be overriden with the following method to force a current context presentation.
// (Default: YES)
@property(nonatomic, readonly) BOOL shouldPresentInFullscreen;

// Indicate whether the view controller's view we are transitioning from will be removed from the window in the end of the
// presentation transition
// (Default: NO)
@property(nonatomic, readonly) BOOL shouldRemovePresentersView;
#else
- (nullable UIView *)presentedView;

// Position of the presented view in the container view by the end of the presentation transition.
// (Default: container view bounds)
- (CGRect)frameOfPresentedViewInContainerView;

// By default each new presentation is full screen.
// This behavior can be overriden with the following method to force a current context presentation.
// (Default: YES)
- (BOOL)shouldPresentInFullscreen;

// Indicate whether the view controller's view we are transitioning from will be removed from the window in the end of the
// presentation transition
// (Default: NO)
- (BOOL)shouldRemovePresentersView;
#endif

- (void)presentationTransitionWillBegin;
- (void)presentationTransitionDidEnd:(BOOL)completed;
- (void)dismissalTransitionWillBegin;
- (void)dismissalTransitionDidEnd:(BOOL)completed;

// Modifies the trait collection for the presentation controller.
@property(nullable, nonatomic, copy) UITraitCollection *overrideTraitCollection;

@end

NS_ASSUME_NONNULL_END


#else
#import <UIKitCore/UIPresentationController.h>
#endif

开始

先看下主要内容

主要内容:了解如何使用UIPresentationController构建自定义视图控制器转换和展示。

接着看下写作环境

Swift 5, iOS 13, Xcode 11

从iOS早期开始,View Controller的演示一直是iOS开发人员工具包中不可或缺的一部分。

您之前可能使用过present(_:animated:completion :)但是,如果您和很多开发人员一样,那么您仍然使用iOS附带的默认转场样式。

在此UIPresentationController教程中,您将学习如何使用自定义转场和自定义展示样式呈现视图控制器。

您不再局限于全屏或弹出式展示文稿和标准转场动画。你将从一些单调,毫无生气的视图控制器演示开始,并将它们变有活力。

当你完成时,你将学习如何:

  • 1) 创建UIPresentationController子类。
  • 2) 使用UIPresentationController进行光滑的自定义展示。呈现的视图控制器甚至不必知道它在那里。
  • 3) 通过调整展示参数,为各种控制器重用UIPresentationController
  • 4) 进行支持应用可能遇到的任何屏幕尺寸的自适应展示。

随着2020年夏季比赛一年之后,一位客户让您创建一个应用程序,以确定竞争国家的奖牌数量。

虽然功能要求非常简单,但您的赞助商要求提供一个相当酷的类似滑入的转换来呈现赛事列表。

起初,你感到有些恐慌。但是你意识到你确实拥有触手可及的过渡构建工具,你就会放心了。

打开已有的创建项目,花一点时间熟悉项目和以下元素:

  • MainViewController.swift:此项目的主控制器,所有展示都从该控制器开始。这将是您要修改的项目中唯一的现有文件。
  • GamesTableViewController.swift:显示用户可以选择的比赛列表。
  • MedalCountViewController.swift:显示所选体育赛事的奖牌数。
  • GamesDataStore.swift:表示项目中的模型层。它负责创建和存储模型对象。

在开始更改内容之前,请构建并运行应用程序以查看其功能。您将看到以下屏幕:

首先,点击Summer以显示夏季菜单,Games TableViewController

请注意,菜单显示是默认菜单,从底部开始。 客户希望看到一个滑动的幻灯片展示。

接下来,点击London 2012以关闭菜单并返回主屏幕。 这一次,您将看到更新的徽标。

最后,点击Medal Count为2012年赛事调出MedalCountViewController

如您所见,此控制器还使用旧的自下而上的默认展示。点按屏幕即可将其关闭。

现在您已经看到了要升级的应用程序,现在是时候将注意力转向UIPresentationController的一些核心概念和理论。


Core Concepts for iOS Transition

当你调用present(_:animated:completion :)时,iOS会做三件事。

首先,它实例化一个UIPresentationController。其次,它将呈现的视图控制器附加到自身。最后,它使用内置的模态展示(modal presentation )样式之一呈现它。

您有能力重写此机制并为自定义展示提供您自己的UIPresentationController子类。

如果您想在应用程序中构建精美的展示,必须了解这些关键组件:

  • 1) 呈现的视图控制器具有transitioning delegate,其负责加载UIPresentationController以及呈现和dismiss动画控制器。该代理是一个符合UIViewControllerTransitioningDelegate的对象。
  • 2) UIPresentationController子类是一个具有许多展示自定义方法的对象。您将在本教程后面看到其中的一些内容。
  • 3) animation controller对象负责演示和dismiss动画。它符合UIViewControllerAnimatedTransitioning。请注意,某些用例需要两个控制器:一个用于展示,一个用于dismiss
  • 4) 展示控制器的代理告诉展示控制器在其特征集合发生变化时要做什么。为了适应性,代理必须是符合UIAdaptivePresentationControllerDelegate的对象。

在你深入之前,这就是你需要知道的!


Creating the Transitioning Delegate

transitioningDelegate继承自NSObject并符合UIViewControllerTransitioningDelegate

UIViewControllerTransitioningDelegate协议,声明了五种用于管理转换的可选方法。

在本教程中,您将使用其中的三种方法。

1. Setting Up the Framework

转到File ▸ New ▸ File…,选择iOS ▸ Source ▸ Cocoa Touch Class,然后单击Next。 将名称设置为SlideInPresentationManager,使其成为NSObject的子类并将语言设置为Swift

单击Next,将组设置为Presentation,然后单击Create

注意:您将SlideInPresentationManager声明为NSObject,因为UIViewControllertransitioningDelegate必须符合NSObjectProtocol

打开SlideInPresentationManager.swift并添加以下扩展:

// MARK: - UIViewControllerTransitioningDelegate
extension SlideInPresentationManager: UIViewControllerTransitioningDelegate {
}

在这里,您使SlideInPresentationManager符合UIViewControllerTransitioningDelegate

MainViewController中,您可以看到两个赛季的按钮:左侧为夏季,右侧为冬季。 底部还有Medal Count

要使展示适合每个按钮的上下文,您将向SlideInPresentationManager添加方向属性。 稍后,您将此属性传递给展示和动画控制器。

将以下内容添加到SlideInPresentationManager.swift的顶部:

enum PresentationDirection {
  case left
  case top
  case right
  case bottom
}

在这里,您声明一个简单的枚举来表示展示的方向。

接下来,将以下属性添加到SlideInPresentationManager

var direction: PresentationDirection = .left

在这里,您要添加一个direction并给它一个默认值left

要为您呈现的每个控制器指定SlideInPresentationManager的实例作为transitioningDelegate,请打开MainViewController.swift并在dataStore定义以下添加下面代码:

lazy var slideInTransitioningDelegate = SlideInPresentationManager()

你可能会问自己为什么要在MainViewController上添加它作为属性。 有两个原因:

  • 1) transitioningDelegate是一个weak属性,因此您必须在某处保留对该委托的强引用。
  • 2) 您不希望在展示的控制器本身上保留此引用,因为您可能希望在不同的展示样式上重用它。 确定要使用的展示类型现在是演示控制器的任务。

接下来,找到prepare(for:sender :),并将其替换为以下内容:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if let controller = segue.destination as? GamesTableViewController {
    if segue.identifier == "SummerSegue" {
      controller.gamesArray = dataStore.allGames.summer

      //1
      slideInTransitioningDelegate.direction = .left
    } else if segue.identifier == "WinterSegue" {
      controller.gamesArray = dataStore.allGames.winter

      //2
      slideInTransitioningDelegate.direction = .right
    }
    controller.delegate = self

    //3
    controller.transitioningDelegate = slideInTransitioningDelegate

    //4
    controller.modalPresentationStyle = .custom
  } else if let controller = segue.destination as? MedalCountViewController {
    controller.medalWinners = presentedGames?.medalWinners

    //5
    slideInTransitioningDelegate.direction = .bottom
    controller.transitioningDelegate = slideInTransitioningDelegate
    controller.modalPresentationStyle = .custom
  }
}

以下是您已设置内容的逐节说明:

  • 1) 夏季赛季菜单的展示方向是.left
  • 2) 冬季赛季菜单的展示方向是.right
  • 3) 赛季控制器的transitioningDelegate现在是之前声明的slideInTransitioningDelegate
  • 4) modalPresentationStyle.custom。这使得呈现的控制器期望自定义展示而不是iOS默认展示。
  • 5) MedalCountViewController的呈现方向现在是.bottomtransitioningDelegatemodalPresentationStyle的设置与步骤3和4中的设置相同。

你已经到了本节的最后,并为你下一个重大事件所需的基础工作做好准备:UIPresentationController子类。


Creating the UIPresentationController

坐下来想象一下:展示的控制器将占据屏幕的三分之二,剩下的三分之一将显示为暗淡。要关闭控制器,只需点击调暗部分即可。你能想象吗?

好的,清醒一下,回到项目中。在这一部分中,您将处理三个关键部分:子类,暗视图和自定义转换。

1. Creating and Initializing a UIPresentationController Subclass

转到File ▸ New ▸ File…,选择iOS ▸ Source ▸ Cocoa Touch Class,然后单击Next。将名称设置为SlideInPresentationController,使其成为UIPresentationController的子类,并将语言设置为Swift

单击Next,将组设置为Presentation

单击Create以创建新文件,并在SlideInPresentationController.swift中的类定义中添加以下内容:

//1
// MARK: - Properties
private var direction: PresentationDirection

//2  
init(presentedViewController: UIViewController,
     presenting presentingViewController: UIViewController?,
     direction: PresentationDirection) {
  self.direction = direction

  //3
  super.init(presentedViewController: presentedViewController,
             presenting: presentingViewController)
}

下面进行细分:

  • 1) 声明direction以表示展示的方向。
  • 2) 声明一个初始化程序,它接受presented and presenting view controllers,以及展示的方向。
  • 3) 为UIPresentationController调用指定的初始化程序。

2. Setting Up the Dimming View

如前所述,presentation controller将具有暗淡的背景。 将以下内容添加到SlideInPresentationController,就在direction上方:

private var dimmingView: UIView!

下面,添加下面的扩展

// MARK: - Private
private extension SlideInPresentationController {
  func setupDimmingView() {
    dimmingView = UIView()
    dimmingView.translatesAutoresizingMaskIntoConstraints = false
    dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
    dimmingView.alpha = 0.0
  }
}

在此处创建dimming view,为自动布局准备并设置其背景颜色。 请注意,您尚未将其添加到superview。 您将在展示转换开始时执行此操作,您将在本节后面看到。

当你点击暗淡的视图时,控制器需要让自己变得scarce。 在setupDimmingView()下添加以下内容以实现此目的:

@objc func handleTap(recognizer: UITapGestureRecognizer) {
  presentingViewController.dismiss(animated: true)
}

在这里,您可以创建一个UITapGestureRecognizer处理程序来dismiss控制器。

当然,您需要编写UITapGestureRecognizer,因此将以下内容添加到setupDimmingView()的底部:

let recognizer = UITapGestureRecognizer(
  target: self, 
  action: #selector(handleTap(recognizer:)))
dimmingView.addGestureRecognizer(recognizer)

这会为暗视图添加一个点击手势,并将其链接到刚添加的操作方法。

最后,在init(presentedViewController:presenting:direction:)结尾处添加对setupDimmingView()的调用:

setupDimmingView()

3. Override Presentation Controller Methods

在开始自定义转换之前,必须覆盖UIPresentationController中的四个方法和属性。 默认方法不执行任何操作,因此无需调用super

首先,为了平滑过渡,重写presentationTransitionWillBegin()以使暗视图与展示一起淡入。 将以下代码添加到SlideInPresentationController.swift内的主类定义中:

override func presentationTransitionWillBegin() {
  guard let dimmingView = dimmingView else {
    return
  }
  // 1
  containerView?.insertSubview(dimmingView, at: 0)

  // 2
  NSLayoutConstraint.activate(
    NSLayoutConstraint.constraints(withVisualFormat: "V:|[dimmingView]|",
      options: [], metrics: nil, views: ["dimmingView": dimmingView]))
  NSLayoutConstraint.activate(
    NSLayoutConstraint.constraints(withVisualFormat: "H:|[dimmingView]|",
      options: [], metrics: nil, views: ["dimmingView": dimmingView]))

  //3
  guard let coordinator = presentedViewController.transitionCoordinator else {
    dimmingView.alpha = 1.0
    return
  }

  coordinator.animate(alongsideTransition: { _ in
    self.dimmingView.alpha = 1.0
  })
}

这是代码的作用:

  • 1) UIPresentationController有一个名为containerView的属性。 它包含presentation and presented controllers的视图层次结构。 此部分是将dimmingView插入视图层次结构后面的位置。
  • 2) 接下来,将暗视图约束到容器视图的边缘,以使其填满整个屏幕。
  • 3) UIPresentationControllertransitionCoordinator有一个非常酷的方法来在转换期间动画。 在本节中,您在展示转换时将暗视图的alpha设置为1.0。

现在,当您dismiss显示的控制器时,您将隐藏暗视图。 通过在上一个重写方法之后添加此代码来覆盖dismissalTransitionWillBegin()

override func dismissalTransitionWillBegin() {
  guard let coordinator = presentedViewController.transitionCoordinator else {
    dimmingView.alpha = 0.0
    return
  }

  coordinator.animate(alongsideTransition: { _ in
    self.dimmingView.alpha = 0.0
  })
}

presentationTransitionWillBegin()类似,随着dismiss转场,您将暗视图的alpha设置为0.0。 这给出了暗视图渐弱的效果。

下一个重写将响应presentation controllercontainerView中的布局更改。 在上一个重写方法之后添加此代码:

override func containerViewWillLayoutSubviews() {
  presentedView?.frame = frameOfPresentedViewInContainerView
}

在这里,您重置presented viewframe以适应对containerView frame的任何更改。

接下来,您将向presentation controller.提供presented view controller内容的尺寸。 在上一个重写方法之后添加此代码:

override func size(forChildContentContainer container: UIContentContainer,
                   withParentContainerSize parentSize: CGSize) -> CGSize {
  switch direction {
  case .left, .right:
    return CGSize(width: parentSize.width*(2.0/3.0), height: parentSize.height)
  case .bottom, .top:
    return CGSize(width: parentSize.width, height: parentSize.height*(2.0/3.0))
  }
}

此方法接收content container和父视图的大小,然后计算所呈现内容的大小。 在此代码中,通过返回水平宽度的2/3和垂直展示的高度,将显示的视图限制为屏幕的2/3。

除了计算显示视图的大小外,还需要返回其frame。 为此,您将重写frameOfPresentedViewInContainerView。 在类定义顶部的属性下面,添加以下内容:

override var frameOfPresentedViewInContainerView: CGRect {
  //1
  var frame: CGRect = .zero
  frame.size = size(forChildContentContainer: presentedViewController, 
                    withParentContainerSize: containerView!.bounds.size)

  //2
  switch direction {
  case .right:
    frame.origin.x = containerView!.frame.width*(1.0/3.0)
  case .bottom:
    frame.origin.y = containerView!.frame.height*(1.0/3.0)
  default:
    frame.origin = .zero
  }
  return frame
}

逐段查看此代码:

  • 1) 你声明一个frame,并给它size(forChildContentContainer:withParentContainerSize:)计算的size
  • 2) 对于.right.bottom方向,您可以通过移动x原点(.right)y原点(.bottom)宽度或高度的1/3来调整原点。

完成所有重写后,您已准备好完成转场的最后一部分!

4. Implementing the Presentation Styles

请记住在上一节中您是如何创建transitioningDelegate的? 好吧,它在那里,但目前做得不多。 它非常需要一些额外的实现。

打开SlideInPresentationManager.swift,找到UIViewControllerTransitioningDelegate扩展并向其添加以下内容:

func presentationController(
  forPresented presented: UIViewController,
  presenting: UIViewController?,
  source: UIViewController
) -> UIPresentationController? {
  let presentationController = SlideInPresentationController(
    presentedViewController: presented,
    presenting: presenting,
    direction: direction
  )
  return presentationController
}

在这里,您使用SlideInPresentationManager的方向实例化SlideInPresentationController。 您将其返回以用于展示。

现在你正在做事! 构建并运行应用程序。 点击Summer, WinterMedal Count,以查看您喜爱的新展示风格。

相当不同,你不觉得吗? 新的演示样式看起来很清晰,但所有呈现的视图仍然从底部滑入。

客户希望Summer and Winter菜单从侧面滑入。 你需要弯曲你的动画能力才能实现这一点。


Creating the Animation Controller

要添加自定义动画转场,您将创建符合UIViewControllerAnimatedTransitioningNSObject子类。

对于复杂的动画,您通常会创建两个控制器 - 一个用于presentation,一个用于dismissal。 在这个应用程序的情况下,dismissal mirrors presentation,所以你只需要一个动画控制器。

转到File ▸ New ▸ File…,选择iOS ▸ Source ▸ Cocoa Touch Class,然后单击Next。 将名称设置为SlideInPresentationAnimator,使其成为NSObject的子类并将语言设置为Swift

单击Next,将组设置为Presentation,然后单击Create以创建新文件。 打开SlideInPresentationAnimator.swift并用以下内容替换其内容:

import UIKit

final class SlideInPresentationAnimator: NSObject {
  // 1
  // MARK: - Properties
  let direction: PresentationDirection

  //2
  let isPresentation: Bool
  
  //3
  // MARK: - Initializers
  init(direction: PresentationDirection, isPresentation: Bool) {
    self.direction = direction
    self.isPresentation = isPresentation
    super.init()
  }
}

在这里你声明:

  • 1) direction说明了动画控制器应该为视图控制器设置动画的方向。
  • 2) isPresentation告诉动画控制器是否展示或关闭视图控制器。
  • 3) 一个初始化程序,它接受上面的两个声明值。

接下来,通过添加以下扩展名来添加对UIViewControllerAnimatedTransitioning的遵守:

// MARK: - UIViewControllerAnimatedTransitioning
extension SlideInPresentationAnimator: UIViewControllerAnimatedTransitioning {
  func transitionDuration(
    using transitionContext: UIViewControllerContextTransitioning?
  ) -> TimeInterval {
    return 0.3
  }

  func animateTransition(
    using transitionContext: UIViewControllerContextTransitioning
  ) {}
}

该协议有两个required的方法 - 一个用于定义转换所需的时间(在这种情况下为0.3秒)和一个用于执行动画的方法。

用以下内容替换animateTransition(using :)

func animateTransition(
    using transitionContext: UIViewControllerContextTransitioning) {
  // 1
  let key: UITransitionContextViewControllerKey = isPresentation ? .to : .from

  guard let controller = transitionContext.viewController(forKey: key) 
    else { return }
    
  // 2
  if isPresentation {
    transitionContext.containerView.addSubview(controller.view)
  }

  // 3
  let presentedFrame = transitionContext.finalFrame(for: controller)
  var dismissedFrame = presentedFrame
  switch direction {
  case .left:
    dismissedFrame.origin.x = -presentedFrame.width
  case .right:
    dismissedFrame.origin.x = transitionContext.containerView.frame.size.width
  case .top:
    dismissedFrame.origin.y = -presentedFrame.height
  case .bottom:
    dismissedFrame.origin.y = transitionContext.containerView.frame.size.height
  }
    
  // 4
  let initialFrame = isPresentation ? dismissedFrame : presentedFrame
  let finalFrame = isPresentation ? presentedFrame : dismissedFrame
    
  // 5
  let animationDuration = transitionDuration(using: transitionContext)
  controller.view.frame = initialFrame
  UIView.animate(
    withDuration: animationDuration,
    animations: {
      controller.view.frame = finalFrame
  }, completion: { finished in
    if !self.isPresentation {
      controller.view.removeFromSuperview()
    }
    transitionContext.completeTransition(finished)
  })
}

以下是每个部分的作用:

  • 1) 如果这是一个presentation,该方法将询问与.to相关联的视图控制器的transitionContext。这是您要移动的视图控制器。如果dismissal,它会询问与.from关联的视图控制器的transitionContext。这是您正在移动的视图控制器。
  • 2) 如果操作是presentation,则代码会将视图控制器的视图添加到视图层次结构中。此代码使用transitionContext来获取container view
  • 3) 计算您正在制作动画的frame。第一行获取在展示视图frame时的transitionContext。本节的其余部分解决了在视图被dismissed时计算视图frame的棘手任务。此部分根据显示方向设置frame的原点,使其位于可见区域之外。
  • 4) 确定转场的初始和最终frames。当呈现视图控制器时,它从dismissed frame移动到presented frame - 反之亦然。
  • 5) 最后,此方法将视图从初始frame动画到最终frame。如果这是dismissal,则从视图层次结构中删除视图控制器的视图。请注意,您在transitionContext上调用completeTransition(_ :)以通知转场已完成。

1. Wiring Up the Animation Controller

您正处于构建转场的最后一步:Hooking up the animation controller

打开SlideInPresentationManager.swift并将以下两个方法添加到UIViewControllerTransitioningDelegate扩展的末尾:

func animationController(
  forPresented presented: UIViewController,
  presenting: UIViewController,
  source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
  return SlideInPresentationAnimator(direction: direction, isPresentation: true)
}

func animationController(
  forDismissed dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
  return SlideInPresentationAnimator(direction: direction, isPresentation: false)
}

第一种方法返回动画控制器以展示视图控制器。 第二个返回动画控制器以关闭视图控制器。 两者都是SlideInPresentationAnimator的实例,但它们具有不同的isPresentation值。

构建并运行应用程序。 看看那些转场! 点击Summer时,您应该会从左侧看到平滑的滑入式动画。 对于Winter,它来自右侧,Medal Count来自底部。

你的结果正是你打算做的!

只要设备处于纵向,它就可以很好地工作。 尝试旋转到横屏。


Adaptivity

好消息是你做了最难的部分! 转场很完美。 在本节中,您将使效果在所有设备和两个方向上都能很好地工作。

再次构建并运行应用程序,但这次是在iPhone SE上运行它。 如果您没有实际设备,可以使用模拟器。 尝试打开横向Summer菜单。 看到有什么不对吗?

上面看着还不错,但是~~~

但是当你试图展示medal count时会发生什么? 从菜单中选择一年,然后点击Medal Count。 您应该看到以下屏幕:

SlideInPresentationController将视图限制为屏幕的2/3,留下很小的空间来显示medal count view。如果你这样运送应用程序,你一定会听到投诉。

幸运的是,可以进行适配。 iPhone具有纵向的.regular高度尺寸类和横向的.compact高度尺寸类。您所要做的就是对展示进行一些更改以使用此功能!

UIPresentationController有一个符合UIAdaptivePresentationControllerDelegate的代理,它定义了几种支持自适应的方法。你马上就会使用其中的两个。

首先,您将使SlideInPresentationManager成为SlideInPresentationController的委托。这是最佳选择,因为您选择提供的控制器确定应用程序是否应支持紧凑高度。

例如,GamesTableViewControllercompact高度看起来正确,因此不需要限制其显示。但是,您确实要调整MedalCountViewController的展示。

打开SlideInPresentationManager.swiftdirection以下添加:

var disableCompactHeight = false

在这里添加disableCompactHeight以指示展示是否支持compact高度。

接下来,添加符合UIAdaptivePresentationControllerDelegate的扩展,并实现adaptivePresentationStyle(for:traitCollection :),如下所示:

// MARK: - UIAdaptivePresentationControllerDelegate
extension SlideInPresentationManager: UIAdaptivePresentationControllerDelegate {
  func adaptivePresentationStyle(
    for controller: UIPresentationController,
    traitCollection: UITraitCollection
  ) -> UIModalPresentationStyle {
    if traitCollection.verticalSizeClass == .compact && disableCompactHeight {
      return .overFullScreen
    } else {
      return .none
    }
  }
}

此方法接受UIPresentationControllerUITraitCollection并返回所需的UIModalPresentationStyle

接下来,它检查verticalSizeClass是否等于.compact以及是否为此展示禁用了compact高度。

  • 如果是,则返回.overFullScreen的展示样式。 这样,呈现的视图将覆盖整个屏幕 - 不仅仅是SlideInPresentationController中定义的2/3。
  • 如果不是,则返回.none,继续执行UIPresentationController

查找presentationController(forPresented:presents:source :)
通过在return语句上方添加以下行,将SlideInPresentationManager设置为展示控制器的委托:

presentationController.delegate = self

最后,您将告诉SlideInPresentationManager何时禁用compact高度。

打开MainViewController.swift并找到prepare(for:sender :)。 找到segue的目标视图控制器是GamesTableViewController的位置,然后将以下行添加到if块:

slideInTransitioningDelegate.disableCompactHeight = false

找到segue的目标视图控制器是MedalCountViewController的位置,并将以下内容添加到if块:

slideInTransitioningDelegate.disableCompactHeight = true

构建并运行应用程序,调出medal count并将设备旋转到横向。 该视图现在应该占据整个屏幕,如下所示:

这就很好!

1. Overriding the presented controller

对于用例,您的视图只能以常规高度显示,也许那里的东西太高了,不适合compact的高度。 UIAdaptivePresentationControllerDelegate可以帮助您。

通过打开SlideInPresentationManager.swift并将以下方法添加到UIAdaptivePresentationControllerDelegate扩展来设置它:

func presentationController(
  _ controller: UIPresentationController,
  viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle
) -> UIViewController? {
  guard case(.overFullScreen) = style else { return nil }
  return UIStoryboard(name: "Main", bundle: nil)
    .instantiateViewController(withIdentifier: "RotateViewController")
}

此方法接受UIPresentationControllerUIModalPresentationStyle。 它返回一个视图控制器,它覆盖要显示的原始控制器,如果应该显示原始视图控制器,则返回nil

如果展示样式为.overFullScreen,则会创建并返回不同的视图控制器。 这个新控制器只是一个简单的UIViewController,其图像告诉用户将屏幕旋转为纵向。

构建并运行应用程序,调出medal count并将设备旋转到landscape。 您应该看到以下消息:

将设备旋转回竖屏以再次查看medal count

恭喜!你已经建立了一个自定义UIPresentationController

你已经学习了很多!您学习了如何自定义和重用UIPresentationController以从任何方向创建整洁的滑入效果。

您还学习了如何调整各种设备和两个方向的展示,以及如何处理设备无法处理横向方向的情况。

有关UIPresentationController的更多信息,请查看Apple’s documentation

后记

本篇主要讲述了基于UIPresentationController的自定义viewController的转场和展示,感兴趣的给个赞或者关注~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,875评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,569评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,475评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,459评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,537评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,563评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,580评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,326评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,773评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,086评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,252评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,921评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,566评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,190评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,435评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,129评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,125评论 2 352

推荐阅读更多精彩内容