[Swift] 5分で出来る！アプリ専用アラートビューの作り方

**※2016/04/02 更新：最新バージョンに対応**
セレクターの部分をSwift2.2の書き方に直しました．

-------

最近は，[アラート関連のライブラリ](https://www.cocoacontrols.com/search?q=alert)も多く見られるようになりましたね．
綺麗なデザインの物から多機能の物，探すのが大変なくらいです．

しかし！あなたは感じたことがありませんか？
「アプリにマッチしてない．．．」「惜しいんだけど，なんか違う．．．」

感じたことがなくても，個性のあるアプリを作りたい時はライブラリには依存したくないですよね．
そこで今回は，**5分で出来る**オリジナルアラートビューのレシピを紹介します．

※`新人プログラマ応援`タグをつけさせていただいてます．
正直微妙なラインですが，順序よく説明していくのでSwiftコードを書いていく方法がわかれば，という思いです．
どちらかというと初級者->中級者への`移行応援`かもしれない．(私自身もまだもがいている段階ですが．笑)

# 環境
今回は，以下の環境で作っていきます．

OSX El Capitan 10.11.3
Swift ~~2.1.1~~ **2.2**
Xcode ~~7.2.1~~ **7.3**

# Step.0 今回の完成物
先に，全コードを載せます．分かる方はこれと`Step.1`で大体分かるかと思います．
これからダラダラと説明が続くので，適宜読み飛ばしてください．

https://gist.github.com/krimpedance/e85d738a2e8ae89f805f

# Step.1 UIの構築
まずは，アラートのUIを作っていきます．
`File -> New -> File`から，新しいStoryboardを追加します．

<img src="https://qiita-image-store.s3.amazonaws.com/0/93219/cbaf270c-0d14-2a86-6be5-b606e9bb7e71.png" width="500" />

今回は，`MyAlertController.storyboard`とします．
`Object Library`から`UIViewControlelr`をD&Dして，準備は完了です．

<img src="https://qiita-image-store.s3.amazonaws.com/0/93219/c67f8c5c-6e71-f40e-aea8-b678277adacd.png" width="500" />

　
それでは，自由にデザインしていきましょう！

### アラートの背景
以下の二つを設定します．

- `Attributes inspector`の`Presentation`を`Over Current Context`に設定する
- 一番上の階層のViewの背景色は，後ろのVCが見えるように透過させる．(`UIColor(white: 0, alpha: 0.3~0.5)`くらい)

### アラートビュー
メインとなるビュー(コンテンツビュー)です．
まずは，ちゃっとパーツを置いてみましょう

<img src="https://qiita-image-store.s3.amazonaws.com/0/93219/e859dfa1-33d1-b1b5-ef8b-b356db3d2aed.png" width="200" />

AutoLayoutに関してですが，アラートビューは基本的にサイズは固定(メッセージによるが)なので，
今回はコンテンツビューのサイズ，位置(中央)は固定にしました．

<img src="https://qiita-image-store.s3.amazonaws.com/0/93219/3ec91d99-1856-6b43-73bb-cd85bb117a00.png" width="200" />

中のパーツも適切に指定してあげます．
メッセージ表示用のUILabelに関しては，`Attributes inspector`で，`lines`を0にすると，メッセージの量によって自動で調整してくれるので，**高さは指定しない**ようにしましょう．

### ボタンについて
それっぽいものはできましたが，これでは汎用性がなさすぎます．
そこで今回は，ボタン1つの場合と，ボタン2つの場合を考慮していきます．
(この辺は，作成するアプリの使用目的に合わせて拡張していけばいいかと思います．)

コードでframeいじったりしてもいんですが，できるだけシンプルにしたいんで，ExtraViewsを使用します．
ExtraViewsについては以下を参考にしてください．
・[iOS Extra Viewsを使ってみる](http://qiita.com/ekushida/items/7dd0117cb8192bfff5fd)

下の図のように，ViewController上部のアイコンが3つ並んでる部分に，UIViewをD&Dします．
ViewControllerの上の方に，Viewが浮かぶかと思います．
これがExtraViews，インスタンスは生成されるけど`AddSubView()`されてないビューです．

<img src="https://qiita-image-store.s3.amazonaws.com/0/93219/70fc5c42-75c4-6672-090a-6aed30a0344f.png" width="400" />

このExtraViewsで，ボタンのレイアウトを行いましょう．
Viewのサイズは`280x30`としています．


# Step.2 アラートの表示
さて，ようやくレイアウトが終わりました．ここからはコードで各処理を行っていきます．

### `MyAlertController`クラスの作成
まず，`MyAlertController.swift`ファイルを追加します．
`File -> New -> File`から，UIViewControllerのサブクラスを作成してください．

<img src="https://qiita-image-store.s3.amazonaws.com/0/93219/3349f61a-4fbe-ef02-0d91-247c5a5cf105.png" width="500" />

各パーツを接続します．合わせてボタンのアクションも指定してあげます．

```swift:MyAlertController.swift
class MyAlertController: UIViewController {
    @IBOutlet weak var contentView: UIView!  // コンテンツビュー
    @IBOutlet weak var titleLabel: UILabel!  // タイトルラベル
    @IBOutlet weak var messageLabel: UILabel!  // メッセージラベル
    
    @IBOutlet var singleButtonView: UIView!  // ExtraViewsのボタン1つの方
    @IBOutlet var doubleButtonView: UIView!  // ExtraViewsのボタン2つの方

    @IBOutlet weak var singleViewAcceptButton: UIButton!  // ExtraViewsのボタン1つの方のOKボタン
    @IBOutlet weak var doubleViewCancelButton: UIButton!  // ExtraViewsのボタン2つの方のCancelボタン
    @IBOutlet weak var doubleViewAcceptButton: UIButton!  // ExtraViewsのボタン2つの方のOKボタン


    override func viewDidLoad() {
        super.viewDidLoad()
        
        singleViewAcceptButton.addTarget(self, action: #selector(acceptButtonTapped), forControlEvents: .TouchUpInside)
        doubleViewAcceptButton.addTarget(self, action: #selector(acceptButtonTapped), forControlEvents: .TouchUpInside)
        doubleViewCancelButton.addTarget(self, action: #selector(cancelButtonTapped), forControlEvents: .TouchUpInside)
    }                                                   
}


/**
 *  Button actions ---------------------
 */
extension MyAlertController {
    func acceptButtonTapped() {
    }
    
    func cancelButtonTapped() {
    }
}
```

### アラート表示用のメソッド
何はともあれ，いったん表示してみましょう．
表示用のメソッドを追加して，表示させる側のViewControllerから実行してみます．
**`MyAlertController.storyboard`で`isInitialViewController`にチェックが入っていることを確認してください！**

```Swift:MyAlertController.swift
/**
 *  Show ---------------------
 */
extension MyAlertController {
    class func show(presentintViewController vc: UIViewController) {
        guard let alert = UIStoryboard(name: "MyAlertController", bundle: nil).instantiateInitialViewController() as? MyAlertController else { return }
        vc.presentViewController(alert, animated: false, completion: nil)
    }
}
```

```Swift:ViewController.swift
class ViewController: UIViewController {
    ....

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)        
        MyAlertController.show(presentintViewController: self)
    }

    ....
}
```

以下のようになっていれば，とりあえずOKです．

<img src="https://qiita-image-store.s3.amazonaws.com/0/93219/35dff33c-8570-e93d-a703-41443aa4f277.png" width="200" />

確認ができたら，次はラベルのテキストを変更できるようにしましょう．
`show()`メソッドにパラメータを増やす+αだけでいけそうです．

```Swift:MyAlertController.swift
class MyAlertController: UIViewController {
    ....

    var titleText: String = ""
    var messageText: String = ""

    override func viewDidLoad() {
        ....

        titleLabel.text = titleText
        messageLabel.text = messageText
    }                                                   

    ....
}


/**
 *  Show ---------------------
 */
extension MyAlertController {
    class func show(presentintViewController vc: UIViewController, title: String, message: String) {
        guard let alert = UIStoryboard(name: "MyAlertController", bundle: nil).instantiateInitialViewController() as? MyAlertController else { return }
        alert.titleText = title
        alert.messageText = message

        vc.presentViewController(alert, animated: false, completion: nil)
    }
}
```

```Swift:ViewController.swift
class ViewController: UIViewController {
    ....

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)        
        MyAlertController.show(presentintViewController: self, title: "タイトル", message: "メッセージを\n表示しています")
    }

    ....
}
```

実行してみます．
<img src="https://qiita-image-store.s3.amazonaws.com/0/93219/f0fb6d4d-ef97-fe0d-238e-f103aeceef2e.png" width="200" />

いい感じですね．

# Step.3 ボタンの表示
段落の分け方が怪しくなってきましたが，気にせず進めていきましょう．笑

さて，メッセージの表示までできました．次はユーザのアクションを求めるボタンを表示させていきます．
これまでに，二つのボタンパターンを作りましたね．よって，それぞれ追加する用の関数をつくります．

```Swift:MyAlertController.swift
class MyAlertController: UIViewController {
    ....

    private func addSingleButton(title title: String) {
        singleButtonView.frame.origin = CGPoint(x: 0, y: contentView.frame.height-singleButtonView.frame.height)  // 1
        contentView.addSubview(singleButtonView)  // 2
        singleViewAcceptButton.setTitle(title, forState: .Normal)  // 3
    }
    
    private func addDoubleButton(cancelTitle cancelTitle: String, acceptTitle: String) {
        doubleButtonView.frame.origin = CGPoint(x: 0, y: contentView.frame.height-doubleButtonView.frame.height)  // 1
        contentView.addSubview(doubleButtonView)  // 2
        doubleViewCancelButton.setTitle(cancelTitle, forState: .Normal)  // 3
        doubleViewAcceptButton.setTitle(acceptTitle, forState: .Normal)  // 3
    }
}
```

説明は以下の通りです．
1. 追加する位置を指定
2. コンテンツビューに追加
3. ボタンの対鶴を設定


さて，再び`show()`をアップデートしましょう．
今回は，enumでボタンのタイプを指定して，`viewDidLoad()`にて追加メソッドを実行するようにしています．
(バグを見つけて急遽こういう形にしたので，もっといい方法があるかもしれないですが，，，)

```Swift:MyAlertController.swift
// ボタンのタイプ
enum MyAlertControllerType {
    case SingleButton
    case DoubleButton
}

class MyAlertController: UIViewController {
    ....

    var buttonType = MyAlertControllerType.SingleButton("")
    
    override func viewDidLoad() {
        ....
     
        switch buttonType {
        case let .SingleButton(title) :
            addSingleButton(title: title)
            
        case let .DoubleButton(cancel, accept) :
            addDoubleButton(cancelTitle: cancel, acceptTitle: accept)
        }
    }
}


/**
 *  Show ---------------------
 */
extension MyAlertController {
    class func show(presentintViewController vc: UIViewController, title: String, message: String, buttonTitle: String) {
        guard let alert = UIStoryboard(name: "MyAlertController", bundle: nil).instantiateInitialViewController()
            as? MyAlertController else { return }
        alert.titleText = title
        alert.messageText = message
        alert.buttonType = .SingleButton(buttonTitle)
        
        vc.presentViewController(alert, animated: false, completion: nil)
    }
    
    class func show(presentintViewController vc: UIViewController, title: String, message: String, cancelTitle: String, acceptTitle: String) {
        guard let alert = UIStoryboard(name: "MyAlertController", bundle: nil).instantiateInitialViewController()
            as? MyAlertController else { return }
        alert.titleText = title
        alert.messageText = message
        alert.buttonType = .DoubleButton(cancelTitle, acceptTitle)
        
        vc.presentViewController(alert, animated: false, completion: nil)
    }
}
```

これで，無事ボタンも表示できました．

<img src="https://qiita-image-store.s3.amazonaws.com/0/93219/debf2cda-db78-ce40-e7d4-07865b5d7b99.png" width="200" />　<img src="https://qiita-image-store.s3.amazonaws.com/0/93219/8397a2e3-8b1a-9de2-601b-625209398d04.png" width="200" />


# Step.4 ボタンのイベント
さて，ようやく最後のステップです．
Step.2でサラッと追加した`acceptButtonTapped()`と`cancelButtonTapped()`に処理を書いていきます．

ボタンを押した時に呼び出し側で処理をかけるように，`AlertAction`というハンドラを定義します．
ボタンを押した時にこのハンドラを実行することで，呼び出し側のコードを実行できます．
また，どのボタンを押したのか判別できるように，enumで`MyAlertControllerAction`を定義します．

```Swift:MyAlertController.swift
enum MyAlertControllerAction {
    case Cancel
    case Accept
}

class MyAlertController: UIViewController {
    ....

    typealias AlertAction = (action: MyAlertControllerAction)->()
    var handler: AlertAction?

    ....
}


/**
 *  Button actions ---------------------
 */
extension MyAlertController {
    func acceptButtonTapped() {
        handler?(action: .Accept)
        dismissViewControllerAnimated(false, completion: nil)
    }
    
    func cancelButtonTapped() {
        handler?(action: .Cancel)
        dismissViewControllerAnimated(false, completion: nil)
    }
}
```

最後に，`show()`メソッドに，変更を加えます．

```Swift:MyAlertController.swift
/**
 *  Show ---------------------
 */
extension MyAlertController {
    class func show(presentintViewController vc: UIViewController, title: String, message: String, buttonTitle: String, handler: AlertAction?) {
        guard let alert = UIStoryboard(name: "MyAlertController", bundle: nil).instantiateInitialViewController()
            as? MyAlertController else { return }
        alert.titleText = title
        alert.messageText = message
        alert.buttonType = .SingleButton(buttonTitle)
        alert.handler = handler
        
        vc.presentViewController(alert, animated: false, completion: nil)
    }
    
    class func show(presentintViewController vc: UIViewController, title: String, message: String, cancelTitle: String, acceptTitle: String, handler: AlertAction?) {
        guard let alert = UIStoryboard(name: "MyAlertController", bundle: nil).instantiateInitialViewController()                          
            as? MyAlertController else { return }
        alert.titleText = title
        alert.messageText = message
        alert.buttonType = .DoubleButton(cancelTitle, acceptTitle)
        alert.handler = handler
        
        vc.presentViewController(alert, animated: false, completion: nil)
    }
}
```

```Swift:ViewController.swift
class ViewController: UIViewController {
    ....
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        
        MyAlertController.show(presentintViewController: self, title: "タイトル", message: "メッセージを\n表示しています", cancelTitle: "キャンセル", acceptTitle: "おっけー") { action in
            switch action {
            case .Accept : print("accept!")
            case .Cancel : print("cancel!")
            }
        }
    }

    ....
}
```

動作確認できたでしょうか？
スマートとは言い難いかもしれないですが，これで個性的なアラートビューをつくることができるかと思います．
今回の`MyAlertController.swift`ファイルをベースとして，UIをいじるだけの爆速開発に貢献できることを期待します．

# Step.5　(おまけ）アニメーションをつける
今のままでは表示が少し味気ないですよね．
なので，フェードしながら現れるように，アニメーションをつけてみます．

コンテンツビューを出したり消したりする用のメソッドを作り，`viewDidAppear()`, ボタン押下時(`acceptButtonTapped()`, `cancelButtonTapped()`)に呼び出します．

```Swift:MyAlertController.swift
class MyAlertController: UIViewController {
    ....

    override func viewDidLoad() {
        ....

        animationContentView(true)
    }
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        animationContentView(false)
    }
    
    private func animationContentView(hidden: Bool, completion: (()->())? = nil) {
        let alpha: CGFloat
        if hidden { alpha = 0 }
        else { alpha = 1 }
        
        UIView.animateWithDuration(0.2, animations: {
            self.view.alpha = alpha
        }) { _ in
            completion?()
        }
    }

    ....
}


/**
 *  Button actions ---------------------
 */
extension MyAlertController {
    func acceptButtonTapped() {
        animationContentView(true) {
            self.handler?(action: .Accept)
            self.dismissViewControllerAnimated(false, completion: nil)
        }
    }
    
    func cancelButtonTapped() {
        animationContentView(true) {
            self.handler?(action: .Cancel)
            self.dismissViewControllerAnimated(false, completion: nil)
        }
    }
}
```