酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

UIAlertViewを使って縦並びにボタンを配置したアラートを表示する

「本当に○○を削除しますが、よろしいでしょうか?」や「このURLをお気に入りに追加しますか?」など、ユーザーとの対話にアラートを使用するアプリは結構あります。

UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"タイトル" 
                                                message:@"本文"
                                               delegate:nil
                                      cancelButtonTitle:@"キャンセル" 
                                      otherButtonTitles:@"ボタン1",nil];
[alert show];
[alert release];

上記のコードを実行すると、このような表示になります。


ボタンを追加するのはUX的に考えてあまりよろしくないですが、2つくらいなら許されると思います。ボタンをもう一つ追加してみましょう。サンプルコードをご覧下さい。

対話の選択肢としてのボタンの数を増やしたい場合、otherButtonTitlesに文言を追加します。otherButtonTitlesは、可変長引数ですので引数の終わりを示す「nil」をきちんと付けてあげましょう。

UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"タイトル" 
                                                message:@"本文"
                                               delegate:nil
                                      cancelButtonTitle:@"キャンセル" 
                                      otherButtonTitles:@"ボタン1",@"ボタン2",nil];
[alert show];
[alert release];

上記のコードを実行すると、このような表示になります。

ボタンが縦表示になります。ボタンが(キャンセルボタンを含めて)2つまでの場合は横一列に並び、ボタンが3つを超えると縦に並ぶのが判ります。

ボタンが2つでもボタンを縦表示したい(非公式APIを使う)

ボタンが3つ以上の場合は、ボタンは縦に並びます。2つの場合でもボタンを縦並びに統一したいというのが人の心ではないでしょうか。

UIAlertViewには、setNumberOfRowsメソッドという非公式APIを使う事で、簡単にボタンを縦並びに配置する事が可能です。

UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"タイトル" 
                                                message:@"本文"
                                               delegate:nil
                                      cancelButtonTitle:@"キャンセル" 
                                      otherButtonTitles:@"ボタン1",nil];
[alert setNumberOfRows:2];
[alert show];
[alert release];

上記のコードを実行すると、このような表示になります。

これで対応が完了すれば良いのですが、前述したとおりsetNumberOfRowsメソッドは非公式APIでUndocumentなAPIです。もしかするとAppStoreの審査に通らない可能性があるので、出来るだけお客様のいるプロジェクトでは使用出来ません。

少し強引な手になりますが(むしろ正当かも?)、UIAlertViewクラスを継承してカスタマイズしたクラスを作ってしまいましょう。

ボタンが2つでもボタンを縦表示したい(UIAlertViewをカスタマイズする)

適当な名前のクラスを作ります。ボタンを縦並びにするので「TateAlertView」とでも名付けましょう。安直なクラス名で申し訳ないです。

ヘッダは簡単にUIAlertViewを継承するだけにとどめておきます。

//
//  TateAlertView.h
//  
//
//  Created by Wada Kenji on 11/07/18.
//  Copyright 2011 Softbuild. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface TateAlertView : UIAlertView {
}
@end

実装もシンプルにしたつもりですが、コードがごちゃごちゃしてしまっています。これは単に僕のスキルが低いからです。setFrameメソッドでアラート自体の高さを定めます。

次にSubviewの描画前等適切なタイミングでlayoutSubviewsメソッドが呼び出されます。ここでアラートのボタンを縦並びに配置しなおします。

layoutSubviewsメソッドで、ボタンの配置を変更しているのですが、この際に並び変えに使用されるボタンのクラスは、UIButtonクラスではなくUIThreePartButtonクラスが使われています。

UIThreePartButtonクラスのオブジェクトを発見する毎に、規定の位置から順番にボタンを並べていっています。

//
//  TateAlertView.m
//  
//
//  Created by Wada Kenji on 11/07/18.
//  Copyright 2011 Softbuild. All rights reserved.
//

#import "TateAlertView.h"

@implementation TateAlertView

// ボタンの大きさ
#define BUTTON_HEIGHT (50.0)

- (id)initWithFrame:(CGRect)frame {
    
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code.
    }
    return self;
}

- (void) setFrame:(CGRect)rect {
    const int frameBaseHeight = 90;	// フレームの大きさ
	
    // ボタンの数からViewの大きさを決める
    int cnt = [[self valueForKey:@"_buttons"] count];
    CGFloat frameHeight = frameBaseHeight + BUTTON_HEIGHT * cnt;

    [super setFrame:CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, frameHeight)];
}

- (void) layoutSubviews {	
    const CGFloat buttonOriginX = 12;	// ボタンの描画を開始する横座標
    const CGFloat buttonOriginY = 80;	// ボタンの描画を開始する縦座標
    const CGFloat buttonWidth = 260;	// ボタンの幅
	
    // UIAlertViewのボタンの位置をここで設定しなおす
    // 非公式APIを使用すれば使えば一発だがボタンを一つずつ配置しなおしていく
    int row = 0;
    for (UIView* view in self.subviews) {	
        // UIAlertView上で使用されるボタンはUIButtonではなく
        // UIThreePartButtonという専用なのでクラス名をチェックしていく
        if ([view isKindOfClass:NSClassFromString(@"UIThreePartButton")]) {	
            CGRect r = view.frame;
            view.frame = CGRectMake(buttonOriginX, 
                                    buttonOriginY + (BUTTON_HEIGHT * row), 
                                    buttonWidth, r.size.height);
            row++;
        }
     }
}

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

@end

上記のコードを実行すると、このような表示になります。

それっぽい表示になったのではないかと思います。しかし、まだ詰めが甘いようです。何が問題か?縦に3つボタンが並んだ場合、キャンセルボタンが一番下に来ているのが判りますでしょうか。

ボタンを押下するとキャンセルボタンから順に0,1,2...とIndexが割り振られます。現状の実装だとsubviewsの配列に先に格納されている順に、アラートの上から配置してしまいます。subviewsの配列の先に格納された順に、アラートの下からボタンを配置していけば解決しそうです。

なので、

- (void) layoutSubviews {	
    const CGFloat buttonOriginX = 12;	// ボタンの描画を開始する横座標
    const CGFloat buttonOriginY = 80;	// ボタンの描画を開始する縦座標
    const CGFloat buttonWidth = 260;	// ボタンの幅
	
    // UIAlertViewのボタンの位置をここで設定しなおす
    // 非公式APIを使用すれば使えば一発だがボタンを一つずつ配置しなおしていく
    int row = 0;
    for (UIView* view in self.subviews) {	
        // UIAlertView上で使用されるボタンはUIButtonではなく
        // UIThreePartButtonという専用なのでクラス名をチェックしていく
        if ([view isKindOfClass:NSClassFromString(@"UIThreePartButton")]) {	
            CGRect r = view.frame;
            view.frame = CGRectMake(buttonOriginX, 
                                    self.frame.size.height - (BUTTON_HEIGHT * row) - buttonOriginY, 
                                    buttonWidth, r.size.height);
            row++;
        }
     }
}

上記のコードを実行すると、このような表示になります。

これで「ボタンが2つの場合も縦並びにしたい」という要求には答えられたかなと思います。