iOS 編程:模態(tài)顯示密碼框輸入視圖

廢話不多說勾拉,本教程將教你實(shí)現(xiàn)以上樣式的密碼框輸入視圖炒考。要點(diǎn)如下:

  • 該密碼輸入頁面是以自定義模態(tài)視圖的轉(zhuǎn)場(chǎng)動(dòng)畫的方式實(shí)現(xiàn)的宰翅。通過本教程噩死,你會(huì)對(duì) UIPresentationController颤难、<UIViewControllerTransitioningDelegate><UIViewControllerAnimatedTransitioning> 等幾個(gè)類有所了解已维;
  • 受益于站在巨人的肩膀上的優(yōu)勢(shì)行嗤,本教程中使用了 Masonry 框架實(shí)現(xiàn)相關(guān)視圖元素的自動(dòng)布局,還使用了 YYKit 框架中的部分工具類方法垛耳。

自定義 UIPresentationController 子類對(duì)象

首先栅屏,你需要自定義一個(gè) UIPresentationController 子類對(duì)象。

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface HQLVerticalPresentationController : UIPresentationController <UIViewControllerTransitioningDelegate>

@end

NS_ASSUME_NONNULL_END

等一下堂鲜,UIPresentationController 是何許人也栈雳?

UIPresentationController 對(duì)象為所呈現(xiàn)的視圖控制器提供高級(jí)視圖轉(zhuǎn)換管理功能,我們通過它實(shí)現(xiàn)視圖控制器之間的轉(zhuǎn)場(chǎng)動(dòng)畫缔莲。

通過模態(tài)方式呈現(xiàn)視圖控制器的常用方法是:

UIViewController *viewControllerA = [[UIViewController alloc] init];
UIViewController *viewControllerB = [[UIViewController alloc] init];
[viewControllerA presentViewController:viewControllerB animated:YES completion:NULL];

當(dāng)通過 presentViewController:animated:completion: 方式讓視圖控制器 A 以模態(tài)方式呈現(xiàn)視圖控制器 B 時(shí)哥纫,兩個(gè)視圖控制器之間的層次結(jié)構(gòu)是這樣的:

視圖控制器 A 被稱之為 presenting View Controller。

視圖控制器 B 被稱之為 presented View Controller痴奏。

UIPresentationController 類在其中充當(dāng)著協(xié)調(diào)器的作用蛀骇,它并不會(huì)顯示在視圖層次結(jié)構(gòu)中,借用網(wǎng)友的一張圖表示 UIPresentationController 在其中的位置:

?? 為方便起見读拆,本教程中會(huì)把 UIPresentationController 命名為「呈現(xiàn)控制器」擅憔。

我們需要?jiǎng)?chuàng)建一個(gè)繼承 UIPresentationController 的子類,負(fù)責(zé)「被呈現(xiàn)」及「負(fù)責(zé)呈現(xiàn)」的控制器以外的 controller, 比如顯示帶漸變效果的黑色半透明背景視圖建椰,還可以在其中創(chuàng)建帶有陰影或者圓角的中間層視圖雕欺,本教程中會(huì)教大家如何在密碼輸入框的左上角和右上角設(shè)置圓角。

在此步驟中棉姐,起碼需要重寫以下 5 個(gè)方法:

  1. - (void)presentationTransitionWillBegin;屠列, 跳轉(zhuǎn)將要開始時(shí)執(zhí)行。
  2. - (void)presentationTransitionDidEnd:(BOOL)completed;伞矩,跳轉(zhuǎn)已經(jīng)結(jié)束時(shí)執(zhí)行笛洛。
  3. - (void)dismissalTransitionWillBegin;,返回將要開始時(shí)執(zhí)行乃坤。
  4. - (void)dismissalTransitionDidEnd:(BOOL)completed;苛让,返回已經(jīng)結(jié)束時(shí)執(zhí)行沟蔑。
  5. frameOfPresentedViewInContainerView,跳轉(zhuǎn)完成后狱杰,被呈現(xiàn)視圖在容器視圖中的位置瘦材。

其中,frameOfPresentedViewInContainerView 是一個(gè)只讀屬性仿畸,用于在過渡動(dòng)畫呈現(xiàn)結(jié)束時(shí)食棕,設(shè)置被呈現(xiàn)的視圖在容器視圖中的位置。

@property(nonatomic, readonly) CGRect frameOfPresentedViewInContainerView;

我們會(huì)在子類中實(shí)現(xiàn) Getter 方法错沽,返回被呈現(xiàn)視圖在容器視圖中的位置簿晓。

在該頭文件中,我們還需要聲明該類遵守并實(shí)現(xiàn) <UIViewControllerTransitioningDelegate> 協(xié)議千埃。

遵守 <UIViewControllerTransitioningDelegate> 協(xié)議的作用:告訴控制器憔儿,誰是動(dòng)畫主管 (UIPresentationController),哪個(gè)類負(fù)責(zé)開始動(dòng)畫的具體細(xì)節(jié)放可、哪個(gè)類負(fù)責(zé)結(jié)束動(dòng)畫的具體細(xì)節(jié)谒臼、是否需要實(shí)現(xiàn)可交互的轉(zhuǎn)場(chǎng)動(dòng)畫。

這個(gè)協(xié)議中一共有 5 個(gè)可選的實(shí)現(xiàn)方法吴侦,大致瀏覽一下:

@protocol UIViewControllerTransitioningDelegate <NSObject>

@optional
// 返回的對(duì)象控制 Presented 時(shí)的動(dòng)畫 (該類負(fù)責(zé)開始動(dòng)畫的具體細(xì)節(jié))
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;

// 由返回的控制器控制 dismissed 時(shí)的動(dòng)畫 (該類負(fù)責(zé)結(jié)束動(dòng)畫的具體細(xì)節(jié))
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

// 實(shí)現(xiàn)可交互轉(zhuǎn)換動(dòng)畫屋休,呈現(xiàn)動(dòng)畫
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;

// 實(shí)現(xiàn)可交互轉(zhuǎn)換動(dòng)畫,dismiss 動(dòng)畫
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;

// 告訴控制器备韧,誰是動(dòng)畫主管(UIPresentationController)劫樟,因?yàn)榇祟惱^承了 UIPresentationController,就返回了 self
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source API_AVAILABLE(ios(8.0));

@end

在呈現(xiàn)密碼輸入框視圖時(shí)织堂,我們需要在密碼輸入框和 presenting View Controller 之間顯示一個(gè)半透明的遮罩層叠艳,這個(gè)遮罩層視圖我們稱之為 dimmingView,它就可以被添加到我們創(chuàng)建的 UIPresentationController 子類對(duì)象中易阳。因此附较,我們會(huì)在 HQLVerticalPresentationController.m 的類擴(kuò)展中創(chuàng)建一個(gè)屬性:

@interface HQLVerticalPresentationController () 
@property (nonatomic, strong) UIView *dimmingView;
@end

另外,我們還需要通過一個(gè)容器視圖來顯示密碼輸入框左上角和右上角的圓角潦俺,因此再添加一個(gè)帶圓角的包裝視圖:

@interface HQLVerticalPresentationController ()
@property (nonatomic, strong) UIView *dimmingView;
@property (nonatomic, strong) UIView *presentationRoundedCornerWrappingView;
@end

實(shí)現(xiàn) UIPresentationController 子類對(duì)象的指定初始化方法:

- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController {
    self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController];
    if (self) {
        presentedViewController.modalPresentationStyle = UIModalPresentationCustom;
    }
    return self;
}

對(duì)于使用 UIPresentationController 子類來自定義呈現(xiàn)控制器時(shí)拒课,presented view controller 的 modalPresentationStyle 屬性必須設(shè)置為 UIModalPresentationCustom

在實(shí)現(xiàn)文件中事示,需要重載的幾個(gè)父類方法如下早像,幾乎每一處代碼都有注釋說明:

