Last active
September 6, 2019 20:00
-
-
Save noordawod/24d32b2ce8363627ea73d7e5991009a0 to your computer and use it in GitHub Desktop.
Custom UIViewController that takes care of repositioning its view above a raised window. Written for Swift 3.0.
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
// | |
// The MIT License (MIT) | |
// | |
// Copyright (c) 2017 Noor Dawod (noor@fine47.com) | |
// | |
// 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. | |
// | |
// Inspired by Pavle Mijatovic: https://stackoverflow.com/a/45323660/4202478 | |
// | |
import UIKit | |
/// A custom UIViewController that automatically panes its underlying view when a keyboard is shown | |
/// and a text field is focused. | |
open class KeyboardAwareUIViewController: UIViewController { | |
// MARK: - Overridden methods. | |
override open func viewDidAppear(_ animated: Bool) { | |
super.viewDidAppear(animated) | |
self.addKeyboardObservers() | |
} | |
override open func viewDidDisappear(_ animated: Bool) { | |
super.viewDidAppear(animated) | |
self.removeKeyboardObservers() | |
} | |
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { | |
self.resignAllFirstResponders() | |
} | |
// MARK: - Custom implementations. | |
/// Add observers to be notified when the keyboard is shown, changed or hidden. If you override | |
/// this method and don't provide your own observers, remember to call | |
/// super.addKeyboardObservers(). | |
func addKeyboardObservers() { | |
NotificationCenter.default.addObserver( | |
self, | |
selector: #selector(self.keyboardWillChangeFrame), | |
name: NSNotification.Name.UIKeyboardWillChangeFrame, | |
object: nil | |
) | |
NotificationCenter.default.addObserver( | |
self, | |
selector: #selector(self.keyboardWillHide), | |
name: NSNotification.Name.UIKeyboardWillHide, | |
object: nil | |
) | |
} | |
/// Remove observers that were added previously. If you override this method and don't provide | |
/// your own observers, remember to call removeKeyboardObservers(). | |
func removeKeyboardObservers() { | |
NotificationCenter.default.removeObserver( | |
self, | |
name: NSNotification.Name.UIKeyboardWillChangeFrame, | |
object: self.view.window | |
) | |
NotificationCenter.default.removeObserver( | |
self, | |
name: NSNotification.Name.UIKeyboardWillHide, | |
object: self.view.window | |
) | |
} | |
// MARK: - Observers implementations. | |
/// Method's notified when the keyboard is about to be shown or change its size. | |
/// | |
/// - Parameter notification: System keyboard notification | |
func keyboardWillChangeFrame(notification: NSNotification) { | |
if | |
let window = self.view.window, | |
let responder = KeyboardAwareUIViewController.firstResponder(in: self.view), | |
let userInfo = notification.userInfo, | |
let keyboardFrameValue = userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue, | |
let keyboardAnimationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber | |
{ | |
let keyboardHeight = keyboardFrameValue.cgRectValue.height | |
UIView.animate( | |
withDuration: Double(keyboardAnimationDuration), | |
animations: { | |
window.frame.origin.y = min( | |
0, | |
-min( | |
keyboardHeight, | |
responder.layer.position.y | |
- (window.bounds.height - keyboardHeight - responder.bounds.height) | |
* self.spacingPercentageFromTop() | |
) | |
) | |
self.view.layoutIfNeeded() | |
} | |
) | |
} | |
} | |
/// Method's notified when the keyboard is about to be dismissed. | |
/// | |
/// - Parameter notification: System keyboard notification | |
func keyboardWillHide(notification: NSNotification) { | |
if | |
let window = self.view.window, | |
let userInfo = notification.userInfo, | |
let animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber | |
{ | |
UIView.animate( | |
withDuration: Double(animationDuration), | |
animations: { | |
window.frame.origin.y = 0 | |
self.view.layoutIfNeeded() | |
} | |
) | |
} | |
} | |
// MARK: - Utility methods. | |
/// How much space (in percentage of remaining available space) to designate under the focused | |
/// text field. By default, text fields will be positioned 2/3th the space from top. Override | |
/// if you want to change this, for example 0.5 if you want to center-position the focused field. | |
func spacingPercentageFromTop() -> CGFloat { | |
return 2 / 3 | |
} | |
/// Asks the system to resign all first responders (usually input fields), which effectively | |
/// causes the keyboard to dismiss itself. | |
func resignAllFirstResponders() { | |
self.view.endEditing(true) | |
} | |
} | |
extension KeyboardAwareUIViewController { | |
/// Recursively traverses a view until a focused text field is found. May return nil if none is | |
/// found and all views and subviews were exhausted. | |
/// | |
/// - Parameter subview: Subview to look in | |
/// - Returns: Focused text field if found, nil otherwise | |
static func firstResponder(in subview: UIView) -> UITextField? { | |
var textField: UITextField? | |
// Recursively look for the focused text field. | |
KeyboardAwareUIViewController.firstResponderImpl([subview], textField: &textField) | |
return textField | |
} | |
private static func firstResponderImpl(_ subviews: [UIView], textField: inout UITextField?) { | |
for view in subviews { | |
if let tf = view as? UITextField, view.isFirstResponder { | |
textField = tf | |
return | |
} | |
if !view.subviews.isEmpty { | |
KeyboardAwareUIViewController.firstResponderImpl(view.subviews, textField: &textField) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment