Skip to content

Instantly share code, notes, and snippets.

@oconnelltoby
Last active December 23, 2021 14:13
Show Gist options
  • Save oconnelltoby/f87dc027b47ffa4c53f845947388a110 to your computer and use it in GitHub Desktop.
Save oconnelltoby/f87dc027b47ffa4c53f845947388a110 to your computer and use it in GitHub Desktop.
KeyboardLayoutGuide
import UIKit
import Combine
extension NSLayoutConstraint {
@discardableResult
func withPriority(_ priority: UILayoutPriority) -> Self {
self.priority = priority
return self
}
}
public extension UIView {
private static let id = UUID().uuidString
var oldKeyboardLayoutGuide: UILayoutGuide {
getKeyboardLayoutGuide() ?? makeKeyboardLayoutGuide()
}
private func getKeyboardLayoutGuide() -> UILayoutGuide? {
layoutGuides.first { $0.identifier == Self.id }
}
private func makeKeyboardLayoutGuide() -> UILayoutGuide {
let guide = KeyboardLayoutGuide()
guide.identifier = Self.id
addLayoutGuide(guide)
return guide
}
}
private class KeyboardLayoutGuide: UILayoutGuide {
override var owningView: UIView? {
didSet {
setupConstraints(for: owningView)
}
}
private var topConstraint: NSLayoutConstraint?
private var cancelable: Cancellable?
private lazy var publisher: AnyPublisher<Notification, Never> = {
let notificationNames = [UIApplication.keyboardWillHideNotification, UIApplication.keyboardWillShowNotification]
let publishers = notificationNames.map { NotificationCenter.default.publisher(for: $0, object: nil) }
return Publishers.MergeMany(publishers).eraseToAnyPublisher()
}()
override init() {
super.init()
cancelable = observeKeyboardHeight()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("`init?(coder: NSCoder)` is unsupported.")
}
private func setupConstraints(for view: UIView?) {
guard let view = view else { return }
let topConstraint = topAnchor.constraint(equalTo: view.bottomAnchor).withPriority(.defaultHigh)
NSLayoutConstraint.activate([
topConstraint,
topAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor),
leadingAnchor.constraint(equalTo: view.leadingAnchor),
trailingAnchor.constraint(equalTo: view.trailingAnchor),
bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
self.topConstraint = topConstraint
}
func observeKeyboardHeight() -> Cancellable {
publisher.sink { [weak self] notification in
guard let self = self else { return }
guard let owningView = self.owningView else { return }
guard let endFrame = notification.keyboardFrameEnd else { return }
guard let animationOptions = notification.animationOptions else { return }
guard let duration = notification.animationDuration else { return }
UIView.animate(
withDuration: duration,
delay: 0,
options: [animationOptions, .beginFromCurrentState],
animations: {
let convertedFrame = owningView.convert(endFrame, from: UIScreen.main.coordinateSpace)
let height = max(0, owningView.bounds.maxY - convertedFrame.minY)
self.topConstraint?.constant = -height
self.owningView?.layoutIfNeeded()
}
)
}
}
}
private extension Notification {
var keyboardFrameEnd: CGRect? {
userInfo?[UIApplication.keyboardFrameEndUserInfoKey] as? CGRect
}
var animationOptions: UIView.AnimationOptions? {
let rawValue = userInfo?[UIApplication.keyboardAnimationCurveUserInfoKey] as? Int
let animationCurve = rawValue.flatMap { UIView.AnimationCurve(rawValue: $0) }
return animationCurve.map { UIView.AnimationOptions($0) }
}
var animationDuration: TimeInterval? {
userInfo?[UIApplication.keyboardAnimationDurationUserInfoKey] as? TimeInterval
}
}
private extension UIView.AnimationOptions {
init(_ curve: UIView.AnimationCurve) {
switch curve {
case .easeInOut:
self = .curveEaseInOut
case .easeIn:
self = .curveEaseIn
case .easeOut:
self = .curveEaseOut
case .linear:
self = .curveLinear
@unknown default:
self = .curveEaseInOut
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment