[iOS] iOS7、iOS8互換なアラート・アクションシートをBlocksKitで実装する

2015.05.08

UIAlertController from iOS8

iOS8からUIAlertControllerが導入され、UIAlertView, UIActionSheetがdeprecatedされて久しいですが、Appleの調査では2015/5/8現在、iOSデバイスの比率は17%で、iOS7への対応が必要な場面も多いと思われます。

UIAlertViewとUIActionSheetを従来用いていた画面で単純にif文でOSのバージョンを判断して出し分けるというのも小規模なアプリではなんとか凌げるとは思いますが、大規模なアプリではクラスに互換性を吸収させてメソッド一つでアラートやアクションシートを呼び出したいという要求があります。

iOS7-iOS8互換なアラート、アクションシートのOSSも散見されますが、UIAlertControllerの表示には表示先の画面クラスが必要な関係上からか実装的に内部でキャッシュする等の処理が入るようなものもあるなど内部の実装が不安なものが多く、これといった決定版が現時点では無いような印象です(あくまでこちらの不勉強かもしれませんが)

iOS7-iOS8互換を謳ったOSSの中で決定版といえるものがない以上、自分でフルスクラッチで開発するか、既存のOSSを用いるかの選択肢があると思われます。そこで、本記事では既存のOSS:BlocksKitを用いた選択肢を紹介します。

BlocksKit

BlocksKitFoundation FrameworkUIKit FrameworkなどをBlocksを用いて扱うためのOSSです。

例えばUIKitのUIButtonにはSelectorを用いてTarget-Actionで押下イベントをハンドリングするメソッドが公式APIで提供されていますが、このメソッドをSwiftから使おうとすると、Selectorを文字列で指定して呼び出すため、@selectorディレクティブでセレクタ実装の有無のチェックを行ってくれていたObjective-Cと比べて、タイポミスにコンパイル時には気づかない危険性があります。

このような危険性を解消するためにSwiftではBlocksKitを用いて極力Selectorを用いないように現在開発を行っています。

また、セレクタとして呼び出されるメソッドに対しては@objcをメソッド宣言の前に記述しなければならず、これも不便です。

StoryboardやXibファイルから直接IBActionでイベントハンドラを記述する場合に関しては特に不便を感じませんが、UIAlertControllerのハンドラがBlocksになったことも考えると、UIKitのAPIは現在過渡期で、今後はコード中で直接Selectorをハンドリングするようなメソッドは非推奨になることが予想されます。

長い前置きになってしまいましたが、OS互換のアラート、アクションシートはこのBlocksKitを用いて実装することが出来ます。

導入方法はCocoapodsを用いてPodfileに

pod 'BlocksKit'

を記述し、

pod install

でソースファイルを導入し、Bridging-HeaderにBlocksKit内で用いたいコンポーネントに応じたヘッダを以下のように宣言して下さい

#import <BlocksKit/BlocksKit+UIKit.h>
#import <BlocksKit/BlocksKit+XXXXX.h> // XXXXXには対応したコンポーネント用の名前を入れる

アラート実装例

以上の準備が整ったところでOS互換なアラートを表示するメソッドを紹介します。

まずはアラートに対してです。

/**
iOS7 8互換なアラートを表示するためのクラスです。
*/
class AlertComponent {
    /**
    OKボタンとキャンセルボタンとハンドラを備えたアラートを表示します。
    
    :param: title         タイトル
    :param: message       メッセージ
    :param: forVC         表示対象画面
    :param: buttonHandler ボタンハンドラ
    */
    class func showOkCancelAlertWithTitle(
        title: String,
        message: String,
        forVC: UIViewController,
        buttonHandler: Bool -> Void)
    {
        if objc_getClass("UIAlertController") != nil { // -- (1)
            let controller = UIAlertController(title: title, message: message, preferredStyle: .Alert)
            let okAction =
            UIAlertAction(
                title: "OK", 
                style: .Default) { _ in // -- (2)
                    buttonHandler(true)
            }
            let cancelAction =
            UIAlertAction(
                title: "Cancel",
                style: .Cancel) { _ in // -- (3)
                    buttonHandler(false)
            }
            controller.addAction(okAction)
            controller.addAction(cancelAction)
            forVC.presentViewController(controller, animated: true, completion: nil)
        } else { 
            let alert = UIAlertView.bk_alertViewWithTitle(title, message: message) as! UIAlertView
            alert.bk_addButtonWithTitle("OK") { // -- (4)
                buttonHandler(true)
            }
            alert.bk_setCancelButtonWithTitle("Cancel") { // -- (5)
                buttonHandler(false)
            }
            alert.show()
        }
    }

}

-- (1) ではUIAlertControllerが現在のデバイスから利用可能かどうかを調べています。objc_getClassはクラス名を文字列で渡して、クラスが利用可能なら参照を返しますが、利用出来ない場合はnilを返します。

-- (2) ではUIAlertActionのタイトルを決め打ちで生成し、ハンドラ内部でtrueを渡してOKボタンが押されたことを引数のbuttonHandlerに通知します。

-- (3) も同様でUIAlertActionのキャンセルボタンが押されたことをbuttonHandlerに通知しています。

-- (4)(5) からのelse文ではBlocksKitを用いて指定タイトルのボタンとアクションを追加しています。bk_addButtonWithTitle(title: String, handler: Void -> Void)のメソッドではタイトルを指定しつつ、ボタンが押された時のハンドラを記述することができ、UIAlertControllerとほぼ同じような記述で同様の実装を実現出来ます。bk_setCancelButtonWithTitle(title: String, handler: Void -> Void)も同様ですが、UIAlertView内部のcancelButtonIndexが指定したボタンに設定される点が異なります。

アクションシート実装例

続いてアクションシートに対する互換クラスの実装を紹介します。

/**
iOS7 8互換なアクションシートを表示するためのクラスです。
*/
class ActionSheetComponent {
    
    /**
    キャンセルボタン付きアクションシートを表示します。
    
    :param: actions       タイトル付きボタンアクション
    :param: forVC         表示対象ViewController
    */
    class func showActionSheetWithActions(
        actions: [(String, Void -> Void)],
        forVC: UIViewController)
    {
        if objc_getClass("UIAlertController") != nil {
            let controller = UIAlertController(title: nil, message: nil, preferredStyle: .ActionSheet)
            actions.map { [weak controller] title, handler -> Void in // -- (1)
                let action = UIAlertAction(title: title, style: .Default) { _ in
                    handler()
                }
                controller?.addAction(action)
            }
            let cancelAction = UIAlertAction(
                title: "キャンセル",
                style: .Cancel,
                handler: nil
            )
            
            controller.addAction(cancelAction)
            forVC.presentViewController(controller, animated: true, completion: nil)
        } else {
            let actionSheet = UIActionSheet.bk_actionSheetWithTitle(nil) as! UIActionSheet
            actions.map { [weak actionSheet] title, handler -> Void in // -- (2)
                actionSheet?.bk_addButtonWithTitle(title, handler: handler)
            }
            actionSheet.bk_setCancelButtonWithTitle(
                "キャンセル",
                handler: nil
            )
            actionSheet.showInView(forVC.view) // -- (3)
        }
    }
}

このメソッドは引数としてタイトル用文字列とボタンハンドラ用の関数のタプル配列をとります。

-- (1) 複数のアクションのタイトルとハンドラが設定された配列をmapで回してUIAlertControllerにアクションを次々に設定していきます。 キャンセルボタンにアクションが欲しい場合は適宜引数を渡してカスタマイズしてみてください。actionsのmapメソッド内部でcontrollerを弱参照にしているのはactionsのライフサイクルに内部のcontrollerオブジェクトが依存してしまうのを防ぐためです。

-- (2) 同様に配列をmapで回してBlocksKitのbk_addButtonWithTitle(title: String, handler: Void -> Void)でアクションを登録しています。actionSheetを弱参照にしている理由は(1)と同様です。

-- (3) では対象のUIViewControllerに表示を試みていますが、表示対象によっては微妙な動きを示す場合があるかもしれません(UITableViewControllerに表示した場合など)その場合はUIWindowに表示するかなどの実装の検討も必要です。

補遺

上記実装はあくまでも例です。プロジェクトに応じて互換性をもったメソッドの定義方法は変わってくると思いますのであくまでも参考程度になれば幸いです。

参考サイト