TapGestureの競合の解決方法の検討
発生したTapGestureの競合
個人開発アプリの下記の画像のプロフィール作成画面でTapGestureの競合が起きることで、PhotosPickerの「ライブラリから画像を選択ボタン」が押せる時と押せない時があるというバグが発生していました。
下記のようなコードを書いていました。
PhotosPickerの「ライブラリから画像を選択ボタン」が押せない時はonTapGesture内のfocusedField = nil
が呼ばれてしまっていました。
PhotosPickerのTapGestureとonTapGestureのTapGestureの競合によるバグだと考え、解決方法の検討を行いました。
struct SignInProfileView: View {
@FocusState private var focusedField: Field?
@ObservedObject var viewModel: AuthenticationViewModel
@State var selectedImage: UIImage?
@State var selectedItem: PhotosPickerItem?
...........
//Modifierは省略...
var body: some View {
NavigationStack {
VStack(spacing: 20) {
VStack(spacing: 12) {
Text("プロフィール画像")
Image(uiImage: viewModel.selectedImage ?? UIImage(named: "noImage")!)
PhotosPicker(selection: $selectedItem) {
Text("ライブラリから画像を選択")
}
}
.onChange(of: selectedItem) {
Task {
guard let data = try? await selectedItem?.loadTransferable(type: Data.self) else { return }
guard let uiImage = UIImage(data: data) else { return }
viewModel.selectedImage = uiImage
}
}
VStack(alignment: .leading) {
Text("名前")
InputTextField(
inputTxet: $viewModel.displayName,
count: 10,
placeHolderText: "名前",
focused: $focusedField,
equals: .name
)
.onChange(of: viewModel.displayName) {
viewModel.displayNameTotalCount = viewModel.displayName.count
}
Text("自己紹介")
ZStack(alignment: .topLeading) {
InputTextField(
inputTxet: $viewModel.bio,
count: 20,
placeHolderText: "自己紹介",
focused: $focusedField,
equals: .bio
)
.onChange(of: viewModel.bio) {
viewModel.bioTotalCount = viewModel.bio.count
}
}
}
}
}
.onTapGesture {
focusedField = nil
}
}
}
private extension SignInProfileView {
enum Field: Hashable {
case name
case bio
}
}
下記のInputTextFieldというカスタムTextFieldを使用しています。
struct InputTextField<FocusedValue: Hashable>: View {
@Binding private var text: String
private var maxCount: Int
private var placeHolder: String
private var focused: (FocusState<FocusedValue>.Binding, FocusedValue)
var body: some View {
VStack(spacing: 4) {
TextField(
"\(placeHolder)(\(maxCount)文字以内)",
text: $text
)
.padding(12)
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(Color.primary, lineWidth: 2)
}
HStack {
Spacer()
// 入力文字数の表示
Text(" \(text.count) / \(maxCount)")
}
}
// 10文字以上の時最後の文字を削除制限
.onChange(of: text) {
if $0.count > maxCount {
text.removeLast($0.count - maxCount)
}
}
.ifLet(focused) {
$0.focused($1.0.projectedValue, equals: $1.1)
}
}
}
extension InputTextField {
init(
inputTxet: Binding<String>,
count: Int,
placeHolderText: String,
focused: FocusState<FocusedValue>.Binding,
equals focusedValue: FocusedValue
) {
self.init(
text: inputTxet,
maxCount: count,
placeHolder: placeHolderText,
focused: (focused, focusedValue)
)
}
}
private extension View {
@ViewBuilder
func ifLet<Wrapped, Content: View>(
_ wrapped: Wrapped?,
@ViewBuilder transform: (Self, Wrapped) -> Content
) -> some View {
if let wrapped {
transform(self, wrapped)
} else {
self
}
}
}
解決方法の検討
解決方法検討① ZStackにonTapGestureをつける
まず、NavigationStackにonTapGesture
をつけていましたが、ZStackにonTapGesture
をつけるように変更し、挙動を確認してみました。
NavigationStack { ZStack { } .onTapGesture { focusedState = nil } }
結果: 「ライブラリから画像を選択ボタン」が押せる時と押せない時が発生
解決方法検討② TextFieldの入力フィールドにフォーカスがある時のみ「透明なタップ領域」を作成
下記のようにTextFieldの入力フィールドにフォーカスがある時のみ「透明なタップ領域」が画面に表示され、.onTapGesture { focusedState = nil }
が実行される。
+ if focusedField != nil {
+ Color.clear
+ .contentShape(Rectangle())
+ .onTapGesture {
+ focusedField = nil
+ }
+ }
- .onTapGesture {
- focusedField = nil
- }
結果: キーボードが閉じている状態の時は「ライブラリから画像を選択ボタン」が押せるようになった!
TextFieldの入力フィールドにフォーカスがあたっている時には「透明なタップ領域」が画面に表示されているので、「ライブラリから画像を選択ボタン」を押せず、.onTapGesture { focusedState = nil }
が実行され、キーボードが閉じる。
解決方法検討②の懸念点としては、キーボードが閉じている状態の時のみしか「ライブラリから画像を選択ボタン」が押せないことです。
解決方法検討③
SimultaneousGestureを使用し、同時に発生し得る二つのジェスチャーの実装をしました。
+ .simultaneousGesture(
+ TapGesture()
+ .onEnded {
+ focusedField = nil
+ }
+ )
- .onTapGesture {
- focusedField = nil
- }
結果: どの状態であっても「ライブラリから画像を選択ボタン」が押せるようになった!
SimultaneousGesture
を使用することで、PhotosPickerのTapGestureとonTapGestureのTapGestureの同時に発生し得る二つのジェスチャーに対応することが可能になった。
解決方法の採用
解決方法検討③を採用しました。その理由としては、TextFieldの入力フィールドにフォーカスがあたっている状態からでも「ライブラリから画像を選択ボタン」が押せることで、ユーザーが意図する挙動に近づけると考えたからです。
最後に
上記のSimultaneousGesture
を使用する以外にも、TapGestureの競合の解決方法があれば教えてください!
Discussion