iOSのUIのイベント処理の注意点
iOSのUIのイベント処理周りで期待通りに動かず問題を起こすことがあるので、その点をまとめておきます。
イベントの遅延
例えば、あるボタンをタップしたら1回だけ処理して複数回受け付けてはいけないとします。その場合、以下のようなコードを書くかもしれません。
struct ContentView: View {
@State var isButtonDisabled: Bool = false
var body: some View {
Button("Disable") {
isButtonDisabled = true
print("Disabled")
}
.disabled(isButtonDisabled)
}
}
普通に動かすと、おそらく何の問題もなくDisable
ボタンをタップするとボタンが無効になって、そのあと何度タップしても無視されると思います。ログには1つだけDisabled
が表示されているはずです。
しかし、UIKit・SwiftUI関わらずUIからのイベントが実行されようとした時にメインスレッドで処理が行われていると、イベントが遅延して実行されることがあります。もしメインスレッドの処理中に複数回ボタンがタップされたとしたら、ボタンが無効になる前に各タップのイベントが予約されて実行されてしまいます。
強制的に再現できるようにしてみます。Sleep
ボタンを追加して、タップしたら1秒間メインスレッドをスリープさせます。
struct ContentView: View {
@State var isButtonDisabled: Bool = false
var body: some View {
VStack {
Button("Sleep") {
print("Begin sleep")
Thread.sleep(forTimeInterval: 1.0)
print("End sleep")
}
Button("Disable") {
isButtonDisabled = true
print("Disabled")
}
.disabled(isButtonDisabled)
}
}
}
Sleep
ボタンをタップしてから1秒間の間にDisable
ボタンを連打すると、連打した数だけDisabled
がログに表示されることがわかると思います。
もちろん実際にはメインスレッドをスリープさせるなんてことはしませんが、メインスレッドは細切れに色々な処理が実行されているものなので、意外とこのようにUIのイベントが遅延して実行されることがあります。
すごく気を付けてメインスレッドでの処理を少なくしてもゼロにすることはできないので、この現象は気をつければ避けられるというものではありません。なので、このように動作してしまうことを受け入れてコードを書く必要があります。
UIの更新のタイミング
上記のような挙動はあるとして、ではメインスレッドさえ空いていれば問題ないかというとそういうわけでもありません。UIの更新処理がどうなっているかを確認してみます。
SwiftUIでUI更新をする際のObservableObject
の仕組みを見ると、以下のような流れになっています。
-
objectWillChange
を呼んでUIの更新が必要なことを伝える - UIに反映する値を変更する
- 次のUI更新タイミングが来たら値を取得しUIが更新される
objectWillChange
を呼び出したとしても、そのタイミングではまだUIの状態は変わっていません(値が変更する前の「Will」で伝えているので当然)。ボタンを無効にするためにdisabled
にバインドする値をtrue
に変えても、それが実際に反映されるのは少し時間が経った後になります。
つまり、ボタンなどからのイベント受け取りを防ぎたい場合、「UIの状態を無効にすることでイベントが起きなくなる」という挙動に頼るのはそもそも間違っているということです。もちろん、見た目的に無効な状態にするという意味でUIを更新する必要はありますが、無効になっていても関係なくイベントが呼び出される想定で実装する必要があります。
Discussion