Skip to content

Instantly share code, notes, and snippets.

@KentarouKanno
Last active December 19, 2019 03:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KentarouKanno/53a548821a13b85f398470f687f8c766 to your computer and use it in GitHub Desktop.
Save KentarouKanno/53a548821a13b85f398470f687f8c766 to your computer and use it in GitHub Desktop.

KeyboardObserver.swift

import UIKit

enum KeyboardEventType: CaseIterable {
    case willShow
    case didShow
    case willHide
    case didHide
    case willChangeFrame
    case didChangeFrame
    
    public var notificationName: NSNotification.Name {
        switch self {
        case .willShow:
            return UIResponder.keyboardWillShowNotification
        case .didShow:
            return UIResponder.keyboardDidShowNotification
        case .willHide:
            return UIResponder.keyboardWillHideNotification
        case .didHide:
            return UIResponder.keyboardDidHideNotification
        case .willChangeFrame:
            return UIResponder.keyboardWillChangeFrameNotification
        case .didChangeFrame:
            return UIResponder.keyboardDidChangeFrameNotification
        }
    }
    
    init?(name: NSNotification.Name) {
        switch name {
        case UIResponder.keyboardWillShowNotification:
            self = .willShow
        case UIResponder.keyboardDidShowNotification:
            self = .didShow
        case UIResponder.keyboardWillHideNotification:
            self = .willHide
        case UIResponder.keyboardDidHideNotification:
            self = .didHide
        case UIResponder.keyboardWillChangeFrameNotification:
            self = .willChangeFrame
        case UIResponder.keyboardDidChangeFrameNotification:
            self = .didChangeFrame
        default:
            return nil
        }
    }
    
    static func allEventNames() -> [NSNotification.Name] {
        return allCases.map { $0.notificationName }
    }
}

struct KeyboardEvent {
    public let type: KeyboardEventType
    public let keyboardFrameBegin: CGRect
    public let keyboardFrameEnd: CGRect
    public let curve: UIView.AnimationCurve
    public let duration: TimeInterval
    public var isLocal: Bool?
    
    public var options: UIView.AnimationOptions {
        return UIView.AnimationOptions(rawValue: UInt(curve.rawValue << 16))
    }
    
    init?(notification: Notification) {
        guard let userInfo = (notification as NSNotification).userInfo else { return nil }
        guard let type = KeyboardEventType(name: notification.name) else { return nil }
        guard let begin = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue else { return nil }
        guard let end = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return nil }
        guard
            let curveInt = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.intValue,
            let curve = UIView.AnimationCurve(rawValue: curveInt)
            else { return nil }
        guard
            let durationDouble = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue
            else { return nil }
        
        self.type = type
        self.keyboardFrameBegin = begin
        self.keyboardFrameEnd = end
        self.curve = curve
        self.duration = TimeInterval(durationDouble)
        
        guard let isLocalInt = (userInfo[UIResponder.keyboardIsLocalUserInfoKey] as? NSNumber)?.intValue else { return nil }
        self.isLocal = isLocalInt == 1
    }
}

enum KeyboardState {
    case initial
    case showing
    case shown
    case hiding
    case hidden
    case changing
}

typealias KeyboardEventClosure = ((_ event: KeyboardEvent) -> Void)

final class KeyboardObserver {
    var state = KeyboardState.initial
    var isEnabled = true
    fileprivate var eventClosures = [KeyboardEventClosure]()
    
    deinit {
        eventClosures.removeAll()
        KeyboardEventType.allEventNames().forEach {
            NotificationCenter.default.removeObserver(self, name: $0, object: nil)
        }
    }
    
    init() {
        KeyboardEventType.allEventNames().forEach {
            NotificationCenter.default.addObserver(self, selector: #selector(notified(_:)), name: $0, object: nil)
        }
    }
    
    func observe(_ event: @escaping KeyboardEventClosure) {
        eventClosures.append(event)
    }
}

extension KeyboardObserver {
    @objc func notified(_ notification: Notification) {
        guard let event = KeyboardEvent(notification: notification) else { return }
        
        switch event.type {
        case .willShow:
            state = .showing
        case .didShow:
            state = .shown
        case .willHide:
            state = .hiding
        case .didHide:
            state = .hidden
        case .willChangeFrame:
            state = .changing
        case .didChangeFrame:
            state = .shown
        }
        
        if !isEnabled { return }
        eventClosures.forEach { $0(event) }
    }
}
  • BottomConstraintFixable.swift
protocol BottomConstraintFixable {}

extension BottomConstraintFixable where Self: UIViewController {
    /* Consider safeAreaInsets bottom */
    func animateTextFieldConstraint(event: KeyboardEvent,
                                    bottomSpacing: NSLayoutConstraint? = nil,
                                    scrollView: UIScrollView? = nil) {
        switch event.type {
        case .willShow, .willHide, .willChangeFrame:
            let distance = UIScreen.main.bounds.height - event.keyboardFrameEnd.origin.y - self.view.safeAreaInsets.bottom
            let bottom = max(distance, 0)
            
            if let bottomSpacing = bottomSpacing {
                bottomSpacing.constant = bottom
            }
            
            if let scrollView = scrollView {
                scrollView.contentInset.bottom = bottom
            }
            
            UIView.animate(withDuration: event.duration, delay: 0.0, options: event.options, animations: {
                self.view.layoutIfNeeded()
            })
        default: break
        }
    }
}
  • Usage
import UIKit

final class ViewController: UIViewController, BottomConstraintFixable {
    
    @IBOutlet weak private var textField: UITextField!
    @IBOutlet weak private var bottomConstraint: NSLayoutConstraint!
    let keyboardObserver = KeyboardObserver()

    override func viewDidLoad() {
        super.viewDidLoad()
        textField.becomeFirstResponder()
        
        keyboardObserver.observe { [unowned self] event in
            print(event.type)
            self.animateTextFieldConstraint(event: event,
                                            bottomSpacing: self.bottomConstraint)
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment