Last active
May 1, 2025 07:45
-
-
Save lanserxt/6838a55cb82258cfc865ff201062fe0b to your computer and use it in GitHub Desktop.
UIViewController with TextView and keyboard handling
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
import UIKit | |
import SnapKit | |
import RSKGrowingTextView | |
final class KeyboardInputViewController: UIViewController { | |
var heightDidChange: ((_ fieldHeight: CGFloat, _ totalHeight: CGFloat) -> Void)? | |
private lazy var inputTextView: RSKGrowingTextView = { | |
let view = RSKGrowingTextView() | |
view.delegate = self | |
view.textColor = .black | |
view.backgroundColor = .clear | |
view.font = .systemFont(ofSize: 19) | |
view.maximumNumberOfLines = 10 | |
return view | |
}() | |
private var bottomConstraint: Constraint? | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
view.backgroundColor = DesignSystem.Color.floatingBackground | |
setupInputTextView() | |
registerForKeyboardNotifications() | |
//Setting cursor color | |
UITextField.appearance().tintColor = .red | |
UITextView.appearance().tintColor = .red | |
} | |
private let topInset = 16.0 | |
private func setupInputTextView() { | |
view.addSubview(inputTextView) | |
inputTextView.snp.makeConstraints { make in | |
make.leading.trailing.equalToSuperview().inset(topInset) | |
self.bottomConstraint = make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(topInset / 2.0).constraint | |
} | |
} | |
override func viewWillAppear(_ animated: Bool) { | |
super.viewWillAppear(animated) | |
//Small delay to show the keyboard | |
Task { | |
try? await Task.sleep(for: .milliseconds(300)) | |
inputTextView.placeholder = "To Search, start typing" | |
inputTextView.placeholderColor = DesignSystem.Color.white20 | |
inputTextView.becomeFirstResponder() | |
} | |
} | |
/// Registering keyboard notifs | |
private func registerForKeyboardNotifications() { | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(handleKeyboardWillChangeFrame), | |
name: UIResponder.keyboardWillChangeFrameNotification, | |
object: nil) | |
} | |
//Property to store keyboard height | |
private var keyboardHeight: CGFloat = 0.0 | |
/// Keyboard notification handler | |
/// - Parameter notification: notification | |
@objc private func handleKeyboardWillChangeFrame(notification: Notification) { | |
guard let userInfo = notification.userInfo, | |
let animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval, | |
let animationCurveRawValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else { | |
return | |
} | |
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { | |
let keyboardHeight = keyboardSize.height | |
self.bottomConstraint?.update(offset: -keyboardHeight + topInset) | |
let totalHeight = inputTextView.frame.height + view.safeAreaInsets.bottom + topInset + keyboardHeight | |
heightDidChange?(inputTextView.frame.height, totalHeight) | |
self.keyboardHeight = keyboardHeight | |
//Animate UI | |
let animationOptions = UIView.AnimationOptions(rawValue: animationCurveRawValue << 16) | |
UIView.animate(withDuration: animationDuration, delay: 0, options: animationOptions, animations: { | |
self.view.layoutIfNeeded() | |
}, completion: nil) | |
} | |
} | |
override func viewDidLayoutSubviews() { | |
super.viewDidLayoutSubviews() | |
//Calculating new content height | |
let totalHeight = inputTextView.frame.height + view.safeAreaInsets.bottom + topInset + keyboardHeight | |
heightDidChange?(inputTextView.frame.height, totalHeight) | |
} | |
deinit { | |
NotificationCenter.default.removeObserver(self) | |
} | |
} | |
extension KeyboardInputViewController: RSKGrowingTextViewDelegate { | |
func growingTextView(_ textView: RSKGrowingTextView, willChangeHeight height: CGFloat) { | |
//Animate frame of TextView | |
UIView.animate(withDuration: 0.2) { | |
self.view.layoutIfNeeded() | |
} | |
} | |
} |
Author
lanserxt
commented
Apr 30, 2025
import UIKit
import SnapKit
import FloatingPanel
class ParentViewController: UIViewController {
//Field height
private var searchFieldHeight: CGFloat = 0.0
private func presentChatPanel() {
let fpc = FloatingPanelController()
let inputVC = KeyboardInputViewController()
fpc.set(contentViewController: inputVC)
fpc.surfaceView.appearance.cornerRadius = 12
fpc.surfaceView.grabberHandle.isHidden = false
fpc.surfaceView.grabberHandleSize = .init(width: 32, height: 4)
fpc.surfaceView.grabberHandle.backgroundColor = .gray
fpc.isRemovalInteractionEnabled = true
fpc.surfaceView.backgroundColor = .lightGray
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
fpc.delegate = self
// Initially set a height
fpc.layout = FloatingPanelIntrinsicLayout(initialHeight: 0.0)
inputVC.heightDidChange = { [weak fpc, weak self] fieldHeight, newHeight in
Task { @MainActor in
self?.searchFieldHeight = fieldHeight
print("newHeight \(newHeight)")
guard let fpc = fpc else { return }
(fpc.layout as? FloatingPanelIntrinsicLayout)?.contentHeight = newHeight
fpc.invalidateLayout()
}
}
present(fpc, animated: true, completion: nil)
fpc.backdropView.backgroundColor = .black
fpc.backdropView.alpha = 0.8
floatingPanel = fpc
}
}
extension ParentViewController: @preconcurrency FloatingPanelControllerDelegate {
func floatingPanelDidMove(_ fpc: FloatingPanelController) {
guard fpc.contentViewController is InputViewController else { return }
let minY = fpc.surfaceLocation(for: .half).y + searchFieldHeight
if fpc.surfaceLocation.y > minY {
fpc.dismiss(animated: true)
}
}
func floatingPanelDidChangeState(_ fpc: FloatingPanelController) {
if fpc.state == .tip {
fpc.dismiss(animated: true)
}
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment