YourManDan
YourManDan

Reputation: 373

Is there a way to show the system keyboard and take inputs from it without a SwiftUI TextField?

I want to be able to display the system keyboard and my app take inputs from the keyboard without using a TextField or the like. My simple example app is as follows:

struct TypingGameView: View {
   let text = “Some example text”
   @State var displayedText: String = ""

   var body: some View {
      Text(displayedText)
   }
}

I'm making a memorization app, so when I user types an input on the keyboard, it should take the next word from text and add it to displayedText to display onscreen. The keyboard should automatically pop up when the view is displayed.

If there is, a native SwiftUI solution would be great, something maybe as follows:

struct TypingGameView: View {
   let text = “Some example text”
   @State var displayedText: String = ""

   var body: some View {
      Text(displayedText)
         .onAppear {
            showKeyboard()
         }
         .onKeyboardInput { keyPress in
            displayedText += keyPress
         }
   }
}

A TextField could work if there is some way to 1. Make it so that whatever is typed does not display in the TextField, 2. Disable tapping the text (e.g. moving the cursor or selecting), 3. Disable deleting text.

Upvotes: 4

Views: 3247

Answers (2)

Simon
Simon

Reputation: 890

Here's a possible solution using UIViewRepresentable:

  • Create a subclass of UIView that implements UIKeyInput but doesn't draw anything
  • Wrap it inside a struct implementing UIViewRepresentable, use a Coordinator as a delegate to your custom UIView to carry the edited text "upstream"
  • Wrap it again in a ViewModifier that shows the content, pass a binding to the wrapper and triggers the first responder of your custom UIView when tapped

I'm sure there's a more synthetic solution to find, three classes for such a simple problem seems a bit much.

protocol InvisibleTextViewDelegate {
    func valueChanged(text: String?)
}

class InvisibleTextView: UIView, UIKeyInput {
    var text: String?
    var delegate: InvisibleTextViewDelegate?

    override var canBecomeFirstResponder: Bool { true }

    // MARK: UIKeyInput
    var keyboardType: UIKeyboardType = .decimalPad
    
    var hasText: Bool { text != nil }

    func insertText(_ text: String) {
        self.text = (self.text ?? "") + text
        setNeedsDisplay()
        delegate?.valueChanged(text: self.text)
    }

    func deleteBackward() {
        if var text = text {
            _ = text.popLast()
            self.text = text
        }
        setNeedsDisplay()
        delegate?.valueChanged(text: self.text)
    }
}

struct InvisibleTextViewWrapper: UIViewRepresentable {
    typealias UIViewType = InvisibleTextView
    @Binding var text: String?
    @Binding var isFirstResponder: Bool
    
    class Coordinator: InvisibleTextViewDelegate {
        var parent: InvisibleTextViewWrapper
        
        init(_ parent: InvisibleTextViewWrapper) {
            self.parent = parent
        }
        
        func valueChanged(text: String?) {
            parent.text = text
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> InvisibleTextView {
        let view = InvisibleTextView()
        view.delegate = context.coordinator
        return view
    }
    
    func updateUIView(_ uiView: InvisibleTextView, context: Context) {
        if isFirstResponder {
            uiView.becomeFirstResponder()
        } else {
            uiView.resignFirstResponder()
        }
    }
    
    
}

struct EditableText: ViewModifier {
    @Binding var text: String?
    @State var editing: Bool = false
    
    func body(content: Content) -> some View {
        content
            .background(InvisibleTextViewWrapper(text: $text, isFirstResponder: $editing))
            .onTapGesture {
                editing.toggle()
            }
            .background(editing ? Color.gray : Color.clear)
    }
}

extension View {
    func editableText(_ text: Binding<String?>) -> some View {
        modifier(EditableText(text: text))
    }
}


struct CustomTextField_Previews: PreviewProvider {
    struct Container: View {
        @State private var value: String? = nil
        
        var body: some View {
            HStack {
                if let value = value {
                    Text(value)
                    Text("meters")
                        .font(.subheadline)
                } else {
                    Text("Enter a value...")
                }
            }
            .editableText($value)
        }
    }
    
    static var previews: some View {
        Group {
            Container()
        }
    }
}

Upvotes: 3

abcross92
abcross92

Reputation: 33

you can have the textfield on screen but set opacity to 0 if you don't want it shown. Though this would not solve for preventing of deleting the text

You can then programmatically force it to become first responder using something like this https://stackoverflow.com/a/56508132/3919220

Upvotes: 0

Related Questions