背景
- プロジェクトには共通部品がありました。
- TextField(単行入力)
- TextArea(複数行入力)
-
SearchBar(検索バー、iOS ネイティブ
UITextFieldのラッパー)
-
TextField と TextArea は、Dart 側で
maxBytesを指定すると「全角=2/半角=1」のバイト換算で文字数制限が効くのに、SearchBar は制限が効かない状態でした。
結論(原因と対策)
- 原因: SearchBar の iOS プラグイン(Swift)に「Dart 側で正規化(トリム)した値をネイティブ UI に反映する処理」が実装されていなかった。
-
対策: Swift のプラグインに「update 受信時に
UITextField.textを更新する処理(テキスト反映)」を追加したことで、Dart 側のmaxBytes制御が UI にも反映されるようになった。
なぜそうなるのか
- 通常の入力は、ユーザーが
UITextFieldに直接タイプするため、ネイティブ UI は自動で最新になります(Dart 側は通知されるだけ)。 - しかし、バイト制限を Dart 側で行う場合、入力超過時に Dart で文字列をトリムします。つまり「正しい文字列」は Dart 側にあり、ネイティブの
UITextFieldは“古いまま”になります。 - そのため、Dart 側で正規化した文字列を、ネイティブ UI に「書き戻す」必要があります。これが SearchBar の Swift 側に欠けていた部分でした(TextField/TextArea はすでに実装済み)。
実装(サンプルコード)
1) Dart 側(SearchBar VM 側)での入力時バイト制限と巻き戻し
- 入力されるたびにバイト長を判定し、超過時は先頭から
maxBytes分だけを残す。 - その後、ネイティブ UI にも即時反映(巻き戻し)する。
/// サンプル: SearchBar VM 側
void _onNativeInput(String raw, int maxBytes, String ident) {
var next = raw;
if (getBytes(next) > maxBytes) {
next = left(argTargetStr: next, argBytes: maxBytes); // 全角2/半角1換算でトリム
updateTextOnNative(text: next, ident: ident); // ネイティブUIへ巻き戻し
}
if (_previousValue == next) return; // ループ防止
_previousValue = next;
state = state.copyWith(text: next);
onChangedValue(next);
}
/// サンプル: ネイティブへ反映
Future<void> updateTextOnNative({required String text, required String ident}) async {
final channel = MethodChannel('native_search_bar#$ident');
await channel.invokeMethod('update', {'text': text});
}
補足:
-
getBytes/leftは「全角=2/半角=1」でのバイト換算・先頭切り出しのユーティリティ(実装は任意)。 -
_previousValueで前回値を保持し、不要な再通知を避ける。
2) iOS(Swift)側プラグインでの「テキスト反映」実装
- Dart → iOS の
updateが来たら、UITextField.textを更新する。 - 空文字は既存のクリア相当の処理に委譲。
- 不要なレイアウト振れを避けるなら、キャレット位置維持(オプション)。
// サンプル: SearchBar の Swift プラグイン
_channel.setMethodCallHandler { [weak self] call, result in
guard let self = self else { return }
switch call.method {
case "update":
if let args = call.arguments as? [String: Any?] {
let newText = (args["text"] as? String) ?? ""
if newText.isEmpty {
_ = self.textFieldShouldClear(self._textField) // クリア相当
} else {
// (任意)フォーカス中のキャレット位置を保持
let wasEditing = self._textField.isFirstResponder
var previousOffset: Int?
if let range = self._textField.selectedTextRange {
previousOffset = self._textField.offset(from: self._textField.beginningOfDocument, to: range.start)
}
// テキスト反映
self._textField.text = newText
// (任意)キャレット復元(短くなったら末尾へクランプ)
if wasEditing, let prev = previousOffset {
let clamped = max(0, min(newText.count, prev))
if let pos = self._textField.position(from: self._textField.beginningOfDocument, offset: clamped),
let textRange = self._textField.textRange(from: pos, to: pos) {
self._textField.selectedTextRange = textRange
}
}
}
result(nil)
} else {
result(FlutterError(code: "invalid_args", message: "update: invalid arguments", details: nil))
}
default:
result(FlutterMethodNotImplemented)
}
}
ポイント:
- 「通常のタイピング」ではネイティブが UI 更新しますが、「Dart 側でトリムした文字列」はこの
updateでネイティブに書き戻さないと UI に反映されません。 -
result(nil)を返すことで MethodChannel 呼び出しを正しく完了。 - ループを避けるため、ここで
inputを送り返す必要はありません(送り返す場合は Dart の差分ガードが必須)。
まとめ
-
問題: SearchBar だけ
maxBytes制限が効かなかった。Dart でトリムしても UI が更新されない。 - 原因: iOS プラグイン(Swift)に「Dart→ネイティブのテキスト反映」がなかった(TextField/TextArea は実装済み)。
-
対策:
- Dart 側: 入力都度バイト制限→必要時に
updateでネイティブへ巻き戻し - Swift 側:
update受信でUITextField.textを更新(任意でキャレット維持)
- Dart 側: 入力都度バイト制限→必要時に
-
結果: SearchBar でも TextField/TextArea と同様に
maxBytes制限が正しく機能するようになった。
この方針は「ネイティブ側で事前ブロック(全角=2/半角=1のバイト判定)する」実装でも代替可能ですが、既存の TextField/TextArea と同じ「Dart 側で制御+ネイティブに書き戻す」設計に寄せると統一性が高く、移植/保守も容易です。