UIKit框架(二十二) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場(chǎng)和展示(一)

版本記錄

版本號(hào) 時(shí)間
V1.0 2019.07.25 星期四

前言

iOS中有關(guān)視圖控件用戶(hù)能看到的都在UIKit框架里面秘蛇,用戶(hù)交互也是通過(guò)UIKit進(jìn)行的。感興趣的參考上面幾篇文章节仿。
1. UIKit框架(一) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(一)
2. UIKit框架(二) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復(fù)使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復(fù)使用的滑塊(二)
7. UIKit框架(七) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(一)
8. UIKit框架(八) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(二)
9. UIKit框架(九) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(一)
10. UIKit框架(十) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(二)
11. UIKit框架(十一) —— UICollectionView的重用遇绞、選擇和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、選擇和重排序(二)
13. UIKit框架(十三) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(一)
14. UIKit框架(十四) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(二)
15. UIKit框架(十五) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(一)
16. UIKit框架(十六) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(二)
17. UIKit框架(十七) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(三)
18. UIKit框架(十八) —— 基于CALayer屬性的一種3D邊欄動(dòng)畫(huà)的實(shí)現(xiàn)(一)
19. UIKit框架(十九) —— 基于CALayer屬性的一種3D邊欄動(dòng)畫(huà)的實(shí)現(xiàn)(二)
20. UIKit框架(二十) —— 基于UILabel跑馬燈類(lèi)似效果的實(shí)現(xiàn)(一)
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

開(kāi)始

先看下主要內(nèi)容

主要內(nèi)容:了解如何使用UIPresentationController構(gòu)建自定義視圖控制器轉(zhuǎn)換和展示鹉动。

接著看下寫(xiě)作環(huán)境

Swift 5, iOS 13, Xcode 11

從iOS早期開(kāi)始,View Controller的演示一直是iOS開(kāi)發(fā)人員工具包中不可或缺的一部分宏邮。

您之前可能使用過(guò)present(_:animated:completion :)但是泽示,如果您和很多開(kāi)發(fā)人員一樣缸血,那么您仍然使用iOS附帶的默認(rèn)轉(zhuǎn)場(chǎng)樣式。

在此UIPresentationController教程中械筛,您將學(xué)習(xí)如何使用自定義轉(zhuǎn)場(chǎng)和自定義展示樣式呈現(xiàn)視圖控制器捎泻。

您不再局限于全屏或彈出式展示文稿和標(biāo)準(zhǔn)轉(zhuǎn)場(chǎng)動(dòng)畫(huà)。你將從一些單調(diào)埋哟,毫無(wú)生氣的視圖控制器演示開(kāi)始笆豁,并將它們變有活力。

當(dāng)你完成時(shí)赤赊,你將學(xué)習(xí)如何:

  • 1) 創(chuàng)建UIPresentationController子類(lèi)闯狱。
  • 2) 使用UIPresentationController進(jìn)行光滑的自定義展示。呈現(xiàn)的視圖控制器甚至不必知道它在那里抛计。
  • 3) 通過(guò)調(diào)整展示參數(shù)扩氢,為各種控制器重用UIPresentationController
  • 4) 進(jìn)行支持應(yīng)用可能遇到的任何屏幕尺寸的自適應(yīng)展示爷辱。

隨著2020年夏季比賽一年之后录豺,一位客戶(hù)讓您創(chuàng)建一個(gè)應(yīng)用程序,以確定競(jìng)爭(zhēng)國(guó)家的獎(jiǎng)牌數(shù)量饭弓。

雖然功能要求非常簡(jiǎn)單双饥,但您的贊助商要求提供一個(gè)相當(dāng)酷的類(lèi)似滑入的轉(zhuǎn)換來(lái)呈現(xiàn)賽事列表。

起初弟断,你感到有些恐慌咏花。但是你意識(shí)到你確實(shí)擁有觸手可及的過(guò)渡構(gòu)建工具,你就會(huì)放心了阀趴。

打開(kāi)已有的創(chuàng)建項(xiàng)目昏翰,花一點(diǎn)時(shí)間熟悉項(xiàng)目和以下元素:

  • MainViewController.swift:此項(xiàng)目的主控制器,所有展示都從該控制器開(kāi)始刘急。這將是您要修改的項(xiàng)目中唯一的現(xiàn)有文件棚菊。
  • GamesTableViewController.swift:顯示用戶(hù)可以選擇的比賽列表。
  • MedalCountViewController.swift:顯示所選體育賽事的獎(jiǎng)牌數(shù)叔汁。
  • GamesDataStore.swift:表示項(xiàng)目中的模型層统求。它負(fù)責(zé)創(chuàng)建和存儲(chǔ)模型對(duì)象。

在開(kāi)始更改內(nèi)容之前据块,請(qǐng)構(gòu)建并運(yùn)行應(yīng)用程序以查看其功能码邻。您將看到以下屏幕:

首先,點(diǎn)擊Summer以顯示夏季菜單另假,Games TableViewController像屋。

請(qǐng)注意,菜單顯示是默認(rèn)菜單边篮,從底部開(kāi)始己莺。 客戶(hù)希望看到一個(gè)滑動(dòng)的幻燈片展示因苹。

接下來(lái),點(diǎn)擊London 2012以關(guān)閉菜單并返回主屏幕篇恒。 這一次,您將看到更新的徽標(biāo)凶杖。

最后胁艰,點(diǎn)擊Medal Count為2012年賽事調(diào)出MedalCountViewController

如您所見(jiàn)智蝠,此控制器還使用舊的自下而上的默認(rèn)展示腾么。點(diǎn)按屏幕即可將其關(guān)閉。

現(xiàn)在您已經(jīng)看到了要升級(jí)的應(yīng)用程序杈湾,現(xiàn)在是時(shí)候?qū)⒆⒁饬D(zhuǎn)向UIPresentationController的一些核心概念和理論解虱。


Core Concepts for iOS Transition

當(dāng)你調(diào)用present(_:animated:completion :)時(shí),iOS會(huì)做三件事漆撞。

首先殴泰,它實(shí)例化一個(gè)UIPresentationController。其次浮驳,它將呈現(xiàn)的視圖控制器附加到自身悍汛。最后,它使用內(nèi)置的模態(tài)展示(modal presentation )樣式之一呈現(xiàn)它至会。

您有能力重寫(xiě)此機(jī)制并為自定義展示提供您自己的UIPresentationController子類(lèi)离咐。

如果您想在應(yīng)用程序中構(gòu)建精美的展示,必須了解這些關(guān)鍵組件:

  • 1) 呈現(xiàn)的視圖控制器具有transitioning delegate奉件,其負(fù)責(zé)加載UIPresentationController以及呈現(xiàn)和dismiss動(dòng)畫(huà)控制器宵蛀。該代理是一個(gè)符合UIViewControllerTransitioningDelegate的對(duì)象。
  • 2) UIPresentationController子類(lèi)是一個(gè)具有許多展示自定義方法的對(duì)象县貌。您將在本教程后面看到其中的一些內(nèi)容术陶。
  • 3) animation controller對(duì)象負(fù)責(zé)演示和dismiss動(dòng)畫(huà)。它符合UIViewControllerAnimatedTransitioning煤痕。請(qǐng)注意瞳别,某些用例需要兩個(gè)控制器:一個(gè)用于展示,一個(gè)用于dismiss杭攻。
  • 4) 展示控制器的代理告訴展示控制器在其特征集合發(fā)生變化時(shí)要做什么祟敛。為了適應(yīng)性,代理必須是符合UIAdaptivePresentationControllerDelegate的對(duì)象兆解。

在你深入之前馆铁,這就是你需要知道的!


Creating the Transitioning Delegate

transitioningDelegate繼承自NSObject并符合UIViewControllerTransitioningDelegate锅睛。

UIViewControllerTransitioningDelegate協(xié)議埠巨,聲明了五種用于管理轉(zhuǎn)換的可選方法历谍。

在本教程中,您將使用其中的三種方法辣垒。

1. Setting Up the Framework

轉(zhuǎn)到File ? New ? File…望侈,選擇iOS ? Source ? Cocoa Touch Class,然后單擊Next勋桶。 將名稱(chēng)設(shè)置為SlideInPresentationManager脱衙,使其成為NSObject的子類(lèi)并將語(yǔ)言設(shè)置為Swift

單擊Next例驹,將組設(shè)置為Presentation捐韩,然后單擊Create

注意:您將SlideInPresentationManager聲明為NSObject,因?yàn)?code>UIViewController的transitioningDelegate必須符合NSObjectProtocol鹃锈。

打開(kāi)SlideInPresentationManager.swift并添加以下擴(kuò)展:

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

在這里荤胁,您使SlideInPresentationManager符合UIViewControllerTransitioningDelegate

MainViewController中屎债,您可以看到兩個(gè)賽季的按鈕:左側(cè)為夏季仅政,右側(cè)為冬季。 底部還有Medal Count盆驹。

要使展示適合每個(gè)按鈕的上下文已旧,您將向SlideInPresentationManager添加方向?qū)傩浴?稍后,您將此屬性傳遞給展示和動(dòng)畫(huà)控制器召娜。

將以下內(nèi)容添加到SlideInPresentationManager.swift的頂部:

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

在這里运褪,您聲明一個(gè)簡(jiǎn)單的枚舉來(lái)表示展示的方向。

接下來(lái)玖瘸,將以下屬性添加到SlideInPresentationManager

var direction: PresentationDirection = .left

在這里秸讹,您要添加一個(gè)direction并給它一個(gè)默認(rèn)值left

要為您呈現(xiàn)的每個(gè)控制器指定SlideInPresentationManager的實(shí)例作為transitioningDelegate雅倒,請(qǐng)打開(kāi)MainViewController.swift并在dataStore定義以下添加下面代碼:

lazy var slideInTransitioningDelegate = SlideInPresentationManager()

你可能會(huì)問(wèn)自己為什么要在MainViewController上添加它作為屬性璃诀。 有兩個(gè)原因:

  • 1) transitioningDelegate是一個(gè)weak屬性,因此您必須在某處保留對(duì)該委托的強(qiáng)引用蔑匣。
  • 2) 您不希望在展示的控制器本身上保留此引用劣欢,因?yàn)槟赡芟M诓煌恼故緲邮缴现赜盟?確定要使用的展示類(lèi)型現(xiàn)在是演示控制器的任務(wù)。

接下來(lái)裁良,找到prepare(for:sender :)凿将,并將其替換為以下內(nèi)容:

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
  }
}

以下是您已設(shè)置內(nèi)容的逐節(jié)說(shuō)明:

  • 1) 夏季賽季菜單的展示方向是.left
  • 2) 冬季賽季菜單的展示方向是.right价脾。
  • 3) 賽季控制器的transitioningDelegate現(xiàn)在是之前聲明的slideInTransitioningDelegate牧抵。
  • 4) modalPresentationStyle.custom。這使得呈現(xiàn)的控制器期望自定義展示而不是iOS默認(rèn)展示。
  • 5) MedalCountViewController的呈現(xiàn)方向現(xiàn)在是.bottom犀变。 transitioningDelegatemodalPresentationStyle的設(shè)置與步驟3和4中的設(shè)置相同妹孙。

你已經(jīng)到了本節(jié)的最后,并為你下一個(gè)重大事件所需的基礎(chǔ)工作做好準(zhǔn)備:UIPresentationController子類(lèi)获枝。


Creating the UIPresentationController

坐下來(lái)想象一下:展示的控制器將占據(jù)屏幕的三分之二蠢正,剩下的三分之一將顯示為暗淡。要關(guān)閉控制器省店,只需點(diǎn)擊調(diào)暗部分即可嚣崭。你能想象嗎?

好的萨西,清醒一下,回到項(xiàng)目中旭旭。在這一部分中谎脯,您將處理三個(gè)關(guān)鍵部分:子類(lèi),暗視圖和自定義轉(zhuǎn)換持寄。

1. Creating and Initializing a UIPresentationController Subclass

轉(zhuǎn)到File ? New ? File…源梭,選擇iOS ? Source ? Cocoa Touch Class,然后單擊Next稍味。將名稱(chēng)設(shè)置為SlideInPresentationController废麻,使其成為UIPresentationController的子類(lèi),并將語(yǔ)言設(shè)置為Swift模庐。

單擊Next烛愧,將組設(shè)置為Presentation

單擊Create以創(chuàng)建新文件掂碱,并在SlideInPresentationController.swift中的類(lèi)定義中添加以下內(nèi)容:

//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)
}

下面進(jìn)行細(xì)分:

  • 1) 聲明direction以表示展示的方向怜姿。
  • 2) 聲明一個(gè)初始化程序,它接受presented and presenting view controllers疼燥,以及展示的方向沧卢。
  • 3) 為UIPresentationController調(diào)用指定的初始化程序。

2. Setting Up the Dimming View

如前所述醉者,presentation controller將具有暗淡的背景但狭。 將以下內(nèi)容添加到SlideInPresentationController,就在direction上方:

private var dimmingView: UIView!

下面撬即,添加下面的擴(kuò)展

// 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
  }
}

在此處創(chuàng)建dimming view立磁,為自動(dòng)布局準(zhǔn)備并設(shè)置其背景顏色。 請(qǐng)注意剥槐,您尚未將其添加到superview息罗。 您將在展示轉(zhuǎn)換開(kāi)始時(shí)執(zhí)行此操作,您將在本節(jié)后面看到才沧。

當(dāng)你點(diǎn)擊暗淡的視圖時(shí)迈喉,控制器需要讓自己變得scarce绍刮。 在setupDimmingView()下添加以下內(nèi)容以實(shí)現(xiàn)此目的:

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

在這里,您可以創(chuàng)建一個(gè)UITapGestureRecognizer處理程序來(lái)dismiss控制器挨摸。

當(dāng)然孩革,您需要編寫(xiě)UITapGestureRecognizer,因此將以下內(nèi)容添加到setupDimmingView()的底部:

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

這會(huì)為暗視圖添加一個(gè)點(diǎn)擊手勢(shì)得运,并將其鏈接到剛添加的操作方法膝蜈。

最后,在init(presentedViewController:presenting:direction:)結(jié)尾處添加對(duì)setupDimmingView()的調(diào)用:

setupDimmingView()

3. Override Presentation Controller Methods

在開(kāi)始自定義轉(zhuǎn)換之前熔掺,必須覆蓋UIPresentationController中的四個(gè)方法和屬性饱搏。 默認(rèn)方法不執(zhí)行任何操作,因此無(wú)需調(diào)用super置逻。

首先推沸,為了平滑過(guò)渡,重寫(xiě)presentationTransitionWillBegin()以使暗視圖與展示一起淡入券坞。 將以下代碼添加到SlideInPresentationController.swift內(nèi)的主類(lèi)定義中:

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有一個(gè)名為containerView的屬性鬓催。 它包含presentation and presented controllers的視圖層次結(jié)構(gòu)。 此部分是將dimmingView插入視圖層次結(jié)構(gòu)后面的位置恨锚。
  • 2) 接下來(lái)宇驾,將暗視圖約束到容器視圖的邊緣,以使其填滿(mǎn)整個(gè)屏幕猴伶。
  • 3) UIPresentationControllertransitionCoordinator有一個(gè)非晨紊幔酷的方法來(lái)在轉(zhuǎn)換期間動(dòng)畫(huà)。 在本節(jié)中他挎,您在展示轉(zhuǎn)換時(shí)將暗視圖的alpha設(shè)置為1.0布卡。

現(xiàn)在,當(dāng)您dismiss顯示的控制器時(shí)雇盖,您將隱藏暗視圖忿等。 通過(guò)在上一個(gè)重寫(xiě)方法之后添加此代碼來(lái)覆蓋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()類(lèi)似,隨著dismiss轉(zhuǎn)場(chǎng)崔挖,您將暗視圖的alpha設(shè)置為0.0贸街。 這給出了暗視圖漸弱的效果。

下一個(gè)重寫(xiě)將響應(yīng)presentation controllercontainerView中的布局更改狸相。 在上一個(gè)重寫(xiě)方法之后添加此代碼:

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

在這里薛匪,您重置presented viewframe以適應(yīng)對(duì)containerView frame的任何更改。

接下來(lái)脓鹃,您將向presentation controller.提供presented view controller內(nèi)容的尺寸逸尖。 在上一個(gè)重寫(xiě)方法之后添加此代碼:

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和父視圖的大小,然后計(jì)算所呈現(xiàn)內(nèi)容的大小。 在此代碼中娇跟,通過(guò)返回水平寬度的2/3和垂直展示的高度岩齿,將顯示的視圖限制為屏幕的2/3。

除了計(jì)算顯示視圖的大小外苞俘,還需要返回其frame盹沈。 為此,您將重寫(xiě)frameOfPresentedViewInContainerView吃谣。 在類(lèi)定義頂部的屬性下面乞封,添加以下內(nèi)容:

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) 你聲明一個(gè)frame,并給它size(forChildContentContainer:withParentContainerSize:)計(jì)算的size岗憋。
  • 2) 對(duì)于.right.bottom方向肃晚,您可以通過(guò)移動(dòng)x原點(diǎn)(.right)y原點(diǎn)(.bottom)寬度或高度的1/3來(lái)調(diào)整原點(diǎn)。

完成所有重寫(xiě)后仔戈,您已準(zhǔn)備好完成轉(zhuǎn)場(chǎng)的最后一部分关串!

4. Implementing the Presentation Styles

請(qǐng)記住在上一節(jié)中您是如何創(chuàng)建transitioningDelegate的? 好吧杂穷,它在那里悍缠,但目前做得不多卦绣。 它非常需要一些額外的實(shí)現(xiàn)耐量。

打開(kāi)SlideInPresentationManager.swift,找到UIViewControllerTransitioningDelegate擴(kuò)展并向其添加以下內(nèi)容:

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

在這里滤港,您使用SlideInPresentationManager的方向?qū)嵗?code>SlideInPresentationController廊蜒。 您將其返回以用于展示。

現(xiàn)在你正在做事溅漾! 構(gòu)建并運(yùn)行應(yīng)用程序山叮。 點(diǎn)擊Summer, WinterMedal Count总放,以查看您喜愛(ài)的新展示風(fēng)格缀踪。

相當(dāng)不同狂巢,你不覺(jué)得嗎本缠? 新的演示樣式看起來(lái)很清晰融虽,但所有呈現(xiàn)的視圖仍然從底部滑入好唯。

客戶(hù)希望Summer and Winter菜單從側(cè)面滑入勺卢。 你需要彎曲你的動(dòng)畫(huà)能力才能實(shí)現(xiàn)這一點(diǎn)疗杉。


Creating the Animation Controller

要添加自定義動(dòng)畫(huà)轉(zhuǎn)場(chǎng)往衷,您將創(chuàng)建符合UIViewControllerAnimatedTransitioningNSObject子類(lèi)钞翔。

對(duì)于復(fù)雜的動(dòng)畫(huà),您通常會(huì)創(chuàng)建兩個(gè)控制器 - 一個(gè)用于presentation席舍,一個(gè)用于dismissal布轿。 在這個(gè)應(yīng)用程序的情況下,dismissal mirrors presentation,所以你只需要一個(gè)動(dòng)畫(huà)控制器汰扭。

轉(zhuǎn)到File ? New ? File…稠肘,選擇iOS ? Source ? Cocoa Touch Class,然后單擊Next东且。 將名稱(chēng)設(shè)置為SlideInPresentationAnimator启具,使其成為NSObject的子類(lèi)并將語(yǔ)言設(shè)置為Swift

