Final Product: https://imgur.com/a/Q85GOWj
I've decided to delete my other answer and give the code a go for practice. First, you need to create a UITextField that is always a first responder. You can do so like this:
struct PermanentKeyboard: UIViewRepresentable {
@Binding var text: String
class Coordinator: NSObject, UITextFieldDelegate {
var parent: PermanentKeyboard
init(_ parent: PermanentKeyboard) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//Async to prevent updating state during view update
DispatchQueue.main.async {
if let last = string.last {
//Check if last character is alpha
if last.isLetter {
//Changes the binding to the last character of input, UPPERCASED
self.parent.text = String(last).uppercased()
}
//Allows backspace
} else if string == "" {
self.parent.text = ""
}
}
return false
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextField {
let textfield = UITextField()
textfield.delegate = context.coordinator
//Makes textfield invisible
textfield.tintColor = .clear
textfield.textColor = .clear
return textfield
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
//Makes keyboard permanent
if !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
//Reduces space textfield takes up as much as possible
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
}
}
This is wrapped in a UIViewRepresentable so that we can use it in our SwiftUI view. This textfield will automatically focus when our view is visible and its keyboard will never be dismissed.
Now we have to make our SwiftUI view with the actual crossword. To begin, I'm going to define a struct inside our ContentView that will serve as the crossword boxes:
struct CharacterBox: View {
@Binding var character: String
@Binding var selectedInput: Int
let index: Int
var isSelected: Bool {
index == selectedInput
}
var body: some View {
ZStack {
//Required for tap gesture to work
Rectangle()
.fill(Color.white)
//To signify selection
Rectangle()
.stroke(isSelected ? Color.orange : Color.black)
Text(character)
.foregroundColor(.black)
}
.frame(width: 50, height: 50)
.onTapGesture {
selectedInput = index
}
}
}
This CharacterBox struct can bind to a state variable string in our ContentView which will then also be modified by the invisible textfield if it matches the selected input variable.
Onto the ContentView. We need to make a state variable for each CharacterBox to bind to, but obviously we can't just manually do it. So instead we will create an array and have each CharacterBox bind to a String based on its index.
Before that, I've created this function that will help us initialize an array with some empty strings.
func createEmptyStringArray(count: Int) -> [String] {
var array = [String]()
for _ in 0..<count {
array.append("")
}
return array
}
You can put this outside of ContentView if you wish; it will also let you reuse it later. Then we define the inputs we are going to bind to as an array like this:
@State private var inputs = createEmptyStringArray(count: 3)
@State private var selectedInput = 0
The selectedInput is the index of the string in the inputs array that the invisible textfield will bind to. To make the bindings for each CharacterBox, we can place this function in our ContentView:
func getInputBinding(index: Int) -> Binding<String> {
Binding<String>(
get: {
inputs[index]
},
set: {
inputs[index] = $0
}
)
}
Note that because the index of each CharacterBox you define shouldn't change, it's okay to use a function to get a Binding like this. However, for our selectedIndex, we need it to update the binding each time the selectedIndex has changed, so we must use a computed property instead. Define it like this:
var selectedInputBinding: Binding<String> {
Binding<String>(
get: {
inputs[selectedInput]
},
set: {
inputs[selectedInput] = $0
}
)
}
Lastly, bringing it all together, this is our body:
var body: some View {
ZStack {
PermanentKeyboard(text: selectedInputBinding)
//Hides textfield and prevents user from selecting/pasting/copying
Rectangle()
.fill(Color.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
//Your crossword with bindings to inputs here; this is hard coded/manually created
VStack(spacing: 1) {
ForEach(0 ..< inputs.count) { index in
CharacterBox(character: getInputBinding(index: index), selectedInput: $selectedInput, index: index)
}
}
}
}