20

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.

Any help appreciated :-)

Cortado-J
  • 2,035
  • 2
  • 20
  • 32

4 Answers4

18

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)
            }
      }
      )
    }
  }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 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. – Cortado-J Nov 22 '19 at 09:42
  • 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! – Cortado-J Nov 22 '19 at 09:59
  • 2
    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. – Asperi Nov 22 '19 at 10:31
  • 1
    Really useful comment - Just looked at a few applications with situations similar to the one I have and the behaviour is as you describe. – Cortado-J Nov 22 '19 at 15:43
  • 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. – Cortado-J Nov 22 '19 at 15:49
12

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)
}
Tobias Hesselink
  • 1,487
  • 9
  • 17
7

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())
    }
}
viedev
  • 869
  • 1
  • 6
  • 16
1

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
    }
}
ivankh
  • 11
  • 2