Reputation: 373
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
Reputation: 890
Here's a possible solution using UIViewRepresentable
:
UIView
that implements UIKeyInput
but doesn't draw anythingUIViewRepresentable
, use a Coordinator as a delegate to your custom UIView to carry the edited text "upstream"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
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