單擊Next珊泳,將組設(shè)置為Presentation鲁冯,然后單擊Create以創(chuàng)建新文件。 打開(kāi)SlideInPresentationAnimator.swift并用以下內(nèi)容替換其內(nèi)容:

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說(shuō)明了動(dòng)畫(huà)控制器應(yīng)該為視圖控制器設(shè)置動(dòng)畫(huà)的方向色查。
  • 2) isPresentation告訴動(dòng)畫(huà)控制器是否展示或關(guān)閉視圖控制器薯演。
  • 3) 一個(gè)初始化程序,它接受上面的兩個(gè)聲明值秧了。

接下來(lái)跨扮,通過(guò)添加以下擴(kuò)展名來(lái)添加對(duì)UIViewControllerAnimatedTransitioning的遵守:

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

  func animateTransition(
    using transitionContext: UIViewControllerContextTransitioning
  ) {}
}

該協(xié)議有兩個(gè)required的方法 - 一個(gè)用于定義轉(zhuǎn)換所需的時(shí)間(在這種情況下為0.3秒)和一個(gè)用于執(zhí)行動(dòng)畫(huà)的方法。

用以下內(nèi)容替換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)
  })
}

以下是每個(gè)部分的作用:

  • 1) 如果這是一個(gè)presentation验毡,該方法將詢(xún)問(wèn)與.to相關(guān)聯(lián)的視圖控制器的transitionContext衡创。這是您要移動(dòng)的視圖控制器。如果dismissal晶通,它會(huì)詢(xún)問(wèn)與.from關(guān)聯(lián)的視圖控制器的transitionContext璃氢。這是您正在移動(dòng)的視圖控制器。
  • 2) 如果操作是presentation狮辽,則代碼會(huì)將視圖控制器的視圖添加到視圖層次結(jié)構(gòu)中一也。此代碼使用transitionContext來(lái)獲取container view
  • 3) 計(jì)算您正在制作動(dòng)畫(huà)的frame喉脖。第一行獲取在展示視圖frame時(shí)的transitionContext椰苟。本節(jié)的其余部分解決了在視圖被dismissed時(shí)計(jì)算視圖frame的棘手任務(wù)。此部分根據(jù)顯示方向設(shè)置frame的原點(diǎn)树叽,使其位于可見(jiàn)區(qū)域之外舆蝴。
  • 4) 確定轉(zhuǎn)場(chǎng)的初始和最終frames。當(dāng)呈現(xiàn)視圖控制器時(shí)题诵,它從dismissed frame移動(dòng)到presented frame - 反之亦然洁仗。
  • 5) 最后,此方法將視圖從初始frame動(dòng)畫(huà)到最終frame仇轻。如果這是dismissal京痢,則從視圖層次結(jié)構(gòu)中刪除視圖控制器的視圖。請(qǐng)注意篷店,您在transitionContext上調(diào)用completeTransition(_ :)以通知轉(zhuǎn)場(chǎng)已完成祭椰。

1. Wiring Up the Animation Controller

您正處于構(gòu)建轉(zhuǎn)場(chǎng)的最后一步:Hooking up the animation controller臭家!

打開(kāi)SlideInPresentationManager.swift并將以下兩個(gè)方法添加到UIViewControllerTransitioningDelegate擴(kuò)展的末尾:

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)
}

第一種方法返回動(dòng)畫(huà)控制器以展示視圖控制器。 第二個(gè)返回動(dòng)畫(huà)控制器以關(guān)閉視圖控制器方淤。 兩者都是SlideInPresentationAnimator的實(shí)例钉赁,但它們具有不同的isPresentation值。

構(gòu)建并運(yùn)行應(yīng)用程序携茂。 看看那些轉(zhuǎn)場(chǎng)你踩! 點(diǎn)擊Summer時(shí),您應(yīng)該會(huì)從左側(cè)看到平滑的滑入式動(dòng)畫(huà)讳苦。 對(duì)于Winter带膜,它來(lái)自右側(cè),Medal Count來(lái)自底部鸳谜。

