宣言的UIのススメ~自作して理解する宣言的UI~
はじめに
宣言的UIのススメと書きましたが著者自身が宣言的UIとは何かというのを分かっていませんw
UIKitをSwiftUIのように書けるようになるテクニックまとめ、ぐらいに捉えてください。
UIAlertControllerの呼び出し
UIAlertControllerの呼び出しを例にして説明していきます。
標準
iOSアプリをUIKitで開発している場合、アラートを表示させるためには以下のように書くかと思います。
//アラートのインスタンスを生成
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)
この書き方の場合、わざわざalert
やdef
といった定数を用意しないといけなくなり、alert.addAction(def)
と記載している時にそれぞれalert
やdef
には何が代入されどんなふうに設定したのかを気にしながら書くことになります。
宣言的に書く
この記事の最後まで読むと最終的にはこのような書き方で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
に代入しています
let alert = UIAlertController(title: "アラート", message: nil, preferredStyle: .alert)
/*
省略
*/
alert.addAction(def)
alert.addAction(destructive)
alert.addAction(cancel)
では、なぜaddAction()
を呼んだあとにaddAction(:_)
が呼べないのかと言いますと、addAction(:_)
が呼べるのはUIAlertController
のインスタンスであるalert
定数であるのに対し、addAction(:_)
が実行されると返ってくるのはVoid
であるためです。
メソッドチェーンを実現するために、addAction(:_)
を実行した後にまたalert
インスタンスそのものを返すメソッドを用意します。
extension UIAlertController {
//addAction(alert: _)を実行後に自身が返ってくるのでまたメソッドが呼べる
func addAction(alert: UIAlertAction) -> Self {
//元からあるaddActionを実行
self.addAction(alert)
//自身を返す
return self
}
このextension
だけで以下のようにメソッドチェーンでaddAction(alert:_)
を何度も呼ぶことができるようになります。
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: _)
を何回も呼ぶのが面倒であれば配列を渡すメソッドを用意することでシンプルに書けるようになります。
extension UIAlertController {
/*
省略
*/
//[UIAlertAction]を受け取り自身を返すメソッドを追加
func addActions(_ alerts: [UIAlertAction]) -> Self {
//配列で受け取ったUIAlertActionをひとつずつaddAction(: _)していく
alerts.forEach {
self.addAction($0)
}
//自身を返す
return self
}
}
呼び出しはこのようになります。
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
がいらなくなります。
extension UIAlertController {
/*
省略
*/
func present(from: UIViewController, animated: Bool) -> Self {
from.present(self, animated: animated)
return self
}
}
使う側は以下のようになります
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を実現するために追加されたと言っても過言ではないと思ってます。
より便利な機能は以下のリンクにも記載されていますが、今回は最低限必要な機能だけ定義します。
resultBuilder
を利用するにはbuildBlock(_ component:)
メソッドを定義する必要があります。
この時にパラメータをUIAlertAction
の可変長引数(...と記述するやつ)にすることで複数のUIAlertAction
を受け取ることができます。
@resultBuilder
enum ArrayUIAlertActionBuilder {
static func buildBlock(_ components: UIAlertAction...) -> [UIAlertAction] {
return components
}
}
そしてextension UIAlertController
に以下のメソッドを追加します。
extension UIAlertController {
/*
省略
*/
func addActions(@ArrayUIAlertActionBuilder alertsBuilder: () -> [UIAlertAction] ) -> Self {
alertsBuilder().forEach({
self.addAction($0)
})
return self
}
}
こうすることでaddActions([~~,~~,~~])
で必要であった[~~,~~,~~]
の記述が不要になり、よりSwiftUIのように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
}
実装のまとめ
実装のまとめです
@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
を使うことでUIKit
のUIAlertController
をSwiftUI
のような呼び出し方ができるようにしてみました。
もちろんUIAlertController
の他のパラメータや他のクラスでも同様にextension
を用意してやれば宣言的に記述していけます。
他にも細かいテクニックを使っていますが、これらを駆使してUIKit
を宣言的に書けるライブラリのDeclarativeUIKit
を公開していますので、自作宣言的UIライブラリを開発する時の参考になれば幸いです。
Discussion