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

#初めに
`UIAlertController`で表示されるダイアログを、好きにデザイン出来ないものか？
それを実現する為に、`UIAlertController`に似せたクラスを自作します。
![スクリーンショット 2017-02-10 14.11.49.png](https://qiita-image-store.s3.amazonaws.com/0/46091/dda18746-1896-1a27-5fd9-958ecf0c7b9e.png)

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

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

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

##コード内容
###PNAlertController.h
```objc: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
```objc: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`
の２つのViewを用意します。
PNAlertControllerの標準viewには、`View`の方を割り当てます。
![スクリーンショット 2017-02-09 20.10.13.png](https://qiita-image-store.s3.amazonaws.com/0/46091/b96c73ba-feef-43cb-2598-fa299babf907.png)


##使用手順
UIAlertControllerと同じです。

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

```objc
// インスタンス生成
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の表示スタイルの設定。これにより、背景を透過した際、遷移元画面が表示されます。

```objc:initWithMessage
// モーダルの表示形式（透過、表示領域など）
self.modalPresentationStyle = UIModalPresentationCustom;
```

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

```objc:initWithMessage
// トランジション形式（クロスフェードに）
self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
```

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

```objc: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`配列に格納します。

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

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

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

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

```objc: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];
}
```

```objc: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];
        }
    }
}
```

```objc:PNAlertAction
// アクションの実行
-(void)action
{
    if (actionHandler) {
        actionHandler();
    }
}
```

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

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

以上

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

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

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

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

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

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

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

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