你的結(jié)果正是你打算做的膝藕!

只要設(shè)備處于縱向,它就可以很好地工作咐扭。 嘗試旋轉(zhuǎn)到橫屏芭挽。


Adaptivity

好消息是你做了最難的部分! 轉(zhuǎn)場(chǎng)很完美蝗肪。 在本節(jié)中袜爪,您將使效果在所有設(shè)備和兩個(gè)方向上都能很好地工作。

再次構(gòu)建并運(yùn)行應(yīng)用程序薛闪,但這次是在iPhone SE上運(yùn)行它辛馆。 如果您沒(méi)有實(shí)際設(shè)備,可以使用模擬器逛绵。 嘗試打開(kāi)橫向Summer菜單怀各。 看到有什么不對(duì)嗎倔韭?

上面看著還不錯(cuò)术浪,但是~~~

但是當(dāng)你試圖展示medal count時(shí)會(huì)發(fā)生什么? 從菜單中選擇一年寿酌,然后點(diǎn)擊Medal Count胰苏。 您應(yīng)該看到以下屏幕:

SlideInPresentationController將視圖限制為屏幕的2/3,留下很小的空間來(lái)顯示medal count view醇疼。如果你這樣運(yùn)送應(yīng)用程序硕并,你一定會(huì)聽(tīng)到投訴。

幸運(yùn)的是秧荆,可以進(jìn)行適配倔毙。 iPhone具有縱向的.regular高度尺寸類(lèi)和橫向的.compact高度尺寸類(lèi)。您所要做的就是對(duì)展示進(jìn)行一些更改以使用此功能乙濒!

UIPresentationController有一個(gè)符合UIAdaptivePresentationControllerDelegate的代理陕赃,它定義了幾種支持自適應(yīng)的方法卵蛉。你馬上就會(huì)使用其中的兩個(gè)。

首先么库,您將使SlideInPresentationManager成為SlideInPresentationController的委托傻丝。這是最佳選擇,因?yàn)槟x擇提供的控制器確定應(yīng)用程序是否應(yīng)支持緊湊高度诉儒。

例如葡缰,GamesTableViewControllercompact高度看起來(lái)正確,因此不需要限制其顯示忱反。但是泛释,您確實(shí)要調(diào)整MedalCountViewController的展示。

打開(kāi)SlideInPresentationManager.swiftdirection以下添加:

var disableCompactHeight = false

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

接下來(lái)胁澳,添加符合UIAdaptivePresentationControllerDelegate的擴(kuò)展,并實(shí)現(xiàn)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韭畸。

接下來(lái),它檢查verticalSizeClass是否等于.compact以及是否為此展示禁用了compact高度蔓搞。

  • 如果是胰丁,則返回.overFullScreen的展示樣式。 這樣喂分,呈現(xiàn)的視圖將覆蓋整個(gè)屏幕 - 不僅僅是SlideInPresentationController中定義的2/3锦庸。
  • 如果不是,則返回.none蒲祈,繼續(xù)執(zhí)行UIPresentationController甘萧。

查找presentationController(forPresented:presents:source :)
通過(guò)在return語(yǔ)句上方添加以下行梆掸,將SlideInPresentationManager設(shè)置為展示控制器的委托:

presentationController.delegate = self

最后扬卷,您將告訴SlideInPresentationManager何時(shí)禁用compact高度。

打開(kāi)MainViewController.swift并找到prepare(for:sender :)酸钦。 找到segue的目標(biāo)視圖控制器是GamesTableViewController的位置怪得,然后將以下行添加到if塊:

slideInTransitioningDelegate.disableCompactHeight = false

找到segue的目標(biāo)視圖控制器是MedalCountViewController的位置,并將以下內(nèi)容添加到if塊:

slideInTransitioningDelegate.disableCompactHeight = true

