I have a custom keypad that works with a custom text field. It looks great on the iPhone but not so great on the iPad. I want to control the text field with the buttons of the keypad but not have the keypad as a custom keyboard.
Here is my custom text field:
struct WrappedTextField: UIViewRepresentable {
final class ViewModel {
var placeholder: String
var text: Binding<String>
var font: UIFont?
var textColor: UIColor?
var viewController: WrappedTextFieldViewController?
init(_ placeholder: String, _ text: Binding<String>) {
self.placeholder = placeholder
self.text = text
}
}
private var model: ViewModel
init(_ placeholder: String, text: Binding<String>) {
model = ViewModel(placeholder, text)
}
// MARK: Modifiers
func font(_ font: UIFont) -> WrappedTextField {
model.font = font
return self
}
func textColor(_ textColor: Color) -> WrappedTextField {
model.textColor = UIColor(textColor)
return self
}
func viewController(_ viewController: WrappedTextFieldViewController) -> WrappedTextField {
model.viewController = viewController
return self
}
// MARK: Lifecycle Methods
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.inputView = UIView()
textField.autocapitalizationType = .allCharacters
textField.autocorrectionType = .no
textField.textAlignment = .right
textField.delegate = context.coordinator
textField.becomeFirstResponder()
if let viewController = model.viewController {
textField.inputView = viewController.view
viewController.addTextField(textField)
}
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.placeholder = model.placeholder
uiView.text = model.text.wrappedValue
uiView.font = model.font
uiView.textColor = model.textColor
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
}
func makeCoordinator() -> WrappedTextField.Coordinator {
return Coordinator(self)
}
}
extension WrappedTextField {
final class Coordinator: NSObject, UITextFieldDelegate {
var parent: WrappedTextField
init(_ parent: WrappedTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
if let text = textField.text {
parent.model.text.wrappedValue = text
}
}
}
}
Here is the view controller that implements the custom keyboard:
final class WrappedTextFieldViewController: UIHostingController<KeypadView> {
convenience init() {
self.init(rootView: KeypadView())
}
private override init(rootView: KeypadView) {
super.init(rootView: rootView)
view.frame = CGRect(x: 0, y: 0, width: 0, height: 370)
}
@objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented. Exiting...")
}
func addTextField(_ textField: UITextField) {
rootView.wrappedTextField = textField
}
}
Here is my keypad view:
struct KeypadView: View {
@State private var isDisabled: Bool
var wrappedTextField: UITextField
init() {
wrappedTextField = UITextField()
if let text = wrappedTextField.text {
_isDisabled = State(initialValue: text.isEmpty)
} else {
_isDisabled = State(initialValue: true)
}
}
var body: some View {
HStack(spacing: 8) {
VStack(spacing: 8) {
Button("N") { clickButton("N") }
Button("S") { clickButton("S") }
Button("E") { clickButton("E") }
Button("W") { clickButton("W") }
}
VStack(spacing: 8) {
Button("1") { clickButton("1") }
Button("4") { clickButton("4") }
Button("7") { clickButton("7") }
Button(".") { clickButton(".") }
}
VStack(spacing: 8) {
Button("2") { clickButton("2") }
Button("5") { clickButton("5") }
Button("8") { clickButton("8") }
Button("0") { clickButton("0") }
}
VStack(spacing: 8) {
Button("3") { clickButton("3") }
Button("6") { clickButton("6") }
Button("9") { clickButton("9") }
Button("Del") { clickDeleteButton() }
.disabled(isDisabled)
}
}
}
func clickButton(_ buttonText: String) {
if let text = wrappedTextField.text {
// Make sure the number is inserted at the cursor.
if let selectedRange = wrappedTextField.selectedTextRange {
let cursorStart = wrappedTextField.offset(from: wrappedTextField.beginningOfDocument, to: selectedRange.start)
let cursorEnd = wrappedTextField.offset(from: wrappedTextField.beginningOfDocument, to: selectedRange.end)
wrappedTextField.text = String(text.prefix(cursorStart)) + buttonText + String(text.suffix(text.count - cursorEnd))
if let newPosition = wrappedTextField.position(from: selectedRange.start, offset: 1) {
wrappedTextField.selectedTextRange = wrappedTextField.textRange(from: newPosition, to: newPosition)
}
isDisabled = disableDeleteButton()
}
}
}
func clickDeleteButton() {
self.wrappedTextField.deleteBackward()
isDisabled = disableDeleteButton()
}
func disableDeleteButton() -> Bool {
if let text = wrappedTextField.text {
return text.isEmpty
} else {
return true
}
}
}
My content view for testing:
struct ContentView: View {
@State private var text: String = ""
var body: some View {
VStack {
HStack {
Text("Label Here:")
WrappedTextField("Press Buttons", text: $text)
.viewController(WrappedTextFieldViewController())
}
KeypadView()
}
}
}
What I want is a single view with a hidden/disabled keyboard. I know I can keep the keyboard hidden by changing textField.inputView = viewController.view
to textField.inputView = UIView()
. But the buttons in the view don't interact with the UITextField
.
I don't understand how to link the buttons from the KeypadView
to the WrappedTextField
when they're not part of the input view as set by textField.inputView = viewController.view
. The wrappedTextField
variable updates properly in the clickButton
function but never propagates to the actual WrappedTextField
. I suspect I need to change from a UIHostingController
to something else, but I have very little UIKit experience; I started iOS programming with SwiftUI.