Skip to content

Commit

Permalink
Add API for keyboard observation on ScrollView
Browse files Browse the repository at this point in the history
  • Loading branch information
kyleve committed Mar 30, 2020
1 parent 720de55 commit 141eabf
Show file tree
Hide file tree
Showing 11 changed files with 712 additions and 134 deletions.
5 changes: 5 additions & 0 deletions BlueprintUICommonControls.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ Pod::Spec.new do |s|
s.source_files = 'BlueprintUICommonControls/Sources/**/*.swift'

s.dependency 'BlueprintUI'

s.test_spec 'Tests' do |test_spec|
test_spec.source_files = 'BlueprintUICommonControls/Tests/**/*.swift'
test_spec.framework = 'XCTest'
end
end
178 changes: 178 additions & 0 deletions BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//
// KeyboardObserver.swift
// BlueprintUICommonControls
//
// Created by Kyle Van Essen on 2/16/20.
//

import UIKit


protocol KeyboardObserverDelegate : AnyObject {

func keyboardFrameWillChange(
for observer : KeyboardObserver,
animationDuration : Double,
options : UIView.AnimationOptions
)
}

/**
Implementation borrowed from Listable:
https://github.com/kyleve/Listable/blob/master/Listable/Sources/Internal/KeyboardObserver.swift
iOS Docs for keyboard management:
https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html
*/
final class KeyboardObserver {

private let center : NotificationCenter

weak var delegate : KeyboardObserverDelegate?

//
// MARK: Initialization
//

init(center : NotificationCenter = .default) {

self.center = center

/// We need to listen to both `will` and `keyboardDidChangeFrame` notifications. Why?
/// When dealing with an undocked or floating keyboard, moving the keyboard
/// around the screen does NOT call `willChangeFrame`; only `didChangeFrame` is called.
/// Before calling the delegate, we compare `old.endingFrame != new.endingFrame`,
/// which ensures that the delegate is notified if the frame really changes, and
/// prevents duplicate calls.

self.center.addObserver(self, selector: #selector(keyboardFrameChanged(_:)), name: UIWindow.keyboardWillChangeFrameNotification, object: nil)
self.center.addObserver(self, selector: #selector(keyboardFrameChanged(_:)), name: UIWindow.keyboardDidChangeFrameNotification, object: nil)
}

private var latestNotification : NotificationInfo?

//
// MARK: Handling Changes
//

enum KeyboardFrame : Equatable {

/// The current frame does not overlap the current view at all.
case nonOverlapping

/// The current frame does overlap the view, by the provided rect, in the view's coordinate space.
case visible(frame: CGRect)
}

/// How the keyboard overlaps the view provided. If the view is not on screen (eg, no window),
/// or the observer has not yet learned about the keyboard's position, this method returns nil.
func currentFrame(in view : UIView) -> KeyboardFrame? {

guard view.window != nil else {
return nil
}

guard let notification = self.latestNotification else {
return nil
}

let frame = view.convert(notification.endingFrame, from: nil)

if frame.intersects(view.bounds) {
return .visible(frame: frame)
} else {
return .nonOverlapping
}
}

//
// MARK: Receiving Updates
//

private func receivedUpdatedKeyboardInfo(_ new : NotificationInfo) {

let old = self.latestNotification

self.latestNotification = new

/// Only communicate a frame change to the delegate if the frame actually changed.

if let old = old {
guard old.endingFrame != new.endingFrame else {
return
}
}

/**
Create an animation curve with the correct curve for showing or hiding the keyboard.
This is unfortunately a private UIView curve. However, we can map it to the animation options' curve
like so: https://stackoverflow.com/questions/26939105/keyboard-animation-curve-as-int
*/
let animationOptions = UIView.AnimationOptions(rawValue: new.animationCurve << 16)

self.delegate?.keyboardFrameWillChange(
for: self,
animationDuration: new.animationDuration,
options: animationOptions
)
}

//
// MARK: Notification Listeners
//

@objc private func keyboardFrameChanged(_ notification : Notification) {

do {
let info = try NotificationInfo(with: notification)
self.receivedUpdatedKeyboardInfo(info)
} catch {
assertionFailure("Blueprint could not read system keyboard notification. This error needs to be fixed in Blueprint. Error: \(error)")
}
}
}

extension KeyboardObserver
{
struct NotificationInfo : Equatable {

var endingFrame : CGRect = .zero

var animationDuration : Double = 0.0
var animationCurve : UInt = 0

init(with notification : Notification) throws {

guard let userInfo = notification.userInfo else {
throw ParseError.missingUserInfo
}

guard let endingFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {
throw ParseError.missingEndingFrame
}

self.endingFrame = endingFrame

guard let animationDuration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue else {
throw ParseError.missingAnimationDuration
}

self.animationDuration = animationDuration

guard let animationCurve = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue else {
throw ParseError.missingAnimationCurve
}

self.animationCurve = animationCurve
}

enum ParseError : Error, Equatable {

case missingUserInfo
case missingEndingFrame
case missingAnimationDuration
case missingAnimationCurve
}
}
}
Loading

0 comments on commit 141eabf

Please sign in to comment.