SwiftUI searchbar - ios

I'm trying to build an App which can display different pdfs via sheet. So far so good, but I want the user to be able to filter the Button which triggers the sheet modifier via a searchbar. This is what doesn't work for me. I tried different solutions from web and Stack Overflow, but none of them worked for me. I think I have to filter the Text from the Buttons which trigger the Boolean for sheet. But how? With a List or a ForEach I can't Buttons with every time different var/Bool.
Below is my Searchbar class, with just a List and filtered it works.
class SearchBar: NSObject, ObservableObject {
let searchController: UISearchController = UISearchController(searchResultsController: nil)
#Binding var text: String
let hide : Bool
let placeholder : String
let cancelButton : Bool
let autocapitalization : UITextAutocapitalizationType
init(text: Binding<String>, hide: Bool, placeholder: String, cancelButton: Bool, autocapitalization: UITextAutocapitalizationType) {
self._text = text
self.hide = hide
self.placeholder = placeholder
self.cancelButton = cancelButton
self.autocapitalization = autocapitalization
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
self.searchController.hidesNavigationBarDuringPresentation = hide
self.searchController.automaticallyShowsCancelButton = cancelButton
self.searchController.searchBar.placeholder = placeholder
self.searchController.searchBar.autocapitalizationType = autocapitalization
}
}
extension SearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}

My suggestion is to replace the Binding with an action which calls a closure and passes the entered text.
Replace the line #Binding var text: String with
var action : ((String) -> Void)?
Replace updateSearchResults with
func updateSearchResults(for searchController: UISearchController) {
if let searchBarText = searchController.searchBar.text {
DispatchQueue.main.async { self.action?(searchBarText) }
}
}
and the ViewModifier and the view extension with
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
let action : ((String) -> Void)?
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
searchBar.action = action
viewController.navigationItem.searchController = self.searchBar.searchController
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar, action : ((String) -> Void)?) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar, action: action))
}
}
In the view you can add the search bar with
.add(self.searchBar, action: { query in
// do something with query
})
Any time the user presses a key the closure is being called.
If you want to call the closure once when the user presses the Search/Go key you have to implement searchBarSearchButtonClicked and add another action.

Related

How to create a UIBarButtonItem in UIViewControllerRepresentable Struct?

I am trying to add a button in my custom navigation bar, Problem is when I am adding bar button item to the navigation bar it doesn't allow me to create an #objc method, to avoid it I created a different class for objc methods but that doesn't seem to work either, code accepts the selector method but doesn't trigger when tapped during runtime.
Plan is to launch a Side menu on the Tap of this button, The button is added and visible, but the action doesn't work, any work around or help will be greatly appreciated.
struct CustomNavigationBar: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
return CustomNavigationBar.Coordinator(parent: self)
}
// Properties
/// Ease of Use
var view : AnyView
var title : String //Will be location of the user
/// On Search and On Cancel Closures
var onSearch : (String) -> ()
var onCancel : () -> ()
var didTapMenu : () -> ()
// Require Closures on initialization
init(view: AnyView, title : String, onSearch : #escaping (String)->(), onCancel : #escaping () -> (), didTapMenu : #escaping () -> ()) {
self.view = view
self.title = title
self.onSearch = onSearch
self.onCancel = onCancel
self.didTapMenu = didTapMenu
}
// Integrating UIKit Navigation Controller with SwiftUI View
func makeUIViewController(context: Context) -> UINavigationController {
//Requires SwiftUIView
let childView = UIHostingController(rootView: view)
let controller = UINavigationController(rootViewController: childView)
// Navigation Bar Data
controller.navigationBar.topItem?.title = title
controller.navigationBar.prefersLargeTitles = false
// Navigation Bar Customization
controller.navigationBar.barTintColor = UIColor(Color.grubSoulRed)
controller.navigationBar.tintColor = UIColor.white
controller.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white, .font: UIFont(name: "Poppins-Bold", size: 17)!]
//Adding Navigation Button // ===> Problem Here
let menuButton = UIBarButtonItem(image: UIImage(named: "hamburgerMenu"), style: .plain, target: nil, action: #selector(navigationActions.hamburgerTapped(_:)))
childView.navigationItem.leftBarButtonItem = menuButton
// Search Bar
let searchController = UISearchController()
searchController.searchBar.placeholder = "Search here for products..."
/// Search Bar Customization
searchController.searchBar.searchTextField.backgroundColor = UIColor.white
searchController.searchBar.searchTextField.layer.masksToBounds = true
searchController.searchBar.searchTextField.layer.cornerRadius = 17
searchController.obscuresBackgroundDuringPresentation = true
/// Setting search bar delegate
searchController.searchBar.delegate = context.coordinator
/// Adding Search Bar
controller.navigationBar.topItem?.searchController = searchController
controller.navigationBar.topItem?.hidesSearchBarWhenScrolling = false
return controller
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
}
// Search Bar delegate
class Coordinator: NSObject, UISearchBarDelegate {
var parent : CustomNavigationBar
init(parent: CustomNavigationBar) {
self.parent = parent
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
self.parent.onSearch(searchText)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
self.parent.onCancel()
}
}}
open class navigationActions {
#objc func hamburgerTapped(_ sender : UIButton) {
print("hamburger tapped")
// Launch Hamburger menu
}}
No need to create a new class,
Declare hamburgerTapped function inside the Coordinator class and set the target as Coordinator class and access from the Coordinator
let menuButton = UIBarButtonItem(image: UIImage(named: "hamburgerMenu"), style: .plain, target: context.coordinator, action: #selector(context.coordinator.hamburgerTapped(_:))) // <--- Here!!
and inside the Coordinator class
// Search Bar delegate
class Coordinator: NSObject, UISearchBarDelegate {
// Other code
#objc func hamburgerTapped(_ sender : UIButton) { // <-- Here!!
print("hamburger tapped")
// Launch Hamburger menu
}
}

Add Custom UIMenuController in SwiftUI

I want to highlight some sentence inside the TextEditor by giving selection on the text, then I want a new custom UIMenuController but in Swift UI, I've followed some tutorial and it works in UIKit but it can't appear when I try to implement in inside swiftUI using UIRepresentable, any ideas to add that custom UIMenuController in SwiftUI?
class Coordinator: NSObject, UITextViewDelegate
{
var text: Binding<String>
var model: ScriptField_Model
init(_ text: Binding<String>, _ model: ScriptField_Model) {
self.text = text
self.model = model
}
func textViewDidChange(_ textView: UITextView) {
self.text.wrappedValue = textView.text
self.model.text = textView.text
}
func textViewDidChangeSelection(_ textView: UITextView) {
let menuItem1: UIMenuItem = UIMenuItem(title: "Menu 1", action: #selector(onMenu1(sender:)))
// Store MenuItem in array.
let myMenuItems: [UIMenuItem] = [menuItem1]
// Added MenuItem to MenuController.
UIMenuController.shared.menuItems = myMenuItems
UIMenuController.shared.hideMenu()
}
func textFieldShouldReturn(_ textView: UITextView) -> Bool {
return true
}
#objc internal func onMenu1(sender: UIMenuItem) {
print("onMenu1")
}
}
this is the part i try to add when select the sentence :
func textViewDidChangeSelection(_ textView: UITextView) {
let menuItem1: UIMenuItem = UIMenuItem(title: "Menu 1", action: #selector(onMenu1(sender:)))
// Store MenuItem in array.
let myMenuItems: [UIMenuItem] = [menuItem1]
// Added MenuItem to MenuController.
UIMenuController.shared.menuItems = myMenuItems
UIMenuController.shared.hideMenu()
}
Any Help and reference, i will appreciate it.. thankyouu
first create class like CVEDictTextView
import Foundation
import SwiftUI
class CVEDictTextView: UITextView {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
let newInstanceItem = UIMenuItem(title: "Lookup", action:#selector(lookup))
UIMenuController.shared.menuItems = [newInstanceItem]
UIMenuController.shared.hideMenu()
if(action == #selector(lookup)){
return true
}
return false
}
#objc func lookup() {
if(self.selectedRange.location != NSNotFound ){
let str = self.value(forKey: "text") as! String
var str2 = String(str.dropFirst(self.selectedRange.location))
str2 = String(str2.dropLast(str2.count - self.selectedRange.length))
print(str2)
}
}
}
and create TextView UIViewRepresentable
struct TextView: UIViewRepresentable {
#Binding var text: String
#Binding var textStyle: UIFont.TextStyle
func makeUIView(context: Context) -> UITextView {
let textView = CVEDictTextView()
textView.delegate = context.coordinator
textView.font = UIFont.preferredFont(forTextStyle: textStyle)
textView.autocapitalizationType = .sentences
textView.isSelectable = true
textView.isUserInteractionEnabled = true
textView.isEditable = false
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
}
func makeCoordinator() -> Coordinator {
Coordinator($text)
}
class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
init(_ text: Binding<String>) {
self.text = text
}
func textViewDidChange(_ textView: UITextView) {
self.text.wrappedValue = textView.text
}
}
}
in SwitfUI view
using it
#State private var message = "hello how are you \n\t are you ok? email : hung.phuoc.tran#gmail.com"
#State private var textStyle = UIFont.TextStyle.body
TextView(text: $message, textStyle: $textStyle)
.padding(.horizontal)

UIViewControllerRepresentable does not receive focus updates when in a Scrollview

I have a SwiftUI UIViewControllerRepresentable that I've embedded into a ScrollView (see below)
import SwiftUI
struct ContactView: View {
#State private var text = ""
var body: some View {
ScrollView{
TextFieldView(text: $text, dismissKeyboardCallback: nil)
}
}
}
When I tap the TextField nothing happens. If I replace the parent ScrollView with a VStack, the TextFieldRepresentable is able to receive focus as normal (a soft keyboard appears and I'm able to input data). Is this a bug, or am I doing something incorrect? The UIViewControllerRepresentable is included below if it helps at all:
import SwiftUI
struct TextFieldView: View {
var text: Binding<String>
var onDismissKeyboard: (() -> Void)?
var body: some View {
TextFieldRepresentable(text: text, dismissKeyboardCallback: self.onDismissKeyboard)
.frame(height: 32, alignment: .leading)
}
}
struct TextFieldRepresentable: UIViewControllerRepresentable {
let dismissKeyboardCallback: (() -> Void)?
let viewController: TextFieldViewController
init (text: Binding<String>, dismissKeyboardCallback: (() -> Void)?) {
self.dismissKeyboardCallback = dismissKeyboardCallback
self.viewController = TextFieldViewController(text: text, onDismiss: dismissKeyboardCallback)
}
func makeUIViewController(context: Context) -> UIViewController {
return viewController
}
func updateUIViewController(_ viewController: UIViewController, context: Context) {}
}
And the TextFieldViewController itself:
class TextFieldViewController: UIViewController, UITextFieldDelegate {
let text: Binding<String>?
let onDismiss: (() -> Void)?
init (text: Binding<String>, onDismiss: (() -> Void)?) {
self.text = text
self.onDismiss = onDismiss
super.init( nibName: "TextField", bundle: Bundle.main)
}
required init?(coder: NSCoder) {
self.text = nil
self.onDismiss = nil
super.init(coder: coder)
}
fileprivate func getTextField() -> UITextField? {
return view.subviews.first as? UITextField
}
override func viewDidLoad() {
let textField = self.getTextField()
textField?.delegate = self
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
toolbar.barStyle = UIBarStyle.default
toolbar.items = [UIBarButtonItem(title: "Done", style: UIBarButtonItem.Style.done, target: self, action: #selector(self.onSet))]
textField?.inputAccessoryView = toolbar
}
#objc private func onSet() {
let textField = self.getTextField()
textField?.resignFirstResponder()
self.text?.wrappedValue = textField?.text ?? ""
self.onDismiss?()
}
}

How do I allow text selection on a Text label in SwiftUI?

When I create a text view:
Text("Hello World")
I can't allow the user to select text when they long press.
I've looked at using a TextField but that doesn't seem to allow for turning off text editing.
I just want to be able to display a body of text and allow the user to highlight a selection using the system text selector.
Thanks!
iOS 15.0+, macOS 12.0+, Mac Catalyst 15.0+
As of Xcode 13.0 beta 2 you can use
Text("Selectable text")
.textSelection(.enabled)
Text("Non selectable text")
.textSelection(.disabled)
// applying `textSelection` to a container
// enables text selection for all `Text` views inside it
VStack {
Text("Selectable text1")
Text("Selectable text2")
// disable selection only for this `Text` view
Text("Non selectable text")
.textSelection(.disabled)
}.textSelection(.enabled)
See also the textSelection Documentation.
iOS 14 and lower
Using TextField("", text: .constant("Some text")) has two problems:
Minor: The cursor shows up when selecting
Mayor: When a user selects some text he can tap in the context menu cut, paste and other items which can change the text regardless of using .constant(...)
My solution to this problem involves subclassing UITextField and using UIViewRepresentable to bridge between UIKit and SwiftUI.
At the end I provide the full code to copy and paste into a playground in Xcode 11.3 on macOS 10.14
Subclassing the UITextField:
/// This subclass is needed since we want to customize the cursor and the context menu
class CustomUITextField: UITextField, UITextFieldDelegate {
/// (Not used for this workaround, see below for the full code) Binding from the `CustomTextField` so changes of the text can be observed by `SwiftUI`
fileprivate var _textBinding: Binding<String>!
/// If it is `true` the text field behaves normally.
/// If it is `false` the text cannot be modified only selected, copied and so on.
fileprivate var _isEditable = true {
didSet {
// set the input view so the keyboard does not show up if it is edited
self.inputView = self._isEditable ? nil : UIView()
// do not show autocorrection if it is not editable
self.autocorrectionType = self._isEditable ? .default : .no
}
}
// change the cursor to have zero size
override func caretRect(for position: UITextPosition) -> CGRect {
return self._isEditable ? super.caretRect(for: position) : .zero
}
// override this method to customize the displayed items of 'UIMenuController' (the context menu when selecting text)
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
// disable 'cut', 'delete', 'paste','_promptForReplace:'
// if it is not editable
if (!_isEditable) {
switch action {
case #selector(cut(_:)),
#selector(delete(_:)),
#selector(paste(_:)):
return false
default:
// do not show 'Replace...' which can also replace text
// Note: This selector is private and may change
if (action == Selector("_promptForReplace:")) {
return false
}
}
}
return super.canPerformAction(action, withSender: sender)
}
// === UITextFieldDelegate methods
func textFieldDidChangeSelection(_ textField: UITextField) {
// update the text of the binding
self._textBinding.wrappedValue = textField.text ?? ""
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Allow changing the text depending on `self._isEditable`
return self._isEditable
}
}
Using UIViewRepresentable to implement SelectableText
struct SelectableText: UIViewRepresentable {
private var text: String
private var selectable: Bool
init(_ text: String, selectable: Bool = true) {
self.text = text
self.selectable = selectable
}
func makeUIView(context: Context) -> CustomUITextField {
let textField = CustomUITextField(frame: .zero)
textField.delegate = textField
textField.text = self.text
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return textField
}
func updateUIView(_ uiView: CustomUITextField, context: Context) {
uiView.text = self.text
uiView._textBinding = .constant(self.text)
uiView._isEditable = false
uiView.isEnabled = self.selectable
}
func selectable(_ selectable: Bool) -> SelectableText {
return SelectableText(self.text, selectable: selectable)
}
}
The full code
In the full code below I also implemented a CustomTextField where editing can be turned off but still be selectable.
Playground view
Code
import PlaygroundSupport
import SwiftUI
/// This subclass is needed since we want to customize the cursor and the context menu
class CustomUITextField: UITextField, UITextFieldDelegate {
/// Binding from the `CustomTextField` so changes of the text can be observed by `SwiftUI`
fileprivate var _textBinding: Binding<String>!
/// If it is `true` the text field behaves normally.
/// If it is `false` the text cannot be modified only selected, copied and so on.
fileprivate var _isEditable = true {
didSet {
// set the input view so the keyboard does not show up if it is edited
self.inputView = self._isEditable ? nil : UIView()
// do not show autocorrection if it is not editable
self.autocorrectionType = self._isEditable ? .default : .no
}
}
// change the cursor to have zero size
override func caretRect(for position: UITextPosition) -> CGRect {
return self._isEditable ? super.caretRect(for: position) : .zero
}
// override this method to customize the displayed items of 'UIMenuController' (the context menu when selecting text)
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
// disable 'cut', 'delete', 'paste','_promptForReplace:'
// if it is not editable
if (!_isEditable) {
switch action {
case #selector(cut(_:)),
#selector(delete(_:)),
#selector(paste(_:)):
return false
default:
// do not show 'Replace...' which can also replace text
// Note: This selector is private and may change
if (action == Selector("_promptForReplace:")) {
return false
}
}
}
return super.canPerformAction(action, withSender: sender)
}
// === UITextFieldDelegate methods
func textFieldDidChangeSelection(_ textField: UITextField) {
// update the text of the binding
self._textBinding.wrappedValue = textField.text ?? ""
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Allow changing the text depending on `self._isEditable`
return self._isEditable
}
}
struct CustomTextField: UIViewRepresentable {
#Binding private var text: String
private var isEditable: Bool
init(text: Binding<String>, isEditable: Bool = true) {
self._text = text
self.isEditable = isEditable
}
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> CustomUITextField {
let textField = CustomUITextField(frame: .zero)
textField.delegate = textField
textField.text = self.text
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
return textField
}
func updateUIView(_ uiView: CustomUITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = self.text
uiView._textBinding = self.$text
uiView._isEditable = self.isEditable
}
func isEditable(editable: Bool) -> CustomTextField {
return CustomTextField(text: self.$text, isEditable: editable)
}
}
struct SelectableText: UIViewRepresentable {
private var text: String
private var selectable: Bool
init(_ text: String, selectable: Bool = true) {
self.text = text
self.selectable = selectable
}
func makeUIView(context: Context) -> CustomUITextField {
let textField = CustomUITextField(frame: .zero)
textField.delegate = textField
textField.text = self.text
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return textField
}
func updateUIView(_ uiView: CustomUITextField, context: Context) {
uiView.text = self.text
uiView._textBinding = .constant(self.text)
uiView._isEditable = false
uiView.isEnabled = self.selectable
}
func selectable(_ selectable: Bool) -> SelectableText {
return SelectableText(self.text, selectable: selectable)
}
}
struct TextTestView: View {
#State private var selectableText = true
var body: some View {
VStack {
// Even though the text should be constant, it is not because the user can select and e.g. 'cut' the text
TextField("", text: .constant("Test SwiftUI TextField"))
.background(Color(red: 0.5, green: 0.5, blue: 1))
// This view behaves like the `SelectableText` however the layout behaves like a `TextField`
CustomTextField(text: .constant("Test `CustomTextField`"))
.isEditable(editable: false)
.background(Color.green)
// A non selectable normal `Text`
Text("Test SwiftUI `Text`")
.background(Color.red)
// A selectable `text` where the selection ability can be changed by the button below
SelectableText("Test `SelectableText` maybe selectable")
.selectable(self.selectableText)
.background(Color.orange)
Button(action: {
self.selectableText.toggle()
}) {
Text("`SelectableText` can be selected: \(self.selectableText.description)")
}
// A selectable `text` which cannot be changed
SelectableText("Test `SelectableText` always selectable")
.background(Color.yellow)
}.padding()
}
}
let viewController = UIHostingController(rootView: TextTestView())
viewController.view.frame = CGRect(x: 0, y: 0, width: 400, height: 200)
PlaygroundPage.current.liveView = viewController.view
A simple workaround solution I found is to just use context menus instead:
Text($someText)
.contextMenu(ContextMenu(menuItems: {
Button("Copy", action: {
UIPasteboard.general.string = someText
})
}))
I ran into a similar problem, where I wanted in essence to select the text without allowing editing. In my case, I wanted to show the UIMenuController when the text was tapped on, without allowing editing of the text or showing the cursor or keyboard. Building on the prior answers:
import SwiftUI
import UIKit
struct SelectableText: UIViewRepresentable {
var text: String
#Binding var isSelected: Bool
func makeUIView(context: Context) -> SelectableLabel {
let label = SelectableLabel()
label.textColor = .white
label.font = .systemFont(ofSize: 60, weight: .light)
label.minimumScaleFactor = 0.6
label.adjustsFontSizeToFitWidth = true
label.textAlignment = .right
label.numberOfLines = 1
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
label.text = text
return label
}
func updateUIView(_ uiView: SelectableLabel, context: Context) {
uiView.text = text
if isSelected {
uiView.showMenu()
} else {
let _ = uiView.resignFirstResponder()
}
}
}
class SelectableLabel: UILabel {
override var canBecomeFirstResponder: Bool {
return true
}
override init(frame: CGRect) {
super.init(frame: .zero)
highlightedTextColor = .gray
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
switch action {
case #selector(copy(_:)), #selector(paste(_:)), #selector(delete(_:)):
return true
default:
return super.canPerformAction(action, withSender: sender)
}
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = self.stringValue
}
override func paste(_ sender: Any?) {
guard let string = UIPasteboard.general.string else { return }
NotificationCenter.default.post(name: Notification.Name.Paste, object: nil, userInfo: [Keys.PastedString: string])
}
override func delete(_ sender: Any?) {
NotificationCenter.default.post(name: Notification.Name.Delete, object: nil)
}
override func resignFirstResponder() -> Bool {
isHighlighted = false
return super.resignFirstResponder()
}
public func showMenu() {
becomeFirstResponder()
isHighlighted = true
let menu = UIMenuController.shared
menu.showMenu(from: self, rect: bounds)
}
}
I use custom paste and delete notifications to message my model object, where the paste and delete actions are processed to update the display appropriately, which works for my purposes. Bindings could also be used.
To use:
SelectableText(text: text, isSelected: self.$isSelected)
.onTapGesture {
self.isSelected.toggle()
}
.onReceive(NotificationCenter.default.publisher(for: UIMenuController.willHideMenuNotification)) { _ in
self.isSelected = false
}
Or we can use something like this when we want show "copy" tooltip for text without allowing editing.
As benefit we will have possibility to using native view "Text" which gives us the opportunity using native methods ".font()", ".foregroundColor()" and etc. Also we can use it for group of views, for exapmple cell.
Building on the prior answers.
Playground code
import PlaygroundSupport
import SwiftUI
private class SelectableUIView: UIView {
var text: String?
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
func setup() {
self.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.showMenu)))
}
#objc func showMenu(_ recognizer: UILongPressGestureRecognizer) {
becomeFirstResponder()
let menu = UIMenuController.shared
if !menu.isMenuVisible {
menu.showMenu(from: self, rect: frame)
}
}
override func copy(_ sender: Any?) {
let board = UIPasteboard.general
board.string = text
UIMenuController.shared.hideMenu()
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(UIResponderStandardEditActions.copy)
}
}
struct SelectableView: UIViewRepresentable {
var text: String
func makeUIView(context: Context) -> UIView {
let view = SelectableUIView()
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
guard let view = uiView as? SelectableUIView else {
return
}
view.text = text
}
}
struct SelectableContainer<Content: View>: View {
private let content: () -> Content
private var text: String
public init(text: String, #ViewBuilder content: #escaping () -> Content) {
self.text = text
self.content = content
}
public var body: some View {
ZStack {
content()
SelectableView(text: text)
.layoutPriority(-1)
}
}
}
struct SelectableText: View {
private var text: String
public init(_ text: String) {
self.text = text
}
public var body: some View {
ZStack {
Text(text)
SelectableView(text: text)
.layoutPriority(-1)
}
}
}
struct TextTestView: View {
#State private var text = "text"
var body: some View {
VStack {
SelectableContainer(text: text) {
VStack(alignment: .leading) {
Text("Header")
.font(.body)
Text(text)
.background(Color.orange)
}
}
.background(Color.yellow)
SelectableText(text)
.background(Color.black)
.foregroundColor(.white)
.font(.largeTitle)
}.padding()
}
}
let viewController = UIHostingController(rootView: TextTestView())
viewController.view.frame = CGRect(x: 0, y: 0, width: 400, height: 200)
PlaygroundPage.current.liveView = viewController.view
Playground view
Playground view

