60

Goal

I want to make a custom keyboard that is only used within my app, not a system keyboard that needs to be installed.

What I have read and tried

Documentation

The first article above states:

Make sure a custom, systemwide keyboard is indeed what you want to develop. To provide a fully custom keyboard for just your app or to supplement the system keyboard with custom keys in just your app, the iOS SDK provides other, better options. Read about custom input views and input accessory views in Custom Views for Data Input in Text Programming Guide for iOS.

That is what led me to the second article above. However, that article did not have enough detail to get me started.

Tutorials

I was able to get a working keyboard from the second tutorial in the list above. However, I couldn't find any tutorials that showed how to make an in app only keyboard as described in the Custom Views for Data Input documentation.

Stack Overflow

I also asked (and answered) these questions on my way to answering the current question.

Question

Does anyone have a minimal example (with even one button) of an in app custom keyboard? I am not looking for a whole tutorial, just a proof of concept that I can expand on myself.

Community
  • 1
  • 1
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
  • In how many places do you have to enter input with this keyboard? If it's just one and there isn't much stuff in the view, you could just make the keyboard part of the view. – Arc676 Nov 02 '15 at 10:46
  • Eventually I would like to make a keyboard with 20 or 30 buttons. First, though, I would like to make a very simple keyboard with just a button or two to learn the process. – Suragch Nov 02 '15 at 13:13

3 Answers3

81

enter image description here

This is a basic in-app keyboard. The same method could be used to make just about any keyboard layout. Here are the main things that need to be done:

  • Create the keyboard layout in an .xib file, whose owner is a .swift file that contains a UIView subclass.
  • Tell the UITextField to use the custom keyboard.
  • Use a delegate to communicate between the keyboard and the main view controller.

Create the .xib keyboard layout file

  • In Xcode go to File > New > File... > iOS > User Interface > View to create the .xib file.
  • I called mine Keyboard.xib
  • Add the buttons that you need.
  • Use auto layout constraints so that no matter what size the keyboard is, the buttons will resize accordingly.
  • Set the File's Owner (not the root view) to be the Keyboard.swift file. This is a common source of error. See the note at the end.

Create the .swift UIView subclass keyboard file

  • In Xcode go to File > New > File... > iOS > Source > Cocoa Touch Class to create the .swift file.
  • I called mine Keyboard.swift
  • Add the following code:

    import UIKit
    
    // The view controller will adopt this protocol (delegate)
    // and thus must contain the keyWasTapped method
    protocol KeyboardDelegate: class {
        func keyWasTapped(character: String)
    }
    
    class Keyboard: UIView {
    
        // This variable will be set as the view controller so that 
        // the keyboard can send messages to the view controller.
        weak var delegate: KeyboardDelegate?
    
        // MARK:- keyboard initialization
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            initializeSubviews()
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            initializeSubviews()
        }
    
        func initializeSubviews() {
            let xibFileName = "Keyboard" // xib extention not included
            let view = Bundle.main.loadNibNamed(xibFileName, owner: self, options: nil)![0] as! UIView
            self.addSubview(view)
            view.frame = self.bounds
        }
    
        // MARK:- Button actions from .xib file
    
        @IBAction func keyTapped(sender: UIButton) {
            // When a button is tapped, send that information to the 
            // delegate (ie, the view controller)
            self.delegate?.keyWasTapped(character: sender.titleLabel!.text!) // could alternatively send a tag value
        }
    
    }
    
  • Control drag from the buttons in the .xib file to the @IBAction method in the .swift file to hook them all up.

  • Note that the protocol and delegate code. See this answer for a simple explanation about how delegates work.

Set up the View Controller

  • Add a UITextField to your main storyboard and connect it to your view controller with an IBOutlet. Call it textField.
  • Use the following code for the View Controller:

    import UIKit
    
    class ViewController: UIViewController, KeyboardDelegate {
    
        @IBOutlet weak var textField: UITextField!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // initialize custom keyboard
            let keyboardView = Keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: 300))
            keyboardView.delegate = self // the view controller will be notified by the keyboard whenever a key is tapped
    
            // replace system keyboard with custom keyboard
            textField.inputView = keyboardView
        }
    
        // required method for keyboard delegate protocol
        func keyWasTapped(character: String) {
            textField.insertText(character)
        }
    }
    
  • Note that the view controller adopts the KeyboardDelegate protocol that we defined above.

Common error

If you are getting an EXC_BAD_ACCESS error, it is probably because you set the view's custom class as Keyboard.swift rather than do this for the nib File's Owner.

Select Keyboard.nib and then choose File's Owner.

enter image description here

Make sure that the custom class for the root view is blank.

enter image description here

Rajiv
  • 215
  • 3
  • 9
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
  • When I click keys not call `keyWasTapped` method. any idea? – Piraba Sep 22 '16 at 07:27
  • 1
    @Piraba, Is `@IBAction func keyTapped` called? If no, then you probably didn't hook up the IBActions. If yes, then you probably didn't set up the delegate correctly. – Suragch Sep 22 '16 at 07:37
  • `keyTapped` called. Not passed to `keyWasTapped` delegate method – Piraba Sep 22 '16 at 07:57
  • Do you know, how to control custom keyboard withing our application if we use the app extension keyboard – Piraba Sep 22 '16 at 08:34
  • @Piraba, This sounds like a bigger question than can be answered in the comments. I don't have experience with this. If you can't find the answer elsewhere try asking a [new question](http://stackoverflow.com/questions/ask). Feel free to link back to it from here. – Suragch Sep 22 '16 at 08:42
  • @Surangch This is the question http://stackoverflow.com/questions/39634740/swift-custom-system-keyboard-within-my-app-only – Piraba Sep 22 '16 at 09:07
  • I am unable to drag from UIButton to keyTapped function. I try creating my own function but nothing happens when I press the button. File owner is set to the Keyboard Swift file. The key difference is that I'm doing all of this in an actual custom keyboard extension. Anyone got any hints? – Jack Robson Feb 12 '17 at 10:40
  • 1
    @Suragch, I had an issue with let keyboardView = Keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: 300)). I do not know if a recent update to Swift caused it. With the "width" initially set to 0, the buttons did not respond to touch up inside. Even though the view automatically resized to the full width and was visible. I changed the call to be the minimum size of the frame in XIB to include the buttons and everything worked perfectly. Thanks for the post! This was exactly what I was looking for. – McInvale Jun 27 '17 at 21:38
  • @McInvale, thanks for the note. I'll add this question to my list of things to update. – Suragch Jun 28 '17 at 05:02
  • The delegate method is not called correctly, you are missing `character:` – Daniel Aug 14 '17 at 08:59
  • @Daniel, I haven't looked at this for a while. I'm not really sure what you are talking about. Calling `self.delegate?.keyWasTapped(sender.titleLabel!.text!)` sends "character" (which is a `String`, probably poor naming), doesn't it? – Suragch Aug 15 '17 at 13:16
  • @Daniel, Ah, now I understand. Updated Swift syntax. Thanks for editing it in. – Suragch Aug 15 '17 at 15:04
  • Hi, I was wondering how can I set this custom keyboard for all of my textField's inputViews? – Kawe Sep 12 '18 at 21:13
  • @KeyhanKamangar, I haven't worked on this for a while, but I think if you have multiple text fields then you can check which one has focus and set it to the correct one in `keyWasTapped`. – Suragch Sep 13 '18 at 00:12
  • @Suragch, Well I achieved it by creating a subclass of UITextField and set it's inputView to my custom input view but unfortunately I have problem with textField.insertText() function. It seems that by using this function for inserting text into text field, the delegate function (shouldChangeCharactersIn) does not get called. – Kawe Sep 15 '18 at 09:35
  • Height should be always 300 or does not matter ? – Yaroslav Dukal Oct 26 '18 at 05:36
  • @MaksimKniazev No, you don't need to use 300 for the height. – Suragch Oct 26 '18 at 11:01
  • @Piraba how do you connect buttons with action – Anjuka Koralage Mar 28 '19 at 03:23
  • Here are some tutorials: https://riptutorial.com/ios/example/16976/create-a-custom-in-app-keyboard (the same in the answer) https://digitalleaves.com/blog/2016/12/custom-in-app-keyboards/ – Andres Paladines Jun 20 '19 at 13:44
  • 1
    FWIW, I’d suggest using the existing `UIKeyInput` protocol, rather than defining your own. – Rob Jul 28 '19 at 21:06
  • @Suragch - I've got everything wired up and running, but I have a couple of noob questions: what do I put in the ````@IBAction```` that I hooked up to the buttons? (Can you I've me one example?) Should ````@IBAction func keyTapped```` be hooked to anything? I don't see that function getting called at all. – Greg Oct 05 '20 at 02:25
  • 1
    @Greg, I haven't worked on iOS IMEs for a while but you can study the source code of this one that I did a while back: https://github.com/suragch/MongolianIPAKeyboardContainer That one is a system keyboard, though. This one includes an in app keyboard: https://github.com/suragch/Chimee-iOS – Suragch Oct 05 '20 at 03:10
33

The key is to use the existing UIKeyInput protocol, to which UITextField already conforms. Then your keyboard view need only to send insertText() and deleteBackward() to the control.

The following example creates a custom numeric keyboard:

class DigitButton: UIButton {
    var digit: Int = 0
}

class NumericKeyboard: UIView {
    weak var target: (UIKeyInput & UITextInput)?
    var useDecimalSeparator: Bool

    var numericButtons: [DigitButton] = (0...9).map {
        let button = DigitButton(type: .system)
        button.digit = $0
        button.setTitle("\($0)", for: .normal)
        button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
        button.setTitleColor(.black, for: .normal)
        button.layer.borderWidth = 0.5
        button.layer.borderColor = UIColor.darkGray.cgColor
        button.accessibilityTraits = [.keyboardKey]
        button.addTarget(self, action: #selector(didTapDigitButton(_:)), for: .touchUpInside)
        return button
    }

    var deleteButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("⌫", for: .normal)
        button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
        button.setTitleColor(.black, for: .normal)
        button.layer.borderWidth = 0.5
        button.layer.borderColor = UIColor.darkGray.cgColor
        button.accessibilityTraits = [.keyboardKey]
        button.accessibilityLabel = "Delete"
        button.addTarget(self, action: #selector(didTapDeleteButton(_:)), for: .touchUpInside)
        return button
    }()

    lazy var decimalButton: UIButton = {
        let button = UIButton(type: .system)
        let decimalSeparator = Locale.current.decimalSeparator ?? "."
        button.setTitle(decimalSeparator, for: .normal)
        button.titleLabel?.font = .preferredFont(forTextStyle: .largeTitle)
        button.setTitleColor(.black, for: .normal)
        button.layer.borderWidth = 0.5
        button.layer.borderColor = UIColor.darkGray.cgColor
        button.accessibilityTraits = [.keyboardKey]
        button.accessibilityLabel = decimalSeparator
        button.addTarget(self, action: #selector(didTapDecimalButton(_:)), for: .touchUpInside)
        return button
    }()

    init(target: UIKeyInput & UITextInput, useDecimalSeparator: Bool = false) {
        self.target = target
        self.useDecimalSeparator = useDecimalSeparator
        super.init(frame: .zero)
        configure()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

// MARK: - Actions

extension NumericKeyboard {
    @objc func didTapDigitButton(_ sender: DigitButton) {
        insertText("\(sender.digit)")
    }

    @objc func didTapDecimalButton(_ sender: DigitButton) {
        insertText(Locale.current.decimalSeparator ?? ".")
    }

    @objc func didTapDeleteButton(_ sender: DigitButton) {
        target?.deleteBackward()
    }
}

// MARK: - Private initial configuration methods

private extension NumericKeyboard {
    func configure() {
        autoresizingMask = [.flexibleWidth, .flexibleHeight]
        addButtons()
    }

    func addButtons() {
        let stackView = createStackView(axis: .vertical)
        stackView.frame = bounds
        stackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        addSubview(stackView)

        for row in 0 ..< 3 {
            let subStackView = createStackView(axis: .horizontal)
            stackView.addArrangedSubview(subStackView)

            for column in 0 ..< 3 {
                subStackView.addArrangedSubview(numericButtons[row * 3 + column + 1])
            }
        }

        let subStackView = createStackView(axis: .horizontal)
        stackView.addArrangedSubview(subStackView)

        if useDecimalSeparator {
            subStackView.addArrangedSubview(decimalButton)
        } else {
            let blank = UIView()
            blank.layer.borderWidth = 0.5
            blank.layer.borderColor = UIColor.darkGray.cgColor
            subStackView.addArrangedSubview(blank)
        }

        subStackView.addArrangedSubview(numericButtons[0])
        subStackView.addArrangedSubview(deleteButton)
    }

    func createStackView(axis: NSLayoutConstraint.Axis) -> UIStackView {
        let stackView = UIStackView()
        stackView.axis = axis
        stackView.alignment = .fill
        stackView.distribution = .fillEqually
        return stackView
    }

    func insertText(_ string: String) {
        guard let range = target?.selectedRange else { return }

        if let textField = target as? UITextField, textField.delegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) == false {
            return
        }

        if let textView = target as? UITextView, textView.delegate?.textView?(textView, shouldChangeTextIn: range, replacementText: string) == false {
            return
        }

        target?.insertText(string)
    }
}

// MARK: - UITextInput extension

extension UITextInput {
    var selectedRange: NSRange? {
        guard let textRange = selectedTextRange else { return nil }

        let location = offset(from: beginningOfDocument, to: textRange.start)
        let length = offset(from: textRange.start, to: textRange.end)
        return NSRange(location: location, length: length)
    }
}

Then you can:

textField.inputView = NumericKeyboard(target: textField)

That yields:

enter image description here

Or, if you want a decimal separator, too, you can:

textField.inputView = NumericKeyboard(target: textField, useDecimalSeparator: true)

The above is fairly primitive, but it illustrates the idea: Make you own input view and use the UIKeyInput protocol to communicate keyboard input to the control.

Also please note the use of accessibilityTraits to get the correct “Spoken Content” » “Speak Screen” behavior. And if you use images for your buttons, make sure to set accessibilityLabel, too.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I'm accepting this not because I've tried it yet, but because you look like you know what you are doing more than I do. Thanks for the answer. I'm planning to do more IME work in the future so I'll add a comment or ping you with a question if I run into any problems. – Suragch Jul 31 '19 at 23:08
  • 2
    I would like to add Done button in this keyboard, i add that but now on action how to resign keyboard – Protocol Oct 04 '19 at 05:20
  • 2
    @RahulPhate - Change `target` from a `UIKeyInput?` to a `(UIResponder & UIKeyInput)?` (and make the corresponding change to the `init` method, too). Then you can call `target?.resignFirstResponder()` in your button handler. – Rob Dec 26 '19 at 19:40
  • Thank-you Rob. The code works beautifully. I made a hexadecimal version of your code. https://github.com/PepperoniJoe/HexadecimalKeyboard – Marcy Jul 25 '20 at 02:57
  • @Rob - this is great! I would like to place the keyboard in specific place on the screen, preferably inside a UIView. How would I go about doing that? – Greg Oct 05 '20 at 01:19
  • 2
    The idea of `inputView` is to use your own custom keyboard/view in place of the standard keyboard. But if you don’t want it presented where a keyboard normally is, then perhaps you don’t want to use `inputView` at all, but rather just a bunch of buttons placed in some view that update some label as you tap on them. If you have additional questions, though, I’d suggest that comments here under this answer might not be the right place for this discussion. If you don’t find anything after doing some research, just post your own question... – Rob Oct 05 '20 at 04:13
  • @Rob - I'd like to be able to show the keyboard on just half of the screen when the phone is rotated into Landscape mode. Is that possible using ````inputView````? – Greg Oct 08 '20 at 16:40
  • how to use this with swiftui? – Ibrahim Benzer Nov 02 '20 at 17:54
  • Hello, thank you for the solution, I've been looking for a solution. but don't you think we need to that " let button = DigitButton(type: .system)" because here every time we create the button which stays forever in the memory. so whenever I visit a screen digitButtons accumulate (even if I exit a screen and go back for it the number doubles). Is it a kind of memory leak or what? and do you have any best solution for it ? or if I am missing something and I am wrong please tell me. Thank you – Elin Apr 01 '21 at 22:01
  • 2
    @Elin If you don't instantiate buttons, you won't have anything to tap on. Lol. If you are leaking these buttons, it is likely because you are leaking the `UITextField` or `UITextView` for which you have set this `inputView`. That can happen if you are leaking your view controller, for whose view/storyboard/nib is being used. But the problem is not in the instantiation of the buttons, undoubtedly something higher up in the view and/or view controller hierarchy. But I just tested this, and buttons are instantiated and released fine. In short, your problem rests elsewhere. – Rob Apr 02 '21 at 00:39
  • Thank you for the clarification, it was really helpful you put me on the right path:) but is a new keyboard created every time i have a new textfield ? So if I have 5 textfields, so i have 5 keyboards which means 100 digitButtons in the memory. Right? – Elin Apr 02 '21 at 11:36
  • 2
    Yeah, you want a different keyboard instance for every control (so that each can communicate to its respective `UITextField`). So, yes, that means that if you have twelve buttons on each keyboard, and five text fields, then yes, you are instantiating 60 buttons. 12 – Rob Apr 02 '21 at 15:44
12

Building on Suragch's answer, I needed a done and backspace button and if you're a noob like me heres some errors you might encounter and the way I solved them.

Getting EXC_BAD_ACCESS errors? I included:

@objc(classname)
class classname: UIView{ 
}

fixed my issue however Suragch's updated answer seems to solve this the more appropriate/correct way.

Getting SIGABRT Error? Another silly thing was dragging the connections the wrong way, causing SIGABRT error. Do not drag from the function to the button but instead the button to the function.

Adding a Done Button I added this to the protocol in keyboard.swift:

protocol KeyboardDelegate: class {
    func keyWasTapped(character: String)
    func keyDone()
}

Then connected a new IBAction from my done button to keyboard.swift like so:

@IBAction func Done(sender: UIButton) {
    self.delegate?.keyDone()
}

and then jumped back to my viewController.swift where i am using this keyboard and added this following after the function keyWasTapped:

func keyDone() {
    view.endEditing(true)
}

Adding Backspace This tripped me up a lot, because you must set the textField.delegate to self in the viewDidLoad() method (shown later).

First: In keyboard.swift add to the protocol func backspace():

protocol KeyboardDelegate: class {
    func keyWasTapped(character: String)
    func keyDone()
    func backspace()
}

Second: Connect a new IBAction similar to the Done action:

@IBAction func backspace(sender: UIButton) {
    self.delegate?.backspace()
}

Third: Over to the viewController.swift where the NumberPad is appearing.

Important: In viewDidLoad() set all textFields that will be using this keyboard. So your viewDidLoad() should look something like this:

 override func viewDidLoad() {
    super.viewDidLoad()

    self.myTextField1.delegate = self
    self.myTextField2.delegate = self

    // initialize custom keyboard
    let keyboardView = keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: 240))
    keyboardView.delegate = self // the view controller will be notified by the keyboard whenever a key is tapped

    // replace system keyboard with custom keyboard
    myTextField1.inputView = keyboardView
    myTextField2.inputView = keyboardView
}

I'm not sure how to, if there is a way to just do this to all textFields that are in the view. This would be handy...

Forth: Still in viewController.swift we need to add a variable and two functions. It will look like this:

var activeTextField = UITextField()

func textFieldDidBeginEditing(textField: UITextField) {
    print("Setting Active Textfield")
    self.activeTextField = textField
    print("Active textField Set!")
}

func backspace() {
    print("backspaced!")
    activeTextField.deleteBackward()
}

Explanation of whats happening here:

  1. You make a variable that will hold a textField.
  2. When the "textFieldDidBeginEditing" is called it sets the variable so it knows which textField we are dealing with. I've added a lot of prints() so we know everything is being executed.
  3. Our backspace function then checks the textField we are dealing with and uses .deleteBackward(). This removes the immediate character before the cursor.

And you should be in business. Many thanks to Suragchs for helping me get this happening.

Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
Steve
  • 4,372
  • 26
  • 37
  • 1
    Use `textField.deleteBackward()` to backspace. If you are interested, here is a [demo view controller](https://github.com/suragch/MongolAppDevelopment-iOS/blob/master/Mongol%20App%20Componants/KeyboardDemoVC.swift) and [this](https://github.com/suragch/MongolAppDevelopment-iOS/blob/master/Mongol%20App%20Componants/EnglishKeyboard.swift) is one of the keyboards that I use in it. It shows where I have come since first starting with the example here. I ended up abandoning nibs (for an unrelated reason) and just laying things out programmatically, though I don't necessary recommend that for you. – Suragch Feb 02 '16 at 14:08
  • if have two textfields.. so need a keyboard custom in first textfield and second textfield can keyboard default by iphone ?.. i wanna hide keyboard default and them show keyboard default – marlonpya Aug 22 '17 at 22:47
  • I'm not sure I understand your question marlonpya. If you want the default keyboard for one `textfield` then you do not need to address it in the `viewDidLoad()`. And then if you do want the custom keyboard for the other `textfield` then in `viewDidLoad()` you must set the `textfield` delegate to self and replace the keyboard with the custom keyboard as outlined in the third step above. – Steve Aug 23 '17 at 02:51