I strongly believe that any in app keyboard layout is best built in SwiftUI, no matter if its used in SwiftUI or UIKit. With that lightweight package I think I offer you the necessary tools to do so in the UI Framework that we all started to embrace and love. SwiftUI!
My goal is to keep this framework as lightweight as it is, to not add bloat to it with features most people do not need. This ensures keeping the complexity of the package at its minimum, which translates to a certain stability and reliability. I sincerely hope with this package I managed to deliver pretty much unrestricted possibilities for you.
- Build the entire keyboard layout in SwiftUI
- Doesn't even have to be a keyboard, build literally anything that pops up for focused text fields!
- Can play native iOS/iPadOS keyboard sounds and haptic feedback
- Use it in UIKit or SwiftUI
- Interact with the focused text using the UITextDocumentProxy closure parameter
- Use it parallelly to any native keyboard
- Works with SwiftUI's new
scrollDismissesKeyboard(:)
modifiers etc. - Works flawlessly on iOS and iPadOS
- Works with the native
onSubmit
modifier, but behaviour can be fully customized by usingonCustomSubmit
instead
Simply extend the CustomKeyboard class and provide a static CustomKeyboard (or CustomKeyboardBuilder) instance, additionally use the UITextDocumentProxy
instance to modify/delete the focused text and move the cursor. Use the playSystemFeedback closure to play system sounds on Button
presses. See the example below:
extension CustomKeyboard {
static let yesnt = CustomKeyboardBuilder { textDocumentProxy, submit, playSystemFeedback in
VStack {
HStack {
Button("Yes!") {
textDocumentProxy.insertText("Yes")
playSystemFeedback?()
}
Button("No!") {
textDocumentProxy.insertText("No")
playSystemFeedback?()
}
}
Button("Maybe") {
textDocumentProxy.insertText("?")
playSystemFeedback?()
}
Button("Idk") {
textDocumentProxy.insertText("Idk")
playSystemFeedback?()
}
Button("Can you repeat the question?") {
playSystemFeedback?()
submit()
}
}
.buttonStyle(.bordered)
.padding()
}
}
Once declared, you can use the custom keyboard with the .customKeyboard(:)
View modifer and using your statically defined property
struct ContentView: View {
@State var text: String = ""
var body: some View {
VStack {
Text(text)
TextField("", text: $text)
.customKeyboard(.yesnt)
}
}
}
The custom keyboard supports the native onSubmit
modifier to pass a closure to perform actions after the submit button has been tapped.
In order to fully customize the behaviour of the submit button to not follow the native behaviour (e.g. not closing the keyboard) the onCustomSubmit
modifier shall be used.
struct ContentView: View {
@State var text: String = ""
var body: some View {
VStack {
Text(text)
TextField("", text: $text)
.customKeyboard(.yesnt)
.onCustomSubmit {
print("do something when SubmitHandler has been called")
}
}
}
}
If both modifiers are used onCustomSubmit
takes precedence over onSubmit
and performs the closure inside onCustomSubmit
only. So please make sure to only use one of the two ideally.
Once declared, you can assign your CustomKeyboard
's keyboardInputView
property to the UITextFields inputView
.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let customKeyboard = CustomKeyboard.yesnt
customKeyboard.onSubmit = { print("do something when SubmitHandler called") }
myTextField.inputView = customKeyboard.keyboardInputView
}
You can also directly use the customKeyboard(view:)
modifier that allows you to build the custom keyboard within the view body itself, if you need to access some View properties or constants etc.
Example:
struct ContentView: View {
@State var text = "0"
var body: some View {
TextField("", text: $text)
.customKeyboard { textDocumentProxy, onSubmit, playFeedback in
VStack {
numberButton(text: "1", uiTextDocumentProxy: textDocumentProxy, playFeedback: playFeedback)
numberButton(text: "2", uiTextDocumentProxy: textDocumentProxy, playFeedback: playFeedback)
Button("DEL") {
textDocumentProxy.deleteBackward()
playFeedback?()
}
}
}
}
func numberButton(text: String, uiTextDocumentProxy: UITextDocumentProxy, playFeedback: (() -> ())?) -> some View {
Button(text) {
uiTextDocumentProxy.insertText(text)
playFeedback?()
}
}
}
This works equally with TextEditor
Check out the video below for the following example code and see how it works perfectly side by side with native keyboards.
struct ContentView: View {
@State var text0: String = ""
@State var text1: String = ""
@State var text2: String = ""
@State var text3: String = ""
var body: some View {
VStack {
Group {
TextField("ABC", text: $text0)
.customKeyboard(.alphabet)
TextField("Numpad", text: $text1)
.keyboardType(.numberPad)
TextField("ABC", text: $text2)
.customKeyboard(.alphabet)
TextField("Normal", text: $text3)
//normal keyboard
}
.background(Color.gray)
}
.padding()
}
}
extension CustomKeyboard {
static let alphabet = CustomKeyboardBuilder { textDocumentProxy, submit, playSystemFeedback in
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".map { $0 }
let gridItem = GridItem.init(.adaptive(minimum: 25))
return LazyVGrid(columns: [gridItem], spacing: 5) {
ForEach(letters, id: \.self) { char in
Button(char.uppercased()) {
textDocumentProxy.insertText("\(char)")
playSystemFeedback?()
}
.frame(width: 25, height: 40)
.background(Color.white)
.foregroundColor(Color.black)
.cornerRadius(8)
.shadow(radius: 2)
}
}
.frame(height: 150)
.padding()
}
}
Simulator.Screen.Recording.-.iPhone.14.-.2022-11-29.at.19.00.36.mp4
The code comes with no warranty of any kind. I hope it'll be useful to you (it certainly is to me), but I make no guarantees regarding its functionality or otherwise.
You really don't have to pay anything to use this package. But if you feel generous today and would like to donate because this package helped you so much, here's a PayPal donation link: https://www.paypal.com/donate/?hosted_button_id=JYL8DBGA2X4YQ
or just buy me a hot chocolate: https://www.buymeacoffee.com/paescebu