初めに
UIAlertController
で表示されるダイアログを、好きにデザイン出来ないものか?
それを実現する為に、UIAlertController
に似せたクラスを自作します。
実装
まずはコード全文を記載。個々の説明は後述します。
概要
1つにまとめてますが、PNAlertAction ,PNAlertController
と、UIAlertControllerと同様2つのクラスから成ります。
xibには、透過背景としてのView、ダイアログとしてView、2つのViewを用意します。
デザインは各自お好きにと言う事で、最小限の極めてシンプルな内容です。
とりあえず、ボタン3つ固定のダイアログとして作りました。
コード内容
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
#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
の方を割り当てます。
使用手順
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の表示スタイルの設定。これにより、背景を透過した際、遷移元画面が表示されます。
// モーダルの表示形式(透過、表示領域など)
self.modalPresentationStyle = UIModalPresentationCustom;
遷移のアニメーション設定。今回はクロスフェードに。
// トランジション形式(クロスフェードに)
self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
背景Viewのレイアウト。
Frameは画面全域、背景色は透過色に。
/* ********************
* 背景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
配列に格納します。
@implementation PNAlertController {
NSMutableArray<PNAlertAction*> *actions; // PNAlertActionの格納先
}
// PNAlertActionの格納
-(void)addAction:(PNAlertAction *)action
{
[actions addObject:action];
}
viewDidLoad
の際、各ボタンのタップイベントを設定。
格納したPNAlertAction
の処理内容を、実行するようにします。
ボタン設定のコーディングは、各自のデザイン次第ですね。
// ボタンの設定(各自のデザイン次第)
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)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];
}
}
}
// アクションの実行
-(void)action
{
if (actionHandler) {
actionHandler();
}
}
UIAlertController
は、表示時に遷移元画面をグレースケール化します。これはtintAdjustmentMode
で行います。
// 遷移元画面をグレースケール
self.presentingViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
// 遷移元画面のグレースケールを解除
self.presentingViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
以上
後はこれをベースに、
AlertViewのデザインを変えたり、領域外のタップでダイアログを閉じるなど、好みにカスタマイズすればよいかと思います。
おまけ
出来上がってみるととてもシンプルな作りですが、仕様がよくわからない点が多くかなり苦労しました…
悩んだ点などをメモ程度に。
ViewControllerの領域
modalPresentationStyle
の設定は、UIModalPresentationOverCurrentContext
でも透過する為、一見問題なさそうですが、ViewControllerの有効領域が、遷移元ViewControllerの範囲内に限定されます。
遷移元が画面全域でない場合、変な表示になってしまいます。
リファレンスが要領を得ない事もあり、原因がmodalPresentationStyle
にあるという事に気づくまで時間を食いました。
// モーダルの表示形式(透過はするが、表示領域は遷移元ViewControllerに依存する)
self.modalPresentationStyle = UIModalPresentationOverCurrentContext;
グレースケール
コードを見ての通り、グレースケールは遷移元ViewControllerに対してしか行われていません。
これはUIAlertController
も同様の仕様で、透過背景に他ViewControllerが見えた場合、そちらはグレースケールになっていません。
まあ、半透過の背景色だとあまり気になりません。
// 遷移元画面をグレースケール
self.presentingViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
// 遷移元画面のグレースケールを解除
self.presentingViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
また、RenderがOriginalのイメージ、デフォルトカラー以外の色設定のものは、グレースケールになりません。
これもUIAlertController
と同様の仕様です。