1

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.

YourManDan
  • 277
  • 1
  • 14
  • I have done very little with SwiftUI, but is there a way to place a `TextField` outside the screen bounds, and would that get you what you need? – Phillip Mills Dec 29 '21 at 21:18
  • @PhillipMills Potentially, it wouldn't be the most elegant of solutions, so if there's something built into SwiftUI for this functionality I'd rather take that, but might just have to place it outside the screen bounds/behind something if another solution doesn't exist. – YourManDan Dec 29 '21 at 21:27

2 Answers2

1

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()
        }
    }
}
Simon
  • 860
  • 7
  • 23
  • It definitely is helpful and is in the right direct of what I'm looking for, though I think at that point it would be simpler and would use a more native approach to just use a `TextField` and hide it behind some elements. – YourManDan Jan 05 '22 at 16:53
  • I think it depends on the context really. You have more control on what is happening this way, you can even build the API you asked for. TextField is using UITextField behind the scene so I'm not sure there's one solution that is more idiomatic than the other. – Simon Jan 06 '22 at 13:03
  • A fair point, I'll fork my project and try implementing this to see if its what I'm looking for, thanks :) – YourManDan Jan 06 '22 at 21:54
0

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

abcross92
  • 33
  • 5
  • Appreciate the response, but then the ```TextField``` would still be interactive, which I don't want. And I'm looking for a solution without using a ```TextField``` (if possible). – YourManDan Jan 01 '22 at 22:09