Skip to content

Instantly share code, notes, and snippets.

@lanserxt
Last active May 1, 2025 07:45
Show Gist options
  • Save lanserxt/6838a55cb82258cfc865ff201062fe0b to your computer and use it in GitHub Desktop.
Save lanserxt/6838a55cb82258cfc865ff201062fe0b to your computer and use it in GitHub Desktop.
UIViewController with TextView and keyboard handling
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()
}
}
}
@lanserxt
Copy link
Author

import FloatingPanel
import UIKit

final class FloatingPanelIntrinsicLayout: FloatingPanelLayout {
    //Our panel content height property
    var contentHeight: CGFloat
    
    init(initialHeight: CGFloat) {
        self.contentHeight = initialHeight
    }
    
    var position: FloatingPanelPosition { .bottom }
    
    var initialState: FloatingPanelState { .half }
    
    //Anchoring to the bottom of superview
    var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
        return [
            .half: FloatingPanelLayoutAnchor(absoluteInset: contentHeight, edge: .bottom, referenceGuide: .superview)
        ]
    }
    
    //Alpha of the backdrop
    func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
        switch state {
        case .full, .half: return 0.5
        default: return 0.0
        }
    }
}

@lanserxt
Copy link
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
    }
}

@lanserxt
Copy link
Author

lanserxt commented Apr 30, 2025

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