SwiftUI - Search in List Header - uitableview

I am trying to to recreate what everyone know from UITableView with SwiftUI: A simple search field in the header of the tableview:
However, the List View in SwiftUI does not even seem to have a way to add a header or footer. You can set a header with a TextField to sections like this:
#State private var searchQuery: String = ""
var body: some View {
List {
Section(header:
Group{
TextField($searchQuery, placeholder: Text("Search"))
.background(Color.white)
}) {
ListCell()
ListCell()
ListCell()
}
}
}
However, I am not sure if this is the best way to do it because:
The header does not hide when you scroll down as you know it from UITableView.
The SearchField does not look like the search field we know and love.
Has anyone found a good approach? I don't want to fall back on UITableView.

Xcode 13 / SwiftUI 3
You can now use .searchable to make a List... searchable!
struct ContentView: View {
#State private var searchQuery: String = ""
var body: some View {
NavigationView {
List {
ForEach(Array(1...100)
.map { "\($0)" }
.filter { searchQuery.isEmpty ? true : $0.contains(searchQuery) }
,id: \.self) { item in
Text(verbatim: item)
}
}
.navigationTitle("Fancy Numbers")
.searchable(text: $searchQuery)
}
}
}
The search bar seems to appear only if the List is embedded in a NavigationView.
Xcode 12, SwiftUI 1/2
You can port UISearchBar to SwiftUI.
(More about this can be found in the excellent WWDC 2019 talk - Integrating SwiftUI)
struct SearchBar: UIViewRepresentable {
#Binding var text: String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar,
context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
And use it like this:
struct ContentView: View {
#State private var searchQuery: String = ""
var body: some View {
List {
Section(header: SearchBar(text: self.$searchQuery)) {
ForEach(Array(1...100).filter {
self.searchQuery.isEmpty ?
true :
"\($0)".contains(self.searchQuery)
}, id: \.self) { item in
Text("\(item)")
}
}
}
}
}
It's the proper search bar, but it doesn't hide - I'm sure we'll be able to do it at some point via SwiftUI API.
Looks like this:

Perhaps a starting point here. Consider using ZStack for disappear effect on scroll for scrollview.
struct ContentView: View {
#State var search: String
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 0) {
HStack {
Image(systemName: "magnifyingglass")
.padding(.leading, CGFloat(10.0))
TextField("Search", text: $search, onEditingChanged: { active in
print("Editing changed: \(active)")
}, onCommit: {
print("Commited: \(self.search)")
})
.padding(.vertical, CGFloat(4.0))
.padding(.trailing, CGFloat(10.0))
}
.overlay(
RoundedRectangle(cornerRadius: 5.0)
.stroke(Color.secondary, lineWidth: 1.0)
)
.padding()
List {
ForEach(0...100, id: \.self) { e in
Text("Item \(e)")
}
}
}
.navigationBarTitle(title(for: self.search))
}
}
private func title(for value: String?) -> String {
guard let value = value, value.count > 0 else {
return "No search"
}
return #"Searching for "\#(value)""#
}
}

I implemented my country picker project Columbus using SwiftUI. I implemented a custom publisher CountryListViewModel and connected that with the text field. This way I can type, search the data source, filter out results / debounce and update the table view when all operations are done. Works pretty well đź‘Ť
https://github.com/Blackjacx/Columbus/tree/swift-ui/Source/Classes

2021 — Xcode 13 / SwiftUI 3
Native SwiftUI solution:
List {
ForEach(0..<5) {
index in
Text("item \(index)")
}
}
}
.searchable(text: .constant("search_value")) // should be a Binding

Related

Keyboard leave a blank space after saving

Since the new Swift version, it looks like the keyboard leaves a blank space when I save the data.
MyView
import SwiftUI
struct AddUpdateUrl: View {
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
#EnvironmentObject var controller: PlacesViewController
#Binding var place: Place
#State var url = ""
var updateMode = false
var body: some View {
VStack {
Text("Website")
.font(.callout)
TextField("", text: $url)
.keyboardType(.URL)
.border(/*#START_MENU_TOKEN#*/Color.black/*#END_MENU_TOKEN#*/, width: 1)
}
.navigationBarItems(
trailing: Button(action: {save()}) {Text("Save")}.disabled(url.isEmpty)
)
.resignKeyboardOnDragGesture()
}
private func save() {
if !url.isEmpty {
place.url = url
if updateMode {
controller.update(place: place)
}
}
presentationMode.wrappedValue.dismiss()
}
}
And the result.
The view before trying to update the website
The view to add the website
After clicking the save button
I have the same behavior on the simulator and on my iPhone 11 (ios 14.3)
The .resignKeyboardOnDragGesture() is an extension that removes the keyboard.
struct ResignKeyboardOnDragGesture: ViewModifier {
var gesture = DragGesture().onChanged {_ in
UIApplication.shared.endEditing(true)
}
func body(content: Content) -> some View {
content.gesture(gesture)
}
}
extension View {
func resignKeyboardOnDragGesture() -> some View {
return modifier(ResignKeyboardOnDragGesture())
}
}
I know this is very late but if you still have this issues.
Add the following to your view:
.ignoresSafeArea(.keyboard)
This should no longer happen.

How to make a button (or any other element) show SwiftUI's DatePicker popup on tap?

I'm trying to achieve the simplest possible use case, but I can't figure it out.
I have a picture of calendar. All I want is to show DatePicker popup when tapping the picture.
I tried to put it inside ZStack, but by doing it I can't hide default data textfields:
ZStack {
Image("icon-calendar")
.zIndex(1)
DatePicker("", selection: $date)
.zIndex(2)
}
How to make this simple layout natively without ridiculous workarounds?
I'm having this problem too. I couldn't sleep for a few days thinking about the solution. I have googled hundred times and finally, I found a way to achieve this. It's 1:50 AM in my timezone, I can sleep happily now. Credit goes to chase's answer here
Demo here: https://media.giphy.com/media/2ILs7PZbdriaTsxU0s/giphy.gif
The code that does the magic
struct ContentView: View {
#State var date = Date()
var body: some View {
ZStack {
DatePicker("label", selection: $date, displayedComponents: [.date])
.datePickerStyle(CompactDatePickerStyle())
.labelsHidden()
Image(systemName: "calendar")
.resizable()
.frame(width: 32, height: 32, alignment: .center)
.userInteractionDisabled()
}
}
}
struct NoHitTesting: ViewModifier {
func body(content: Content) -> some View {
SwiftUIWrapper { content }.allowsHitTesting(false)
}
}
extension View {
func userInteractionDisabled() -> some View {
self.modifier(NoHitTesting())
}
}
struct SwiftUIWrapper<T: View>: UIViewControllerRepresentable {
let content: () -> T
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: content())
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {}
}
Tried using Hieu's solution in a navigation bar item but it was breaking. Modified it by directly using SwiftUIWrapper and allowsHitTesting on the component I want to display and it works like a charm.
Also works on List and Form
struct StealthDatePicker: View {
#State private var date = Date()
var body: some View {
ZStack {
DatePicker("", selection: $date, in: ...Date(), displayedComponents: .date)
.datePickerStyle(.compact)
.labelsHidden()
SwiftUIWrapper {
Image(systemName: "calendar")
.resizable()
.frame(width: 32, height: 32, alignment: .topLeading)
}.allowsHitTesting(false)
}
}
}
struct SwiftUIWrapper<T: View>: UIViewControllerRepresentable {
let content: () -> T
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: content())
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {}
}
My answer to this was much simpler... just create a button with a popover that calls this struct I created...
struct DatePopover: View {
#Binding var dateIn: Date
#Binding var isShowing: Bool
var body: some View {
VStack {
DatePicker("", selection: $dateIn, displayedComponents: [.date])
.datePickerStyle(.graphical)
.onChange(of: dateIn, perform: { value in
isShowing.toggle()
})
.padding(.all, 20)
}.frame(width: 400, height: 400, alignment: .center)
}
}
Not sure why, but it didn't format my code like I wanted...
( Original asnwer had button, onChange is better solution)
Sample of my Button that calls it... it has my vars in it and may not make complete sense to you, but it should give you the idea and use in the popover...
Button(item.dueDate == nil ? "" : dateValue(item.dueDate!)) {
if item.dueDate != nil { isUpdatingDate = true }
}
.onAppear { tmpDueDate = item.dueDate ?? .now }
.onChange(of: isUpdatingDate, perform: { value in
if !value {
item.dueDate = tmpDueDate
try? moc.save()
}
})
.popover(isPresented: $isUpdatingDate) {
DatePopover(dateIn: $tmpDueDate, isShowing: $isUpdatingDate)
}
FYI, dateValue() is a local func I created - it simply creates a string representation of the Date in my format
struct ZCalendar: View {
#State var date = Date()
#State var isPickerVisible = false
var body: some View {
ZStack {
Button(action: {
isPickerVisible = true
}, label: {
Image(systemName: "calendar")
}).zIndex(1)
if isPickerVisible{
VStack{
Button("Done", action: {
isPickerVisible = false
}).padding()
DatePicker("", selection: $date).datePickerStyle(GraphicalDatePickerStyle())
}.background(Color(UIColor.secondarySystemBackground))
.zIndex(2)
}
}//Another way
//.sheet(isPresented: $isPickerVisible, content: {DatePicker("", selection: $date).datePickerStyle(GraphicalDatePickerStyle())})
}
}
For those still looking for a simple solution, I was looking for something similar and found a great example of how to do this in one of Kavasoft's tutorials on YouTube at 20:32 into the video.
This is what he used:
import SwiftUI
struct DatePickerView: View {
#State private var birthday = Date()
#State private var isChild = false
#State private var ageFilter = ""
var body: some View {
Image(systemName: "calendar")
.font(.title3)
.overlay{ //MARK: Place the DatePicker in the overlay extension
DatePicker(
"",
selection: $birthday,
displayedComponents: [.date]
)
.blendMode(.destinationOver) //MARK: use this extension to keep the clickable functionality
.onChange(of: birthday, perform: { value in
isChild = checkAge(date:birthday)
})
}
}
//MARK: I added this function to show onChange functionality remains the same
func checkAge(date: Date) -> Bool {
let today = Date()
let diffs = Calendar.current.dateComponents([.year], from: date, to: today)
let formatter = DateComponentsFormatter()
let outputString = formatter.string(from: diffs)
self.ageFilter = outputString!.filter("0123456789.".contains)
let ageTest = Int(self.ageFilter) ?? 0
if ageTest > 18 {
return false
}else{
return true
}
}
}
The key is put the DatePicker in an overlay under the Image. Once done, the .blendmode extension needs to be set to .desintationOver for it to be clickable. I added a simple check age function to show onChange functionality remains the same when using it in this way.
I tested this code in Xcode 14 (SwiftUI 4.0 and IOS 16).
I hope this helps others!
Demo
Please understand that my sentence is weird because I am not good at English.
In the code above, if you use .frame() & .clipped().
Clicks can be controlled exactly by the icon size.
In the code above, I modified it really a little bit.
I found the answer.
Thank you.
import SwiftUI
struct DatePickerView: View {
#State private var date = Date()
var body: some View {
ZStack{
DatePicker("", selection: $date, displayedComponents: .date)
.labelsHidden()
.datePickerStyle(.compact)
.frame(width: 20, height: 20)
.clipped()
SwiftUIWrapper {
Image(systemName: "calendar")
.resizable()
.frame(width: 20, height: 20, alignment: .topLeading)
}.allowsHitTesting(false)
}//ZStack
}
}
struct DatePickerView_Previews: PreviewProvider {
static var previews: some View {
DatePickerView()
}
}
struct SwiftUIWrapper<T: View>: UIViewControllerRepresentable {
let content: () -> T
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: content())
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {}
}

