Skip to content

Instantly share code, notes, and snippets.

@rabidaudio
Last active January 19, 2017 23:15
Show Gist options
  • Save rabidaudio/2498939a25b39c498c20 to your computer and use it in GitHub Desktop.
Save rabidaudio/2498939a25b39c498c20 to your computer and use it in GitHub Desktop.
View for moving views out from under keyboard (Swift)
//
// KeyboardAvoidViewController.swift
//
// Created by Charles Julian Knight on 2/3/16.
// Copyright (c) 2017 Charles Julian Knight
//
// 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 UIKit
/// This is a pretty easy to use `KeyboardAvoidView` for allowing views to move into screen when the keyboard shows.
/// Simply set the class of the top-level view of the controller to a `KeyboardAvoidView`. No other changes are
/// necessary. This will slide the currently focused view to be in the center of the remaining screen space. It
/// handles changing focus and auto-closing the keyboard when leaving focus. It also correctly handles hardware
/// keyboards like in the emulator (that is, it does nothing).
///
/// One cavat to setting the root view of the view controller to a `KeyboardAvoidView` is related to AutoLayout.
/// Setting constraints relative to Leading and Trailing Space of Container Margin will confuse the scroll view about
/// the width of it's content (which is expected to always be the width of it's bounds (generally the screen width).
/// However, constraints relative to the width of the scroll view will work fine. One thing you can do is create a
/// view of zero height and equal width to the scroll view at the top, and then add constraints relative to leading
/// and trailing of this view instead. The alternative is to leave the root view as a generic `View`, add your
/// `KeyboardAvoidView` as the only child of it, and put all your views inside `KeyboardAvoidView` but make your
/// constraints relative to the root view.
class KeyboardAvoidView: UIScrollView {
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
// make scrollView uninteractable
self.isScrollEnabled = false
self.isPagingEnabled = false
self.bounces = false
self.showsHorizontalScrollIndicator = false
self.showsVerticalScrollIndicator = false
self.minimumZoomScale = 1
self.maximumZoomScale = 1
self.contentScaleFactor = 1
self.enableKeyboardAvoidance(dismissOnTap: true)
}
override func keyboardWillHide(notification: NSNotification) {
super.keyboardWillHide(notification: notification)
// we need to return the position to the top (which isn't necessarily [0,0] e.g. if there's a navigation bar)
self.setContentOffset(CGPoint(x: 0, y: -1 * self.contentInset.top), animated: true)
}
}
extension UIScrollView {
/// Helper method to enable scroll views to shift content out of the way when the keyboard opens.
func enableKeyboardAvoidance(dismissOnTap: Bool) {
if dismissOnTap {
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(UIScrollView.onTap)))
}
// listen for keyboard show/hide
NotificationCenter.default.addObserver(self,
selector: #selector(UIScrollView.keyboardWillShow(notification:)),
name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(UIScrollView.keyboardWillHide(notification:)),
name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
/// Calculate the relative coordinates of the selected view in this view's coordinate system, and if
/// scroll up such that the view is at least above half-way between the keyboard and the top of this view
private func scrollToCenter(childView view: UIView, withKeyboardHeight keyboardHeight: CGFloat) {
let frame = frameOfViewInScrollView(view: view)
let topOffset = self.contentInset.top
// the new origin to scroll to is the one which centers the selected view in the space above the keyboard
// to find this offset, we take the y position of the view and subtract the y position of the new center point
let newCenter = ((self.bounds.height - topOffset - keyboardHeight) / 2) + topOffset
let viewCenter = frame.height / 2 + frame.origin.y
let diffCenter = viewCenter - newCenter
//only scroll up to center a view, don't scroll down
let verticalScroll = diffCenter + topOffset > 0 ? diffCenter : -1 * self.contentInset.top
self.setContentOffset(CGPoint(x: 0, y: verticalScroll), animated: true)
}
open func keyboardWillShow(notification: Notification) {
if let keyboardHeight = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect)?.size.height,
let view = self.selectedChild {
self.scrollToCenter(childView: view, withKeyboardHeight: keyboardHeight)
}
}
open func keyboardWillHide(notification: NSNotification) {
}
@objc private func onTap() {
//resign first responder so the keyboard will close
self.selectedChild?.resignFirstResponder()
}
/// The first responder view can be nested arbitrarily deep in the scroll view hirearchy.
/// Find the frame of the view relative to the scroll view.
private func frameOfViewInScrollView(view: UIView) -> CGRect {
var rect = view.frame
var superview = view.superview
while superview != self {
guard let parent = superview else {
fatalError("provided view is not in ScrollView")
}
rect = rect.offsetBy(dx: parent.frame.origin.x, dy: parent.frame.origin.y)
superview = parent.superview
}
return rect
}
}
extension UIView {
///search recursively for the first responder inside self (if any)
fileprivate var selectedChild: UIView? {
if self.isFirstResponder {
return self
}
for subview in subviews {
if let selected = subview.selectedChild {
return selected
}
}
return nil
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment