Skip to content

Instantly share code, notes, and snippets.

@noordawod
Last active September 6, 2019 20:00
Show Gist options
  • Save noordawod/24d32b2ce8363627ea73d7e5991009a0 to your computer and use it in GitHub Desktop.
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.
//
// 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