SwiftUI - NavigationLink cell in a Form stays highlighted after detail pop

In iOS 14, it appears that NavigationLinks do not become deselected after returning in a Form context.
This is also true for Form Pickers and anything else that causes the presentation of another View from a list (giving a highlight context to the presenting cell).
I didn't notice this behaviour in iOS 13.
Is there a way to 'deselect' the highlighted row once the other view is dismissed?
Example code:
struct ContentView: View {
var body: some View {
Form {
NavigationLink(destination: Text("Detail")) {
Text("Link")
}
}
}
}
(Different) Example visual:
In my case this behaviour appeared when using any Viewcontent (e.g. Text(), Image(), ...) between my NavigationView and List/Form.
var body: some View {
NavigationView {
VStack {
Text("This text DOES make problems.")
List {
NavigationLink(destination: Text("Doesn't work correct")) {
Text("Doesn't work correct")
}
}
}
}
}
Putting the Text() beneath the List does not make any problems:
var body: some View {
NavigationView {
VStack {
List {
NavigationLink(destination: Text("Does work correct")) {
Text("Does work correct")
}
}
Text("This text doesn't make problems.")
}
}
}
This is definitely a XCode 12 bug. As more people report this, as earlier it gets resolved.
I have also run into this issue and believed I found the root cause in my case.
In my case I had.a structure like the following:
struct Page1View: View {
var body: some View {
NavigationView {
List {
NavigationLink("Page 2", destination: Page2View())
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 1")
}
}
}
struct Page2View: View {
var body: some View {
List {
NavigationLink("Page 3", destination: Text("Page 3"))
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 2")
}
}
This issue would occur on the NavigationLink to Page 3. In the console output this error was showing when that link was used:
2021-02-13 16:41:00.599844+0000 App[59157:254215] [Assert] displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract.
I discovered that I needed to apply .navigationViewStyle(StackNavigationViewStyle()) to the NavigationView and this solved the problem.
I.e.
struct Page1View: View {
var body: some View {
NavigationView {
List {
NavigationLink("Page 2", destination: Page2View())
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 1")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Been fighting this issue half day today and came to this post that helped me to understand that issue appears if Text, Button or something else placed between NavigationView and in my case List. And I found solution that worked for me. Just add .zIndex() for the item. .zIndex() must be higher than for List Tried with Xcode 12.5.
var body: some View {
NavigationView {
VStack {
Text("This text DOES make problems.")
.zIndex(1.0)
List {
NavigationLink(destination: Text("Doesn't work correct")) {
Text("Doesn't work correct")
}
}
}
}
}
I did a bit more tinkering, it turns out this was caused due by having the UIHostingController being nested in a UINavigationController and using that navigation controller. Changing the navigation stack to use a SwiftUI NavigationView instead resolved this issue.
Similar to what #pawello2222 says in the question comments, I think the underlying cause is something to do with SwiftUI not understanding the proper navigation hierarchy when the external UINavigationController is used.
This is just one instance where this is fixed though, I'm still experiencing the issue in various other contexts depending on how my view is structured.
I've submitted an issue report FB8705430 to Apple, so hopefully this is fixed sometime soon.
Before (broken):
struct ContentView: View {
var body: some View {
Form {
NavigationLink(destination: Text("test")) {
Text("test")
}
}
}
}
// (UIKit presentation context)
let view = ContentView()
let host = UIHostingController(rootView: view)
let nav = UINavigationController(rootViewController: host)
present(nav, animated: true, completion: nil)
After (working):
struct ContentView: View {
var body: some View {
NavigationView {
Form {
NavigationLink(destination: Text("test")) {
Text("test")
}
}
}
}
}
// (UIKit presentation context)
let view = ContentView()
let host = UIHostingController(rootView: view)
present(host, animated: true, completion: nil)
This is definitely a bug in List, for now, my work-around is refreshing the List by changing the id, like this:
struct YourView: View {
#State private var selectedItem: String?
#State private var listViewId = UUID()
var body: some View {
List(items, id: \.id) {
NavigationLink(destination: Text($0.id),
tag: $0.id,
selection: $selectedItem) {
Text("Row \($0.id)")
}
}
.id(listViewId)
.onAppear {
if selectedItem != nil {
selectedItem = nil
listViewId = UUID()
}
}
}
}
I made a modifier based on this that you can use:
struct RefreshOnAppearModifier<Tag: Hashable>: ViewModifier {
#State private var viewId = UUID()
#Binding var selection: Tag?
func body(content: Content) -> some View {
content
.id(viewId)
.onAppear {
if selection != nil {
viewId = UUID()
selection = nil
}
}
}
}
extension View {
func refreshOnAppear<Tag: Hashable>(selection: Binding<Tag?>? = nil) -> some View {
modifier(RefreshOnAppearModifier(selection: selection ?? .constant(nil)))
}
}
use it like this:
List { ... }
.refreshOnAppear(selection: $selectedItem)
I managed to solve it by adding ids to the different components of the list, using binding and resetting the binding on .onDisappear
struct ContentView: View {
#State var selection: String? = nil
var body: some View {
NavigationView {
VStack {
Text("Hello, world!")
.padding()
List {
Section {
NavigationLink( destination: Text("Subscreen1"), tag: "link1", selection: $selection ) {
Text("Subscreen1")
}.onDisappear {
self.selection = nil
}
NavigationLink( destination: Text("Subscreen2"), tag: "link2", selection: $selection ) {
Text("Subscreen2")
}.onDisappear {
self.selection = nil
}
}.id("idSection1")
}
.id("idList")
}
}
}
}
I've also run into this issue and it seemed related to sheets as mentioned here.
My solution was to swizzle UITableView catch selections, and deselect the cell. The code for doing so is here. Hopefully this will be fixed in future iOS.
Adding .navigationViewStyle(StackNavigationViewStyle()) to NavigationView fixed it for me.
Suggested in this thread: https://developer.apple.com/forums/thread/660468
This is my solution to this issue.
// This in a stack in front of list, disables large navigation title from collapsing by disallowing list from scrolling on top of navigation title
public struct PreventCollapseView: View {
#State public var viewColor: Color?
public init(color: Color? = nil) {
self.viewColor = color
}
public var body: some View {
Rectangle()
.fill(viewColor ?? Color(UIColor(white: 0.0, alpha: 0.0005)))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 1)
}
}
// handy modifier..
extension List {
public func uncollapsing(_ viewUpdater: Bool) -> some View {
VStack(spacing: 0.0) {
Group {
PreventCollapseView()
self
}.id(viewUpdater)
}
}
}
struct TestView: View {
#State var updater: Bool = false
var body: some View {
List {
Text("Item one")
Text("Item two")
Text("Manually refresh")
.onTapGesture { DispatchQueue.main.async { updater.toggle() } }
.onAppear { print("List was refreshed") }
}
.uncollapsing(updater)
.clipped()
.onAppear { DispatchQueue.main.async { updater.toggle() }} // Manually refreshes list always when re-appearing/appearing
}
}
Add a NavigationView, configure for largeTitle, and embed TestView and it's all set. Toggle updater to refresh.
Having the same Problem. The weird thing is, that the exact same code worked in iOS13.
I'm having this issue with a simple list:
struct TestList: View {
let someArray = ["one", "two", "three", "four", "five"]
var body: some View {
List(someArray, id: \.self) { item in
NavigationLink(
destination: Text(item)) {
Text(item)
}.buttonStyle(PlainButtonStyle())
}.navigationBarTitle("testlist")
}
}
This is embedded in:
struct ListControllerView: View {
#State private var listPicker = 0
var body: some View {
NavigationView{
Group{
VStack{
Picker(selection: $listPicker, label: Text("Detailoverview")) {
Text("foo").tag(0)
Text("bar").tag(1)
Text("TestList").tag(2)
}
This is inside a Tabbar.
This is the workaround I've been using until this List issue gets fixed. Using the Introspect library, I save the List's UITableView.reloadData method and call it when it appears again.
import SwiftUI
import Introspect
struct MyView: View {
#State var reload: (() -> Void)? = nil
var body: some View {
NavigationView {
List {
NavigationLink("Next", destination: Text("Hello"))
}.introspectTableView { tv in
self.reload = tv.reloadData
}.onAppear {
self.reload?()
}
}
}
}

SwiftUI - dismissing keyboard on tapping anywhere in the view - issues with other interactive elements

I have a TextField and some actionable elements like Button, Picker inside a view. I want to dismiss the keyboard when the use taps outside the TextField. Using the answers in this question, I achieved it. However the problem comes with other actionable items.
When I tap a Button, the action takes place but the keyboard is not dismissed. Same with a Toggle switch.
When I tap on one section of a SegmentedStyle Picker, the keyboard is dimissed but the picker selection doesn't change.
Here is my code.
struct SampleView: View {
#State var selected = 0
#State var textFieldValue = ""
var body: some View {
VStack(spacing: 16) {
TextField("Enter your name", text: $textFieldValue)
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
Picker(selection: $selected, label: Text(""), content: {
Text("Word").tag(0)
Text("Phrase").tag(1)
Text("Sentence").tag(2)
}).pickerStyle(SegmentedPickerStyle())
Button(action: {
self.textFieldValue = "button tapped"
}, label: {
Text("Tap to change text")
})
}.padding()
.onTapGesture(perform: UIApplication.dismissKeyboard)
// .gesture(TapGesture().onEnded { _ in UIApplication.dismissKeyboard()})
}
}
public extension UIApplication {
static func dismissKeyboard() {
let keyWindow = shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(true)
}
}
As you can see in the code, I tried both options to get the tap gesture and nothing worked.
You can create an extension on View like so
extension View {
func endTextEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
and use it for the Views you want to dismiss the keyboard.
.onTapGesture {
self.endTextEditing()
}
I have just seen this solution in a recent raywenderlich tutorial so I assume it's currently the best solution.
Dismiss the keyboard by tapping anywhere (like others suggested) could lead to very hard to find bug (or unwanted behavior).
you loose default build-in TextField behaviors, like partial text
selection, copy, share etc.
onCommit is not called
I suggest you to think about gesture masking based on the editing state of your fields
/// Attaches `gesture` to `self` such that it has lower precedence
/// than gestures defined by `self`.
public func gesture<T>(_ gesture: T, including mask: GestureMask = .all) -> some View where T : Gesture
this help us to write
.gesture(TapGesture().onEnded({
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}), including: (editingFlag) ? .all : .none)
Tap on the modified View will dismiss the keyboard, but only if editingFlag == true. Don't apply it on TextField! Otherwise we are on the beginning of the story again :-)
This modifier will help us to solve the trouble with Picker but not with the Button. That is easy to solve while dismiss the keyboard from its own action handler. We don't have any other controls, so we almost done
Finally we have to find the solution for rest of the View, so tap anywhere (excluding our TextFields) dismiss the keyboard. Using ZStack filled with some transparent View is probably the easiest solution.
Let see all this in action (copy - paste - run in your Xcode simulator)
import SwiftUI
struct ContentView: View {
#State var selected = 0
#State var textFieldValue0 = ""
#State var textFieldValue1 = ""
#State var editingFlag = false
#State var message = ""
var body: some View {
ZStack {
// TODO: make it Color.clear istead yellow
Color.yellow.opacity(0.1).onTapGesture {
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}
VStack {
TextField("Salutation", text: $textFieldValue0, onEditingChanged: { editing in
self.editingFlag = editing
}, onCommit: {
self.onCommit(txt: "salutation commit")
})
.padding()
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
TextField("Welcome message", text: $textFieldValue1, onEditingChanged: { editing in
self.editingFlag = editing
}, onCommit: {
self.onCommit(txt: "message commit")
})
.padding()
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
Picker(selection: $selected, label: Text(""), content: {
Text("Word").tag(0)
Text("Phrase").tag(1)
Text("Sentence").tag(2)
})
.pickerStyle(SegmentedPickerStyle())
.gesture(TapGesture().onEnded({
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}), including: (editingFlag) ? .all : .none)
Button(action: {
self.textFieldValue0 = "Hi"
print("button pressed")
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}, label: {
Text("Tap to change salutation")
.padding()
.background(Color.yellow)
.cornerRadius(10)
})
Text(textFieldValue0)
Text(textFieldValue1)
Text(message).font(.largeTitle).foregroundColor(Color.red)
}
}
}
func onCommit(txt: String) {
print(txt)
self.message = [self.textFieldValue0, self.textFieldValue1].joined(separator: ", ").appending("!")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you miss onCommit (it is not called while tap outside TextField), just add it to your TextField onEditingChanged (it mimics typing Return on keyboard)
TextField("Salutation", text: $textFieldValue0, onEditingChanged: { editing in
self.editingFlag = editing
if !editing {
self.onCommit(txt: "salutation")
}
}, onCommit: {
self.onCommit(txt: "salutation commit")
})
.padding()
.background(Color(UIColor.secondarySystemFill))
.cornerRadius(4)
I'd like to take Mark T.s Answer even further and add the entire function to an extension for View:
extension View {
func hideKeyboardWhenTappedAround() -> some View {
return self.onTapGesture {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
}
Can then be called like:
var body: some View {
MyView()
// ...
.hideKeyboardWhenTappedAround()
// ...
}
#user3441734 is smart to enable the dismiss gesture only when needed. Rather than forcing every crevice of your forms to track state, you can:
Monitor UIWindow.keyboardWillShowNotification / willHide
Pass the current keyboard state via an EnvironmentKey set at the/a root view
Tested for iOS 14.5.
Attach dismiss gesture to the form
Form { }
.dismissKeyboardOnTap()
Setup monitor in root view
// Root view
.environment(\.keyboardIsShown, keyboardIsShown)
.onDisappear { dismantleKeyboarMonitors() }
.onAppear { setupKeyboardMonitors() }
// Monitors
#State private var keyboardIsShown = false
#State private var keyboardHideMonitor: AnyCancellable? = nil
#State private var keyboardShownMonitor: AnyCancellable? = nil
func setupKeyboardMonitors() {
keyboardShownMonitor = NotificationCenter.default
.publisher(for: UIWindow.keyboardWillShowNotification)
.sink { _ in if !keyboardIsShown { keyboardIsShown = true } }
keyboardHideMonitor = NotificationCenter.default
.publisher(for: UIWindow.keyboardWillHideNotification)
.sink { _ in if keyboardIsShown { keyboardIsShown = false } }
}
func dismantleKeyboarMonitors() {
keyboardHideMonitor?.cancel()
keyboardShownMonitor?.cancel()
}
SwiftUI Gesture + Sugar
struct HideKeyboardGestureModifier: ViewModifier {
#Environment(\.keyboardIsShown) var keyboardIsShown
func body(content: Content) -> some View {
content
.gesture(TapGesture().onEnded {
UIApplication.shared.resignCurrentResponder()
}, including: keyboardIsShown ? .all : .none)
}
}
extension UIApplication {
func resignCurrentResponder() {
sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
extension View {
/// Assigns a tap gesture that dismisses the first responder only when the keyboard is visible to the KeyboardIsShown EnvironmentKey
func dismissKeyboardOnTap() -> some View {
modifier(HideKeyboardGestureModifier())
}
/// Shortcut to close in a function call
func resignCurrentResponder() {
UIApplication.shared.resignCurrentResponder()
}
}
EnvironmentKey
extension EnvironmentValues {
var keyboardIsShown: Bool {
get { return self[KeyboardIsShownEVK] }
set { self[KeyboardIsShownEVK] = newValue }
}
}
private struct KeyboardIsShownEVK: EnvironmentKey {
static let defaultValue: Bool = false
}
You can set .allowsHitTesting(false) to your Picker to ignore the tap on your VStack
Apply this to root view
.onTapGesture {
UIApplication.shared.endEditing()
}

SwiftUI - Close Keyboard on Scroll

I have a simple search list:
struct ContentView: View {
#State var text:String = ""
var items = 1...100
var body: some View {
VStack {
List {
TextField("Search", text: $text)
Section{
ForEach(items.filter({"\($0)".contains(text)}),id: \.self){(i) in
Text("option \(i)")
}
}
}
}
}
}
How can I make the keyboard close when scrolling for more than 2 cells/few points?
If you are using a ScrollView (probably also with a List but I haven't confirmed it), you could use the UIScrollView appearance, this will affect all ScrollViews though.
UIScrollView.appearance().keyboardDismissMode = .onDrag
A thorough discussion on how to resign the keyboard with various answers can be found for this question.
One solution to resign the keyboard on a drag gesture in the list is using a method on UIApplication window as shown below. For easier handling I created an extension on UIApplication and view modifier for this extension and finally an extension to View:
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
struct ResignKeyboardOnDragGesture: ViewModifier {
var gesture = DragGesture().onChanged{_ in
UIApplication.shared.endEditing(true)
}
func body(content: Content) -> some View {
content.gesture(gesture)
}
}
extension View {
func resignKeyboardOnDragGesture() -> some View {
return modifier(ResignKeyboardOnDragGesture())
}
}
So the final modifier for resigning the keyboard is just one modifier that has to be placed on the list like this:
List {
ForEach(...) {
//...
}
}
.resignKeyboardOnDragGesture()
I have also implemented a pure swiftUI version of a search bar that might be interesting for you. You can find it in this answer.
As for now, since iOS 16 beta we have a new modifier scrollDismissesKeyboard() that allows to do exactly what you need.
In your example it should look like
struct ContentView: View {
#State var text: String = ""
var items = 1...100
var body: some View {
List {
TextField("Search", text: $text)
Section {
ForEach(items.filter({"\($0)".contains(text)}), id: \.self) { (i) in
Text("option \(i)")
}
}
}
.scrollDismissesKeyboard(.interactively) // <<-- Put this line
}
}
The scrollDismissesKeyboard() modifier has a parameter that determine the dismiss rules. Here are the possible values:
.automatic: Dismissing based on the context of the scroll.
.immediately: The keyboard will be dismissed as soon as any scroll happens.
.interactively: The keyboard will move/disappear inline with the user’s gesture.
.never: The keyboard will never dismissed when user is scrolling.
Form {
...
}.gesture(DragGesture().onChanged { _ in
UIApplication.shared.windows.forEach { $0.endEditing(false) }
})
#FocusState wrapper along with .focused() TextField modifier can be useful.
struct ContentView: View {
#FocusState private var focusedSearchField: Bool
#State var text:String = ""
var items = 1...100
var body: some View {
VStack {
List {
TextField("Search", text: $text)
.focused($focusedSearchField)
Section{
ForEach(items.filter({"\($0)".contains(text)}),id: \.self){(i) in
Text("option \(i)")
}
}
} // to also allow swipes on items (theoretically)
.simultaneousGesture(DragGesture().onChanged({ _ in
focusedSearchField = false
}))
.onTapGesture { // dissmis on tap as well
focusedSearchField = false
}
}
}
}
struct EndEditingKeyboardOnDragGesture: ViewModifier {
func body(content: Content) -> some View {
content.highPriorityGesture (
DragGesture().onChanged { _ in
UIApplication.shared.endEditing()
}
)
}
}
extension View {
func endEditingKeyboardOnDragGesture() -> some View {
return modifier(EndEditingKeyboardOnDragGesture())
}
}

Resources