magnuskahr

writing code

Creating a custom input in swift

Call it an app-only keyboard; a custom keyboard you may need a few places in your app. Perhaps it would be a bit-only keyboard? So let's build it!

Every UIView has a property called inputView, which when the UIView becomes the firstResponder, will present its inputView, just as a UITextField presents a keyboard.

Let's use that knowledge to create the keyboard:

class BinaryKeyboard: UIInputView {
    init() {
        super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 300), inputViewStyle: .keyboard)
    }
}

By setting the inputViewStyle to .keyboard, iOS will provide a background that looks like the real keyboard. We will be needing to add some buttons onto the keyboard, and for that, we will need a stack view, add the following before init():

private lazy var stackview: UIStackView = {
    let stackview = UIStackView()
    stackview.axis = .horizontal
    stackview.frame = frame
    stackview.spacing = 10
    stackview.distribution = .fillEqually
    addSubview(stackview)
        
    stackview.translatesAutoresizingMaskIntoConstraints = false
    
    let guide = safeAreaLayoutGuide
    
    var constraints = [
        stackview.centerXAnchor.constraint(equalTo: centerXAnchor),
        stackview.topAnchor.constraint(equalToSystemSpacingBelow: guide.topAnchor, multiplier: 1),
        stackview.bottomAnchor.constraint(equalTo: guide.bottomAnchor, constant: -8)
    ]
        
    switch UIDevice.current.userInterfaceIdiom {
        case .phone:
            constraints.append(stackview.leftAnchor.constraint(equalToSystemSpacingAfter: guide.leftAnchor, multiplier: 1))
            constraints.append(stackview.rightAnchor.constraint(equalTo: guide.rightAnchor, constant: -8))
        case .pad:
            constraints.append(stackview.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5))
            default: break
    }
        
    NSLayoutConstraint.activate(constraints)
        
    return stackview
}()

Wow... that's a lot of code you say, what does it all do? Well really, it is just a UIStackView with a bunch of autolayout for both iPhone, iPhone X, and iPad - to make sure the keyboard is shown properly. This means you don't need to care about how it works (but you are welcome to experiment with the code!).

For adding buttons, I will be using my own KeyboardButton from my earlier post Replicating Keys From UIKit, which provides both formatting and easy hit-detection.

In BinaryKeyboard I'll be adding the following function:

private func addButton(with title: CustomStringConvertible, and formatter: KeyboardButtonFormatter) {
    let button = KeyboardButton(title: title, formatter: formatter)
    button.delegate = self
    stackview.addArrangedSubview(button)
}

Which now allows us to setup the keys in our init() method by adding the following lines:

addButton(with: "0", and: NormalKeyButtonFormatter())
addButton(with: "1", and: NormalKeyButtonFormatter())

You can see that the method creates a button and adds it to the stackview, but wait - it also sets some delegate? Well, remember that I said easy hit-detection? Let's implement that delegate method!

extension BinaryKeyboard: KeyboardButtonDelegate {
    func keyWasHit(_ button: KeyboardButton) {
        guard let title = button.titleLabel?.text else {
            return
        }
        print("Did hit \(title)")
    }
}

If we made a inputView and attached the keyboard now, it would pop-up but the keys would not do something, go ahead and try! Add the following in a UIViewController:

let keyboard = BinaryKeyboard()
let field = UITextField(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
field.backgroundColor = .red
field.inputView = keyboard
view.addSubview(field)

To make the textfield react to taps on the keyboard, we will have to create some kind of communication from the keyboard to the textfield, this we will describe using the following protocol:

protocol BinaryKeyboardObserver: class {
    func add(_ string: String)
}

It is a protocol for classes since we need the keyboard to have a weak reference its textfield to not create retain cycles, therefore add a reference to BinaryKeyboard:

weak var observer: BinaryKeyboardObserver?

Now in our KeyWasHit(_ button: KeyboardButton) method, we can add the following call:

observer?.add(title)

Which tells our observer that it has to append some string title. Now, we are soon done, all we have to do is creating a UITextField which adheres to BinaryKeyboardObserver:

class BinaryTextField: UITextField, BinaryKeyboardObserver {
    func add(_ string: String) {
        self.text?.append(string)
    }
}

Simply we have just created a simple subclass of UITextField, which implements BinaryKeyboardObserver. Now in our viewcontroller, lets change the code to the following:

let keyboard = BinaryKeyboard()
let field = BinaryTextField(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
field.backgroundColor = .red
field.inputView = keyboard
        
keyboard.observer = field
view.addSubview(field)

That is it! Run the code, and see the keyboard change the textfield. One last thing though, if you would like to provide sound feedback when a key is tapped, one has to implement UIInputViewAudioFeedback, so add the following extension:

extension BinaryKeyboard: UIInputViewAudioFeedback {
    /// Required for playing system click sound
    var enableInputClicksWhenVisible: Bool { return true }
}

Now we can add a last line to our KeyWasHit(_ button: KeyboardButton)method:

UIDevice.current.playInputClick()

And sounds should play! Did you have trouble following along? See my GitHub project for a full runnable project: iOSBinaryKeyboard