版本记录
版本号 | 时间 |
---|---|
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
,因为UIViewController
的transitioningDelegate
必须符合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
的呈现方向现在是.bottom
。transitioningDelegate
和modalPresentationStyle
的设置与步骤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)
UIPresentationController
的transitionCoordinator
有一个非常酷的方法来在转换期间动画。 在本节中,您在展示转换时将暗视图的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 controller
的containerView
中的布局更改。 在上一个重写方法之后添加此代码:
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
在这里,您重置presented view
的frame
以适应对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, Winter
和Medal Count
,以查看您喜爱的新展示风格。
相当不同,你不觉得吗? 新的演示样式看起来很清晰,但所有呈现的视图仍然从底部滑入。
客户希望Summer and Winter
菜单从侧面滑入。 你需要弯曲你的动画能力才能实现这一点。
Creating the Animation Controller
要添加自定义动画转场,您将创建符合UIViewControllerAnimatedTransitioning
的NSObject
子类。
对于复杂的动画,您通常会创建两个控制器 - 一个用于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
的委托。这是最佳选择,因为您选择提供的控制器确定应用程序是否应支持紧凑高度。
例如,GamesTableViewController
在compact
高度看起来正确,因此不需要限制其显示。但是,您确实要调整MedalCountViewController
的展示。
打开SlideInPresentationManager.swift
并direction
以下添加:
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
}
}
}
此方法接受UIPresentationController
和UITraitCollection
并返回所需的UIModalPresentationStyle
。
接下来,它检查verticalSizeClass
是否等于.compac
t以及是否为此展示禁用了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")
}
此方法接受UIPresentationController
和UIModalPresentationStyle
。 它返回一个视图控制器,它覆盖要显示的原始控制器,如果应该显示原始视图控制器,则返回nil
。
如果展示样式为.overFullScreen
,则会创建并返回不同的视图控制器。 这个新控制器只是一个简单的UIViewController
,其图像告诉用户将屏幕旋转为纵向。
构建并运行应用程序,调出medal count
并将设备旋转到landscape
。 您应该看到以下消息:
将设备旋转回竖屏以再次查看medal count
。
恭喜!你已经建立了一个自定义UIPresentationController
。
你已经学习了很多!您学习了如何自定义和重用UIPresentationController
以从任何方向创建整洁的滑入效果。
您还学习了如何调整各种设备和两个方向的展示,以及如何处理设备无法处理横向方向的情况。
有关UIPresentationController
的更多信息,请查看Apple’s documentation。
后记
本篇主要讲述了基于UIPresentationController的自定义viewController的转场和展示,感兴趣的给个赞或者关注~~~