$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

SwiftUIでテキストを連続入力させたい時、ソフトウェアキーボードが一瞬閉じる問題に対処する

SwiftUIでテキストの連続入力を実装したいと思ったが、iOSで一瞬ソフトウェアキーボードが閉じてしまう問題があった。そのあたりの解消の工夫をしたのでブログに書いておく。

やりたかったことと課題

以下のように、入力 => done => 入力 => doneと繰り返して、連続入力させたかった。

しかしSwiftUIのTextField + forcused + onSubmitだと、doneを押した時に一瞬だけソフトウェアキーボードが閉じ、また開くという挙動をする。めちゃくちゃ体験が悪い。

初期のシンプルなonSubmitの実装

struct ContentView: View {
  @State private var inputText = ""
  @State private var items: [String] = []
  @FocusState private var isInputFocused: Bool

  var body: some View {
    VStack {
      ForEach(items, id: \.self) { item in
        Text(item)
      }

      TextField("新しいアイテムを追加", text: $inputText)
        .focused($isInputFocused)
        .onSubmit {
          addItem()
          isInputFocused = true
        }
    }
  }

  private func addItem() {
    let trimmedText = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !trimmedText.isEmpty else { return }

    items.append(trimmedText)
    inputText = ""
  }
}

解決策

この解決にはいろいろなやり方がありそうだが、今回は複数行入力できるTextField + onChangeで実装した。ただし、onChangeのみ使うとmacOSではEnterキーが効かないという問題が別で起こってしまったので、プラットフォーム別に分岐して実装した。

import SwiftUI

struct ContentView: View {
  @State private var inputText = ""
  @State private var items: [String] = []
  @FocusState private var isInputFocused: Bool

  var body: some View {
    VStack {
      ForEach(items, id: \.self) { item in
        Text(item)
      }

      // axis: .verticalで複数行入力に対応
      TextField("新しいアイテムを追加", text: $inputText, axis: .vertical)
        // 追加時にisInputFocusedの状態を変えないので、入力し続けられる
        .focused($isInputFocused)
        #if os(iOS)
          // iOS: onChangeで改行文字を検知して追加処理を実行
          // onSubmitを使うとキーボードが一瞬閉じてしまう
          .onChange(of: inputText) { _, newValue in
            guard isInputFocused else { return }

            // 改行文字(\n)が含まれているなら追加処理を実行
            guard newValue.contains("\n") else { return }
            addItem()
          }
        #else
          // macOSなどiOS以外: 通常通りonSubmitでEnterキー押下を検知
          // macOSでonChangeを使うとEnterキーで追加できなくなる
          .onSubmit {
            addItem()
          }
        #endif
    }
  }

  private func addItem() {
    let trimmedText = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !trimmedText.isEmpty else { return }

    items.append(trimmedText)
    inputText = ""
  }
}

まとめ

上記実装により、最初やりたかったことは解決し、iOSでもmacOSでも両方良い感じに連続入力できるようになった。

一方、iOSでハードウェアキーボードがある時はどうするの?という問題も別である。本当にちゃんとやりたいなら以下のようなやり方もありそうだ。

  • ソフトウェアキーボードが開いているかを判断して切り替え
  • UIKitでうまいこと実装してSwiftUIに統合する

参考