228 lines
8.7 KiB
Swift
228 lines
8.7 KiB
Swift
/*
|
|
The MIT License (MIT)
|
|
|
|
Copyright (c) 2015-present Badoo Trading Limited.
|
|
|
|
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
|
|
|
|
class KeyboardTracker {
|
|
|
|
private enum KeyboardStatus {
|
|
case Hidden
|
|
case Showing
|
|
case Shown
|
|
}
|
|
|
|
private var keyboardStatus: KeyboardStatus = .Hidden
|
|
private let view: UIView
|
|
private let inputContainerBottomConstraint: NSLayoutConstraint
|
|
var trackingView: UIView {
|
|
return self.keyboardTrackerView
|
|
}
|
|
private lazy var keyboardTrackerView: KeyboardTrackingView = {
|
|
let trackingView = KeyboardTrackingView()
|
|
trackingView.positionChangedCallback = { [weak self] in
|
|
self?.layoutInputAtTrackingViewIfNeeded()
|
|
}
|
|
return trackingView
|
|
}()
|
|
|
|
var isTracking = false
|
|
var inputContainer: UIView
|
|
private var notificationCenter: NSNotificationCenter
|
|
|
|
init(viewController: UIViewController, inputContainer: UIView, inputContainerBottomContraint: NSLayoutConstraint, notificationCenter: NSNotificationCenter) {
|
|
self.view = viewController.view
|
|
self.inputContainer = inputContainer
|
|
self.inputContainerBottomConstraint = inputContainerBottomContraint
|
|
self.notificationCenter = notificationCenter
|
|
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardWillShow(_:)), name: UIKeyboardWillShowNotification, object: nil)
|
|
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardDidShow(_:)), name: UIKeyboardDidShowNotification, object: nil)
|
|
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardWillHide(_:)), name: UIKeyboardWillHideNotification, object: nil)
|
|
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardWillChangeFrame(_:)), name: UIKeyboardWillChangeFrameNotification, object: nil)
|
|
}
|
|
|
|
deinit {
|
|
self.notificationCenter.removeObserver(self)
|
|
}
|
|
|
|
func startTracking() {
|
|
self.isTracking = true
|
|
}
|
|
|
|
func stopTracking() {
|
|
self.isTracking = false
|
|
}
|
|
|
|
@objc
|
|
private func keyboardWillShow(notification: NSNotification) {
|
|
guard self.isTracking else { return }
|
|
let bottomConstraint = self.bottomConstraintFromNotification(notification)
|
|
guard bottomConstraint > 0 else { return } // Some keyboards may report initial willShow/DidShow notifications with invalid positions
|
|
self.keyboardStatus = .Showing
|
|
self.inputContainerBottomConstraint.constant = bottomConstraint
|
|
self.view.layoutIfNeeded()
|
|
self.adjustTrackingViewSizeIfNeeded()
|
|
}
|
|
|
|
@objc
|
|
private func keyboardDidShow(notification: NSNotification) {
|
|
guard self.isTracking else { return }
|
|
let bottomConstraint = self.bottomConstraintFromNotification(notification)
|
|
guard bottomConstraint > 0 else { return } // Some keyboards may report initial willShow/DidShow notifications with invalid positions
|
|
self.keyboardStatus = .Shown
|
|
self.inputContainerBottomConstraint.constant = bottomConstraint
|
|
self.view.layoutIfNeeded()
|
|
self.layoutTrackingViewIfNeeded()
|
|
}
|
|
|
|
@objc
|
|
private func keyboardWillChangeFrame(notification: NSNotification) {
|
|
guard self.isTracking else { return }
|
|
let bottomConstraint = self.bottomConstraintFromNotification(notification)
|
|
if bottomConstraint == 0 {
|
|
self.keyboardStatus = .Hidden
|
|
self.layoutInputAtBottom()
|
|
}
|
|
}
|
|
|
|
@objc
|
|
private func keyboardWillHide(notification: NSNotification) {
|
|
guard self.isTracking else { return }
|
|
self.keyboardStatus = .Hidden
|
|
self.layoutInputAtBottom()
|
|
}
|
|
|
|
private func bottomConstraintFromNotification(notification: NSNotification) -> CGFloat {
|
|
guard let rect = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() else { return 0 }
|
|
guard rect.height > 0 else { return 0 }
|
|
let rectInView = self.view.convertRect(rect, fromView: nil)
|
|
guard rectInView.maxY >= self.view.bounds.height else { return 0 } // Undocked keyboard
|
|
return max(0, self.view.bounds.height - rectInView.minY - self.trackingView.bounds.height)
|
|
}
|
|
|
|
private func bottomConstraintFromTrackingView() -> CGFloat {
|
|
let trackingViewRect = self.view.convertRect(self.keyboardTrackerView.bounds, fromView: self.keyboardTrackerView)
|
|
return max(0, self.view.bounds.height - trackingViewRect.maxY)
|
|
}
|
|
|
|
func layoutTrackingViewIfNeeded() {
|
|
guard self.isTracking && self.keyboardStatus == .Shown else { return }
|
|
self.adjustTrackingViewSizeIfNeeded()
|
|
if #available(iOS 9, *) {
|
|
// Working fine on iOS 9
|
|
} else {
|
|
// Workaround for iOS 8
|
|
self.trackingView.window?.setNeedsLayout()
|
|
self.trackingView.window?.layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
private func adjustTrackingViewSizeIfNeeded() {
|
|
let inputContainerHeight = self.inputContainer.bounds.height
|
|
let trackerViewHeight = self.trackingView.bounds.height
|
|
if trackerViewHeight != inputContainerHeight {
|
|
self.keyboardTrackerView.bounds.size.height = inputContainerHeight
|
|
}
|
|
}
|
|
|
|
private func layoutInputAtBottom() {
|
|
self.keyboardTrackerView.bounds.size.height = 0
|
|
self.inputContainerBottomConstraint.constant = 0
|
|
self.view.layoutIfNeeded()
|
|
}
|
|
|
|
func layoutInputAtTrackingViewIfNeeded() {
|
|
guard self.isTracking && self.keyboardStatus == .Shown else { return }
|
|
let newBottomConstraint = self.bottomConstraintFromTrackingView()
|
|
self.inputContainerBottomConstraint.constant = newBottomConstraint
|
|
self.view.layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
private class KeyboardTrackingView: UIView {
|
|
|
|
var positionChangedCallback: (() -> Void)?
|
|
var observedView: UIView?
|
|
|
|
deinit {
|
|
if let observedView = self.observedView {
|
|
observedView.removeObserver(self, forKeyPath: "center")
|
|
}
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
self.commonInit()
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
super.init(coder: aDecoder)
|
|
self.commonInit()
|
|
}
|
|
|
|
private func commonInit() {
|
|
self.autoresizingMask = .FlexibleHeight
|
|
self.userInteractionEnabled = false
|
|
self.backgroundColor = UIColor.clearColor()
|
|
self.hidden = true
|
|
}
|
|
|
|
override var bounds: CGRect {
|
|
didSet {
|
|
if oldValue.size != self.bounds.size {
|
|
self.invalidateIntrinsicContentSize()
|
|
}
|
|
}
|
|
}
|
|
|
|
private override func intrinsicContentSize() -> CGSize {
|
|
return self.bounds.size
|
|
}
|
|
|
|
override func willMoveToSuperview(newSuperview: UIView?) {
|
|
if let observedView = self.observedView {
|
|
observedView.removeObserver(self, forKeyPath: "center")
|
|
self.observedView = nil
|
|
}
|
|
|
|
if let newSuperview = newSuperview {
|
|
newSuperview.addObserver(self, forKeyPath: "center", options: [.New, .Old], context: nil)
|
|
self.observedView = newSuperview
|
|
}
|
|
|
|
super.willMoveToSuperview(newSuperview)
|
|
}
|
|
|
|
private override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
|
|
guard let object = object, superview = self.superview else { return }
|
|
if object === superview {
|
|
guard let sChange = change else { return }
|
|
let oldCenter = (sChange[NSKeyValueChangeOldKey] as! NSValue).CGPointValue()
|
|
let newCenter = (sChange[NSKeyValueChangeNewKey] as! NSValue).CGPointValue()
|
|
if oldCenter != newCenter {
|
|
self.positionChangedCallback?()
|
|
}
|
|
}
|
|
}
|
|
}
|