I strongly believe that any in-app keyboard layout is best built in SwiftUI, no matter if its used in SwiftUI or UIKit. With this lightweight package, I offer you the necessary tools to do so in the UI framework we’ve all started to embrace and love: SwiftUI!
My goal is to keep this framework as lightweight as it is, to avoid adding bloat with features most people don’t 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 via the UITextDocumentProxy closure parameter
- Use it in parallel 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 Keyboard
class and provide a static Keyboard (or CustomKeyboard) 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 Keyboard {
static let yesnt = CustomKeyboard { 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 .keyboardType(:)
View modifer and using your statically defined property.
The .keyboardType modifier is overloading Apple’s native API with UIKeyboardType values (like .numberPad) to also support your own custom keyboards (like .yesnt).
This ties seamlessly into the familiar SwiftUI API, so you don’t have to learn a new modifier or break existing code — you just get more flexibility.
struct ContentView: View {
@State var text: String = ""
var body: some View {
VStack {
Text(text)
TextField("", text: $text)
.keyboardType(.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 should be used.
struct ContentView: View {
@State var text: String = ""
var body: some View {
VStack {
Text(text)
TextField("", text: $text)
.keyboardType(.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 Keyboard
's keyboardInputView
property to the UITextFields inputView
.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let customKeyboard = Keyboard.yesnt
customKeyboard.onSubmit = { print("do something when SubmitHandler called") }
myTextField.inputView = customKeyboard.keyboardInputView
}
You can also directly use the keyboardType(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)
.keyboardType { 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 just as well with TextEditor
You can also switch between keyboards dynamically at runtime by binding the .keyboardType
to some state. For example, toggling between the system number pad and your own .yesnt
keyboard:
struct ContentView: View {
@State private var text = ""
@State private var useCustom = false
var body: some View {
VStack {
Toggle("Use Custom Keyboard", isOn: $useCustom)
.padding()
TextField("Enter text", text: $text)
.keyboardType(useCustom ? .yesnt : .system(.numberPad))
}
}
}
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)
.keyboardType(.alphabet)
TextField("Numpad", text: $text1)
.keyboardType(.numberPad)
TextField("ABC", text: $text2)
.keyboardType(.alphabet)
TextField("Normal", text: $text3)
//normal keyboard
}
.background(Color.gray)
}
.padding()
}
}
extension Keyboard {
static let alphabet = CustomKeyboard { 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
This 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