Handling keyboard notifications with ReactiveSwift

Marc is a good friend of mine. He has a lot of qualities and I even thanked him in my book for all the support he provided me. That being said, he’s still using a super-small iPhone 5S and when I sent him the first build of this project, I forgot to handle the keyboard and it was impossible for him to log into the application. Fortunately for the both of us, handling keyboard-related notifications in an iOS application is super easy. Let’s see how Swift and ReactiveSwift can make it even easier.

TL;DR.

https://twitter.com/Palleas/status/795018640135516160

Parsing the notification

The UIKeyboardWillShowNotification notification has been available since iOS 2.0 and the approach is really simple: you subscribe to a bunch of notifications which gives you informations about the keyboard. Extracting those informations is still a bit rough. The notification’s userInfo property that contains the informations is [String: Any] so some force-casting is required.

func parse(keyboardNotification notification: Notification) -> (CGFloat, TimeInterval, UIViewAnimationOptions)? {
    guard let info = notification.userInfo else { 
        return nil 
    }
    
    guard let height = (info[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height else { 
        return nil 
    }
    
    guard let duration = info[UIKeyboardAnimationDurationUserInfoKey] as? TimeInterval else { 
        return nil 
    }
    
    guard let curveRaw = info[UIKeyboardAnimationCurveUserInfoKey] as? Int else { 
        return nil 
    }
    
    let curve = UIViewAnimationOptions(rawValue: UInt(curveRaw) << 16)

    return (height, duration, curve)
}

Unfortunately, there is not much we can do about that so let’s move on to the actual handling of the notification.

Adding ReactiveCoca to the mix

I mentionned in a previous post that ReactiveSwift came with a really nice way of making your code more reactive. That includes UIKit’s Notification Center.

let center = NotificationCenter.default.reactive
center.notifications(forName: .UIKeyboardWillShow).observe { notification in
  print("Keyboard will show!")
  // Use `notification.userInfo` to extract informations about the keyboard
}

Since we already have our code to parse the informations about the notification, we can easily plug it into a map, like so:

let center = NotificationCenter.default.reactive
center.notifications(forName: .UIKeyboardWillShow).map(parse).observe { (height, duration, animation) in
  print("Keyboard will show")
  print("Height will be \(height)")
  print("Duration is \(duration)")
  // ...
}

Binding the notification to the UI

Now that we know how the keyboard is going to appear, we need to reflect it in the view. In most cases, it means changing the contentInset of a scroll view containing the whole screen or updating an Auto layout constraint. ReactiveSwift provides a way to easily bind values from a signal to a parameter. That’s why we’ll expose a MutableProperty in the view.

class ComposeMessageView: UIView {
    private let container = UIScrollView()
    
    let keyboardStatus = MutableProperty<(CGFloat, TimeInterval, UIViewAnimationOptions)?>(nil)
    
    // ...
}

Our view can now subscribe to the changes of this property’s value and update the UI accordingly.

class ComposeMessageView: UIView {
    // ...
    
    init() {
        // configure the UI...

        keyboardStatus.producer.startWithValues { keyboard in
            guard let (height, duration, options) = keyboard else { return }

            UIView.animate(withDuration: duration, delay: 0, options: options, animations: {
                self.container.contentInset = .bottom(height)
            }, completion: nil)
        }    
    }
}

I’m not too fond of using custom operators in Swift. Most of the time I feel they make the code more confusing for little gain. In some rare cases though, I feel they do make sense, for example when you want to bind values from a signal to a property.

let center = NotificationCenter.default.reactive
let notification = center.notifications(forName: .UIKeyboardWillShow).map(parse)
composeMessageView.keyboardStatus <~ notification

Showing, Hiding and changing the size of the keyboard

A complete handling of the keyboard in your application means handling the notification when it will appear, when it will hide and when it will change (when you activate or deactivate the predictive mode on your keyboard). Only the name of those notifications will change, the parsing and the way we handle them remain the same. That’s why we’ll merge those notifications into one signal.

let center = NotificationCenter.default.reactive
let keyboardNotifications = Signal.merge(
    center.notifications(forName: .UIKeyboardWillChangeFrame),
    center.notifications(forName: .UIKeyboardWillHide),
    center.notifications(forName: .UIKeyboardWillShow)
)

composeMessageView.keyboardStatus <~ keyboardNotifications.map(parse)

Plot twist

I really do think ReactiveSwift and ReactiveCocoa allows you to make your UI code cleaner. In our case, the view controller (that handles the keyboard notification) and the view talk to each other via a predefined property and only via this property. The view controller doesn’t even know about the scrollview.

Liam Nichols replied to me with a tweet: the UIKeyboardWillChangeFrame notification seems to remove the need for the UIKeyboardWillShow and UIKeyboardWillHide. I haven’t found anything about that in the documentation so if you have more informations about that, please ping me on Twitter!