すごいニッチな小ネタですが、アドベントカレンダー20日目の記事です。
iOSアプリをReactive Programmingで実装する場合、代表的なライブラリが2つあります。
- RxSwift
- ReactiveSwift
自分は何年か前からそれらのライブラリを利用させてもらっています。
便利な一方、抽象度の高さやサードパーティ製であるところから生じる扱いにくさもあります。
特にXcode + LLDBを組み合わせたデバッグと相性が悪いなと感じておりました。
例えば実行時にbreakpointを仕掛けてその時点でのStacktraceを見よう...などといった場合に素直なやり方は使えません。
ReactiveSwiftで例を見てみます。
import UIKit
import ReactiveSwift
import Result
class ViewController: UIViewController {
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewModel
.producer
.observe(on: UIScheduler())
.on { (v) in
print(v)
}
.start()
}
}
struct ViewModel {
let producer = SignalProducer<Int, AnyError>([1, 2, 3, 4, 5])
.flatMap(.latest) {
SignalProducer(value: $0 + 1)
}
.reduce(0) { (sum, v) in
sum + v
}
}
上記のコードを実行して、最終的な出力部分でbreakpointを仕掛けると以下のようになります。
上図のようにデータの流れを上手く追うことができません。
ただしRxSwiftであれば、Stacktraceを取る方法自体はいくつかオプションとして用意されています。
- https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#error-handling
- https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#debugging
いずれもコードに手を入れる必要があり、LLDBの機能をうまく使えば実行時に差し込むこともできるものの、
データの流れを手軽に追いかけるという観点では難しそうです。
ReactiveSwiftの場合logEventsを使ったデバッグ方法デバッグ方法が存在します。
これはオブザーバーパターンで実装した場合にはどうしようもない問題でもあります。
標準フレームワークでもNotificationCenterやKVOを利用するとつきまといます。
これまでNotificationCenterやKVOはアプリ内では限定的な利用にすることで、問題を回避しながら有効活用してきました。
そのためReactive Programmingを採用する場合でも、あまりストリームの処理を長くしすぎず使う場所をある程度限定することでしのいでいます。
そんな感じでソフトウェア開発につきものである抽象度が上がったら上がったで生じる苦労に直面し、以前こんなことを書いていました。
iOSでReactiveCocoa使っていても、結局中身のデータがどういう経路を辿って変化していったかを見てデバッグするケースに頻繁に出くわすんだけど、それがめちゃくちゃやりにくい。みんなどうしてるんだろう。
— kazuhiro4949 (@kazuhiro494949) March 24, 2017
今年のwwdcのone more thingでリアクティブを本腰入れてサポートしていきますとか言ってくれんもんか。今のxcodeやuikitの文脈でどうしても扱いにくさを感じる...
— kazuhiro4949 (@kazuhiro494949) April 11, 2017
時が経って2019年のWWDCで、ついにReactiveXに極めて似たフレームワークをAppleが提供し始めました。
純正品に期待していたものの一つである、LLDBによるデバッグはしやすくなっているのでしょうか。
早速試してみましょう。
先程のReactiveSwiftのコードを書き換えた以下のCombine Framework製コードを実行してみます。
import UIKit
import Combine
class ViewController: UIViewController {
let viewModel = ViewModel()
var cancellable: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
cancellable = viewModel.publish()
.sink {
print($0)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewModel.subject.send(1)
viewModel.subject.send(2)
viewModel.subject.send(3)
viewModel.subject.send(4)
viewModel.subject.send(5)
viewModel.subject.send(completion: .finished)
}
}
struct ViewModel {
let subject = PassthroughSubject<Int, Never>()
func publish() -> AnyPublisher<Int, Never> {
subject.flatMap {
Just($0 + 1)
}
.reduce(0, { (sum, v) in
sum + v
})
.map {
$0
}.eraseToAnyPublisher()
}
}
Breakpointを仕掛けてスタックトレースを見てみると次のようになりました。
(当たり前ですが)最初からライブラリ内の処理が見えないので全体的にスッキリしています。
どのクラスのどの行に書いてあるSubjectがストリームにデータを流したかは捉えやすくなっているものの、一番知りたいその途中でどのような流れでデータが変化されていったのかは追えませんでした。
ではCombine frameworkで途中の流れはどのようにデバッグを行うかというと、breakpoint()というメソッドが用意されていて、それを挟むことで実行を止めて途中のデータを見ることができます。
今回知りたかったのは全体的なデータの流れだったので、これ自体は若干意図と違うかなという気がしています。(LLDBの機能をフル活用して工夫すれば、欲しい情報が期待した形で取れるようになるかもしれませんが...)
結果をまとめると以下のとおりです。
- スタックトレースで途中のオペレーターの処理まではトレースできない
- どこで値がsend()されたのかは見える (ReactiveSwiftやRxSwiftと同じ)
- breakpoint()で値ごとにSIGTRAPで止めることができる
残念ながら、画期的な解決策が用意されているようではありませんでした。
このあたりは作り上止む終えないものとして、そもそもの自分自身のプログラミングの捉え方を考え直す必要がありそうです。
また、データの状態を必要以上に強く意識するUIKitではあまり使うことは考えず、同時に提供されたSwiftUIとうまく組み合わせるというのが重要なのかもしれません。
参考資料
- Combine.framwork
- breakpoint
- https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#error-handling
- https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#debugging
- https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/DebuggingTechniques.md