LoginSignup
7
5

More than 5 years have passed since last update.

自作UIAlertControllerで、好きにデザインする

Last updated at Posted at 2017-02-10

初めに

UIAlertControllerで表示されるダイアログを、好きにデザイン出来ないものか?
それを実現する為に、UIAlertControllerに似せたクラスを自作します。
スクリーンショット 2017-02-10 14.11.49.png

実装

まずはコード全文を記載。個々の説明は後述します。

概要

1つにまとめてますが、PNAlertAction ,PNAlertControllerと、UIAlertControllerと同様2つのクラスから成ります。
xibには、透過背景としてのView、ダイアログとしてView、2つのViewを用意します。
デザインは各自お好きにと言う事で、最小限の極めてシンプルな内容です。

とりあえず、ボタン3つ固定のダイアログとして作りました。

コード内容

PNAlertController.h

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

/* ********************
 * PNAlertAction
 ******************** */
@interface PNAlertAction : NSObject

// イニシャライザ
+(instancetype)actionWithTitle:(NSString *)title handler:(void (^)(void))handler;

@property (nonatomic, readonly) NSString *title;

@end

/* ********************
 * PNAlertController
 ******************** */
@interface PNAlertController : UIViewController

// イニシャライザ
+(instancetype)alertControllerWithMessage:(NSString *)message;

@property (nonatomic, readonly) NSString *alertMessage;     // 表示メッセージ

-(void)addAction:(PNAlertAction *)action;   // アクションの格納

// AlertView部品
@property (strong, nonatomic) IBOutlet UIView *alertView;
@property (weak, nonatomic) IBOutlet UILabel *messageLabel;
@property (strong, nonatomic) IBOutletCollection(UIButton) NSArray *actionButtons;

@end

PNAlertController.m

PNAlertController.m
#import "PNAlertController.h"

#pragma mark - PNAlertAction
/* ********************
 * PNAlertAction
 ******************** */
@interface PNAlertAction()
@property (nonatomic, readwrite) NSString *title;
@end

@implementation PNAlertAction {
    void (^actionHandler)(void);    // アクションの格納先
}

#pragma mark initializer
+(instancetype)actionWithTitle:(NSString *)title handler:(void (^)(void))handler
{
    return [[self alloc] initWithTitle:title handler:handler];
}

-(id)initWithTitle:(NSString *)title handler:(void (^)(void))handler
{
    self = [super init];
    if (self) {
        // 要素の格納
        actionHandler = handler;
        self.title = title;

    }
    return self;
}

#pragma mark method
// アクションの実行
-(void)action
{
    if (actionHandler) {
        actionHandler();
    }
}

@end


#pragma mark - PNAlertController
/* ********************
 * PNAlertController
 ******************** */
@interface PNAlertController ()
@property (nonatomic, readwrite) NSString *alertMessage;
@end

@implementation PNAlertController {
    NSMutableArray<PNAlertAction*> *actions;    // PNAlertActionの格納先
}

#pragma mark initializer
+(instancetype)alertControllerWithMessage:(NSString *)message
{
    return [[self alloc] initWithMessage:message];
}

- (id)initWithMessage:(NSString *)message
{
    self = [super init];
    if (self) {
        // 表示要素の格納
        actions = [NSMutableArray array];
        self.alertMessage = message;

        // モーダルの表示形式(透過、表示領域など)
        self.modalPresentationStyle = UIModalPresentationCustom;
        // トランジション形式(クロスフェードに)
        self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
    }
    return self;
}

#pragma mark Event
// ロード後処理
- (void)viewDidLoad {
    [super viewDidLoad];

    /* ********************
     * 背景View レイアウト設定
     ******************** */
    CGRect screenBounds = [UIScreen mainScreen].bounds;
    self.view.frame = CGRectMake(0, 0, screenBounds.size.width, screenBounds.size.height);  // サイズ
    self.view.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.3];      // 背景色(透過)

    /* ********************
     * 表示要素のセットアップ
     ******************** */
    // メッセージ
    self.messageLabel.text = self.alertMessage;

    // ボタンの設定
    for (int i=0; i<self.actionButtons.count; i++) {
        // 登録アクションが無いなら抜ける
        if (i == actions.count) {
            break;
        }

        UIButton *button = self.actionButtons[i];
        PNAlertAction *action = actions[i];

        // イベントのセット
        [button addTarget:self action:@selector(action:) forControlEvents:UIControlEventTouchUpInside];
        // ボタン名のセット
        [button setTitle:action.title forState:UIControlStateNormal];
    }
}

// 画面表示前処理
-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    // alertViewのサイズ
    CGFloat const VIEW_WIDTH = 216;
    CGFloat const VIEW_HEIGHT = 174;

    /* ********************
     * AlertView レイアウト設定
     ******************** */
    // 角丸
    self.alertView.layer.cornerRadius = 10.0f;
    // 影の付与
    [self dropShadow:self.alertView];
    // 背景色
    self.alertView.backgroundColor = [UIColor whiteColor];
    // サイズ・位置
    self.alertView.frame = CGRectMake((self.view.frame.size.width - VIEW_WIDTH) / 2,
                                      (self.view.frame.size.height - VIEW_HEIGHT) / 2,
                                      VIEW_WIDTH,
                                      VIEW_HEIGHT);

    // 子ViewにAlertViewを追加
    [self.view addSubview:self.alertView];

    // 遷移元画面をグレースケール
    self.presentingViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
}

// 画面非表示前処理
-(void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];

    // 遷移元画面のグレースケールを解除
    self.presentingViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

#pragma mark method
// Viewに影を付与
-(void)dropShadow:(UIView *)view
{
    // 領域外をマスクで切らない
    view.layer.masksToBounds = NO;
    // 影方向の指定
    view.layer.shadowOffset = CGSizeMake(10.0f, 10.0f);
    // 透明度
    view.layer.shadowOpacity = 0.7f;
    // ぼかし
    view.layer.shadowRadius = 10.0f;
    // 色
    view.layer.shadowColor = [UIColor blackColor].CGColor;

    // 影の形状指定
    if (view.layer.cornerRadius > 0) {
        view.layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds
                                                           cornerRadius:view.layer.cornerRadius].CGPath;    // 角丸の場合
    } else {
        view.layer.shadowPath = [UIBezierPath bezierPathWithRect:view.bounds].CGPath;                       // 矩形の場合
    }
    // ビットマップレンダリングの有無
    view.layer.shouldRasterize = YES;
    // ラスタイズの縮小率
    view.layer.rasterizationScale = [UIScreen mainScreen].scale;
}

// PNAlertActionの格納
-(void)addAction:(PNAlertAction *)action
{
    [actions addObject:action];
}

// イベント - ボタン押下
-(void)action:(UIButton *)sender
{
    for (int i=0; i<[self.actionButtons count]; i++) {
        if (sender == self.actionButtons[i]) {
            // アクション実行
            PNAlertAction *action = actions[i];
            [action action];

            // ビューを閉じる
            [self dismissViewControllerAnimated:YES completion:nil];
        }
    }
}

@end

PNAlertController.xib

xibには、
・透過する背景としてのView
・表示するダイアログのAlertView
の2つのViewを用意します。
PNAlertControllerの標準viewには、Viewの方を割り当てます。
スクリーンショット 2017-02-09 20.10.13.png

使用手順

UIAlertControllerと同じです。

PNAlertControllerのインスタンスを生成。ボタン数だけPNAlertActionを用意し、addAction:します。
最後に、presentViewController:でダイアログを呼び出します。

// インスタンス生成
PNAlertController *alert = [PNAlertController alertControllerWithMessage:@"保存して終了しますか?"];

// ボタン押下処理のセット
[alert addAction:[PNAlertAction actionWithTitle:@"はい" handler:^{
        /* ボタン押下処理内容 */
}]];
[alert addAction:[PNAlertAction actionWithTitle:@"いいえ" handler:nil]];
[alert addAction:[PNAlertAction actionWithTitle:@"キャンセル" handler:nil]];

// ダイアログの表示
[self presentViewController:alert animated:YES completion:nil];

説明

コード内容の、個々の説明です。

ViewControllerの表示スタイルの設定。これにより、背景を透過した際、遷移元画面が表示されます。

initWithMessage
// モーダルの表示形式(透過、表示領域など)
self.modalPresentationStyle = UIModalPresentationCustom;

遷移のアニメーション設定。今回はクロスフェードに。

initWithMessage
// トランジション形式(クロスフェードに)
self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;

背景Viewのレイアウト。
Frameは画面全域、背景色は透過色に。

viewDidLoad
/* ********************
 * 背景View レイアウト設定
 ******************** */
CGRect screenBounds = [UIScreen mainScreen].bounds;
self.view.frame = CGRectMake(0, 0, screenBounds.size.width, screenBounds.size.height);  // サイズ
self.view.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.3];      // 背景色(透過)

各ボタンの処理内容は、addAction:で受け取りactions配列に格納します。

PNAlertController
@implementation PNAlertController {
    NSMutableArray<PNAlertAction*> *actions;    // PNAlertActionの格納先
}

// PNAlertActionの格納
-(void)addAction:(PNAlertAction *)action
{
    [actions addObject:action];
}

viewDidLoadの際、各ボタンのタップイベントを設定。
格納したPNAlertActionの処理内容を、実行するようにします。

ボタン設定のコーディングは、各自のデザイン次第ですね。

viewDidLoad
// ボタンの設定(各自のデザイン次第)
for (int i=0; i<self.actionButtons.count; i++) {
    // 登録アクションが無いなら抜ける
    if (i == actions.count) {
        break;
    }

    UIButton *button = self.actionButtons[i];
    PNAlertAction *action = actions[i];

    // イベントのセット
    [button addTarget:self action:@selector(action:) forControlEvents:UIControlEventTouchUpInside];
    // ボタン名のセット
    [button setTitle:action.title forState:UIControlStateNormal];
}
PNAlertController
// イベント - ボタン押下
-(void)action:(UIButton *)sender
{
    for (int i=0; i<[self.actionButtons count]; i++) {
        if (sender == self.actionButtons[i]) {
            // アクション実行
            PNAlertAction *action = actions[i];
            [action action];

            // ビューを閉じる
            [self dismissViewControllerAnimated:YES completion:nil];
        }
    }
}
PNAlertAction
// アクションの実行
-(void)action
{
    if (actionHandler) {
        actionHandler();
    }
}

UIAlertControllerは、表示時に遷移元画面をグレースケール化します。これはtintAdjustmentModeで行います。

viewWillAppear
// 遷移元画面をグレースケール
self.presentingViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
viewWillDisappear
// 遷移元画面のグレースケールを解除
self.presentingViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;

以上

後はこれをベースに、
AlertViewのデザインを変えたり、領域外のタップでダイアログを閉じるなど、好みにカスタマイズすればよいかと思います。

おまけ

出来上がってみるととてもシンプルな作りですが、仕様がよくわからない点が多くかなり苦労しました…
悩んだ点などをメモ程度に。

ViewControllerの領域

modalPresentationStyleの設定は、UIModalPresentationOverCurrentContextでも透過する為、一見問題なさそうですが、ViewControllerの有効領域が、遷移元ViewControllerの範囲内に限定されます。
遷移元が画面全域でない場合、変な表示になってしまいます。

リファレンスが要領を得ない事もあり、原因がmodalPresentationStyleにあるという事に気づくまで時間を食いました。

initWithMessage
// モーダルの表示形式(透過はするが、表示領域は遷移元ViewControllerに依存する)
self.modalPresentationStyle = UIModalPresentationOverCurrentContext;

グレースケール

コードを見ての通り、グレースケールは遷移元ViewControllerに対してしか行われていません。
これはUIAlertControllerも同様の仕様で、透過背景に他ViewControllerが見えた場合、そちらはグレースケールになっていません。
まあ、半透過の背景色だとあまり気になりません。

// 遷移元画面をグレースケール
self.presentingViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
// 遷移元画面のグレースケールを解除
self.presentingViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;

また、RenderがOriginalのイメージ、デフォルトカラー以外の色設定のものは、グレースケールになりません。
これもUIAlertControllerと同様の仕様です。

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5