如何始终在 SwiftUI 中显示键盘

时间:2021-01-02 23:56:24

标签: ios swiftui

我正在尝试在 SwiftUI 中开发一个填字游戏应用程序。我想总是显示键盘,并且能够选择一个正方形并让按键在那里输入一个字母。有没有办法始终在视图中显示键盘?如果我使用 TextField,我可以让键盘显示出来,但是让拼图的每个单元格成为一个文本字段似乎很笨拙。提前致谢。

1 个答案:

答案 0 :(得分:1)

最终产品:https://imgur.com/a/Q85GOWj

我决定删除我的其他答案,并尝试练习代码。首先,您需要创建一个始终作为第一响应者的 UITextField。你可以这样做:

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

这被包装在一个 UIViewRepresentable 中,以便我们可以在我们的 SwiftUI 视图中使用它。当我们的视图可见并且它的键盘永远不会被关闭时,此文本字段将自动聚焦。

现在我们必须使用实际的填字游戏制作我们的 SwiftUI 视图。首先,我将在 ContentView 中定义一个结构体,用作填字游戏框:

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

这个 CharacterBox 结构可以绑定到我们 ContentView 中的状态变量字符串,如果它与选定的输入变量匹配,那么它也会被不可见的文本字段修改。

到内容视图。我们需要为每个 CharacterBox 绑定一个状态变量,但显然我们不能手动完成。因此,我们将创建一个数组,并让每个 CharacterBox 根据其索引绑定到一个字符串。

在此之前,我已经创建了这个函数来帮助我们用一些空字符串初始化一个数组。

func createEmptyStringArray(count: Int) -> [String] {
    var array = [String]()
    
    for _ in 0..<count {
        array.append("")
    }
    
    return array
}

如果你愿意,你可以把它放在 ContentView 之外;它还可以让您稍后重用它。然后我们将要绑定的输入定义为这样的数组:

@State private var inputs = createEmptyStringArray(count: 3)
@State private var selectedInput = 0

selectedInput 是输入数组中不可见文本字段将绑定到的字符串的索引。为了给每个 CharacterBox 绑定,我们可以把这个函数放在我们的 ContentView 中:

func getInputBinding(index: Int) -> Binding<String> {
    Binding<String>(
        get: {
            inputs[index]
        },
        set: {
            inputs[index] = $0
        }
    )
}

请注意,因为您定义的每个 CharacterBox 的索引不应更改,因此可以使用函数来获取这样的 Binding。然而,对于我们的 selectedIndex,我们需要它在每次 selectedIndex 改变时更新绑定,所以我们必须改用计算属性。像这样定义它:

var selectedInputBinding: Binding<String> {
    Binding<String>(
        get: {
            inputs[selectedInput]
        },
        set: {
            inputs[selectedInput] = $0
        }
    )
}

最后,把它们放在一起,这就是我们的身体:

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