📱

iOSのUIのイベント処理の注意点

2022/08/15に公開

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の仕組みを見ると、以下のような流れになっています。

  1. objectWillChangeを呼んでUIの更新が必要なことを伝える
  2. UIに反映する値を変更する
  3. 次のUI更新タイミングが来たら値を取得しUIが更新される

objectWillChangeを呼び出したとしても、そのタイミングではまだUIの状態は変わっていません(値が変更する前の「Will」で伝えているので当然)。ボタンを無効にするためにdisabledにバインドする値をtrueに変えても、それが実際に反映されるのは少し時間が経った後になります。

つまり、ボタンなどからのイベント受け取りを防ぎたい場合、「UIの状態を無効にすることでイベントが起きなくなる」という挙動に頼るのはそもそも間違っているということです。もちろん、見た目的に無効な状態にするという意味でUIを更新する必要はありますが、無効になっていても関係なくイベントが呼び出される想定で実装する必要があります。

Discussion