Skip to content

Commit

Permalink
Update KeyboardSupport (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
muukii committed Aug 23, 2023
1 parent f174459 commit cb26127
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ final class DemoKeyboardGuideViewController: UIViewController {
.padding(.bottom, 200)
}

scrollView.enableTrackingKeyboard()
scrollView.setContentInsetAdjustmentForKeyboard(isActive: true)
scrollView.setKeyboardSwipeDownOffscreenGesture(isActive: true)
}

private var observation: KeyboardObservation?
Expand Down
130 changes: 0 additions & 130 deletions Sources/FluidKeyboardSupport/FluidKeyboardSupport.swift

This file was deleted.

10 changes: 10 additions & 0 deletions Sources/FluidKeyboardSupport/KeyboardFrameInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation
import CoreGraphics

public struct KeyboardFrameInfo: Equatable, Sendable {
public var height: CGFloat

init(height: CGFloat) {
self.height = height
}
}
17 changes: 17 additions & 0 deletions Sources/FluidKeyboardSupport/KeyboardObservation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

public final class KeyboardObservation {

private var keyValueObservation: NSKeyValueObservation
private let onInvalidate: () -> Void

init(keyValueObservation: NSKeyValueObservation, onInvalidate: @escaping () -> Void) {
self.keyValueObservation = keyValueObservation
self.onInvalidate = onInvalidate
}

deinit {
keyValueObservation.invalidate()
onInvalidate()
}
}
86 changes: 86 additions & 0 deletions Sources/FluidKeyboardSupport/UIScrollView+HidingKeyboard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import UIKit

fileprivate var ref: Void?

extension UIScrollView {

/**
The pan gesture tracks the location to determine whether it is on or off the keyboard.
When the location falls within the keyboard's boundaries, it hides the keyboard.
*/
@available(iOS 15, *)
@MainActor
public func setKeyboardSwipeDownOffscreenGesture(isActive: Bool) {
if isActive {
let newCoordinator = PanGestureCoordinator(scrollView: self, panGesture: UIPanGestureRecognizer())
self.coordinator = newCoordinator
} else {
self.coordinator = nil
}
}

@available(iOS 15, *)
@MainActor
private var coordinator: PanGestureCoordinator? {
get { objc_getAssociatedObject(self, &ref) as? PanGestureCoordinator }
set { objc_setAssociatedObject(self, &ref, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}

}

@available(iOS 15, *)
@MainActor
private final class PanGestureCoordinator: NSObject, UIGestureRecognizerDelegate {

private let scrollView: UIScrollView
private let panGesture: UIPanGestureRecognizer

init(scrollView: UIScrollView, panGesture: UIPanGestureRecognizer) {
self.scrollView = scrollView
self.panGesture = panGesture
super.init()
panGesture.delegate = self
panGesture.addTarget(self, action: #selector(handlePanGesture(_:)))

scrollView.addGestureRecognizer(panGesture)
}

deinit {
Task { @MainActor [scrollView, panGesture] in
scrollView.removeGestureRecognizer(panGesture)
}
}

@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {

guard let view = gesture.view else {
assertionFailure()
return
}

let location = gesture.location(in: view)

switch gesture.state {
case .began, .changed:

if location.y > view.keyboardLayoutGuide.layoutFrame.minY {
view.endEditing(true)
}

case .ended:
break
default:
break
}

}

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}

func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}

}
73 changes: 73 additions & 0 deletions Sources/FluidKeyboardSupport/UIScrollView+TrackingKeyboard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import UIKit

@MainActor
private var ref: Void?

extension UIScrollView {

@available(iOS 15, *)
private func _observeKeyboard(handler: @escaping @MainActor (KeyboardFrameInfo) -> Void) -> KeyboardObservation {

let dummyView = UIView()
dummyView.accessibilityIdentifier = "keyboard-dummy-view"

addSubview(dummyView)
dummyView.isUserInteractionEnabled = false
dummyView.isHidden = true
dummyView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
dummyView.topAnchor.constraint(equalTo: keyboardLayoutGuide.topAnchor),
dummyView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
dummyView.leadingAnchor.constraint(equalTo: frameLayoutGuide.leadingAnchor),
dummyView.trailingAnchor.constraint(equalTo: frameLayoutGuide.trailingAnchor),
])

let token = dummyView.observe(\.bounds) { @MainActor view, _ in
handler(.init(height: view.bounds.height))
}

return .init(keyValueObservation: token) { [weak dummyView] in
Task { @MainActor in
dummyView?.removeFromSuperview()
}
}

}

/**
It tracks the keyboard frame to set the content inset to prevent from hiding the view behind the keyboard.
*/
@available(iOS 15, *)
@MainActor
public func setContentInsetAdjustmentForKeyboard(isActive: Bool) {

if isActive {

guard keyboardObservation == nil else { return }

keyboardObservation = self._observeKeyboard { [weak self] info in

guard let self else { return }

self.contentInset.bottom = info.height

}
} else {
keyboardObservation = nil
}

}

@MainActor
private var keyboardObservation: KeyboardObservation? {
get {
objc_getAssociatedObject(self, &ref) as? KeyboardObservation
}
set {
objc_setAssociatedObject(self, &ref, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}


}
37 changes: 37 additions & 0 deletions Sources/FluidKeyboardSupport/UIView+TrackingKeyboard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

import UIKit

extension UIView {

@available(iOS 15, *)
public func observeKeyboard(handler: @escaping @MainActor (KeyboardFrameInfo) -> Void) -> KeyboardObservation {

let dummyView = UIView()
dummyView.accessibilityIdentifier = "keyboard-dummy-view"

addSubview(dummyView)
dummyView.isUserInteractionEnabled = false
dummyView.isHidden = true
dummyView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
dummyView.topAnchor.constraint(equalTo: keyboardLayoutGuide.topAnchor),
dummyView.bottomAnchor.constraint(equalTo: bottomAnchor),
dummyView.leadingAnchor.constraint(equalTo: leadingAnchor),
dummyView.trailingAnchor.constraint(equalTo: trailingAnchor),
])

let token = dummyView.observe(\.bounds) { @MainActor view, _ in
handler(.init(height: view.bounds.height))
}

return .init(keyValueObservation: token) { [weak dummyView] in
Task { @MainActor in
dummyView?.removeFromSuperview()
}
}

}

}

0 comments on commit cb26127

Please sign in to comment.