Created
April 28, 2025 03:10
-
-
Save pbk20191/f59f24c2cb041930ada1bcb5ca8d3e58 to your computer and use it in GitHub Desktop.
CompatKeyboardLayoutGuide
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CompatKeyboardLayoutGuide.swift | |
// | |
import UIKit | |
import Combine | |
// Backport LayoutGuide for UIKeyboardLayout (This only works for Docked Keyboard) | |
@MainActor | |
public class CompatDockedKeyboardLayoutGuide: UILayoutGuide { | |
private var backing = ContiguousArray<NSLayoutConstraint>() | |
private var previouseRect = CGRect.null | |
private let arrays = ( | |
UIResponder.keyboardWillShowNotification, | |
UIResponder.keyboardWillHideNotification | |
) | |
private var managed = false | |
public override weak var owningView: UIView? { | |
didSet { | |
if oldValue != owningView { | |
if let owningView { | |
self.configure(owningView) | |
installForInitialState() | |
if !previouseRect.isNull { | |
self.dispatchChange(previouseRect) | |
} | |
} | |
} | |
} | |
willSet { | |
if newValue != owningView { | |
backing.forEach{ | |
$0.isActive = false | |
} | |
showConstraint?.isActive = false | |
} | |
precondition(!(newValue is UIWindow), "You can not attach the KeyboardLayoutGuide directly to the Window") | |
} | |
} | |
private let useLegacyKeyboard = false | |
private var hideConstraint:NSLayoutConstraint? | |
private var showConstraint:NSLayoutConstraint? | |
// private var leading:NSLayoutConstraint? | |
// private var bottom: NSLayoutConstraint? | |
// private var trailing:NSLayoutConstraint? | |
// | |
// // Keyboard height constraint | |
// private var heightConstraint: NSLayoutConstraint? | |
// | |
private func installForInitialState() { | |
guard !managed, previouseRect.isNull else { | |
return | |
} | |
// if this is called before `UIApplicationMain` | |
guard let _ = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) else { | |
managed = true | |
return | |
} | |
// no first responder for now | |
guard UIResponder.searchFirstResponder() != nil else { | |
managed = true | |
return | |
} | |
let keyWindow = UIApplication.shared.connectedScenes | |
.compactMap{ $0 as? UIWindowScene } | |
.compactMap{ | |
if #available(iOS 15.0, *) { | |
$0.keyWindow | |
} else { | |
$0.windows.first(where: \.isKeyWindow) | |
} | |
}.first | |
if let keyWindow, let scene = keyWindow.windowScene { | |
let inputWindow = scene.windows.first { | |
NSStringFromClass(type(of: $0)).hasPrefix("UIText") | |
} | |
if let rect = inputWindow?.rootViewController?.view.subviews.first?.frame { | |
previouseRect = rect | |
} | |
} | |
managed = true | |
} | |
public override init() { | |
super.init() | |
installObserver() | |
} | |
private func installObserver() { | |
if #available(iOS 15.0, *) { | |
if useLegacyKeyboard { | |
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: arrays.0, object: nil) | |
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: arrays.1, object: nil) | |
} | |
} else { | |
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: arrays.0, object: nil) | |
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: arrays.1, object: nil) | |
} | |
} | |
public required init?(coder: NSCoder) { | |
super.init(coder: coder) | |
installObserver() | |
} | |
deinit { | |
NotificationCenter.default.removeObserver(self, name: arrays.0, object: nil) | |
NotificationCenter.default.removeObserver(self, name: arrays.1, object: nil) | |
} | |
private func configure(_ view:UIView) { | |
if #available(iOS 15, *), !useLegacyKeyboard { | |
backing = [] | |
let guide = view.keyboardLayoutGuide | |
backing.append( | |
guide.centerXAnchor.constraint(equalTo: centerXAnchor) | |
) | |
backing.append( | |
guide.centerYAnchor.constraint(equalTo: centerYAnchor) | |
) | |
backing.append( | |
guide.widthAnchor.constraint(equalTo: widthAnchor) | |
) | |
backing.append( | |
guide.heightAnchor.constraint(equalTo: heightAnchor) | |
) | |
backing.forEach { | |
$0.isActive = true | |
} | |
} else { | |
backing = [] | |
backing.append( | |
view.bottomAnchor.constraint(equalTo: bottomAnchor) | |
) | |
backing.append( | |
view.centerXAnchor.constraint(equalTo: centerXAnchor) | |
) | |
backing.append( | |
view.widthAnchor.constraint(equalTo: widthAnchor) | |
) | |
let constraint = view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: topAnchor) | |
backing.append( | |
constraint | |
) | |
backing.forEach{ | |
$0.isActive = true | |
} | |
self.hideConstraint = constraint | |
self.showConstraint = view.bottomAnchor.constraint(equalTo: topAnchor) | |
showConstraint?.isActive = false | |
} | |
} | |
@objc private func handleKeyboardNotification(_ notification: Notification) { | |
guard Thread.isMainThread, notification.name == UIResponder.keyboardWillHideNotification || notification.name == UIResponder.keyboardWillShowNotification else { | |
return | |
} | |
managed = true | |
let isHide = notification.name == UIResponder.keyboardWillHideNotification | |
guard let owningView = owningView, | |
let userInfo = notification.userInfo, | |
let keyboardFrameEnd = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect, | |
let animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double, | |
let animationCurveRawValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt, | |
let keyboardFrameBegin = userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect | |
else { | |
return | |
} | |
self.previouseRect = keyboardFrameEnd | |
let animationOption = if animationCurveRawValue == 0 { | |
[] as UIView.AnimationOptions | |
} else { | |
UIView.AnimationOptions(rawValue: animationCurveRawValue << 16) | |
} | |
// "UIKeyboardCenterBeginUserInfoKey" | |
// "UIKeyboardCenterEndUserInfoKey" | |
// "UIKeyboardBoundsUserInfoKey" | |
let convertedKeyboardFrameEnd = UIScreen.main.coordinateSpace.convert(keyboardFrameEnd, to: owningView) | |
let viewIntersection = owningView.bounds.intersection(convertedKeyboardFrameEnd) | |
let bottomOffset = if isHide { | |
0.0 as CGFloat | |
} else if !viewIntersection.isEmpty { | |
viewIntersection.height | |
} else { | |
0.0 as CGFloat | |
} | |
UIView.animate(withDuration: animationDuration, delay: 0, options: animationOption) { [showConstraint, owningView, hideConstraint] in | |
if bottomOffset.isZero { | |
showConstraint?.isActive = !bottomOffset.isZero | |
hideConstraint?.isActive = bottomOffset.isZero | |
} else { | |
hideConstraint?.isActive = bottomOffset.isZero | |
showConstraint?.isActive = !bottomOffset.isZero | |
} | |
showConstraint?.constant = bottomOffset | |
owningView.layoutIfNeeded() | |
} | |
} | |
@usableFromInline | |
func dispatchChange(_ rect:CGRect) { | |
guard let owningView else { | |
return | |
} | |
let converted = UIScreen.main.coordinateSpace.convert(rect, to: owningView) | |
let intersection = owningView.bounds.intersection(converted) | |
let offset = if !intersection.isEmpty { | |
intersection.height | |
} else { | |
0.0 as CGFloat | |
} | |
hideConstraint?.isActive = offset.isZero | |
showConstraint?.isActive = !offset.isZero | |
showConstraint?.constant = offset | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment