UIPresentationControllerでポップアップを作る

やりたいこと

UIAlertControllerではできないようなレイアウトを作る場合、
独自ポップアップにする必要があります。
例では背景を押したら閉じる挙動にしています。
f:id:maximInstantcoffee:20200625100359g:plain

どうつくるか

UIPresentationControllerを使います。
一般的なポップアップの動きにしたいので、
UIViewControllerAnimatedTransitioningでアニメーションも調整します

つくりかた

UIPresentationControllerのサブクラスを作る

#import <UIKit/UIKit.h>

@interface PopPresentationController : UIPresentationController

@end
#import "PopPresentationController.h"

@interface PopPresentationController(){
}

@property (nonatomic) UIView *overlayView;

@end

@implementation PopPresentationController

- (void)presentationTransitionWillBegin
{
    [super presentationTransitionWillBegin];
    
    self.overlayView = [[UIView alloc]init];
    // 表示トランジション開始前の処理
    self.overlayView.frame = self.containerView.bounds;
    self.overlayView.backgroundColor = [UIColor blackColor];
    self.overlayView.alpha = 0.0;
    [self.containerView insertSubview:self.overlayView atIndex:0];
    
    id <UIViewControllerTransitionCoordinator> coordinator = [self.presentedViewController transitionCoordinator];
    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        self.overlayView.alpha = 0.5;
    } completion:nil];
}

- (void)dismissalTransitionWillBegin
{
    [super dismissalTransitionWillBegin];
    // 非表示トランジション開始前の処理
    id <UIViewControllerTransitionCoordinator> coordinator = [self.presentedViewController transitionCoordinator];
    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        self.overlayView.alpha = 0.0;
    } completion:nil];
}

- (void)dismissalTransitionDidEnd:(BOOL)completed
{
    [super dismissalTransitionDidEnd:completed];
    
    // 非表示トランジション終了時の処理
    if (completed){
        [self.overlayView removeFromSuperview];
    }
}

- (CGRect)frameOfPresentedViewInContainerView
{
    return self.containerView.bounds;
}

- (void)containerViewWillLayoutSubviews
{
    [super containerViewWillLayoutSubviews];
        
    // レイアウト開始前の処理
    self.overlayView.frame = self.containerView.bounds;
    self.presentedView.frame = self.frameOfPresentedViewInContainerView;
}

@end

UIViewControllerAnimatedTransitioningの実装クラスを作る

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface DialogAnimationController : NSObject<UIViewControllerAnimatedTransitioning>

@property BOOL forPresented;
- (id)init:(BOOL)forPresented;

@end
#import "DialogAnimationController.h"

@interface DialogAnimationController()

@end

@implementation DialogAnimationController

- (id)init:(BOOL)forPresented
{
    self.forPresented = forPresented;
    return self;
}

// アニメーション時間
- (NSTimeInterval)transitionDuration:(nullable id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.2;
}

- (void)animateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext {
    if (self.forPresented) {
        [self presentAnimateTransition:transitionContext];
    } else {
        [self dismissAnimateTransition:transitionContext];
    }
}

// 表示時に使用するアニメーション
- (void)presentAnimateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController *viewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *containerView = transitionContext.containerView;
    [containerView addSubview:viewController.view];
    viewController.view.alpha = 0.0;
    viewController.view.transform = CGAffineTransformMakeScale(0.8, 0.8);
    
    [UIView animateWithDuration:[self transitionDuration:transitionContext]
                          delay:0.0f options:UIViewAnimationOptionCurveEaseOut animations:^{
        viewController.view.alpha = 1.0;
        viewController.view.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:YES];
    }];
}

// 非表示時に使用するアニメーション
- (void)dismissAnimateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController *viewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    
    [UIView animateWithDuration:[self transitionDuration:transitionContext]
                          delay:0.0f options:UIViewAnimationOptionCurveEaseOut animations:^{
        viewController.view.alpha = 0.0;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:YES];
    }];
}

@end

ポップアップ用のViewControllerをつくる

storybordには閉じる用のボタンを画面全体に配置
f:id:maximInstantcoffee:20200625102734p:plain

#import <UIKit/UIKit.h>
@interface SamplePopUpViewController : UIViewController<UIViewControllerTransitioningDelegate>

@end
#import "SamplePopUpViewController.h"
#import "PopPresentationController.h"
#import "DialogAnimationController.h"

@interface SamplePopUpViewController ()
{
    BOOL initContent;
}
@property (weak, nonatomic) IBOutlet UIView *popview;

@end

@implementation SamplePopUpViewController

- (id)initWithCoder:(NSCoder*)decoder
{
    self = [super initWithCoder:decoder];
    self.transitioningDelegate = self;
    self.modalPresentationStyle = UIModalPresentationCustom;
    if (!self) {
        return nil;
    }
    // write something.

    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    if (initContent){
        return;
    }
    initContent = YES;
    _popview.layer.cornerRadius = 22.0;
}

- (IBAction)closeButtonAction:(id)sender {
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source
{
    return [[PopPresentationController alloc]initWithPresentedViewController:presented presentingViewController:presenting];
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    return [[DialogAnimationController alloc] init:YES];
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [[DialogAnimationController alloc] init:NO];
}
@end

つかいかた

UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"SamplePopUp" bundle:nil];
SamplePopUpViewController *viewcontroller = [storyboard instantiateViewControllerWithIdentifier:@"SamplePopUp"];
[self presentViewController:viewcontroller animated:YES completion:nil];

GitHub

https://github.com/maximinstantcoffee/popup