-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add API for keyboard observation on ScrollView
- Loading branch information
Showing
11 changed files
with
712 additions
and
134 deletions.
There are no files selected for viewing
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
178 changes: 178 additions & 0 deletions
178
BlueprintUICommonControls/Sources/Internal/KeyboardObserver.swift
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
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 | ||
} | ||
} | ||
} |
Oops, something went wrong.