Skip to content

Instantly share code, notes, and snippets.

@soffes
Last active April 1, 2021 21:33
Show Gist options
  • Save soffes/70784d4dfb6ad355eb98db53a96ae3ae to your computer and use it in GitHub Desktop.
Save soffes/70784d4dfb6ad355eb98db53a96ae3ae to your computer and use it in GitHub Desktop.
KeyboardLayoutGuide
override func viewDidLoad() {
super.viewDidLoad()
let keyboardGuide = KeyboardLayoutGuide()
view.addLayoutGuide(keyboardGuide)
NSLayoutConstraint.activate([
someTextField.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottom, constant: -8),
someTextField.bottomAnchor.constraint(lessThanOrEqualTo: keyboardGuide.topAnchor, constant: -8),
])
}
import UIKit
public struct KeyboardInfo {
// MARK: - Properties
public let frame: CGRect
public let animationDuration: TimeInterval?
public let animationOptions: UIView.AnimationOptions?
// MARK: - Initializers
init(frame: CGRect) {
self.frame = frame
animationDuration = nil
animationOptions = nil
}
init?(notification: Notification) {
guard let userInfo = notification.userInfo,
let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else
{
return nil
}
self.frame = frame
animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
let animationCurve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt
animationOptions = animationCurve.flatMap { UIView.AnimationOptions(rawValue: $0 << 16) }
}
// MARK: - Calculations
public func height(in view: UIView) -> CGFloat {
view.frame.intersection(view.convert(frame, from: nil)).height
}
// MARK: - Animating
public func animateAlongsideKeyboard(_ animations: @escaping () -> Void) {
guard let animationDuration = animationDuration, let animationOptions = animationOptions,
animationDuration > 0 else
{
animations()
return
}
UIView.animate(withDuration: animationDuration, delay: 0, options: animationOptions, animations: animations)
}
}
import Combine
import UIKit
/// Layout guide to automatically track the keyboard.
///
/// - note: If the view changes its frame, you must call `update`.
public final class KeyboardLayoutGuide: UILayoutGuide {
// MARK: - Properties
private var constraints = [NSLayoutConstraint]() {
willSet {
NSLayoutConstraint.deactivate(constraints)
}
didSet {
NSLayoutConstraint.activate(constraints)
}
}
private var owningViewFrame: CGRect?
private var subscriptions = Set<AnyCancellable>()
// MARK: - Initializers
public override init() {
super.init()
identifier = "Keyboard"
KeyboardManager.shared.$info.sink { [weak self] info in
self?.update(with: info)
}.store(in: &subscriptions)
update()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - UILayoutConstraint
public override var owningView: UIView? {
didSet {
owningViewFrame = owningView?.frame
update()
}
}
// MARK: - Updating
/// Must call this when the `owningView` changes its frame
public func update() {
guard let owningView = owningView else {
return
}
if owningView.frame != owningViewFrame {
owningViewFrame = owningView.frame
update(with: KeyboardManager.shared.info)
}
}
// MARK: - Private
private func update(with keyboardInfo: KeyboardInfo?) {
guard let view = owningView, let info = keyboardInfo else {
return
}
let keyboardFrame = view.convert(info.frame, from: nil)
constraints = [
heightAnchor.constraint(equalToConstant: view.frame.intersection(keyboardFrame).height),
// This assumes the keyboard is full width and at the bottom
widthAnchor.constraint(equalTo: view.widthAnchor),
leftAnchor.constraint(equalTo: view.leftAnchor),
bottomAnchor.constraint(equalTo: view.bottomAnchor),
]
info.animateAlongsideKeyboard { [weak view] in
view?.layoutIfNeeded()
}
}
}
import Combine
import UIKit
final class KeyboardManager {
// MARK: - Properties
static let shared = KeyboardManager()
@Published private(set) var info: KeyboardInfo?
// MARK: - Initializers
private init() {
let center = NotificationCenter.default
center.publisher(for: UIApplication.keyboardWillChangeFrameNotification)
.merge(with: center.publisher(for: UIApplication.keyboardDidChangeFrameNotification))
.map(KeyboardInfo.init)
.assign(to: &$info)
}
}
@chrisbrandow
Copy link

Not sure if you intended to include it, but KeyboardInfo is not defined

@soffes
Copy link
Author

soffes commented Mar 17, 2021

@chrisbrandow added!

@chrisbrandow
Copy link

thanks! Also, I'm sure this is a well worn practice, but I've either forgotten, or never knew this is how you use the animation curve value.

let animationCurve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt
animationOptions = animationCurve.flatMap { UIView.AnimationOptions(rawValue: $0 << 16) }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment