Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
129 views
in Technique[技术] by (71.8m points)

ios - How to always show the keyboard in SwiftUI

I'm trying to develop a crossword puzzle app in SwiftUI. I'd like to always show the keyboard, and be able to select a square and have the key press enter a letter there. Is there a way to always show the keyboard in a View? I can get the keyboard to show up if I use a TextField, but it seems rather clunky to make each cell of the puzzle a text field. Thanks in advance.

question from:https://stackoverflow.com/questions/65545374/how-to-always-show-the-keyboard-in-swiftui

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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)
            }
        }
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...