//
|
// IQKeyboardManager+UIKeyboardNotification.swift
|
// https://github.com/hackiftekhar/IQKeyboardManager
|
// Copyright (c) 2013-20 Iftekhar Qurashi.
|
//
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// of this software and associated documentation files (the "Software"), to deal
|
// in the Software without restriction, including without limitation the rights
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// copies of the Software, and to permit persons to whom the Software is
|
// furnished to do so, subject to the following conditions:
|
//
|
// The above copyright notice and this permission notice shall be included in
|
// all copies or substantial portions of the Software.
|
//
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
// THE SOFTWARE.
|
|
// import Foundation - UIKit contains Foundation
|
import UIKit
|
|
// MARK: UIKeyboard Notifications
|
@available(iOSApplicationExtension, unavailable)
|
public extension IQKeyboardManager {
|
|
typealias SizeBlock = (_ size: CGSize) -> Void
|
|
private final class KeyboardSizeObserver: NSObject {
|
weak var observer: NSObject?
|
var sizeHandler: (_ size: CGSize) -> Void
|
|
init(observer: NSObject?, sizeHandler: @escaping (_ size: CGSize) -> Void) {
|
self.observer = observer
|
self.sizeHandler = sizeHandler
|
}
|
}
|
|
private struct AssociatedKeys {
|
static var keyboardSizeObservers: Int = 0
|
static var keyboardLastNotifySize: Int = 0
|
static var keyboardShowing: Int = 0
|
static var keyboardShowNotification: Int = 0
|
static var keyboardFrame: Int = 0
|
static var animationDuration: Int = 0
|
static var animationCurve: Int = 0
|
}
|
|
private var keyboardLastNotifySize: CGSize {
|
get {
|
return objc_getAssociatedObject(self, &AssociatedKeys.keyboardLastNotifySize) as? CGSize ?? .zero
|
}
|
set(newValue) {
|
objc_setAssociatedObject(self, &AssociatedKeys.keyboardLastNotifySize, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
}
|
}
|
|
private var keyboardSizeObservers: [AnyHashable: SizeBlock] {
|
get {
|
return objc_getAssociatedObject(self, &AssociatedKeys.keyboardSizeObservers) as? [AnyHashable: SizeBlock] ?? [:]
|
}
|
set(newValue) {
|
objc_setAssociatedObject(self, &AssociatedKeys.keyboardSizeObservers, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
}
|
}
|
|
@objc func registerKeyboardSizeChange(identifier: AnyHashable, sizeHandler: @escaping SizeBlock) {
|
keyboardSizeObservers[identifier] = sizeHandler
|
}
|
|
@objc func unregisterKeyboardSizeChange(identifier: AnyHashable) {
|
keyboardSizeObservers[identifier] = nil
|
}
|
|
internal func notifyKeyboardSize(size: CGSize) {
|
|
guard !size.equalTo(keyboardLastNotifySize) else {
|
return
|
}
|
|
keyboardLastNotifySize = size
|
|
for block in keyboardSizeObservers.values {
|
block(size)
|
}
|
}
|
|
/**
|
Boolean to know if keyboard is showing.
|
*/
|
@objc private(set) var keyboardShowing: Bool {
|
get {
|
return objc_getAssociatedObject(self, &AssociatedKeys.keyboardShowing) as? Bool ?? false
|
}
|
set(newValue) {
|
objc_setAssociatedObject(self, &AssociatedKeys.keyboardShowing, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
}
|
}
|
|
/** To save keyboardWillShowNotification. Needed for enable keyboard functionality. */
|
internal var keyboardShowNotification: Notification? {
|
get {
|
return objc_getAssociatedObject(self, &AssociatedKeys.keyboardShowNotification) as? Notification
|
}
|
set(newValue) {
|
objc_setAssociatedObject(self, &AssociatedKeys.keyboardShowNotification, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
}
|
}
|
|
/** To save keyboard rame. */
|
@objc private(set) var keyboardFrame: CGRect {
|
get {
|
return objc_getAssociatedObject(self, &AssociatedKeys.keyboardFrame) as? CGRect ?? .zero
|
}
|
set(newValue) {
|
objc_setAssociatedObject(self, &AssociatedKeys.keyboardFrame, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
}
|
}
|
|
/** To save keyboard animation duration. */
|
internal var animationDuration: TimeInterval {
|
get {
|
return objc_getAssociatedObject(self, &AssociatedKeys.animationDuration) as? TimeInterval ?? 0.25
|
}
|
set(newValue) {
|
objc_setAssociatedObject(self, &AssociatedKeys.animationDuration, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
}
|
}
|
|
/** To mimic the keyboard animation */
|
internal var animationCurve: UIView.AnimationOptions {
|
get {
|
return objc_getAssociatedObject(self, &AssociatedKeys.animationCurve) as? UIView.AnimationOptions ?? .curveEaseOut
|
}
|
set(newValue) {
|
objc_setAssociatedObject(self, &AssociatedKeys.animationCurve, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
}
|
}
|
|
/* UIKeyboardWillShowNotification. */
|
@objc internal func keyboardWillShow(_ notification: Notification) {
|
|
keyboardShowNotification = notification
|
|
// Boolean to know keyboard is showing/hiding
|
keyboardShowing = true
|
|
let oldKBFrame = keyboardFrame
|
|
if let info = notification.userInfo {
|
|
// Getting keyboard animation.
|
if let curve = info[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt {
|
animationCurve = UIView.AnimationOptions(rawValue: curve).union(.beginFromCurrentState)
|
} else {
|
animationCurve = UIView.AnimationOptions.curveEaseOut.union(.beginFromCurrentState)
|
}
|
|
// Getting keyboard animation duration
|
if let duration = info[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval, duration != 0 {
|
animationDuration = duration
|
} else {
|
animationDuration = 0.25
|
}
|
|
// Getting UIKeyboardSize.
|
if let kbFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
|
keyboardFrame = kbFrame
|
notifyKeyboardSize(size: keyboardFrame.size)
|
showLog("UIKeyboard Frame: \(keyboardFrame)")
|
}
|
}
|
|
guard privateIsEnabled() else {
|
restorePosition()
|
topViewBeginOrigin = IQKeyboardManager.kIQCGPointInvalid
|
topViewBeginSafeAreaInsets = .zero
|
return
|
}
|
|
let startTime = CACurrentMediaTime()
|
showLog("⌨️>>>>> \(#function) started >>>>>", indentation: 1)
|
|
showLog("Notification Object:\(notification.object ?? "NULL")")
|
|
// (Bug ID: #5)
|
if let textFieldView = textFieldView, topViewBeginOrigin.equalTo(IQKeyboardManager.kIQCGPointInvalid) {
|
|
// keyboard is not showing(At the beginning only). We should save rootViewRect.
|
rootViewController = textFieldView.parentContainerViewController()
|
if let controller = rootViewController {
|
|
if rootViewControllerWhilePopGestureRecognizerActive == controller {
|
topViewBeginOrigin = topViewBeginOriginWhilePopGestureRecognizerActive
|
} else {
|
topViewBeginOrigin = controller.view.frame.origin
|
topViewBeginSafeAreaInsets = controller.view.safeAreaInsets
|
}
|
|
rootViewControllerWhilePopGestureRecognizerActive = nil
|
topViewBeginOriginWhilePopGestureRecognizerActive = IQKeyboardManager.kIQCGPointInvalid
|
|
self.showLog("Saving \(controller) beginning origin: \(self.topViewBeginOrigin)")
|
}
|
}
|
|
// If last restored keyboard size is different(any orientation accure), then refresh. otherwise not.
|
if keyboardFrame.equalTo(oldKBFrame) == false {
|
|
// If textFieldView is inside UITableViewController then let UITableViewController to handle it (Bug ID: #37) (Bug ID: #76) See note:- https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html If it is UIAlertView textField then do not affect anything (Bug ID: #70).
|
|
if keyboardShowing,
|
let textFieldView = textFieldView,
|
textFieldView.isAlertViewTextField() == false {
|
|
// keyboard is already showing. adjust position.
|
self.adjustPosition()
|
}
|
}
|
|
let elapsedTime = CACurrentMediaTime() - startTime
|
showLog("⌨️<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
|
}
|
|
/* UIKeyboardWillHideNotification. So setting rootViewController to it's default frame. */
|
@objc internal func keyboardWillHide(_ notification: Notification?) {
|
|
// If it's not a fake notification generated by [self setEnable:NO].
|
if notification != nil {
|
keyboardShowNotification = nil
|
}
|
|
// Boolean to know keyboard is showing/hiding
|
keyboardShowing = false
|
|
if let info = notification?.userInfo {
|
|
// Getting keyboard animation.
|
if let curve = info[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt {
|
animationCurve = UIView.AnimationOptions(rawValue: curve).union(.beginFromCurrentState)
|
} else {
|
animationCurve = UIView.AnimationOptions.curveEaseOut.union(.beginFromCurrentState)
|
}
|
|
// Getting keyboard animation duration
|
if let duration = info[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval, duration != 0 {
|
animationDuration = duration
|
} else {
|
animationDuration = 0.25
|
}
|
}
|
|
// If not enabled then do nothing.
|
guard privateIsEnabled() else {
|
return
|
}
|
|
let startTime = CACurrentMediaTime()
|
showLog("⌨️>>>>> \(#function) started >>>>>", indentation: 1)
|
showLog("Notification Object:\(notification?.object ?? "NULL")")
|
|
// Commented due to #56. Added all the conditions below to handle WKWebView's textFields. (Bug ID: #56)
|
// We are unable to get textField object while keyboard showing on WKWebView's textField. (Bug ID: #11)
|
// if (_textFieldView == nil) return
|
|
// Restoring the contentOffset of the lastScrollView
|
if let lastScrollView = lastScrollView {
|
|
UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
|
|
if lastScrollView.contentInset != self.startingContentInsets {
|
self.showLog("Restoring contentInset to: \(self.startingContentInsets)")
|
lastScrollView.contentInset = self.startingContentInsets
|
lastScrollView.scrollIndicatorInsets = self.startingScrollIndicatorInsets
|
}
|
|
if lastScrollView.shouldRestoreScrollViewContentOffset, !lastScrollView.contentOffset.equalTo(self.startingContentOffset) {
|
self.showLog("Restoring contentOffset to: \(self.startingContentOffset)")
|
|
let animatedContentOffset = self.textFieldView?.superviewOfClassType(UIStackView.self, belowView: lastScrollView) != nil // (Bug ID: #1365, #1508, #1541)
|
|
if animatedContentOffset {
|
lastScrollView.setContentOffset(self.startingContentOffset, animated: UIView.areAnimationsEnabled)
|
} else {
|
lastScrollView.contentOffset = self.startingContentOffset
|
}
|
}
|
|
// TODO: restore scrollView state
|
// This is temporary solution. Have to implement the save and restore scrollView state
|
var superScrollView: UIScrollView? = lastScrollView
|
|
while let scrollView = superScrollView {
|
|
let contentSize = CGSize(width: max(scrollView.contentSize.width, scrollView.frame.width), height: max(scrollView.contentSize.height, scrollView.frame.height))
|
|
let minimumY = contentSize.height - scrollView.frame.height
|
|
if minimumY < scrollView.contentOffset.y {
|
|
let newContentOffset = CGPoint(x: scrollView.contentOffset.x, y: minimumY)
|
if scrollView.contentOffset.equalTo(newContentOffset) == false {
|
|
let animatedContentOffset = self.textFieldView?.superviewOfClassType(UIStackView.self, belowView: scrollView) != nil // (Bug ID: #1365, #1508, #1541)
|
|
if animatedContentOffset {
|
scrollView.setContentOffset(newContentOffset, animated: UIView.areAnimationsEnabled)
|
} else {
|
scrollView.contentOffset = newContentOffset
|
}
|
|
self.showLog("Restoring contentOffset to: \(self.startingContentOffset)")
|
}
|
}
|
|
superScrollView = scrollView.superviewOfClassType(UIScrollView.self) as? UIScrollView
|
}
|
})
|
}
|
|
restorePosition()
|
|
// Reset all values
|
lastScrollView = nil
|
keyboardFrame = .zero
|
notifyKeyboardSize(size: keyboardFrame.size)
|
startingContentInsets = .zero
|
startingScrollIndicatorInsets = .zero
|
startingContentOffset = CGPoint.zero
|
topViewBeginOrigin = IQKeyboardManager.kIQCGPointInvalid
|
topViewBeginSafeAreaInsets = .zero
|
|
let elapsedTime = CACurrentMediaTime() - startTime
|
showLog("⌨️<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
|
}
|
}
|