//  這是 presentation controller 在呈現(xiàn)視圖之初時(shí)首先被調(diào)用的方法之一。
//  當(dāng)這個(gè)方法被調(diào)用時(shí)肖爵,containerView 已經(jīng)在視圖層次結(jié)構(gòu)中被創(chuàng)建卢鹦。
//  但是,presentedView 還沒有被檢索到劝堪。
- (void)presentationTransitionWillBegin
{
    // presentedView 屬性的 Getter 方法默認(rèn)返回 self.presentedViewController.view
    UIView *presentedViewControllerView = [super presentedView];
        
    // 將 presented view controller 的視圖包裹在一個(gè)中間視圖層次結(jié)構(gòu)中冀自。
    // 該中間層視圖在左上角和右上角應(yīng)用了陰影和圓角效果揉稚。
    // 最終的效果是使用兩個(gè)中間視圖構(gòu)建的。
    //
    // presentationWrapperView =
    //   |- presentationRoundedCornerView   <- 添加 rounded corners (masksToBounds) 圓角
    //        |- presentedViewControllerWrapperView
    //             |- presentedViewControllerView (presentedViewController.view)
    //
    {
        /**
         presentationRoundedCornerView 的高度比 presented view controller 的視圖高 CORNER_RADIUS 的值熬粗。
         這是因?yàn)?cornerRadius 默認(rèn)被應(yīng)用于視圖的所有角搀玖。
         由于該效果只要求對(duì)頂部兩個(gè)角進(jìn)行圓角處理,我們將視圖底部的 CORNER_RADIUS 點(diǎn)調(diào)整為位于
         屏幕的底部邊緣以下位置荐糜。
         */
        UIView *presentationRoundedCornerView = [[UIView alloc] initWithFrame:UIEdgeInsetsInsetRect(self.frameOfPresentedViewInContainerView, UIEdgeInsetsMake(0, 0, -CORNER_RADIUS, 0))];
        presentationRoundedCornerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        presentationRoundedCornerView.layer.cornerRadius = CORNER_RADIUS;
        presentationRoundedCornerView.layer.masksToBounds = YES;
        self.presentationRoundedCornerWrappingView = presentationRoundedCornerView;
        
        /**
         為了撤銷加在 presentationRoundedCornerView 上的額外高度巷怜,
         presentationViewControllerWrapperView 被 CORNER_RADIUS 點(diǎn)插入.
         這也使 presentationViewControllerWrapperView 的邊界大小與 frameOfPresentedViewInContainerView 的大小相匹配。
         */
        UIView *presentedViewControllerWrapperView = [[UIView alloc] initWithFrame:UIEdgeInsetsInsetRect(presentationRoundedCornerView.bounds, UIEdgeInsetsMake(0, 0, CORNER_RADIUS, 0))];
        presentedViewControllerWrapperView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        
        // 將 presentedViewControllerView 添加到 presentedViewControllerWrapperView.
        presentedViewControllerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        presentedViewControllerView.frame = presentedViewControllerWrapperView.bounds;
        [presentedViewControllerWrapperView addSubview:presentedViewControllerView];
        
        // 將 presentedViewControllerWrapperView 添加到 presentationRoundedCornerWrappingView.
        [presentationRoundedCornerView addSubview:presentedViewControllerWrapperView];
    }
    
    /**
     在 presentationWrapperView 后面添加一個(gè)黑色半透明背景視圖暴氏,
     self.presentationView 是稍后添加的(由動(dòng)畫添加),所以這里添加的任何視圖都會(huì)出現(xiàn)在 presentedView 后面绣张。
     */
    {
        UIView *dimmingView = [[UIView alloc] initWithFrame:self.containerView.bounds];
        dimmingView.backgroundColor = [UIColor blackColor];
        dimmingView.opaque = NO;
        dimmingView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        [dimmingView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dimmingViewTapped:)]];
        self.dimmingView = dimmingView;
        [self.containerView addSubview:dimmingView];
        
        // 獲取過渡期協(xié)調(diào)器以實(shí)現(xiàn)呈現(xiàn)答渔,這樣我們就可以在呈現(xiàn)動(dòng)畫的同時(shí),淡化 dimmingView侥涵。
        id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
        
        self.dimmingView.alpha = 0.f;
        [transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
            self.dimmingView.alpha = 0.5f;
        } completion:NULL];
    }
}

// 在呈現(xiàn)過渡結(jié)束時(shí)被調(diào)用的沼撕,并且該方法提供一個(gè)布爾變量來判斷過渡效果是否完成
- (void)presentationTransitionDidEnd:(BOOL)completed
{
    /**
     Completed 參數(shù)的值與動(dòng)畫傳遞給 -completeTransition: 方法的值相同。
     在取消交互轉(zhuǎn)換的情況下芜飘,它可能是 NO务豺。
     
     就是說,在呈現(xiàn)動(dòng)畫發(fā)生時(shí)嗦明,異常終止了動(dòng)畫笼沥,這時(shí)我們需要手動(dòng)釋放未正常添加的視圖
     */
    if (completed == NO)
    {
        /**
         系統(tǒng)會(huì)將 presented view controller 的視圖從它的 superview 中移除,并同時(shí)處理 containerView娶牌。
         這隱含了從視圖層次結(jié)構(gòu)中刪除在 -presentationTransitionWillBegin: 中創(chuàng)建的視圖奔浅。 然而,我們?nèi)匀恍枰艞墝?duì)這些視圖的強(qiáng)引用诗良。
         */
        self.presentationRoundedCornerWrappingView = nil;
        self.dimmingView = nil;
    }
}

// 消失過渡即將開始的時(shí)候被調(diào)用的
- (void)dismissalTransitionWillBegin
{
    // 獲取過渡期協(xié)調(diào)器以實(shí)現(xiàn)退出汹桦,這樣我們就可以在退出動(dòng)畫的同時(shí),淡化 dimmingView鉴裹。
    id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
    
    [transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        self.dimmingView.alpha = 0.f;
    } completion:NULL];
}

// 消失過渡完成之后調(diào)用舞骆,此時(shí)應(yīng)該將視圖移除,防止強(qiáng)引用
- (void)dismissalTransitionDidEnd:(BOOL)completed
{
    // The value of the 'completed' argument is the same value passed to the
    // -completeTransition: method by the animator.  It may
    // be NO in the case of a cancelled interactive transition.
    if (completed == YES)
    {
        // The system removes the presented view controller's view from its
        // superview and disposes of the containerView.  This implicitly
        // removes the views created in -presentationTransitionWillBegin: from
        // the view hierarchy.  However, we still need to relinquish our strong
        // references to those view.
        self.presentationRoundedCornerWrappingView = nil;
        self.dimmingView = nil;
    }
}

你會(huì)注意到径荔,我們?cè)?dimmingView 上添加了一個(gè)手勢(shì)識(shí)別器督禽,當(dāng)點(diǎn)擊密碼輸入框上方的半透明視圖時(shí),可以實(shí)現(xiàn)密碼輸入框的 dismiss 效果猖凛,手勢(shì)識(shí)別器處理程序如下:

//  IBAction for the tap gesture recognizer added to the dimmingView.
//  Dismisses the presented view controller.
//
- (IBAction)dimmingViewTapped:(UITapGestureRecognizer*)sender
{
    [self.presentingViewController dismissViewControllerAnimated:YES completion:NULL];
}

動(dòng)態(tài)方式實(shí)現(xiàn)四個(gè)布局方法:

//  This method is invoked whenever the presentedViewController's
//  preferredContentSize property changes.  It is also invoked just before the
//  presentation transition begins (prior to -presentationTransitionWillBegin).
//
//  當(dāng) presentedViewController 控制器內(nèi)容大小變化時(shí)赂蠢,就會(huì)調(diào)用這個(gè)方法, 比如適配橫豎屏幕時(shí)辨泳,翻轉(zhuǎn)屏幕時(shí)
//  可以使用 UIContentContainer 的方法來調(diào)整任何子視圖控制器的大小或位置虱岂。
- (void)preferredContentSizeDidChangeForChildContentContainer:(id<UIContentContainer>)container
{
    [super preferredContentSizeDidChangeForChildContentContainer:container];
    
    if (container == self.presentedViewController)
        [self.containerView setNeedsLayout];
}

//  When the presentation controller receives a
//  -viewWillTransitionToSize:withTransitionCoordinator: message it calls this
//  method to retrieve the new size for the presentedViewController's view.
//  The presentation controller then sends a
//  -viewWillTransitionToSize:withTransitionCoordinator: message to the
//  presentedViewController with this size as the first argument.
//
//  Note that it is up to the presentation controller to adjust the frame
//  of the presented view controller's view to match this promised size.
//  We do this in -containerViewWillLayoutSubviews.
//
- (CGSize)sizeForChildContentContainer:(id<UIContentContainer>)container withParentContainerSize:(CGSize)parentSize
{
    if (container == self.presentedViewController)
        return ((UIViewController*)container).preferredContentSize;
    else
        return [super sizeForChildContentContainer:container withParentContainerSize:parentSize];
}

- (CGRect)frameOfPresentedViewInContainerView
{
    CGRect containerViewBounds = self.containerView.bounds;
    CGSize presentedViewContentSize = [self sizeForChildContentContainer:self.presentedViewController withParentContainerSize:containerViewBounds.size];
    
    // The presented view extends presentedViewContentSize.height points from
    // the bottom edge of the screen.
    CGRect presentedViewControllerFrame = containerViewBounds;
    presentedViewControllerFrame.size.height = presentedViewContentSize.height;
    presentedViewControllerFrame.origin.y = CGRectGetMaxY(containerViewBounds) - presentedViewContentSize.height;
    return presentedViewControllerFrame;
}

//  This method is similar to the -viewWillLayoutSubviews method in
//  UIViewController.  It allows the presentation controller to alter the
//  layout of any custom views it manages.
//
- (void)containerViewWillLayoutSubviews
{
    [super containerViewWillLayoutSubviews];
    
    self.dimmingView.frame = self.containerView.bounds;
    self.presentationRoundedCornerWrappingView.frame = self.frameOfPresentedViewInContainerView;
}

我們的 UIPresentationController 子類對(duì)象還需要遵守并實(shí)現(xiàn) <UIViewControllerAnimatedTransitioning> 協(xié)議玖院。你當(dāng)然可以把實(shí)現(xiàn)這兩個(gè)協(xié)議的方法通過兩個(gè)不同的類來實(shí)現(xiàn),分別實(shí)現(xiàn) UIViewControllerTransitioningDelegate 協(xié)議和 <UIViewControllerAnimatedTransitioning> 協(xié)議第岖。但這兩個(gè)協(xié)議的方法具有較強(qiáng)的關(guān)聯(lián)性难菌,通常的做法是全都在某一個(gè)類內(nèi)部實(shí)現(xiàn)了。

這里我們?cè)?code>UIPresentationController 子類對(duì)象的擴(kuò)展中設(shè)置讓它遵守<UIViewControllerAnimatedTransitioning> 協(xié)議:

@interface HQLVerticalPresentationController () <UIViewControllerAnimatedTransitioning>
@property (nonatomic, strong) UIView *dimmingView;
@property (nonatomic, strong) UIView *presentationRoundedCornerWrappingView;
@end

實(shí)現(xiàn)兩個(gè)動(dòng)畫轉(zhuǎn)換的核心方法:

// This is used for percent driven interactive transitions, as well as for
// container controllers that have companion animations that might need to
// synchronize with the main animation.
// 設(shè)置交互動(dòng)畫的百分比時(shí)間蔑滓,容器控制器的動(dòng)畫需要與主動(dòng)畫保持同步
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return [transitionContext isAnimated] ? 0.35 : 0;
}

//  The presentation animation is tightly integrated with the overall
//  presentation so it makes the most sense to implement
//  <UIViewControllerAnimatedTransitioning> in the presentation controller
//  rather than in a separate object.
//  核心郊酒,動(dòng)畫效果的實(shí)現(xiàn)
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    // 獲取源控制器、目標(biāo)控制器键袱、動(dòng)畫容器 view
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    UIView *containerView = transitionContext.containerView;
    
    // 獲取源控制器燎窘、目標(biāo)控制器的 View,但是注意二者在開始動(dòng)畫蹄咖,消失動(dòng)畫褐健,身份是不一樣的:
    // 對(duì)于呈現(xiàn)動(dòng)畫來說
    // For a Presentation:
    //      fromView = The presenting view.
    //      toView   = The presented view.
    // 對(duì)于返回動(dòng)畫來說
    // For a Dismissal:
    //      fromView = The presented view.
    //      toView   = The presenting view.
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    // If NO is returned from -shouldRemovePresentersView, the view associated
    // with UITransitionContextFromViewKey is nil during presentation.  This
    // intended to be a hint that your animator should NOT be manipulating the
    // presenting view controller's view.  For a dismissal, the -presentedView
    // is returned.
    //
    // Why not allow the animator manipulate the presenting view controller's
    // view at all times?  First of all, if the presenting view controller's
    // view is going to stay visible after the animation finishes during the
    // whole presentation life cycle there is no need to animate it at all — it
    // just stays where it is.  Second, if the ownership for that view
    // controller is transferred to the presentation controller, the
    // presentation controller will most likely not know how to layout that
    // view controller's view when needed, for example when the orientation
    // changes, but the original owner of the presenting view controller does.
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
        
    // 判斷是 present 還是 dismiss 動(dòng)畫
    BOOL isPresenting = (fromViewController == self.presentingViewController);
    
    // This will be the current frame of fromViewController.view.
    CGRect __unused fromViewInitialFrame = [transitionContext initialFrameForViewController:fromViewController];
    // For a presentation which removes the presenter's view, this will be
    // CGRectZero.  Otherwise, the current frame of fromViewController.view.
    CGRect fromViewFinalFrame = [transitionContext finalFrameForViewController:fromViewController];
    // This will be CGRectZero.
    CGRect toViewInitialFrame = [transitionContext initialFrameForViewController:toViewController];
    // For a presentation, this will be the value returned from the
    // presentation controller's -frameOfPresentedViewInContainerView method.
    CGRect toViewFinalFrame = [transitionContext finalFrameForViewController:toViewController];
    
    // We are responsible for adding the incoming view to the containerView
    // for the presentation (will have no effect on dismissal because the
    // presenting view controller's view was not removed).
    // 我們需要負(fù)責(zé)將傳入的視圖添加到容器視圖中進(jìn)行 presentation/dismissal
    [containerView addSubview:toView];
    
    if (isPresenting) {
        toViewInitialFrame.origin = CGPointMake(CGRectGetMinX(containerView.bounds), CGRectGetMaxY(containerView.bounds));
        toViewInitialFrame.size = toViewFinalFrame.size;
        toView.frame = toViewInitialFrame;
    } else {
        // Because our presentation wraps the presented view controller's view
        // in an intermediate view hierarchy, it is more accurate to rely
        // on the current frame of fromView than fromViewInitialFrame as the
        // initial frame (though in this example they will be the same).
        fromViewFinalFrame = CGRectOffset(fromView.frame, 0, CGRectGetHeight(fromView.frame));
    }
    
    NSTimeInterval transitionDuration = [self transitionDuration:transitionContext];
    
    [UIView animateWithDuration:transitionDuration animations:^{
        if (isPresenting)
            toView.frame = toViewFinalFrame;
        else
            fromView.frame = fromViewFinalFrame;
        
    } completion:^(BOOL finished) {
        /**
         當(dāng)我們的動(dòng)畫執(zhí)行完成后,需要給 transition context 傳遞一個(gè) BOOL 值
         以表示動(dòng)畫是否執(zhí)行完成
         */
        BOOL wasCancelled = [transitionContext transitionWasCancelled];
        [transitionContext completeTransition:!wasCancelled];
    }];
}

創(chuàng)建 UIViewController 子類對(duì)象作為 presented View Controller

因?yàn)閯?chuàng)建的被呈現(xiàn)視圖控制器并不是全屏顯示的澜汤,所以需要用一個(gè)指定初始化方法來傳遞當(dāng)前視圖控制器視圖的 frame 屬性蚜迅。

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface HQLVerticalPresentedViewController : UIViewController

- (instancetype)initWithFrame:(CGRect)frame;

@end

NS_ASSUME_NONNULL_END

在實(shí)現(xiàn)文件中,核心代碼就是通過 frame 屬性的值更新被呈現(xiàn)視圖控制器的尺寸俊抵。

#import "HQLVerticalPresentedViewController.h"
#import <YYKit/YYCGUtilities.h>
#import "HQLVerticalPresentationController.h"

@interface HQLVerticalPresentedViewController ()
@property (nonatomic, assign, readwrite) CGRect frame;
@end

@implementation HQLVerticalPresentedViewController

#pragma mark - Initialize

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithNibName:nil bundle:nil];
    if (self) {
        _frame = frame;
    }
    return self;
}

#pragma mark - View life cycle

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self updatePreferredContentSizeWithTraitCollection:self.traitCollection];
    self.preferredContentSize = _frame.size;
}

#pragma mark - Private

//| ----------------------------------------------------------------------------
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    [super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
    
    // When the current trait collection changes (e.g. the device rotates),
    // update the preferredContentSize.
    [self updatePreferredContentSizeWithTraitCollection:newCollection];
}

//| ----------------------------------------------------------------------------
//! Updates the receiver's preferredContentSize based on the verticalSizeClass
//! of the provided traitCollection.
//
- (void)updatePreferredContentSizeWithTraitCollection:(UITraitCollection *)traitCollection
{
    self.preferredContentSize = CGSizeMake(self.view.bounds.size.width, traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact ? 270 : 420);
}

@end

其實(shí)谁不,創(chuàng)建完以上兩個(gè)視圖控制器之后,我們就可以以半模態(tài)視圖的樣式來呈現(xiàn)視圖了徽诲,具體使用步驟:

  1. 初始化 HQLVerticalPresentedViewController 或其子類實(shí)例:

    HQLVerticalPresentedViewController *presentationViewController = [[HQLVerticalPresentedViewController alloc] init];
    
  2. 初始化 HQLPresentationController 實(shí)例:

    HQLVerticalPresentationController *presentationController NS_VALID_UNTIL_END_OF_SCOPE;
    
    presentationController = [[HQLVerticalPresentationController alloc] initWithPresentedViewController:presentationViewController presentingViewController:self];
    
  3. 設(shè)置 UIViewControllerTransitioningDelegate

    presentationViewController.transitioningDelegate = presentationController;
    
  4. 模態(tài)呈現(xiàn):

    [self presentViewController:presentationViewController animated:YES completion:NULL];
    

HQLVerticalPresentedViewController 子類對(duì)象中添加你想要呈現(xiàn)的視圖

我們可以通過子類化 HQLVerticalPresentedViewController 的方式刹帕,在該視圖控制器中添加密碼輸入框視圖,當(dāng)中包括了一些密碼輸入框相關(guān)的視圖元素馏段。

密碼輸入框視圖元素的實(shí)現(xiàn)我大概參考了之前的實(shí)現(xiàn)轩拨,當(dāng)然也略有優(yōu)化和修改。

之前的實(shí)現(xiàn)文章:密碼輸入框:HQLPasswordViewDemo

所以院喜,有了密碼輸入框視圖 HQLPasswordsView 之后亡蓉,我們直接創(chuàng)建一個(gè)HQLVerticalPresentedViewController 的子類對(duì)象,暫且命名為 HQLPasswordViewController 好了:

#import "HQLVerticalPresentedViewController.h"

NS_ASSUME_NONNULL_BEGIN

@interface HQLPasswordViewController : HQLVerticalPresentedViewController

@end

NS_ASSUME_NONNULL_END

實(shí)現(xiàn)代碼中喷舀,把密碼輸入框視圖 HQLPasswordsView 作為該視圖控制器的屬性引入即可砍濒,然后通過 Block 方式實(shí)現(xiàn)了交互回調(diào):

#import "HQLPasswordViewController.h"
#import "HQLPasswordsView.h"

@interface HQLPasswordViewController ()
@property (nonatomic, strong) HQLPasswordsView *passwordView;
@end

@implementation HQLPasswordViewController


#pragma mark - View life cycle

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.view addSubview:self.passwordView];
    [self configurePasswordView];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    // 自動(dòng)彈出鍵盤
    [self.passwordView.pwdTextField becomeFirstResponder];
}

#pragma mark - Custom Accessors

- (HQLPasswordsView *)passwordView {
    if (!_passwordView) {
        _passwordView = [[HQLPasswordsView alloc] initWithFrame:self.view.bounds];
    }
    return _passwordView;
}


#pragma mark - Private

- (void)configurePasswordView {
    __weak __typeof(self)weakSelf = self;
    
    // MARK: 關(guān)閉按鈕
    self.passwordView.closeBlock = ^{
        [weakSelf dismissViewControllerAnimated:YES completion:NULL];
    };

    // MARK: 忘記密碼
    self.passwordView.forgetPasswordBlock = ^{
        [weakSelf dismissViewControllerAnimated:YES completion:NULL];
        
        // 執(zhí)行找回密碼流程
    };
    
    // MARK: 輸入所有密碼,發(fā)起網(wǎng)絡(luò)請(qǐng)求硫麻,校驗(yàn)密碼
    self.passwordView.finishBlock = ^(NSString *inputPassword) {
        __strong __typeof(weakSelf)strongSelf = weakSelf;
        
        NSLog(@"password = %@",inputPassword);
        
        // 通過 GCD 模擬網(wǎng)絡(luò)請(qǐng)求
        double delayInSeconds = 3.0;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW,
                                                delayInSeconds *NSEC_PER_SEC);
        dispatch_after(popTime, dispatch_get_main_queue(), ^{
            [strongSelf.passwordView requestComplete:YES message:@"支付成功"];
        });
    };
}

