SwiftUI TextField and voice dictation

Hi everyone,

I'm building an app with some TextField and I have some issues using the keyboard voice dictation.


I created a SingleView project with a single TextField in the ContentView.


struct ContentView: View {
    @State var text: String = ""

    var body: some View {
        TextField("Description", text: $text)
            .padding()
    }
}

When editing this TextField in the app, if I click on the mic button (in my keyboard), I only receive the first letter in the TextField.


Is it a SwiftUI bug ? Like a view refresh breaking the link between the @State var and the voice dictation content ?

Answered by cederacheBiB in 411597022

I solved this bug by using a TextField from UIKit in a UIViewRepresentable.


Here is my script of a focusable TextField :


import Combine
import SwiftUI

struct TextFieldWithFocus: UIViewRepresentable {
    class Coordinator: NSObject, UITextFieldDelegate {
        @Binding var text: String
        @Binding var isFirstResponder: Bool
        var didBecomeFirstResponder = false

        var onCommit: () -> Void

        init(text: Binding<String>, isFirstResponder: Binding<Bool>, onCommit: @escaping () -> Void) {
            _text = text
            _isFirstResponder = isFirstResponder
            self.onCommit = onCommit
        }

        func textFieldDidChangeSelection(_ textField: UITextField) {
            text = textField.text ?? ""
        }

        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            isFirstResponder = false
            didBecomeFirstResponder = false
            onCommit()
            return true
        }
        
        func textFieldDidEndEditing(_ textField: UITextField) {
            isFirstResponder = false
            didBecomeFirstResponder = false
        }
        
        func textFieldDidBeginEditing(_ textField: UITextField) {
            isFirstResponder = true
        }
    }

    @Binding var text: String
    var placeholder: String
    @Binding var isFirstResponder: Bool
    var textAlignment: NSTextAlignment = .left
    var isSecure: Bool = false
    var keyboardType: UIKeyboardType = .default
    var returnKeyType: UIReturnKeyType = .default
    var textContentType: UITextContentType?
    var textFieldBorderStyle: UITextField.BorderStyle = .none
    var enablesReturnKeyAutomatically: Bool = false

    var onCommit: (() -> Void)?

    func makeUIView(context: UIViewRepresentableContext<TextFieldWithFocus>) -> UITextField {
        let textField = UITextField(frame: .zero)
        textField.delegate = context.coordinator
        textField.placeholder = NSLocalizedString(placeholder, comment: "")
        textField.textAlignment = textAlignment
        textField.isSecureTextEntry = isSecure
        textField.keyboardType = keyboardType
        textField.returnKeyType = returnKeyType
        textField.textContentType = textContentType
        textField.borderStyle = textFieldBorderStyle
        textField.enablesReturnKeyAutomatically = enablesReturnKeyAutomatically

        return textField
    }

    func makeCoordinator() -> TextFieldWithFocus.Coordinator {
        return Coordinator(text: $text, isFirstResponder: $isFirstResponder, onCommit: {
            self.onCommit?()
        })
    }

    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<TextFieldWithFocus>) {
        uiView.text = text
        if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
            uiView.becomeFirstResponder()
            context.coordinator.didBecomeFirstResponder = true
        }
    }
}

struct TextFieldWithFocus_Previews: PreviewProvider {
    static var previews: some View {
        TextFieldWithFocus(text: .constant(""), placeholder: "placeholder", isFirstResponder: .constant(false))
    }
}


Just call it that way from SwiftUI side :


TextFieldWithFocus(text: $title,
   placeholder: NSLocalizedString("summary", comment: ""), isFirstResponder: $titleFocused, onCommit: {
   UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
   self.titleFocused = false
})

I'm having the same problem here. It captures 2-3 letters, then disconnects...

Accepted Answer

I solved this bug by using a TextField from UIKit in a UIViewRepresentable.


Here is my script of a focusable TextField :


import Combine
import SwiftUI

struct TextFieldWithFocus: UIViewRepresentable {
    class Coordinator: NSObject, UITextFieldDelegate {
        @Binding var text: String
        @Binding var isFirstResponder: Bool
        var didBecomeFirstResponder = false

        var onCommit: () -> Void

        init(text: Binding<String>, isFirstResponder: Binding<Bool>, onCommit: @escaping () -> Void) {
            _text = text
            _isFirstResponder = isFirstResponder
            self.onCommit = onCommit
        }

        func textFieldDidChangeSelection(_ textField: UITextField) {
            text = textField.text ?? ""
        }

        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            isFirstResponder = false
            didBecomeFirstResponder = false
            onCommit()
            return true
        }
        
        func textFieldDidEndEditing(_ textField: UITextField) {
            isFirstResponder = false
            didBecomeFirstResponder = false
        }
        
        func textFieldDidBeginEditing(_ textField: UITextField) {
            isFirstResponder = true
        }
    }

    @Binding var text: String
    var placeholder: String
    @Binding var isFirstResponder: Bool
    var textAlignment: NSTextAlignment = .left
    var isSecure: Bool = false
    var keyboardType: UIKeyboardType = .default
    var returnKeyType: UIReturnKeyType = .default
    var textContentType: UITextContentType?
    var textFieldBorderStyle: UITextField.BorderStyle = .none
    var enablesReturnKeyAutomatically: Bool = false

    var onCommit: (() -> Void)?

    func makeUIView(context: UIViewRepresentableContext<TextFieldWithFocus>) -> UITextField {
        let textField = UITextField(frame: .zero)
        textField.delegate = context.coordinator
        textField.placeholder = NSLocalizedString(placeholder, comment: "")
        textField.textAlignment = textAlignment
        textField.isSecureTextEntry = isSecure
        textField.keyboardType = keyboardType
        textField.returnKeyType = returnKeyType
        textField.textContentType = textContentType
        textField.borderStyle = textFieldBorderStyle
        textField.enablesReturnKeyAutomatically = enablesReturnKeyAutomatically

        return textField
    }

    func makeCoordinator() -> TextFieldWithFocus.Coordinator {
        return Coordinator(text: $text, isFirstResponder: $isFirstResponder, onCommit: {
            self.onCommit?()
        })
    }

    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<TextFieldWithFocus>) {
        uiView.text = text
        if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
            uiView.becomeFirstResponder()
            context.coordinator.didBecomeFirstResponder = true
        }
    }
}

struct TextFieldWithFocus_Previews: PreviewProvider {
    static var previews: some View {
        TextFieldWithFocus(text: .constant(""), placeholder: "placeholder", isFirstResponder: .constant(false))
    }
}


Just call it that way from SwiftUI side :


TextFieldWithFocus(text: $title,
   placeholder: NSLocalizedString("summary", comment: ""), isFirstResponder: $titleFocused, onCommit: {
   UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
   self.titleFocused = false
})

Thank you for sharing, cederacheBiB 😀

Hi, cederachBiB,


Did a small change to your code that I'd like to share back. I was having problems with TextFieldWithFocus filling up too much space when I wanted a single-line TextField similar to SwiftUI's.

I tried adding a .frame(height: ) to my TextFieldWithFocus element, but it crashed the app. So I added this code to the end of the properties and beginning of the makeUIView(context:)


    var enablesReturnKeyAutomatically: Bool = false
    @State var frameRect: CGRect = .zero
  
    var onCommit: (() -> Void)?
    
    func setFrameRect(_ rect: CGRect) -> some View {
        self.frameRect = rect
        
        return self
    }
  
    func makeUIView(context: UIViewRepresentableContext) -> UITextField {
        let textField = UITextField(frame: self.frameRect)
        textField.delegate = context.coordinator
        textField.placeholder = NSLocalizedString(placeholder, comment: "")


And by using a GeometryReader around the element (or elements in a HStack), it's now possbile to set the frame this way, for example:


GeometryReader { geometry in
    HStack {
        TextFieldWithFocus(text: self.$newString, placeholder: "placeholder", isFirstResponder: self.$textHasFocus,
            onCommit: { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, 
            from: nil, for: nil)
            self.textHasFocus = false
        })
            .setFrameRect(geometry.frame(in: .global))
            .frame(height: 36)
            .border(Color.black, width: 1)
            .padding(.horizontal)
...
}

Hope this can be of use, and thanks again for sharing your fix to our problem!


Best, Tarq

SwiftUI TextField and voice dictation
 
 
Q