diff --git a/Sources/KeyboardAvoiding.swift b/Sources/KeyboardAvoiding.swift index d9dd059..6df48b1 100644 --- a/Sources/KeyboardAvoiding.swift +++ b/Sources/KeyboardAvoiding.swift @@ -36,7 +36,7 @@ import UIKit } } public static var keyboardAvoidingMode = KeyboardAvoidingMode.minimum - @objc public static var avoidingBlock: ((Bool, CGFloat, CGFloat, UIView.AnimationOptions)->Void)? { + @objc public static var avoidingBlock: ((Bool, CGFloat, CGFloat, UIView.AnimationOptions) -> Void)? { willSet { self.initialise() } @@ -55,74 +55,88 @@ import UIKit self.setAvoidingView(newValue, withOptionalTriggerView: newValue) } } - class func didChange(_ notification: Foundation.Notification) { var isKeyBoardShowing = false // isKeyBoardShowing and is it merged and docked. - - let isPortrait = UIApplication.shared.statusBarOrientation.isPortrait + let isPortrait = UIWindow.getOrientation() == .portrait // get the keyboard & window frames - - let keyboardFrame = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect + guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { + return + } // keyboardHeightDiff used when user is switching between different keyboards that have different heights - var keyboardFrameBegin = notification.userInfo![UIResponder.keyboardFrameBeginUserInfoKey] as! CGRect - + var mutablekeyboardFrameBegin: CGRect! + let strKeyboardFrame = UIResponder.keyboardFrameBeginUserInfoKey + guard let keyboardFrameBegin = notification.userInfo?[strKeyboardFrame] as? CGRect else { + return + } + mutablekeyboardFrameBegin = keyboardFrameBegin // hack for bug in iOS 11.2 - let keyboardFrameEnd = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect - if keyboardFrameEnd.size.height > keyboardFrameBegin.size.height { - keyboardFrameBegin = CGRect(x: keyboardFrameBegin.origin.x, y: keyboardFrameBegin.origin.y, width: keyboardFrameBegin.size.width, height: keyboardFrameEnd.size.height) + if let keyboardFrameEnd = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { + if keyboardFrameEnd.size.height > keyboardFrameBegin.size.height { + let kbX = keyboardFrameBegin.origin.x + let kbY = keyboardFrameBegin.origin.y + let kbWidth = keyboardFrameBegin.size.width + let kbHeight = keyboardFrameBegin.size.height + mutablekeyboardFrameBegin = CGRect(x: kbX, y: kbY, width: kbWidth, height: kbHeight) + } } - - var keyboardHeightDiff:CGFloat = 0.0 - if keyboardFrameBegin.size.height > 0 { - keyboardHeightDiff = keyboardFrameBegin.size.height - keyboardFrame.size.height + + var keyboardHeightDiff: CGFloat = 0.0 + if mutablekeyboardFrameBegin.size.height > 0 { + keyboardHeightDiff = mutablekeyboardFrameBegin.size.height - keyboardFrame.size.height } let screenSize = UIScreen.main.bounds.size - let animationCurve = notification.userInfo![UIResponder.keyboardAnimationCurveUserInfoKey] as! Int - let animationOptions = animationCurve << 16 + var animationOptions = 0 + if let animationCurve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int { + animationOptions = animationCurve << 16 + } + // if split keyboard is being dragged, then skip notification - - if keyboardFrame.size.height == 0 && (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad) { + + if keyboardFrame.size.height == 0 && (UIDevice.current.userInterfaceIdiom == UIUserInterfaceIdiom.pad) { if isPortrait && keyboardFrameBegin.origin.y + keyboardFrameBegin.size.height == screenSize.height { return - } - else if !isPortrait && keyboardFrameBegin.origin.x + keyboardFrameBegin.size.width == screenSize.width { + } else if !isPortrait && keyboardFrameBegin.origin.x + keyboardFrameBegin.size.width == screenSize.width { return } } + let kbfY = keyboardFrame.origin.y + let kbfHeight = keyboardFrame.size.height // calculate if we are to move up the avoiding view - if !keyboardFrame.isEmpty && (keyboardFrame.origin.y == 0 || (keyboardFrame.origin.y + keyboardFrame.size.height == screenSize.height)) { + if !keyboardFrame.isEmpty && (kbfY == 0 || (kbfY + kbfHeight == screenSize.height)) { isKeyBoardShowing = true self.lastNotification = notification } - + // get animation duration - var animationDuration = notification.userInfo![UIResponder.keyboardAnimationDurationUserInfoKey] as! CGFloat + var animationDuration: CGFloat! + let strKeyboardAnimation = UIResponder.keyboardAnimationDurationUserInfoKey + if let animationDurationTemp = notification.userInfo?[strKeyboardAnimation] as? CGFloat { + animationDuration = animationDurationTemp + } if animationDuration == 0 { // custom keyboards often dont animate, its too clanky so have to manually set this animationDuration = 0.1 } + if isKeyBoardShowing { self.showingAnimationCount = 0 for triggerView in self.triggerViews { - - //showing and docked + + // showing and docked var diff: CGFloat = 0.0 if keyboardHeightDiff != 0 { // if keyboard height is changing and avoidingView is currently moved diff = keyboardHeightDiff - } - else { + } else { let originInWindow = triggerView.convert(triggerView.bounds.origin, to: nil) - switch UIApplication.shared.statusBarOrientation { + switch UIWindow.getOrientation() { case .portrait, .landscapeLeft: diff = keyboardFrame.origin.y - diff = diff - (originInWindow.y + triggerView.frame.size.height) - break + diff -= (originInWindow.y + triggerView.frame.size.height) case .portraitUpsideDown, .landscapeRight: diff = screenSize.height - keyboardFrame.size.height - diff = diff - (originInWindow.y + triggerView.frame.size.height) - break + diff -= (originInWindow.y + triggerView.frame.size.height) default: break } @@ -133,27 +147,30 @@ import UIKit switch self.keyboardAvoidingMode { case .maximum: self.minimumAnimationDuration = animationDuration - break case .minimumDelayed: let minimumDisplacement = max(displacement, diff) - self.minimumAnimationDuration = animationDuration * (minimumDisplacement / displacement) + self.minimumAnimationDuration = animationDuration * CGFloat(minimumDisplacement / displacement) displacement = minimumDisplacement - self.paddingForCurrentAvoidingView delay = (animationDuration - self.minimumAnimationDuration) animationDuration = self.minimumAnimationDuration - break default: let minimumDisplacement = max(displacement, diff) - displacement = minimumDisplacement - (keyboardHeightDiff == 0 ? self.paddingForCurrentAvoidingView : 0) - + displacement = minimumDisplacement - (keyboardHeightDiff == 0 + ? self.paddingForCurrentAvoidingView + : 0) } - + if self.avoidingView != nil && self.avoidingView!.superview != nil { if self.avoidingViewUsesAutoLayout { // if view uses constraints var hasFoundFirstConstraint = false for constraint: NSLayoutConstraint in self.avoidingView!.superview!.constraints { - - if let secondItem = constraint.secondItem as? NSObject, secondItem == self.avoidingView! && (constraint.secondAttribute == .centerY || constraint.secondAttribute == .top || constraint.secondAttribute == .bottom) { + let cntrSecAttr = constraint.secondAttribute + let condition2 = (cntrSecAttr == .centerY + || cntrSecAttr == .top + || cntrSecAttr == .bottom) + if let secondItem = constraint.secondItem as? NSObject, + secondItem == self.avoidingView! && condition2 { if !self.updatedConstraints.contains(constraint) { self.updatedConstraints.append(constraint) self.updatedConstraintConstants.append(constraint.constant) @@ -162,11 +179,16 @@ import UIKit hasFoundFirstConstraint = true } } - + if !hasFoundFirstConstraint { - // if the constraint.secondItem wasn't found, sometimes its the constraint.firstItem that needs to be updated + // if the constraint.secondItem wasn't found, + // sometimes its the constraint.firstItem that needs to be updated for constraint: NSLayoutConstraint in self.avoidingView!.superview!.constraints { - if constraint.firstItem as! NSObject == self.avoidingView! && (constraint.firstAttribute == .centerY || constraint.firstAttribute == .top || constraint.firstAttribute == .bottom) { + let cntrFirstAttr = constraint.firstAttribute + let condition2 = ( cntrFirstAttr == .centerY + || cntrFirstAttr == .top + || cntrFirstAttr == .bottom) + if constraint.firstItem as? NSObject == self.avoidingView! && condition2 { if !self.updatedConstraints.contains(constraint) { self.updatedConstraints.append(constraint) self.updatedConstraintConstants.append(constraint.constant) @@ -178,40 +200,44 @@ import UIKit self.avoidingView!.superview!.setNeedsUpdateConstraints() } self.showingAnimationCount += 1 - - UIView.animate(withDuration: TimeInterval(animationDuration), delay: TimeInterval(delay), options: UIView.AnimationOptions(rawValue: UInt(animationOptions)), animations: {() -> Void in + let animationOption = UIView.AnimationOptions(rawValue: UInt(animationOptions)) + UIView.animate(withDuration: TimeInterval(animationDuration), + delay: TimeInterval(delay), + options: animationOption, + animations: {() -> Void in + if self.avoidingViewUsesAutoLayout { self.avoidingView!.superview!.layoutIfNeeded() // to animate constraint changes - } - else { + } else { var transform = self.avoidingView!.transform transform = transform.translatedBy(x: 0, y: displacement) self.avoidingView!.transform = transform } + }, completion: { _ in self.showingAnimationCount -= 1 }) } } if self.avoidingBlock != nil { - self.avoidingBlock!(isKeyBoardShowing, animationDuration, displacement, UIView.AnimationOptions(rawValue: UInt(animationOptions))) + self.avoidingBlock!(isKeyBoardShowing, + animationDuration, + displacement, + UIView.AnimationOptions(rawValue: UInt(animationOptions))) } } - - } - else if self.isKeyboardVisible { + } else if self.isKeyboardVisible { // hiding, undocking or splitting switch self.keyboardAvoidingMode { case .maximum: break case .minimumDelayed: animationDuration = self.minimumAnimationDuration - break default: break } - + // restore state if self.avoidingView != nil && self.avoidingView!.superview != nil { if self.avoidingViewUsesAutoLayout { @@ -222,13 +248,17 @@ import UIKit } self.avoidingView!.superview!.setNeedsUpdateConstraints() } - UIView.animate(withDuration: TimeInterval(animationDuration + CGFloat(0.075)), delay: 0, options: UIView.AnimationOptions(rawValue: UInt(animationOptions)), animations: {() -> Void in + UIView.animate(withDuration: TimeInterval(animationDuration + CGFloat(0.075)), + delay: 0, + options: UIView.AnimationOptions(rawValue: UInt(animationOptions)), + animations: {() -> Void in + if self.avoidingViewUsesAutoLayout { self.avoidingView!.superview!.layoutIfNeeded() - } - else { + } else { self.avoidingView!.transform = CGAffineTransform.identity } + }, completion: {(_ finished: Bool) -> Void in if self.showingAnimationCount <= 0 { self.updatedConstraints.removeAll() @@ -237,28 +267,36 @@ import UIKit }) } if self.avoidingBlock != nil { - self.avoidingBlock!(isKeyBoardShowing, animationDuration + 0.075, 0, UIView.AnimationOptions(rawValue: UInt(animationOptions))) + self.avoidingBlock!(isKeyBoardShowing, + animationDuration + 0.075, + 0, + UIView.AnimationOptions(rawValue: UInt(animationOptions))) } } - self.isKeyboardVisible = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(screenSize.width), height: CGFloat(screenSize.height)).intersects(keyboardFrame) + self.isKeyboardVisible = CGRect(x: CGFloat(0), + y: CGFloat(0), + width: CGFloat(screenSize.width), + height: CGFloat(screenSize.height)).intersects(keyboardFrame) } - + // The triggerView is required if the avoidingView isn't nil @objc public class func setAvoidingView(_ avoidingView: UIView?, withTriggerView triggerView: UIView) { self.setAvoidingView(avoidingView, withOptionalTriggerView: triggerView) } - + private class func setAvoidingView(_ avoidingView: UIView?, withOptionalTriggerView triggerView: UIView?) { self.initialise() - + self._avoidingView = avoidingView - self.avoidingViewUsesAutoLayout = (avoidingView != nil && avoidingView!.superview != nil) ? avoidingView!.superview!.constraints.count > 0 : false - + self.avoidingViewUsesAutoLayout = (avoidingView != nil + && avoidingView!.superview != nil) + ? avoidingView!.superview!.constraints.count > 0 + : false + self.triggerViews.removeAll() if triggerView != nil { self.triggerViews.append(triggerView!) } - self.paddingForCurrentAvoidingView = self.padding self.avoidingBlock = nil if self.isKeyboardVisible && avoidingView != nil && self.lastNotification != nil { @@ -266,34 +304,61 @@ import UIKit self.didChange(self.lastNotification!) } } - + public class func addTriggerView(_ triggerView: UIView) { self.triggerViews.append(triggerView) } - + public class func removeTriggerView(_ triggerView: UIView) { if let index = triggerViews.firstIndex(of: triggerView) { self.triggerViews.remove(at: index) } } - + public class func removeAll() { self.triggerViews.removeAll() self.avoidingView = nil self.avoidingBlock = nil } - + private class func initialise() { // make sure we only add this once if self.avoidingBlock == nil && self.avoidingView == nil { - NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: OperationQueue.main, using: { notification in + let notificationName = UIApplication.didEnterBackgroundNotification + NotificationCenter.default.addObserver(forName: notificationName, + object: nil, + queue: OperationQueue.main, + using: { _ in // Autolayout is reset when app goes into background, so we need to dismiss the keyboard too UIApplication.shared.windows.first?.rootViewController?.view.endEditing(true) }) - NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillChangeFrameNotification, object: nil, queue: OperationQueue.main, using: { notification in + let notificationNameKeyboardWillChange = UIResponder.keyboardWillChangeFrameNotification + NotificationCenter.default.addObserver(forName: notificationNameKeyboardWillChange, + object: nil, + queue: OperationQueue.main, + using: { notification in self.didChange(notification) }) } } } +extension UIWindow { + class func getOrientation() -> UIInterfaceOrientation { + if #available(iOS 13.0, *) { + let sceneFirst = UIApplication.shared.connectedScenes.first + guard let windowScene = sceneFirst as? UIWindowScene, windowScene.activationState == .foregroundActive else { + return .unknown + } + + if windowScene.windows.first != nil { + return windowScene.interfaceOrientation + } + return .unknown + } else { + // Fallback on earlier versions + return UIApplication.shared.statusBarOrientation + } + + } +} diff --git a/Sources/KeyboardDismissingView.swift b/Sources/KeyboardDismissingView.swift index 52c1d52..440636c 100644 --- a/Sources/KeyboardDismissingView.swift +++ b/Sources/KeyboardDismissingView.swift @@ -9,21 +9,21 @@ import UIKit @objc public class KeyboardDismissingView: UIView { - + public var dismissingBlock: (() -> Void)? public var touchEndedBlock: (() -> Void)? - + override public func touchesEnded(_ touches: Set, with event: UIEvent?) { super.touchesEnded(touches, with: event) - + let isDismissing = KeyboardDismissingView.resignAnyFirstResponder(self) - + if isDismissing { self.dismissingBlock?() } self.touchEndedBlock?() } - + @discardableResult public class func resignAnyFirstResponder(_ view: UIView) -> Bool { var hasResigned = false for subView in view.subviews { @@ -33,8 +33,7 @@ import UIKit if let searchBar = subView as? UISearchBar { searchBar.setShowsCancelButton(false, animated: true) } - } - else { + } else { hasResigned = KeyboardDismissingView.resignAnyFirstResponder(subView) || hasResigned } }