@end

當(dāng)處理密碼輸入這種一次性交互時(shí)爸邢,通過 Block 塊的方式處理 presenting view controller 和 presented view controller 之間的數(shù)據(jù)交互是可用的方式之一。

如果要處理列表視圖或者集合視圖中某個(gè) cell 元素的點(diǎn)擊處理事件拿愧,你也可以通過 Delegate 的方式實(shí)現(xiàn)這兩者之間的交互杠河。

將密碼輸入框視圖控制器作為半模態(tài)視圖控制器呈現(xiàn)

在你所需要的業(yè)務(wù)場(chǎng)景中,呈現(xiàn)密碼輸入框視圖控制器:

- (IBAction)presentPasswordViewController:(id)sender {
    
    // 1.初始化 HQLPresentationViewController 實(shí)例
    HQLPasswordViewController *passwordViewController = [[HQLPasswordViewController alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 455)];
    
    // 2.初始化 HQLPresentationController 實(shí)例
    HQLVerticalPresentationController *presentationController NS_VALID_UNTIL_END_OF_SCOPE;
    presentationController = [[HQLVerticalPresentationController alloc] initWithPresentedViewController:passwordViewController presentingViewController:self];
    
    // 3.設(shè)置 UIViewControllerTransitioningDelegate
    passwordViewController.transitioningDelegate = presentationController;

    // 4.模態(tài)呈現(xiàn)
    [self presentViewController:passwordViewController animated:YES completion:NULL];
}

?????? 效果就是你在文章最開始看見的樣子。

千呼萬喚始出來券敌,源碼也不能少唾戚,該示例代碼可以在 iOS-Samples/UIPresentationController相關(guān)示例/UIPresentationController/HQLPasswordView 中找到〈纾可能是藏得比較深了叹坦,為了避免 iOS Demo 過多,并且分散在 GitHub 的多個(gè)倉庫下卑雁,使得 repositories 變得臃腫募书,而且也造成了項(xiàng)目污染,所以我把它們都?xì)w檔在一個(gè)倉庫下了测蹲。

Anyway莹捡,Have Fun!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末扣甲,一起剝皮案震驚了整個(gè)濱河市道盏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌文捶,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件媒咳,死亡現(xiàn)場(chǎng)離奇詭異粹排,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)涩澡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門顽耳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人妙同,你說我怎么就攤上這事射富。” “怎么了粥帚?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵胰耗,是天一觀的道長。 經(jīng)常有香客問我芒涡,道長柴灯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任费尽,我火速辦了婚禮赠群,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘旱幼。我一直安慰自己查描,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著冬三,像睡著了一般匀油。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上长豁,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天钧唐,我揣著相機(jī)與錄音,去河邊找鬼匠襟。 笑死钝侠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的酸舍。 我是一名探鬼主播帅韧,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼啃勉!你這毒婦竟也來了忽舟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤淮阐,失蹤者是張志新(化名)和其女友劉穎叮阅,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體泣特,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡浩姥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了状您。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勒叠。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖膏孟,靈堂內(nèi)的尸體忽然破棺而出眯分,到底是詐尸還是另有隱情,我是刑警寧澤柒桑,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布弊决,位于F島的核電站,受9級(jí)特大地震影響幕垦,放射性物質(zhì)發(fā)生泄漏丢氢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一先改、第九天 我趴在偏房一處隱蔽的房頂上張望疚察。 院中可真熱鬧,春花似錦仇奶、人聲如沸貌嫡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岛抄。三九已至别惦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間夫椭,已是汗流浹背掸掸。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蹭秋,地道東北人扰付。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像仁讨,于是被迫代替她去往敵國和親羽莺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345