How can I remove Textfield focus when I press return or click outside Textfield?
Asked Answered
S

5

26

How can I remove Textfield focus when I press return or click outside Textfield? Note that this is SwiftUI on macOS.

If I do this:

import SwiftUI
    
struct ContentView: View {
    @State var field1: String = "This is the Text Field View"
      
    var body: some View {
        VStack{
          Button("Press") {
            print("Button Pressed")
          }
          
          TextField("Fill in Text", text: Binding(
            get: { print("get") ; return self.field1 },
            set: { print("set") ; self.field1 = $0 }
            )
          )
        }
    }
}

then click into the TextField and edit it then click on the Button the TextField does not lose focus. How can I make it exit editing mode and lose focus?

I would also like to lose focus from the TextField if I press Return. I used the Binding initialiser with get and set because I thought I could somehow intercept keypresses and detect the 'Return' character but this doesn't work.

Sung answered 22/11, 2019 at 4:29 Comment(0)
W
19

Here are possible variants

import SwiftUI
import AppKit

struct ContentView: View {
  @State var field1: String = "This is the Text Field View"

  var body: some View {
    VStack{
      Button("Press") {
        print("Button Pressed")
          NSApp.keyWindow?.makeFirstResponder(nil)
      }

      TextField("Fill in Text", text: Binding(
        get: { print("get") ; return self.field1 },
        set: { print("set") ; self.field1 = $0 }
        ), onCommit: {
            DispatchQueue.main.async {
                NSApp.keyWindow?.makeFirstResponder(nil)
            }
      }
      )
    }
  }
}
Widera answered 22/11, 2019 at 5:50 Comment(5)
Thanks Asperi. This does what I asked for. Oddly, it calls the onCommit twice - not sure why. Doesn't cause me a problem though. I realise now I need something additional to the question but will ask as another question.Sung
I decided to ask here as this extra bit was in the subject line of the original question: Is there a way to lose focus whenever a click/tap happens outside the TextField? Otherwise I'd have to add the makeFirstResponder bit to all other controls of which there are many!Sung
Actually it is default (aka normal) macOS behavior to put TextField in focus by default. So if there are no other focusable elements in the UI it will always be by default in focus. (Note, clicking button does not make button focusable). Thus moving focus out of text field is rather non-mac-like behavior.Widera
Really useful comment - Just looked at a few applications with situations similar to the one I have and the behaviour is as you describe.Sung
I find it interesting that it's easy to just use a UI without really seeing the logic of what is happening. If you had asked me whether clicking a button would take focus away from an edit I would have confidently said yes but I would have been wrong.Sung
T
16

Just add a onTapGesture to your VStack with the following line:

UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)

This code will close the keyboard.

Example:

VStack {
    // ...
}.onTapGesture {
    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
}
Towhead answered 22/11, 2019 at 15:2 Comment(3)
Thanks for your answer - however I think UIApplication is iOS only and I am on macOS.Sung
@Adahus Oh my bad. Just did some research but can't find anything about the focus removal on macOS. If I find anything soon, I'll let you know.Towhead
Still useful for those who end up here, looking for a solution for iOS. :)Salve
M
10

Based on the previous answers I have created a .removeFocusOnTap extension that can be attached to any view that should remove the focus when tapped (I use it on the background view). It works for both iOS and macOS.

public struct RemoveFocusOnTapModifier: ViewModifier {
    public func body(content: Content) -> some View {
        content
#if os (iOS)
            .onTapGesture {
                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
            }
#elseif os(macOS)
            .onTapGesture {
                DispatchQueue.main.async {
                    NSApp.keyWindow?.makeFirstResponder(nil)
                }
            }
#endif
    }
}

Extension:

extension View {
    public func removeFocusOnTap() -> some View {
        modifier(RemoveFocusOnTapModifier())
    }
}
Manicotti answered 27/4, 2022 at 9:36 Comment(0)
L
3

For macOS the good solution is to use another invisible text field. This should fix both TextField problems:

  1. Do not focus main text field on appear.
  2. Remove focus after editing completed.

Sample code:

struct YourView: View {
    @FocusState private var focus: Focus?

    var body: some View {
        TextField("", text: .constant(""))
            .focused($focus, equals: .some(.none))

        TextField("Some placeholder", text: .constant("Editable field"), onCommit: {
            focus = .some(.none)
        })
        .focused($focus, equals: .some(.main))
    }
}

private extension YourView {
    enum Focus: Hashable {
        case none
        case main
    }
}
Leann answered 13/6, 2023 at 15:51 Comment(0)
S
0

I will also post an answer here, as the original ones do not fit if you are working with transparency or EmptyView. With this, you can click anywhere outside of a TextField to remove focus.

This does:

  • Add a transparent view as responder on top
  • As transparent views have no contentShape by default, we assign one
ZStack {
    // Make a general clickable area available to respond to clicks outside of TextView
    Color.clear
        .contentShape(Rectangle())
        .edgesIgnoringSafeArea(.all)
        .onTapGesture {
            DispatchQueue.main.async {
                NSApp.keyWindow?.makeFirstResponder(nil)
            }
        }

    // Your ContentView() goes here
    }

Smolt answered 31/3 at 7:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.