😬

[Swift]UIPickerViewが急にパカパカしだした話

に公開

概要

UIPickerViewを使っているときに、急にパカパカしだしたので、調査した内容をまとめる。
下の動画の例だと、"怪しいボタン"をタップすると、Pickerがパカパカしだす。
(Pickerをスクロールすると、Picker自身が閉じられてしまう)

背景

SwiftUIで作成しているアプリで、Pickerによる入力を実現するために、UIRepresentableを使用してUITextViewinputViewとして、UIPickerViewを設定していた。
正常なケースでは以下のように、textViewをタップすると、下部からUIPickerViewが表示される。
PickerをScrollすることで、Pickerの要素を選択しtextを変更できる。

問題

しかし、PickerをScrollしていると、パカパカする現象に遭遇した。
Pickerをスクロールすると、Picker自身が閉じられてしまう。
何度も試してみたが、再現はめったにせず、原因も分らなかった。

原因 & 解決策

調査を進めると、とある画面を開いたあとに、PickerをScrollすると、Pickerが急にパカパカしだすことが判明した。
その画面の実装を見ると、以下のコードを実行していることがわかった。

UIScrollView.appearance().keyboardDismissMode = .interactive

おそらく、iOS15をサポートしていたときに、ScrollViewをScrollしたときにKeyboardを閉じるために実装していたと考えられる。
そのコードをコメントアウトすると、Pickerがパカパカしなくなった。

上記のコードは、SwiftUIでは変更できない(iOS15ではできなかった)要素を、その内部で使用しているUIKitのプロパティを使用してを変更しているものである。
その変更は、UIKitのグローバルな動作を変更するため、アプリケーション全体のUIScrollViewに影響を与える。
今回のバグも、別の画面で実行したコードの影響で、Pickerがパカパカしだしたと考えられる。

サンプルコード

struct ContentView: View {
    @State private var text: String = ""
    @FocusState private var isFocused: Bool
    
    var body: some View {
        ScrollView(.vertical) {
            VStack {
                Color.clear.frame(height: 200)
                UITextFieldContainer(text: $text)
                    .focused($isFocused)
                Button("怪しいボタン") {
                    UIScrollView.appearance().keyboardDismissMode = .interactive
                }
            }
        }
        .padding(.horizontal, 40)
        .onTapGesture {
            isFocused = false
        }
    }
}

struct UITextFieldContainer: UIViewRepresentable {
    @Binding var text: String
    func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextField {
        let view = UITextField()
        view.clearButtonMode = .whileEditing
        view.delegate = context.coordinator
        view.backgroundColor = .green
        
        let picker = UIPickerView()
        picker.delegate = context.coordinator
        picker.dataSource = context.coordinator
        view.inputView = picker
        return view
    }

    func updateUIView(_ uiView: UITextField, context _: UIViewRepresentableContext<Self>) {
        uiView.text = text
    }

    final class Coordinator: NSObject, UITextFieldDelegate, UIPickerViewDelegate, UIPickerViewDataSource {
        private var textView: UITextFieldContainer
        init(_ textView: UITextFieldContainer) {
            self.textView = textView
            super.init()
        }
        
        // Picker View
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            1
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            10
        }
        
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            "\(row)"
        }
        
        func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
            textView.text = "\(row)"
        }
        
        // TextView
        func textFieldDidChangeSelection(_ textField: UITextField) {
            textView.text = textField.text ?? ""
        }

        func textFieldShouldClear(_: UITextField) -> Bool {
            self.textView.text = ""
            return true
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
}

"怪しいボタン"をタップすると、Pickerがパカパカしだす。
タップ前

タップ後

まとめ

SwiftUIの足りない部分をUIKitのプロパティを変更して補うことがあるが、UIKitのプロパティを変更することで、SwiftUIの挙動が変わることがあるので注意が必要。
このようなバグは、原因となるコードがバグの箇所と関係のない場所にあるため、調査が難航することがある。
UIKitのグローバルなプロパティを変更するときは、そのリスクを考慮する必要がある。

Discussion