【Swift】UIAlertControllerと同じように使える自作ポップアップ

# はじめに
iOSアプリを作っているとき、ポップアップを出したいときにはUIAlertControllerを使っていますか？

最も簡単に実装できるのは良いのですが、少し見た目が物足りません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574728/f066d6c9-e4ba-f32e-dacb-a7a413f9067f.png)

そこでもう少し**見た目にこだわった**、かつ**UIAlertCotrollerと同じように使える**カスタムポップアップを**コードのみ**で作ってみましたので紹介します。

ダークモードやアニメーション、多画面解像度にも対応済みです。使用、改変その他なんでもOKです。

![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574728/4c6cd2bf-7c52-4a35-5020-aed05b16a2ca.png)

![image-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574728/ab025913-b309-acae-ca03-ffe4314c83b5.png)


# 注意事項
UIAlertControllerはStyleが.alertと.actionSheetの2つが使えますが、こちらは現段階では.alertのみの対応となっています
UIAlertControllerではポップアップのボタン配置はいくつでもできますが、こちらは現段階では3つまでの対応となっています

# 最も簡単な使い方
ソースコードが以下のGitHubに置いてあります。
[GitHub@ruemura3](https://github.com/ruemura3/CustomAlertController)

1. 「git clone」または「Download ZIP」でGitHubのソースコードをダウンロード

2. CustomAlertController -> CustomAlertExample -> CustomAlertController.swiftを自分のプロジェクトに追加
（ダウンロードしなくても、自分のプロジェクトでCustomAlertController.swiftを新規作成して、CustomAlertController.swiftの内容をコピペしても大丈夫です。）

3. ポップアップを表示させたい処理を行う部分で以下のように使ってください

``` .swift
// カスタムアラートコントローラの作成
let alert = CustomAlertController(title: "タイトル", message: "メッセージ", preferredStyle: .alert)

// カスタムアラートアクションの作成
let action = CustomAlertAction(title: "OK", style: .default, handler: {_ in
    // このボタンを押したときの処理
    print("キャンセル")
})

// アラートにアクションを追加
alert.addAction(action)

// modalPresentationStyleを.overFullScreenに設定
alert.modalPresentationStyle = .overFullScreen

// modalTransitionStyleを.crossDissolveに設定
alert.modalTransitionStyle = .crossDissolve

// 表示
present(alert, animated: true)
```

:::note info
アラートコントローラの作成 -> アラートアクションの作成 -> コントローラにアクションを追加 -> present()という流れはUIAlertControllerと変わらないのですが、present()の前に.modalPresentationStyle = .overFullScreen、.modalTransitionStyleを.crossDissolveに設定することを忘れないでください。
:::

# 最も簡単なカスタマイズ方法
CustomAlertController.swiftの上部に定数を用意してあり、ここをいじることでポップアップの見た目を変えることができます。

どの定数がなにかはコメントアウトを参考にしてください。

# ソースコード解説
## カスタムアラートアクション
カスタムアラートクラスを自作しますので、アラートアクションも自作します。

イニシャライザを作成することで、標準のUIAlertActionのように`CustomAlertAction(title: “OK”, style: .default, handler: {_ in })`で作成できるようになります。

titleがボタンに表示される文字列、styleがボタンのスタイル、handlerにはボタン押下時の処理が入ります。

```　.swift
/// カスタムアラートアクションの種類
enum CustomAlertControllerAction {
    case cancel
    case `default`
    case destructive
}

/// カスタムアラートアクションクラス
class CustomAlertAction : NSObject {
    /// タイトル
    var title:String
    /// スタイル
    var style:CustomAlertControllerAction
    /// ハンドラ
    var handler: ((_ action: CustomAlertAction)->())?
    
    /// 初期化
    init(title: String, style: CustomAlertControllerAction, handler: ((_ action: CustomAlertAction) -> ())?){
        self.title = title
        self.style = style
        self.handler = handler
    }
}
```

## カスタムアラートコントローラ
スタイルは現時点では.alertのみ。

``` .swift
/// カスタムアラートコントローラの種類（.actionSheetは現段階では非対応）
enum CustomAlertControlerStyle {
    case alert
}

/// カスタムアラートコントローラクラス
class CustomAlertController: UIViewController, UIViewControllerTransitioningDelegate {
    // 省略
}
```

# カスタムアラートコントローラの中身
イニシャライザを用意してUIAlertControllerと同じように`CustomAlertController(title: “タイトル”, message: “メッセージ”, preferredStyle: .alert)`で生成できるようにする。

```.swift
    /// アラートタイトル
    var alertTitle: String?
    
    /// アラートメッセージ
    var alertMessage: String?

    /// 初期化
    internal convenience init(title: String?, message: String?, preferredStyle: CustomAlertControlerStyle) {
        self.init()
        self.alertTitle = title
        self.alertMessage = message
    }
```

ビューやラベル、ボタンなどをすべてコードから生成している。

```.swift
    // タイトルラベル
    var titleLabel = UILabel()
    
    // メッセージラベル
    var messageLabel = UILabel()
    
    // アラート背景
    var alertBackground = UIView()
    
    /// アラートビュー
    var alertView = UIView()
    
    /// 1つ目のボタン
    var firstButton = UIButton()
    
    /// 2つ目のボタン
    var secondButton = UIButton()
    
    /// 3つ目のボタン
    var thirdButton = UIButton()
```

UIAlertControllerと同じようにalert.addAction(action)でアクションを追加できるようにaddActionメソッドを用意。

現段階ではアクションは3つまでしか追加できない仕様のため、すでに3つ以上ある場合はエラーを出すようにしている。

```.swift
    /// アクション格納配列
    var actions = [CustomAlertAction]()

    /// アクション追加
    func addAction(_ action:CustomAlertAction){
        // 現段階ではアクションは3つまで追加可能
        if actions.count >= 3 {
            abort()
        }
        actions.append(action)
    }
```

viewDidLoad()にてポップアップの土台となるviewの.autoresizesSubviewsをfalseに設定する。

これをしないと、ポップアップ土台のサイズを変えたときに中身のラベルやボタンの大きさまで変わってしまう。

```.swift
    /// ビューが読み込まれたときに呼ばれる処理
    override func viewDidLoad() {
        // 自身でサイズを決めているためリサイズを切る
        alertView.autoresizesSubviews = false
    }
```
setUIParts()が実際にポップアップを生成する関数。

こういうレイアウト系はviewDidLoad()やviewWillAppear()の中では正常に作動しないため、viewDidLayoutSubview()の中でsetUIParts()を呼ぶ。

しかしviewDidLayoutSubview()は複数回呼ばれる処理のため、lazyを使った変数で定義することで_ = initViewLayoutと呼ばれた最初の一回のみ処理を行うようにする。

```.swift
    /// サブビューをレイアウトした際に呼ばれる処理
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        _ = initViewLayout
    }
    
    /// viewDidLayoutSubview()は複数回呼び出されるため最初の1度だけ呼び出されるようにlazyを利用
    private lazy var initViewLayout : Void = {
        setUIParts()
    }()

    /// UI部品をセットする
    private func setUIParts() {
        // 省略
    }
```

あとは**コードのゴリ押し**でviewを作成する。慣れたらストーリーボードを使うより手っ取り早かったりする。

コード可読性のために別のプライベートメソッドへ切り出しを行っている。

``` .swift
    /// UI部品をセットする
    private func setUIParts() {
        // 現在のY軸位置（作業用）
        var tmpY = 2 * INTERVAL
        
        // タイトル文言がある場合はタイトルラベルを作成
        if self.alertTitle != nil {
            // タイトルラベルの作成
            setUpTitle(tmpY: tmpY)
            // tmpYの更新
            tmpY += titleLabel.frame.size.height + INTERVAL
        }
        
        // メッセージ文言がある場合はメッセージラベルを作成
        if self.alertMessage != nil {
            // メッセージラベルの作成
            setUpMessage(tmpY: tmpY)
            // tmpYの更新
            tmpY += messageLabel.frame.size.height + INTERVAL
        }
        
        // 1つ目のアクションがある場合は1つ目のボタンを作成
        if actions.count >= 1 {
            // 1つ目のボタンの作成
            setUpButtons(button: firstButton, action: actions[0], tmpY: tmpY)
            // tmpYの更新
            tmpY += BUTTON_HEIGHT + INTERVAL
            
            // 2つ目のアクションがある場合は2つ目のボタンを作成
            if actions.count >= 2 {
                // 2つ目のボタンの作成
                setUpButtons(button: secondButton, action: actions[1], tmpY: tmpY)
                // tmpYの更新
                tmpY += BUTTON_HEIGHT + INTERVAL
                
                // 3つ目のアクションがある場合は3つ目のボタンを作成
                if actions.count >= 3 {
                    // 3つ目のボタンの作成
                    setUpButtons(button: thirdButton, action: actions[2], tmpY: tmpY)
                    // tmpYの更新
                    tmpY += BUTTON_HEIGHT + INTERVAL
                }
            }
        }
        
        // アラートビューの作成
        setUpAlertView(tmpY: tmpY)
        
        // アラート背景に追加
        alertBackground.backgroundColor = BACKGROUND_COLOR
        alertBackground.addSubview(alertView)
        
        self.view = alertBackground
    }
```

# さいごに
今回はUIAlertControllerよりもう少し見た目にこだわった、かつUIAlertCotrollerと同じように使えるポップアップをコードのみで作ってみました。

ここにUIImageViewを入れれば画像も扱えますし、自由自在です！
皆様のポップアップの参考になれば幸いです。

ご質問、不具合報告などありましたらコメントやTwitterまで[Twitter@ruemura3](https://twitter.com/ruemura3)