📱

宣言的UIのススメ~自作して理解する宣言的UI~

2022/03/30に公開

はじめに

宣言的UIのススメと書きましたが著者自身が宣言的UIとは何かというのを分かっていませんw
UIKitをSwiftUIのように書けるようになるテクニックまとめ、ぐらいに捉えてください。

UIAlertControllerの呼び出し

UIAlertControllerの呼び出しを例にして説明していきます。

標準

iOSアプリをUIKitで開発している場合、アラートを表示させるためには以下のように書くかと思います。

基本的なUIAlertController
//アラートのインスタンスを生成
let alert = UIAlertController(title: "アラート", message: nil, preferredStyle: .alert)

//def,destructive,cancelボタンのインスタンスを生成
let def = UIAlertAction(title: "default", style: .default) { _ in
    print("OKをタップ")
}

let destructive = UIAlertAction(title: "destructive", style: .destructive) { _ in
    print("destructiveをタップ")
}

let cancel = UIAlertAction(title: "cancel", style: .cancel) { _ in
    print("cancelをタップ")
}

//ボタンをalertに追加
alert.addAction(def)
alert.addAction(destructive)
alert.addAction(cancel)

//alertの表示
self.present(alert, animated: true)

この書き方の場合、わざわざalertdefといった定数を用意しないといけなくなり、alert.addAction(def)と記載している時にそれぞれalertdefには何が代入されどんなふうに設定したのかを気にしながら書くことになります。

宣言的に書く

この記事の最後まで読むと最終的にはこのような書き方でUIAlertControllerが実行できるようになります。
そのためのいくつかのテクニックを解説していきます。

UIAlertController(title: "アラート", message: nil, preferredStyle: .alert)
    .addActions {
        UIAlertAction(title: "default", style: .default) { _ in
            print("defaultをタップ")
        }

        UIAlertAction(title: "destructive", style: .destructive) { _ in
            print("destructiveをタップ")
        }

        UIAlertAction(title: "cancel", style: .cancel) { _ in
            print("cancelをタップ")
        }
    }
    .present(from: self, animated: true)

メソッドチェーン

メソッドチェーンとはalert.methodA()とメソッドを呼んだ後に、さらにalert.methodA().methodB().など繋いで次々とメソッドを呼んでいく書き方のことです。
例えばUIAlertControllerのメソッドであるaddAction(:_)ではaddAction(:_).addAction(:_)と繋ぐことはできません。
そのためわざわざ以下のUIAlertControllerのインスタンスを生成してlet alertに代入しています

メソッドチェーンできないからalert定数に代入する必要がある
let alert = UIAlertController(title: "アラート", message: nil, preferredStyle: .alert)
/*
省略
*/
alert.addAction(def)
alert.addAction(destructive)
alert.addAction(cancel)

では、なぜaddAction()を呼んだあとにaddAction(:_)が呼べないのかと言いますと、addAction(:_)が呼べるのはUIAlertControllerのインスタンスであるalert定数であるのに対し、addAction(:_)が実行されると返ってくるのはVoidであるためです。

https://developer.apple.com/documentation/uikit/uialertcontroller/1620094-addaction

メソッドチェーンを実現するために、addAction(:_)を実行した後にまたalertインスタンスそのものを返すメソッドを用意します。

addActionをメソッドチェーンで呼べるようにするextension

extension UIAlertController {
    //addAction(alert: _)を実行後に自身が返ってくるのでまたメソッドが呼べる
    func addAction(alert: UIAlertAction) -> Self {
        //元からあるaddActionを実行
        self.addAction(alert)
        //自身を返す
        return self
    }

このextensionだけで以下のようにメソッドチェーンでaddAction(alert:_)を何度も呼ぶことができるようになります。

addActionをメソッドチェーンで呼ぶ

let alert = 
  //UIAlertControllerのインスタンスを生成
  UIAlertController(title: "アラート", message: nil, preferredStyle: .alert)
    //addAction(alert: _)->Selfメソッドを呼び、本来のaddAction(: _)が呼ばれたあとにselfが返される
    .addAction(alert: UIAlertAction(title: "default", style: .default) { _ in
        print("defaultをタップ")
    })
    //selfが返されているため、またaddAction(alert: _)->Selfメソッドを呼べる
    .addAction(alert: UIAlertAction(title: "destructive", style: .destructive) { _ in
        print("destructiveをタップ")
    })
    //selfが返されているため、またaddAction(alert: _)->Selfメソッドを呼べる
    //そして最終的に返されるのもselfであるためUIAlertControllerのインスタンスとしてlet alertに代入できる
    .addAction(alert: UIAlertAction(title: "cancel", style: .cancel) { _ in
        print("cancelをタップ")
    })
//3回のaddAction(: _)が呼ばれたalertをpresentで表示する
self.present(alert, animated: true)

addAction(alert: _)を何回も呼ぶのが面倒であれば配列を渡すメソッドを用意することでシンプルに書けるようになります。

addAction(alerts
extension UIAlertController {

    /*
    省略
    */

    //[UIAlertAction]を受け取り自身を返すメソッドを追加
    func addActions(_ alerts: [UIAlertAction]) -> Self {
        //配列で受け取ったUIAlertActionをひとつずつaddAction(: _)していく
        alerts.forEach {
            self.addAction($0)
        }
        //自身を返す
        return self
    }
}

呼び出しはこのようになります。

addActions(_ alerts

let alert = UIAlertController(title: "アラート", message: nil, preferredStyle: .alert)
    .addActions([
        UIAlertAction(title: "default", style: .default) { _ in
            print("defaultをタップ")
        },

        UIAlertAction(title: "destructive", style: .destructive) { _ in
            print("destructiveをタップ")
        },

        UIAlertAction(title: "cancel", style: .cancel) { _ in
            print("cancelをタップ")
        }
    ])

self.present(alert, animated: true)

さらにpresentメソッドを実行するextensionを用意することでlet alertがいらなくなります。

presentも宣言的に呼び出せるようにするextension
extension UIAlertController {
    /*
    省略
    */

    func present(from: UIViewController, animated: Bool) -> Self {
        from.present(self, animated: animated)
        return self
    }
}

使う側は以下のようになります

addActionとpresentをメソッドチェーンで呼び出す
UIAlertController(title: "アラート", message: nil, preferredStyle: .alert)
    .addActions([
        UIAlertAction(title: "default", style: .default) { _ in
            print("defaultをタップ")
        },

        UIAlertAction(title: "destructive", style: .destructive) { _ in
            print("destructiveをタップ")
        },

        UIAlertAction(title: "cancel", style: .cancel) { _ in
            print("cancelをタップ")
        }
    ])
    .present(from: self, animated: true)

resultBuilderによる内部DSL

これまでの実装でだいぶ宣言的に書いていけるようになりましたが、最後にresultBuilderを利用することで、よりSwiftUIみたいに書けるようにしていきます。
resultBuilderはSwift5.4で新たに追加されたアトリビュートでSwiftUIを実現するために追加されたと言っても過言ではないと思ってます。
より便利な機能は以下のリンクにも記載されていますが、今回は最低限必要な機能だけ定義します。

https://qiita.com/toya108/items/ef62a45f7278a1b019a9

resultBuilderを利用するにはbuildBlock(_ component:)メソッドを定義する必要があります。
この時にパラメータをUIAlertActionの可変長引数(...と記述するやつ)にすることで複数のUIAlertActionを受け取ることができます。

https://dev.classmethod.jp/articles/swift_variadic_parameters/

resultBuilderを使ってUIAlertActionをDSLで書けるようにする
@resultBuilder
enum ArrayUIAlertActionBuilder {
    static func buildBlock(_ components: UIAlertAction...) -> [UIAlertAction] {
        return components
    }
}

そしてextension UIAlertControllerに以下のメソッドを追加します。

ArrayUIAlertActionBuilderを利用してextension
extension UIAlertController {
    /*
    省略
    */
    func addActions(@ArrayUIAlertActionBuilder alertsBuilder: () -> [UIAlertAction] ) -> Self {
        alertsBuilder().forEach({
            self.addAction($0)
        })
        return self
    }
}

こうすることでaddActions([~~,~~,~~])で必要であった[~~,~~,~~]の記述が不要になり、よりSwiftUIのようにUIAlertControllerを記述することができるようになります。

resultBuilderを使ったUIAlertController
UIAlertController(title: "アラート", message: nil, preferredStyle: .alert)
    .addActions{
        UIAlertAction(title: "default", style: .default) { _ in
            print("defaultをタップ")
        }

        UIAlertAction(title: "destructive", style: .destructive) { _ in
            print("destructiveをタップ")
        }

        UIAlertAction(title: "cancel", style: .cancel) { _ in
            print("cancelをタップ")
        }
    }
    .present(from: self, animated: true)

resultBuilderの解説については別記事で書こうかと思います。

Xcodeの警告を消す

Selfを返すメソッドを用意してメソッドチェーンを実現しましたが、最後のメソッドでは返されるSelfを利用しないことがあるためXcodeがResult of call to 'addAction(alert:)' is unusedなどの警告を出してしまいます。
以下のように@discardableResultを付けることで警告が出ないようになります。

extension UIAlertController {
    @discardableResult
    func addAction(alert: UIAlertAction) -> Self {
        self.addAction(alert)
        return self
    }

実装のまとめ

実装のまとめです

extension
@resultBuilder
enum ArrayUIAlertActionBuilder {
    static func buildBlock(_ components: UIAlertAction...) -> [UIAlertAction] {
        return components
    }
}

extension UIAlertController {

    @discardableResult
    func addAction(alert: UIAlertAction) -> Self {
        self.addAction(alert)
        return self
    }

    @discardableResult
    func addActions(_ alerts: [UIAlertAction]) -> Self {
        alerts.forEach {
            self.addAction($0)
        }
        //自身を返す
        return self
    }

    @discardableResult
    func addActions(@ArrayUIAlertActionBuilder alertsBuilder: () -> [UIAlertAction] ) -> Self {
        alertsBuilder().forEach({
            self.addAction($0)
        })
        return self
    }

    @discardableResult
    func present(from: UIViewController, animated: Bool) -> Self {
        from.present(self, animated: animated)
        return self
    }
}
呼び出し側


//以下のどの書き方でも同じアラートが出る

UIAlertController(title: "アラート", message: nil, preferredStyle: .alert)
    .addAction(alert: UIAlertAction(title: "default", style: .default) { _ in
        print("defaultをタップ")
    })
    .addAction(alert: UIAlertAction(title: "destructive", style: .destructive) { _ in
        print("destructiveをタップ")
    })
    .addAction(alert: UIAlertAction(title: "cancel", style: .cancel) { _ in
        print("cancelをタップ")
    })
    .present(from: self, animated: true)

UIAlertController(title: "アラート", message: nil, preferredStyle: .alert)
    .addActions([
        UIAlertAction(title: "default", style: .default) { _ in
            print("defaultをタップ")
        },
        
        UIAlertAction(title: "destructive", style: .destructive) { _ in
            print("destructiveをタップ")
        },
        
        UIAlertAction(title: "cancel", style: .cancel) { _ in
            print("cancelをタップ")
        }
    ])
    .present(from: self, animated: true)

UIAlertController(title: "アラート", message: nil, preferredStyle: .alert)
    .addActions {
        UIAlertAction(title: "default", style: .default) { _ in
            print("defaultをタップ")
        }

        UIAlertAction(title: "destructive", style: .destructive) { _ in
            print("destructiveをタップ")
        }

        UIAlertAction(title: "cancel", style: .cancel) { _ in
            print("cancelをタップ")
        }
    }
    .present(from: self, animated: true)

さいご

メソッドチェーンresultBuilderを使うことでUIKitUIAlertControllerSwiftUIのような呼び出し方ができるようにしてみました。
もちろんUIAlertControllerの他のパラメータや他のクラスでも同様にextensionを用意してやれば宣言的に記述していけます。
他にも細かいテクニックを使っていますが、これらを駆使してUIKitを宣言的に書けるライブラリのDeclarativeUIKitを公開していますので、自作宣言的UIライブラリを開発する時の参考になれば幸いです。

https://github.com/sakiyamaK/DeclarativeUIKit

Discussion