Last active
December 2, 2020 02:38
-
-
Save keitaoouchi/a047d25e8c73692d20b19f1e0a9b5949 to your computer and use it in GitHub Desktop.
まぁまぁ再利用しがちなキーボードマネージャー
This file contains 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
import UIKit | |
/// キーボードイベントを監視し、キーボードの開閉に合わせてviewをtransformで上下移動させる | |
final class KeyboardManager { | |
private var view: UIView | |
/// キーボード開閉時に可能なら表示されるように画面スライド位置が調整されるビュー | |
/// 縦長フォームの一番下にあるサブミットボタンみたいなやつを想定 | |
private var subFocusView: UIView? | |
/// キーボードスライド時の最下部ビューとキーボード間のマージン | |
private var margin: CGFloat = 8.0 | |
private var keyboardFrameCache: CGRect? | |
init(in view: UIView, subFocusView: UIView? = nil, margin: CGFloat = 8.0) { | |
self.view = view | |
self.subFocusView = subFocusView | |
self.margin = margin | |
self.observeKeyboardEvents() | |
} | |
deinit { | |
NotificationCenter.default.removeObserver(self) | |
} | |
} | |
private extension KeyboardManager { | |
/// キーボードの開閉に合わせてMessageInputViewControllerがキーボードに隠されないようにする | |
/// - Note: [Managing the Keyboard](https://developer.apple.com/library/content/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html) | |
/// - Note: [キーボードイベント通知](https://developer.apple.com/documentation/uikit/uiwindow/keyboard_notification_user_info_keys?language=objc) | |
func observeKeyboardEvents() { | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(onKeyboardWillShow(notification:)), | |
name: UIWindow.keyboardWillShowNotification, | |
object: nil) | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(onKeyboardWillShow(notification:)), | |
name: UIWindow.keyboardWillChangeFrameNotification, | |
object: nil) | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(onKeyboardWillHide(notification:)), | |
name: UIWindow.keyboardWillHideNotification, | |
object: nil) | |
/// キーボード表示中にテキストフィールドのフォーカスが変更されたときにキーボードスライド量の再計算をする | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(onKeyboardWillShow(notification:)), | |
name: UITextField.textDidBeginEditingNotification, | |
object: nil) | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(onKeyboardWillShow(notification:)), | |
name: UITextView.textDidBeginEditingNotification, | |
object: nil) | |
} | |
/// フォーカスされたサブビューを画面内から探し、フォーカスされたサブビューが表示されるように画面全体を縦方向にスライドさせる | |
@objc private func onKeyboardWillShow(notification: Notification) { | |
if let keyboardFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { | |
// UITextField.textDidBeginEditingNotificationで画面スライドを実行するためにキャッシュする | |
self.keyboardFrameCache = keyboardFrame | |
} | |
guard let keyboardRect = self.keyboardFrameCache else { return } | |
// safeAreaのボトムインセット高さをキーボード高さとボトム位置から除外する | |
let insetBottomAdjustment: CGFloat | |
if #available(iOS 11.0, *) { | |
insetBottomAdjustment = view.safeAreaInsets.bottom | |
} else { | |
insetBottomAdjustment = 0.0 | |
} | |
// 画面スライド時にsafeAreaのトップにかかぶらないようにする | |
let insetTop: CGFloat | |
if #available(iOS 11.0, *) { | |
insetTop = view.safeAreaInsets.top | |
} else { | |
insetTop = 0.0 | |
} | |
let subFocusViewsBottomYFromBottom: CGFloat | |
if let subFocusView = subFocusView { | |
let rect = subFocusView.convert(subFocusView.bounds, to: view) | |
subFocusViewsBottomYFromBottom = view.bounds.height - (rect.maxY + margin + insetBottomAdjustment) | |
} else { | |
subFocusViewsBottomYFromBottom = CGFloat.greatestFiniteMagnitude | |
} | |
if let firstResponder = self.findFirstResponder(in: self.view) { | |
let firstRespondersRect = firstResponder.convert(firstResponder.bounds, to: view) | |
// 画面底からフォーカスビューの底までの高さ(マージン考慮) | |
let bottomYFromBottom = view.bounds.height - (firstRespondersRect.maxY + margin + insetBottomAdjustment) | |
// 画面底からフォーカスビューのトップまでの高さ | |
let topYFromBottom = bottomYFromBottom + firstRespondersRect.height | |
let keyboardHeight = keyboardRect.height - insetBottomAdjustment | |
let deltaY = min(bottomYFromBottom, subFocusViewsBottomYFromBottom) | |
// 一番底のViewを調整位置とした画面スライド量 | |
var adjustmentHeight: CGFloat = keyboardHeight - deltaY | |
// フォーカスビューとサブフォーカスビューが表示領域に収まらなければフォーカスビューを基準位置としてスライド量修正 | |
let threashold = view.bounds.height - insetBottomAdjustment - insetTop | |
if topYFromBottom - subFocusViewsBottomYFromBottom + adjustmentHeight > threashold { | |
adjustmentHeight = keyboardHeight - topYFromBottom | |
} | |
if adjustmentHeight < 0 { | |
// キーボードを表示しきってもフォーカスビューが隠れないのでスライド位置をデフォルト位置に戻す | |
self.animateKeyboard(.identity, with: notification) | |
} else { | |
let transform = CGAffineTransform(translationX: 0, y: -adjustmentHeight) | |
self.animateKeyboard(transform, with: notification) | |
} | |
} | |
} | |
/// キーボード非表示時にviewのスライドを元に戻す | |
@objc func onKeyboardWillHide(notification: Notification) { | |
self.animateKeyboard(.identity, with: notification) | |
} | |
/// フォーカスされているビューを探して返す | |
func findFirstResponder(in view: UIView) -> UIView? { | |
for subView in view.subviews { | |
if subView.isFirstResponder { | |
return subView | |
} else if let firstResponder = self.findFirstResponder(in: subView) { | |
return firstResponder | |
} | |
} | |
return nil | |
} | |
/// Notificationからアニメーション情報を取得して与えられたtransformでアニメーションを実行 | |
func animateKeyboard(_ transform: CGAffineTransform, with notification: Notification) { | |
self.view.layer.removeAllAnimations() | |
guard | |
let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval, | |
let curve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else { | |
UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveEaseInOut, animations: { | |
self.view.transform = transform | |
}) | |
return | |
} | |
let animationCurve = UIView.AnimationOptions(rawValue: curve) | |
UIView.animate(withDuration: duration, delay: 0.0, options: animationCurve, animations: { | |
self.view.transform = transform | |
}) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment