soapergem
soapergem

Reputation: 9979

How can I tap off of a @FocusedField in SwiftUI?

I am new to SwiftUI and starting out by trying to create a simple view that lets you add items to a NavigationStack using SwiftData. Here's my code so far:

import SwiftData
import SwiftUI

struct ItemList: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]
    @State private var isAdding = false
    @State private var newItemName = ""
    @FocusState private var focusedField: String?
    
    var body: some View {
        NavigationStack {
            List {
                Section(header: Text("Items")) {
                    ForEach(items) { item in
                        NavigationLink {
                            Text("Placeholder")
                        } label: {
                            Text(item.name)
                        }
                        .swipeActions(edge: .trailing) {
                            Button(role: .destructive) {
                                modelContext.delete(item)
                            } label: {
                                Label("Delete Item", systemImage: "trash.fill")
                            }
                        }
                    }
                    if isAdding {
                        TextField("Title", text: $newItemTitle, onCommit: {
                            if !newItemTitle.isEmpty {
                                let newItem = Item(name: newItemTitle)
                                modelContext.insert(newItem)
                            }
                            isAdding = false
                            newItemTitle = ""
                            focusedField = nil
                        })
                        .focused($focusedField, equals: "newItem")
                        .onAppear {
                            focusedField = "newItem"
                        }
                        .onDisappear {
                            isAdding = false
                            newItemTitle = ""
                            focusedField = nil
                            appearedCount = 0
                        }
                    }
                }
            }
            .listStyle(.insetGrouped)
            .toolbar {
                ToolbarItem {
                    Button {
                        isAdding = true
                    } label: {
                        Label("New Item", systemImage: "plus")
                    }
                }
            }
        }
    }
}

#Preview {
    ItemList()
        .modelContainer(for: Item.self, inMemory: true)
}

So as you can see, it creates a list of any items stored in SwiftData shown as NavigationLinks, and there's also a toolbar button that lets you add a new item. When you click that button, it reveals a hidden text field at the bottom of the list and focuses it, bringing up the keyboard. That part is great and working as intended.

Because of the onCommit handler, if the user presses Return on the keyboard, it will either dismiss the text field (if they haven't filled anything out), or create a new item before dismissing the field (if they have). But what's missing is that if the user taps anywhere outside of the text field, it still stays active. What I would like to have happen is if they tap anywhere else in the view, outside of that text field, and if they haven't typed anything yet, it should simply dismiss the text field.

I tried adding a TapGesture to the view to do this, but it didn't work, so now I'm stuck. How can I accomplish this?

Upvotes: 0

Views: 63

Answers (1)

sonle
sonle

Reputation: 8866

The quickest way is to overlay the entire List view with a tappable view. It means that when the keyboard shows up, the tappable view will fill up whole screen, and disappear if the keyboard hides. You can try this trick:

List {
    ...
}
.overlay {
    Color.clear
        .contentShape(Rectangle()) //<- Added shape to make Color.clear tappable
        .frame(maxWidth: focusedField != nil ? .infinity : 0,
               maxHeight: focusedField != nil ? .infinity : 0)
        .onTapGesture {
            focusedField = nil
            isAdding = false
        }
}

enter image description here

Upvotes: 0

Related Questions