こんにちは ✋
with で iOS エンジニアをやっている t2low です。
iOS 開発は1年半くらいなのですが、以前は Android 開発をしていたこともあり、それぞれの開発言語の良いところ・足りないところなどが少しずつ見えてきました。
基本的に Kotlin でも Swift でもコードを書く上で困ることはほとんどありませんが、一部 Kotlin では書けるが Swift では書けないコードというものがあります(逆もあります)。
Delegate を実装するときに匿名(無名)クラスがほしいと感じた
Swift にも匿名クラスのような機能がほしいです。
Delegate を実装するときに、 Swift にも匿名(無名)クラスの機能があったらなぁ、と思っています。
Android 開発で書かなかったコード
Android 開発をしているときに、以下のような Activity
に対して、直接 OnClickListener
を実装するようなコードは書かないようにしていました。
class FooActivity: AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
// 略
val button = ...
button.setOnClickListener(this)
}
override fun onClick(view: View?) {
// タップ処理
}
}
書かないようになったのは、以下の記事を書く前からなので、もう10年以上になります。
iOS 開発で一般的なコード
一方で、 iOS 開発では Delegate を ViewController に実装することは一般的のようです。
class FooViewController: UIViewController, BarDelegate {
override func viewDidLoad() {
// 略
let bar = ...
bar.delegate = self
}
func methodBar() {
// Delegate のメソッド処理
}
}
上の Qiita 記事に Android 開発の頃の同様のことが書いてありますが、 Delegate を ViewController に直接実装させることで、メソッドが公開されてしまうことがモヤモヤのポイントです。
iOS 開発を始めた頃も「なんとか別の書き方ができないものか」と考えましたが、 Swift では匿名クラスが作れないため、コスパ良く実装する方法は思い浮かびませんでした。
protocol メソッドの可視性
改めて書くまでもないことですが、 Swift で protocol を実装した場合、 protocol のメソッドは公開されます。
そのため、前述のコードは以下のようなことができてしまいます。
よくない例
Delegate として呼び出される想定の methodBar()
を直接呼び出してしまうとか。
let fooViewController = FooViewController()
fooViewController.methodBar() // 直接呼び出し!
以下のように、全く別の delegate として設定されてしまうようなこともできてしまいます。
class HogeHogeViewController: UIViewController { // 全く別の VC に…
override func viewDidLoad() {
// 略
let bar = ...
bar.delegate = FooViewController() // FooVC を代入!
}
}
Android, Kotlin なら…
前述の例のように interface を直接 Activity に実装してしまえば、Android でも同様のコードは書けてしまいますが、メソッドを公開しない書き方も可能で、言語のサポートもあって(自分の観測範囲では)その方が一般的でした。
SAM 変換
Kotlin には言語に SAM 変換という機能があります。
SAM 変換
SAM interface (Single Abstract Method interface)をラムダ式として記述できる機能。
メソッドを1つだけ持つ interface を簡単な記述で実装できる。
Functional (SAM) interfaces
前述の OnClickListener
は SAM interface なので SAM 変換が利用できます。
Activity に OnClickListener を実装させなくても、以下のように書くことができます。
val button = ...
button.setOnClickListener { view ->
// タップ処理
}
}
匿名クラスとして書けるので、以下のように書くこともできますが、 SAM 変換のおかげでよりスッキリ書けることがわかるかと思います。
val button = ...
button.setOnClickListener(object: View.OnclickListener {
override fun onClick(view: View?) {
// タップ処理
}
}
Java を利用していた頃も匿名クラスとして実装することが多かったので、 Android 開発では interface のメソッドの可視性で悩むことは少なかったです。
そんなコード書かないけどね、という話
よくないコードの例を示しましたが、実際のところはこのようなコードを書くような人はいないでしょう。
しかし、通常のクラスを書く際には外部から使わないメソッドは private にしておくように、 protocol のメソッドもできるなら公開しない方がいいですよね。
protocol を実装して、そのメソッドを非公開にするということはできないので、 ViewController の外部にメソッドを公開しないためには、 Delegate を別クラスに実装した上で保持するくらいしか思い付きません。
それはコストがかさみ過ぎだとも思うので、現状で Delegate を扱うのなら「運用でカバー」するしかないのかな、と思っています。
Delegate 以外の選択肢も増えているので、可視性うんぬんで頭を悩ますくらいなら、いっそ Delegate を使わない方が良いのかもしれません。
iOS の新しいイベント処理
Kotlin でイベント処理をラムダ式で書けるようになったように、 Swift でもクロージャーを使った書き方ができるようになっています。クロージャーで書けるように UIKit が進化したような形です。
(新しいと書いてますが、もうだいぶ前からあるものです)
iOS14 以降で、 UIControl を継承したクラスなら、 addAction()
を使った書き方ができます。
textField.addAction(UIAction() { action in
// UITextField の文字変更時の処理
}, for: .editingChanged)
これなら無用なメソッドが外部に公開されてしまう心配はありません。
クロージャーを使って、シンプルに書くことができてとても良いですね
UIKit のイベント処理をするならば、積極的に利用していきたいと個人的には考えています。
使ったことはないですが、 CombineCocoa を利用することでも同様の記述ができるようですね。
まとめ
Android, Kotlin との比較
Kotlin は匿名クラスが扱えたり、 interface をラムダと同じように書ける機能を提供してくれていたりして、 Android の古いクラスであってもシンプルなコードで書くことができます。
一方で iOS は、 Swift の機能自体は増やさずに、 UIKit に新たな機能を追加することで、シンプルなコードが書けるようにしているようです。
このあたりは、プラットフォームや言語の設計思想の違いからくるものなのでしょう。
Android 開発がベースにあったため、匿名クラスほしいな〜と感じましたが、 iOS は iOS で別の進化をしているんだな、とこれを書きながら再認識しました。
おわりに
ベースとなる技術、思考があると、「ないものをこれまでと同様の方法でどうにかしよう」と考えてしまいがちです。(僕がそうですね)
しかし、一度凝り固まった脳をほぐして、周りをよく見ると、別の解決方法が用意されていることもあるようです。
いい歳したおっさんエンジニアですが、これからも柔軟に変化に対応していけたらいいな、と思います。