Last active
January 19, 2017 23:15
-
-
Save rabidaudio/2498939a25b39c498c20 to your computer and use it in GitHub Desktop.
View for moving views out from under keyboard (Swift)
This file contains hidden or 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
// | |
// 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