構(gòu)建并運(yùn)行應(yīng)用程序卑硫,調(diào)出medal count并將設(shè)備旋轉(zhuǎn)到橫向徒恋。 該視圖現(xiàn)在應(yīng)該占據(jù)整個(gè)屏幕,如下所示:

這就很好欢伏!

1. Overriding the presented controller

對(duì)于用例入挣,您的視圖只能以常規(guī)高度顯示,也許那里的東西太高了硝拧,不適合compact的高度径筏。 UIAdaptivePresentationControllerDelegate可以幫助您风皿。

通過(guò)打開(kāi)SlideInPresentationManager.swift并將以下方法添加到UIAdaptivePresentationControllerDelegate擴(kuò)展來(lái)設(shè)置它:

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。 它返回一個(gè)視圖控制器匠璧,它覆蓋要顯示的原始控制器桐款,如果應(yīng)該顯示原始視圖控制器,則返回nil夷恍。

如果展示樣式為.overFullScreen魔眨,則會(huì)創(chuàng)建并返回不同的視圖控制器。 這個(gè)新控制器只是一個(gè)簡(jiǎn)單的UIViewController酿雪,其圖像告訴用戶(hù)將屏幕旋轉(zhuǎn)為縱向遏暴。

構(gòu)建并運(yùn)行應(yīng)用程序,調(diào)出medal count并將設(shè)備旋轉(zhuǎn)到landscape指黎。 您應(yīng)該看到以下消息:

將設(shè)備旋轉(zhuǎn)回豎屏以再次查看medal count朋凉。

恭喜!你已經(jīng)建立了一個(gè)自定義UIPresentationController醋安。

你已經(jīng)學(xué)習(xí)了很多杂彭!您學(xué)習(xí)了如何自定義和重用UIPresentationController以從任何方向創(chuàng)建整潔的滑入效果。

您還學(xué)習(xí)了如何調(diào)整各種設(shè)備和兩個(gè)方向的展示吓揪,以及如何處理設(shè)備無(wú)法處理橫向方向的情況亲怠。

有關(guān)UIPresentationController的更多信息,請(qǐng)查看Apple’s documentation柠辞。

后記

本篇主要講述了基于UIPresentationController的自定義viewController的轉(zhuǎn)場(chǎng)和展示团秽,感興趣的給個(gè)贊或者關(guān)注~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市叭首,隨后出現(xiàn)的幾起案子习勤,更是在濱河造成了極大的恐慌,老刑警劉巖焙格,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件图毕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡间螟,警方通過(guò)查閱死者的電腦和手機(jī)吴旋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)损肛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)厢破,“玉大人,你說(shuō)我怎么就攤上這事治拿∧幔” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵劫谅,是天一觀的道長(zhǎng)见坑。 經(jīng)常有香客問(wèn)我嚷掠,道長(zhǎng),這世上最難降的妖魔是什么荞驴? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任不皆,我火速辦了婚禮,結(jié)果婚禮上熊楼,老公的妹妹穿的比我還像新娘霹娄。我一直安慰自己,他們只是感情好鲫骗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布犬耻。 她就那樣靜靜地躺著,像睡著了一般执泰。 火紅的嫁衣襯著肌膚如雪枕磁。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,182評(píng)論 1 299
  • 那天术吝,我揣著相機(jī)與錄音计济,去河邊找鬼。 笑死排苍,一個(gè)胖子當(dāng)著我的面吹牛峭咒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纪岁,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼凑队,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了幔翰?” 一聲冷哼從身側(cè)響起漩氨,我...
    開(kāi)封第一講書(shū)人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎遗增,沒(méi)想到半個(gè)月后叫惊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡做修,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年霍狰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饰及。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蔗坯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出燎含,到底是詐尸還是另有隱情宾濒,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布屏箍,位于F島的核電站绘梦,受9級(jí)特大地震影響橘忱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜卸奉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一钝诚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧榄棵,春花似錦敲长、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至尚辑,卻和暖如春辑鲤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背杠茬。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工月褥, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瓢喉。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓宁赤,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親栓票。 傳聞我的和親對(duì)象是個(gè)殘疾皇子决左,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容