How to hide keyboard when using SwiftUI?

How to hide keyboard using SwiftUI for below cases?
Case 1
I have TextField and I need to hide the keyboard when the user clicks the return button.
Case 2
I have TextField and I need to hide the keyboard when the user taps outside.
How I can do this using SwiftUI?
Note:
I have not asked a question regarding UITextField. I want to do it by using SwifUI.TextField.
You can force the first responder to resign by sending an action to the shared application:
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Now you can use this method to close the keyboard whenever you desire:
struct ContentView : View {
#State private var name: String = ""
var body: some View {
VStack {
Text("Hello \(name)")
TextField("Name...", text: self.$name) {
// Called when the user tap the return button
// see `onCommit` on TextField initializer.
UIApplication.shared.endEditing()
}
}
}
}
If you want to close the keyboard with a tap out, you can create a full screen white view with a tap action, that will trigger the endEditing(_:):
struct Background<Content: View>: View {
private var content: Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content()
}
var body: some View {
Color.white
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.overlay(content)
}
}
struct ContentView : View {
#State private var name: String = ""
var body: some View {
Background {
VStack {
Text("Hello \(self.name)")
TextField("Name...", text: self.$name) {
self.endEditing()
}
}
}.onTapGesture {
self.endEditing()
}
}
private func endEditing() {
UIApplication.shared.endEditing()
}
}
iOS 15+
(Done button above the keyboard)
Starting with iOS 15 we can now use #FocusState to control which field should be focused (see this answer to see more examples).
We can also add ToolbarItems directly above the keyboard.
When combined together, we can add a Done button right above the keyboard. Here is a simple demo:
struct ContentView: View {
private enum Field: Int, CaseIterable {
case username, password
}
#State private var username: String = ""
#State private var password: String = ""
#FocusState private var focusedField: Field?
var body: some View {
NavigationView {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .username)
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
}
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
}
}
}
iOS 14+
(Tap anywhere to hide the keyboard)
Here is an updated solution for SwiftUI 2 / iOS 14 (originally proposed here by Mikhail).
It doesn't use the AppDelegate nor the SceneDelegate which are missing if you use the SwiftUI lifecycle:
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
}
}
}
extension UIApplication {
func addTapGestureRecognizer() {
guard let window = windows.first else { return }
let tapGesture = UITapGestureRecognizer(target: window, action: #selector(UIView.endEditing))
tapGesture.requiresExclusiveTouchType = false
tapGesture.cancelsTouchesInView = false
tapGesture.delegate = self
window.addGestureRecognizer(tapGesture)
}
}
extension UIApplication: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true // set to `false` if you don't want to detect tap during other gestures
}
}
If you want to detect other gestures (not only tap gestures) you can use AnyGestureRecognizer as in Mikhail's answer:
let tapGesture = AnyGestureRecognizer(target: window, action: #selector(UIView.endEditing))
Here is an example how to detect simultaneous gestures except Long Press gestures:
extension UIApplication: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return !otherGestureRecognizer.isKind(of: UILongPressGestureRecognizer.self)
}
}
After a lot of attempts I found a solution that (currently) doesn't block any controls - adding gesture recognizer to UIWindow.
If you want to close keyboard only on Tap outside (without handling drags) - then it's enough to use just UITapGestureRecognizer and just copy step 3:
Create custom gesture recognizer class that works with any touches:
class AnyGestureRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if let touchedView = touches.first?.view, touchedView is UIControl {
state = .cancelled
} else if let touchedView = touches.first?.view as? UITextView, touchedView.isEditable {
state = .cancelled
} else {
state = .began
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
state = .ended
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
state = .cancelled
}
}
In SceneDelegate.swift in the func scene, add next code:
let tapGesture = AnyGestureRecognizer(target: window, action:#selector(UIView.endEditing))
tapGesture.requiresExclusiveTouchType = false
tapGesture.cancelsTouchesInView = false
tapGesture.delegate = self //I don't use window as delegate to minimize possible side effects
window?.addGestureRecognizer(tapGesture)
Implement UIGestureRecognizerDelegate to allow simultaneous touches.
extension SceneDelegate: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
Now any keyboard on any view will be closed on touch or drag outside.
P.S. If you want to close only specific TextFields - then add and remove gesture recognizer to the window whenever called callback of TextField onEditingChanged
I experienced this while using a TextField inside a NavigationView.
This is my solution for that. It will dismiss the keyboard when you start scrolling.
NavigationView {
Form {
Section {
TextField("Receipt amount", text: $receiptAmount)
.keyboardType(.decimalPad)
}
}
}
.gesture(DragGesture().onChanged{_ in UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)})
#RyanTCB's answer is good; here are a couple of refinements that make it simpler to use and avoid a potential crash:
struct DismissingKeyboard: ViewModifier {
func body(content: Content) -> some View {
content
.onTapGesture {
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(true)
}
}
}
The 'bug fix' is simply that keyWindow!.endEditing(true) properly should be keyWindow?.endEditing(true) (yes, you might argue it can't happen.)
More interesting is how you can use it. For example, suppose you have a form with multiple editable fields in it. Just wrap it like this:
Form {
.
.
.
}
.modifier(DismissingKeyboard())
Now, tapping on any control that itself doesn't present a keyboard will do the appropriate dismiss.
(Tested with beta 7)
I found another way to dismiss the keyboard that doesn't require accessing the keyWindow property; as a matter of fact the compiler gives back a warning using
UIApplication.shared.keyWindow?.endEditing(true)
'keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes
Instead I used this code:
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
Pure SwiftUI (iOS 15)
SwiftUI in iOS 15 (Xcode 13) gained native support for programmatic focus of TextField using new #FocusState property wrapper.
To dismiss the keyboard, simply set view's focusedField to nil. The return key will dismiss keyboard automatically (since iOS 14).
Docs: https://developer.apple.com/documentation/swiftui/focusstate/
struct MyView: View {
enum Field: Hashable {
case myField
}
#State private var text: String = ""
#FocusState private var focusedField: Field?
var body: some View {
TextField("Type here", text: $text)
.focused($focusedField, equals: .myField)
Button("Dismiss") {
focusedField = nil
}
}
}
Pure SwiftUI (iOS 14 and below)
You can completely avoid interaction with UIKit and implement it in pure SwiftUI. Just add an .id(<your id>) modifier to your TextField and change its value whenever you want to dismiss keyboard (on swipe, view tap, button action, ..).
Sample implementation:
struct MyView: View {
#State private var text: String = ""
#State private var textFieldId: String = UUID().uuidString
var body: some View {
VStack {
TextField("Type here", text: $text)
.id(textFieldId)
Spacer()
Button("Dismiss", action: { textFieldId = UUID().uuidString })
}
}
}
Note that I only tested it in latest Xcode 12 beta, but it should work with older versions (even Xcode 11) without any issue.
SwiftUI
in 'SceneDelegate.swift' file just add: .onTapGesture { window.endEditing(true)}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(
rootView: contentView.onTapGesture { window.endEditing(true)}
)
self.window = window
window.makeKeyAndVisible()
}
}
this is enough for each View using keyboard in your app...
My solution how to hide software keyboard when users tap outside.
You need to use contentShape with onLongPressGesture to detect the entire View container. onTapGesture required to avoid blocking focus on TextField. You can use onTapGesture instead of onLongPressGesture but NavigationBar items won't work.
extension View {
func endEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct KeyboardAvoiderDemo: View {
#State var text = ""
var body: some View {
VStack {
TextField("Demo", text: self.$text)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.onTapGesture {}
.onLongPressGesture(
pressing: { isPressed in if isPressed { self.endEditing() } },
perform: {})
}
}
Since iOS 15, you can use #FocusState
struct ContentView: View {
#Binding var text: String
private enum Field: Int {
case yourTextEdit
}
#FocusState private var focusedField: Field?
var body: some View {
VStack {
TextEditor(text: $speech.text.bound)
.padding(Edge.Set.horizontal, 18)
.focused($focusedField, equals: .yourTextEdit)
}.onTapGesture {
if (focusedField != nil) {
focusedField = nil
}
}
}
}
In iOS15 this is working flawlessly.
VStack {
// Some content
}
.onTapGesture {
// Hide Keyboard
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local).onEnded({ gesture in
// Hide keyboard on swipe down
if gesture.translation.height > 0 {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}))
Nothing else is required on your TextField and both swipe down along with tap will work to hide it. The way I use this is that on my master NavigationView I add this code and then everything below it will work. The only exception would be that any Sheet would need to have this appended to it as that is acting on a different state.
I prefer using the .onLongPressGesture(minimumDuration: 0), which does not cause the keyboard to blink when another TextView is activated (side effect of .onTapGesture). The hide keyboard code can be a reusable function.
.onTapGesture(count: 2){} // UI is unresponsive without this line. Why?
.onLongPressGesture(minimumDuration: 0, maximumDistance: 0, pressing: nil, perform: hide_keyboard)
func hide_keyboard()
{
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
add this modifier to the view you want to detect user taps
.onTapGesture {
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow!.endEditing(true)
}
Because keyWindow is deprecated.
extension View {
func endEditing(_ force: Bool) {
UIApplication.shared.windows.forEach { $0.endEditing(force)}
}
}
Expanding the answer by josefdolezal above, you can hide keyboard when user taps anywhere outside the textfield like below:
struct SwiftUIView: View {
#State private var textFieldId: String = UUID().uuidString // To hidekeyboard when tapped outside textFields
#State var fieldValue = ""
var body: some View {
VStack {
TextField("placeholder", text: $fieldValue)
.id(textFieldId)
.onTapGesture {} // So that outer tap gesture has no effect on field
// any more views
}
.onTapGesture { // whenever tapped within VStack
textFieldId = UUID().uuidString
//^ this will remake the textfields hence loosing keyboard focus!
}
}
}
Keyboard's Return Key
In addition to all answers about tapping outside of the textField, you may want to dismiss the keyboard when the user taps the return key on the keyboard:
define this global function:
func resignFirstResponder() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
And add use in onCommit argument it:
TextField("title", text: $text, onCommit: {
resignFirstResponder()
})
Benefits
You can call it from anywhere
It's not dependent on UIKit or SwiftUI (can be used in mac apps)
It works even in iOS 13
Demo
Updated the answer, working with Swift 5.7:
extension UIApplication {
func dismissKeyboard() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
and then using it where needed like for example as button action:
Button(action: {
// do stuff
UIApplication.shared.dismissKeyboard()
}, label: { Text("MyButton") })
Expanding on the answer by #Feldur (which was based on #RyanTCB's), here is an even more expressive and powerful solution allowing you to dismiss keyboard on other gestures than onTapGesture, you can specify which you want in the function call.
Usage
// MARK: - View
extension RestoreAccountInputMnemonicScreen: View {
var body: some View {
List(viewModel.inputWords) { inputMnemonicWord in
InputMnemonicCell(mnemonicInput: inputMnemonicWord)
}
.dismissKeyboard(on: [.tap, .drag])
}
}
Or using All.gestures (just sugar for Gestures.allCases 🍬)
.dismissKeyboard(on: All.gestures)
Code
enum All {
static let gestures = all(of: Gestures.self)
private static func all<CI>(of _: CI.Type) -> CI.AllCases where CI: CaseIterable {
return CI.allCases
}
}
enum Gestures: Hashable, CaseIterable {
case tap, longPress, drag, magnification, rotation
}
protocol ValueGesture: Gesture where Value: Equatable {
func onChanged(_ action: #escaping (Value) -> Void) -> _ChangedGesture<Self>
}
extension LongPressGesture: ValueGesture {}
extension DragGesture: ValueGesture {}
extension MagnificationGesture: ValueGesture {}
extension RotationGesture: ValueGesture {}
extension Gestures {
#discardableResult
func apply<V>(to view: V, perform voidAction: #escaping () -> Void) -> AnyView where V: View {
func highPrio<G>(
gesture: G
) -> AnyView where G: ValueGesture {
view.highPriorityGesture(
gesture.onChanged { value in
_ = value
voidAction()
}
).eraseToAny()
}
switch self {
case .tap:
// not `highPriorityGesture` since tapping is a common gesture, e.g. wanna allow users
// to easily tap on a TextField in another cell in the case of a list of TextFields / Form
return view.gesture(TapGesture().onEnded(voidAction)).eraseToAny()
case .longPress: return highPrio(gesture: LongPressGesture())
case .drag: return highPrio(gesture: DragGesture())
case .magnification: return highPrio(gesture: MagnificationGesture())
case .rotation: return highPrio(gesture: RotationGesture())
}
}
}
struct DismissingKeyboard: ViewModifier {
var gestures: [Gestures] = Gestures.allCases
dynamic func body(content: Content) -> some View {
let action = {
let forcing = true
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(forcing)
}
return gestures.reduce(content.eraseToAny()) { $1.apply(to: $0, perform: action) }
}
}
extension View {
dynamic func dismissKeyboard(on gestures: [Gestures] = Gestures.allCases) -> some View {
return ModifiedContent(content: self, modifier: DismissingKeyboard(gestures: gestures))
}
}
Word of caution
Please do note that if you use all gestures they might conflict and I did not come up with any neat solution solving that.
Please check https://github.com/michaelhenry/KeyboardAvoider
Just include KeyboardAvoider {} on top of your main view and that's all.
KeyboardAvoider {
VStack {
TextField()
TextField()
TextField()
TextField()
}
}
Well, the easiest solution for me is to simply use the library here.
SwiftUI support is somewhat limited, I use it by placing this code in the #main struct:
import IQKeyboardManagerSwift
#main
struct MyApp: App {
init(){
IQKeyboardManager.shared.enable = true
IQKeyboardManager.shared.shouldResignOnTouchOutside = true
}
...
}
This method allows you to hide the keyboard on spacers!
First add this function (Credit Given To: Casper Zandbergen, from SwiftUI can't tap in Spacer of HStack)
extension Spacer {
public func onTapGesture(count: Int = 1, perform action: #escaping () -> Void) -> some View {
ZStack {
Color.black.opacity(0.001).onTapGesture(count: count, perform: action)
self
}
}
}
Next add the following 2 functions (Credit Given To: rraphael, from this question)
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
The function below would be added to your View class, just refer to the top answer here from rraphael for more details.
private func endEditing() {
UIApplication.shared.endEditing()
}
Finally, you can now simply call...
Spacer().onTapGesture {
self.endEditing()
}
This will make any spacer area close the keyboard now. No need for a big white background view anymore!
You could hypothetically apply this technique of extension to any controls you need to support TapGestures that do not currently do so and call the onTapGesture function in combination with self.endEditing() to close the keyboard in any situation you desire.
Based on #Sajjon's answer, here is a solution allowing you to dismiss keyboard on tap, long press, drag, magnification and rotation gestures according to your choice.
This solution is working in XCode 11.4
Usage to get the behavior asked by #IMHiteshSurani
struct MyView: View {
#State var myText = ""
var body: some View {
VStack {
DismissingKeyboardSpacer()
HStack {
TextField("My Text", text: $myText)
Button("Return", action: {})
.dismissKeyboard(on: [.longPress])
}
DismissingKeyboardSpacer()
}
}
}
struct DismissingKeyboardSpacer: View {
var body: some View {
ZStack {
Color.black.opacity(0.0001)
Spacer()
}
.dismissKeyboard(on: Gestures.allCases)
}
}
Code
enum All {
static let gestures = all(of: Gestures.self)
private static func all<CI>(of _: CI.Type) -> CI.AllCases where CI: CaseIterable {
return CI.allCases
}
}
enum Gestures: Hashable, CaseIterable {
case tap, longPress, drag, magnification, rotation
}
protocol ValueGesture: Gesture where Value: Equatable {
func onChanged(_ action: #escaping (Value) -> Void) -> _ChangedGesture<Self>
}
extension LongPressGesture: ValueGesture {}
extension DragGesture: ValueGesture {}
extension MagnificationGesture: ValueGesture {}
extension RotationGesture: ValueGesture {}
extension Gestures {
#discardableResult
func apply<V>(to view: V, perform voidAction: #escaping () -> Void) -> AnyView where V: View {
func highPrio<G>(gesture: G) -> AnyView where G: ValueGesture {
AnyView(view.highPriorityGesture(
gesture.onChanged { _ in
voidAction()
}
))
}
switch self {
case .tap:
return AnyView(view.gesture(TapGesture().onEnded(voidAction)))
case .longPress:
return highPrio(gesture: LongPressGesture())
case .drag:
return highPrio(gesture: DragGesture())
case .magnification:
return highPrio(gesture: MagnificationGesture())
case .rotation:
return highPrio(gesture: RotationGesture())
}
}
}
struct DismissingKeyboard: ViewModifier {
var gestures: [Gestures] = Gestures.allCases
dynamic func body(content: Content) -> some View {
let action = {
let forcing = true
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
keyWindow?.endEditing(forcing)
}
return gestures.reduce(AnyView(content)) { $1.apply(to: $0, perform: action) }
}
}
extension View {
dynamic func dismissKeyboard(on gestures: [Gestures] = Gestures.allCases) -> some View {
return ModifiedContent(content: self, modifier: DismissingKeyboard(gestures: gestures))
}
}
Something I found that works very nice is
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Then add to the view struct:
private func endEditing() {
UIApplication.shared.endEditing()
}
Then
struct YourView: View {
var body: some View {
ParentView {
//...
}.contentShape(Rectangle()) //<---- This is key!
.onTapGesture {endEditing()}
}
}
So far above options did not work for me, because I have Form and inside buttons, links, picker ...
I create below code that is working, with help from above examples.
import Combine
import SwiftUI
private class KeyboardListener: ObservableObject {
#Published var keyabordIsShowing: Bool = false
var cancellable = Set<AnyCancellable>()
init() {
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.sink { [weak self ] _ in
self?.keyabordIsShowing = true
}
.store(in: &cancellable)
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.sink { [weak self ] _ in
self?.keyabordIsShowing = false
}
.store(in: &cancellable)
}
}
private struct DismissingKeyboard: ViewModifier {
#ObservedObject var keyboardListener = KeyboardListener()
fileprivate func body(content: Content) -> some View {
ZStack {
content
Rectangle()
.background(Color.clear)
.opacity(keyboardListener.keyabordIsShowing ? 0.01 : 0)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.onTapGesture {
let keyWindow = UIApplication.shared.connectedScenes
.filter({ $0.activationState == .foregroundActive })
.map({ $0 as? UIWindowScene })
.compactMap({ $0 })
.first?.windows
.filter({ $0.isKeyWindow }).first
keyWindow?.endEditing(true)
}
}
}
}
extension View {
func dismissingKeyboard() -> some View {
ModifiedContent(content: self, modifier: DismissingKeyboard())
}
}
Usage:
var body: some View {
NavigationView {
Form {
picker
button
textfield
text
}
.dismissingKeyboard()
Simple solution for clicking "outside" that worked for me:
First provide a ZStack before all views. In it, put a background (with the color of your choosing) and supply a tap Gesture. In the gesture call, invoke the 'sendAction' we've seen above:
import SwiftUI
struct MyView: View {
private var myBackgroundColor = Color.red
#State var text = "text..."
var body: some View {
ZStack {
self.myBackgroundColor.edgesIgnoringSafeArea(.all)
.onTapGesture(count: 1) {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
TextField("", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}
}
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
A cleaner SwiftUI-native way to dismiss the keyboard via tap without blocking any complicated forms or whatnot... credit to #user3441734 for flagging GestureMask as a clean approach.
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
}
True SwiftUI Solution
#State var dismissKeyboardToggle = false
var body: some View {
if dismissKeyboardToggle {
textfield
} else {
textfield
}
Button("Hide Keyboard") {
dismissKeyboardToggle.toggle()
}
}
this will work flawlessly
I am trying to hide keyboard while single tap & Picker should also work with single tap in SwiftUIForms.
I searched a lot to find a proper solution but didn't get any which works for me. So I make my own extension which works very well.
Use in your SwiftUI Form View:
var body: some View {
.onAppear { KeyboardManager.shared.setCurrentView(UIApplication.topViewController()?.view)
}
}
KeyboardManager Utility:
enum KeyboardNotificationType {
case show
case hide
}
typealias KeyBoardSizeBlock = ((CGSize?, UIView?, KeyboardNotificationType) -> Void)
class KeyboardManager: NSObject {
static let shared = KeyboardManager()
private weak var view: UIView?
var didReceiveKeyboardEvent: KeyBoardSizeBlock?
#objc public var shouldResignOnTouchOutside = true {
didSet {
resignFirstResponderGesture.isEnabled = shouldResignOnTouchOutside
}
}
#objc lazy public var resignFirstResponderGesture: UITapGestureRecognizer = {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissCurrentKeyboard))
tap.cancelsTouchesInView = false
tap.delegate = self
return tap
}()
private override init() {
super.init()
self.setup()
}
func setCurrentView(_ view: UIView?) {
self.view = view
resignFirstResponderGesture.isEnabled = true
if let view = self.view {
view.addGestureRecognizer(resignFirstResponderGesture)
}
}
private func setup() {
registerForKeyboardWillShowNotification()
registerForKeyboardWillHideNotification()
}
private func topViewHasCurrenView() -> Bool {
if view == nil { return false }
let currentView = UIApplication.topViewController()?.view
if currentView == view { return true }
for subview in UIApplication.topViewController()?.view.subviews ?? [] where subview == view {
return true
}
return false
}
#objc func dismissCurrentKeyboard() {
view?.endEditing(true)
}
func removeKeyboardObserver(_ observer: Any) {
NotificationCenter.default.removeObserver(observer)
}
private func findFirstResponderInViewHierarchy(_ view: UIView) -> UIView? {
for subView in view.subviews {
if subView.isFirstResponder {
return subView
} else {
let result = findFirstResponderInViewHierarchy(subView)
if result != nil {
return result
}
}
}
return nil
}
deinit {
removeKeyboardObserver(self)
}
}
// MARK: - Keyboard Notifications
extension KeyboardManager {
private func registerForKeyboardWillShowNotification() {
_ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: nil, using: { [weak self] notification -> Void in
guard let `self` = self else { return }
guard let userInfo = notification.userInfo else { return }
guard var kbRect = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue else { return }
kbRect.size.height -= self.view?.safeAreaInsets.bottom ?? 0.0
var mainResponder: UIView?
guard self.topViewHasCurrenView() else { return }
if let scrollView = self.view as? UIScrollView {
let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: kbRect.size.height, right: 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
guard let firstResponder = self.findFirstResponderInViewHierarchy(scrollView) else {
return
}
mainResponder = firstResponder
var aRect = scrollView.frame
aRect.size.height -= kbRect.size.height
if (!aRect.contains(firstResponder.frame.origin) ) {
scrollView.scrollRectToVisible(firstResponder.frame, animated: true)
}
} else if let tableView = self.view as? UITableView {
guard let firstResponder = self.findFirstResponderInViewHierarchy(tableView),
let pointInTable = firstResponder.superview?.convert(firstResponder.frame.origin, to: tableView) else {
return
}
mainResponder = firstResponder
var contentOffset = tableView.contentOffset
contentOffset.y = (pointInTable.y - (firstResponder.inputAccessoryView?.frame.size.height ?? 0)) - 10
tableView.setContentOffset(contentOffset, animated: true)
} else if let view = self.view {
guard let firstResponder = self.findFirstResponderInViewHierarchy(view) else {
return
}
mainResponder = firstResponder
var aRect = view.frame
aRect.size.height -= kbRect.size.height
if (!aRect.contains(firstResponder.frame.origin) ) {
UIView.animate(withDuration: 0.1) {
view.transform = CGAffineTransform(translationX: 0, y: -kbRect.size.height)
}
}
}
if let block = self.didReceiveKeyboardEvent {
block(kbRect.size, mainResponder, .show)
}
})
}
private func registerForKeyboardWillHideNotification() {
_ = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil, using: { [weak self] notification -> Void in
guard let `self` = self else { return }
guard let userInfo = notification.userInfo else { return }
guard let kbRect = (userInfo[UIResponder.keyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue else { return }
let contentInsets = UIEdgeInsets.zero
guard self.topViewHasCurrenView() else { return }
if let scrollView = self.view as? UIScrollView {
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
} else if let tableView = self.view as? UITableView {
tableView.contentInset = contentInsets
tableView.scrollIndicatorInsets = contentInsets
tableView.contentOffset = CGPoint(x: 0, y: 0)
} else if let view = self.view {
view.transform = CGAffineTransform(translationX: 0, y: 0)
}
if let block = self.didReceiveKeyboardEvent {
block(kbRect.size, nil, .hide)
}
})
}
}
//MARK: - UIGestureRecognizerDelegate
extension KeyboardManager: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view is UIControl ||
touch.view is UINavigationBar { return false }
return true
}
}
Answer from #Mikhail worked very well; it just has the issue that it cannot support dragging to select text within TextView - keyboard will close on tapping on the selected text. I extended his solution for AnyGesture below to provide better text editing user experience. (Answer from How to check for a UITextRangeView?)
Any recommendations to optimise the while loop?
class AnyGestureRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if let touchedView = touches.first?.view, touchedView is UIControl {
state = .cancelled
} else if let touchedView = touches.first?.view as? UITextView, touchedView.isEditable {
state = .cancelled
} else {
// Check if it is a subview of editable UITextView
if var touchedView = touches.first?.view {
while let superview = touchedView.superview {
if let view = superview as? UITextView, view.isEditable {
state = .cancelled
return
} else {
touchedView = superview
}
}
}
state = .began
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
state = .ended
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
state = .cancelled
}
}
extension UIView{
override open func touchesBegan(_ touches: Set, with event: UIEvent?) {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}

Resources