-3.6 C
New York
Tuesday, January 7, 2025

ios – UITextView Leaves a Hole Under Keyboard When Dismissing on Swipe in Dwell Chat Interface


I am creating an iOS app in Swift with a world dwell chat interface. The interface features a UITableView for messages, a UITextView for typing messages, and a ship button. Every part is ready up utilizing Auto Structure.

The Concern:

When the keyboard seems, the UITextView strikes up appropriately. Nonetheless, when the keyboard is dismissed by swiping down, a spot is left between the UITextView and the underside of the display screen. The UITextView doesn’t reattach to the underside of the display screen correctly.

What I’ve Tried:

Observing UIResponder.keyboardWillShowNotification and UIResponder.keyboardWillHideNotification to regulate the underside constraint of the UITextView.
Utilizing .interactive keyboard dismissal for the UITableView.
Including a pan gesture recognizer to detect swipes on the view to dismiss the keyboard.
Regardless of these, the hole persists. I need the UITextView to remain connected to the keyboard and correctly reattach to the underside when the keyboard is dismissed.

Related Code:

[![@objc func keyboardWillShow(notification: NSNotification) {
    if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
        let keyboardHeight = keyboardFrame.cgRectValue.top
        UIView.animate(withDuration: 0.3) {
            self.messageInputBarBottomConstraint?.fixed = -keyboardHeight + self.view.safeAreaInsets.backside
            self.view.layoutIfNeeded()
            self.scrollToBottom()
        }
    }
}

@objc func keyboardWillHide(notification: NSNotification) {
    UIView.animate(withDuration: 0.3) {
        self.messageInputBarBottomConstraint?.fixed = 0
        self.view.layoutIfNeeded()
    }
}][1]][1]

Full ViewController:

import UIKit
import FirebaseDatabase
import FirebaseAuth

class ChatViewController: BaseViewController, UITableViewDelegate, UITableViewDataSource, UITextViewDelegate {

    non-public var messages: [(text: String, senderID: String)] = []
    var ref: DatabaseReference!

    non-public let tableView = UITableView()
    non-public let messageInputBar = UIView()
    non-public let messageTextView = UITextView()
    non-public let sendButton = UIButton(kind: .system)
    non-public let loadingIndicator = UIActivityIndicatorView(model: .medium)

    // Constraint reference for the underside of the messageInputBar
    non-public var messageInputBarBottomConstraint: NSLayoutConstraint?
    non-public var messageTextViewHeightConstraint: NSLayoutConstraint?

    override func viewDidLoad() {
        tremendous.viewDidLoad()

        // Set the background shade
        view.backgroundColor = UIColor(hex: "#02272D")

        // Arrange the title
        title = "International Chat"

        // Add the shut button
        let closeButton = UIBarButtonItem(barButtonSystemItem: .shut, goal: self, motion: #selector(closeButtonTapped))
        navigationItem.leftBarButtonItem = closeButton

        ref = Database.database().reference()

        setupUI()
        setupLoadingIndicator()
        listenForMessages()

        // Add observers for keyboard notifications
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), title: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), title: UIResponder.keyboardWillHideNotification, object: nil)

        // Initially disable the ship button
        updateSendButtonState()

        // Add swipe down gesture recognizer to dismiss the keyboard
        let swipeDownGesture = UISwipeGestureRecognizer(goal: self, motion: #selector(dismissKeyboard))
        swipeDownGesture.route = .down
        view.addGestureRecognizer(swipeDownGesture)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    @objc non-public func closeButtonTapped() {
        dismiss(animated: true, completion: nil)
    }

    override func viewWillAppear(_ animated: Bool) {
        tremendous.viewWillAppear(animated)
        isModalInPresentation = false // Permit interactive dismissal
    }

    non-public func setupUI() {
        // Arrange message enter bar
        messageInputBar.translatesAutoresizingMaskIntoConstraints = false
        messageInputBar.backgroundColor = .secondarySystemBackground
        view.addSubview(messageInputBar)

        // Arrange textual content view
        messageTextView.font = UIFont.systemFont(ofSize: 16)
        messageTextView.isScrollEnabled = false
        messageTextView.layer.cornerRadius = 18
        messageTextView.layer.borderWidth = 1
        messageTextView.layer.borderColor = UIColor.lightGray.cgColor
        messageTextView.textContainerInset = UIEdgeInsets(high: 8, left: 10, backside: 8, proper: 10)
        messageTextView.delegate = self
        messageTextView.translatesAutoresizingMaskIntoConstraints = false
        messageInputBar.addSubview(messageTextView)

        // Arrange ship button
        let config = UIImage.SymbolConfiguration(pointSize: 16, weight: .medium)
        let upArrowImage = UIImage(systemName: "arrow.up", withConfiguration: config)
        sendButton.setImage(upArrowImage, for: .regular)
        sendButton.tintColor = .white
        sendButton.backgroundColor = .systemBlue
        sendButton.layer.cornerRadius = 20
        sendButton.translatesAutoresizingMaskIntoConstraints = false
        sendButton.addTarget(self, motion: #selector(sendButtonTapped), for: .touchUpInside)
        messageInputBar.addSubview(sendButton)

        // Arrange desk view
        tableView.delegate = self
        tableView.dataSource = self
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.keyboardDismissMode = .interactive
        view.addSubview(tableView)

        // Constraints for the message enter bar
        messageInputBarBottomConstraint = messageInputBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        NSLayoutConstraint.activate([
            messageInputBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            messageInputBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            messageInputBarBottomConstraint!,
            messageInputBar.heightAnchor.constraint(greaterThanOrEqualToConstant: 50)
        ])

        // Constraints for the message textual content view
        messageTextViewHeightConstraint = messageTextView.heightAnchor.constraint(equalToConstant: 36)
        NSLayoutConstraint.activate([
            messageTextView.leadingAnchor.constraint(equalTo: messageInputBar.leadingAnchor, constant: 10),
            messageTextView.topAnchor.constraint(equalTo: messageInputBar.topAnchor, constant: 7),
            messageTextView.bottomAnchor.constraint(equalTo: messageInputBar.bottomAnchor, constant: -7),
            messageTextViewHeightConstraint!,
            messageTextView.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor, constant: -10)
        ])

        // Constraints for the ship button
        NSLayoutConstraint.activate([
            sendButton.widthAnchor.constraint(equalToConstant: 40),
            sendButton.heightAnchor.constraint(equalToConstant: 40),
            sendButton.trailingAnchor.constraint(equalTo: messageInputBar.trailingAnchor, constant: -10),
            sendButton.centerYAnchor.constraint(equalTo: messageInputBar.centerYAnchor)
        ])

        // Constraints for the desk view
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: messageInputBar.topAnchor)
        ])
    }

    non-public func setupLoadingIndicator() {
        loadingIndicator.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(loadingIndicator)

        NSLayoutConstraint.activate([
            loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func sendButtonTapped() {
        guard let textual content = messageTextView.textual content, !textual content.isEmpty else { return }
        sendMessage(textual content: textual content)
        messageTextView.textual content = ""
        messageTextViewHeightConstraint?.fixed = 36
        updateSendButtonState()
    }

    non-public func sendMessage(textual content: String) {
        guard let person = Auth.auth().currentUser else {
            print("Consumer not authenticated")
            return
        }

        let message = [
            "text": text,
            "senderID": user.uid,
            "timestamp": ServerValue.timestamp()
        ] as [String : Any]

        ref.youngster("messages").childByAutoId().setValue(message) { error, _ in
            if let error = error {
                print("Error sending message: (error.localizedDescription)")
            } else {
                print("Message despatched efficiently")
                self.scrollToBottom()
            }
        }
    }

    non-public func listenForMessages() {
        loadingIndicator.startAnimating()

        ref.youngster("messages").queryOrdered(byChild: "timestamp").observe(.childAdded) { snapshot in
            if let messageData = snapshot.worth as? [String: Any],
               let textual content = messageData["text"] as? String,
               let senderID = messageData["senderID"] as? String {

                self.messages.append((textual content: textual content, senderID: senderID))
                self.tableView.reloadData()
                self.scrollToBottom()
            }
            self.loadingIndicator.stopAnimating()
        }
    }

    // MARK: - UITableViewDelegate and UITableViewDataSource Strategies

    func tableView(_ tableView: UITableView, numberOfRowsInSection part: Int) -> Int {
        return messages.rely
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "messageCell") ?? UITableViewCell(model: .subtitle, reuseIdentifier: "messageCell")
        let message = messages[indexPath.row]
        cell.textLabel?.textual content = message.textual content
        cell.detailTextLabel?.textual content = message.senderID
        return cell
    }

    // MARK: - Keyboard Dealing with

    @objc func keyboardWillShow(notification: NSNotification) {
        if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            let keyboardHeight = keyboardFrame.cgRectValue.top
            UIView.animate(withDuration: 0.3) {
                self.messageInputBarBottomConstraint?.fixed = -keyboardHeight + self.view.safeAreaInsets.backside
                self.view.layoutIfNeeded()
                self.scrollToBottom()
            }
        }
    }

    @objc func keyboardWillHide(notification: NSNotification) {
        UIView.animate(withDuration: 0.3) {
            self.messageInputBarBottomConstraint?.fixed = 0
            self.view.layoutIfNeeded()
        }
    }

    @objc non-public func dismissKeyboard() {
        view.endEditing(true)
    }

    non-public func scrollToBottom() {
        if messages.rely > 0 {
            let indexPath = IndexPath(row: messages.rely - 1, part: 0)
            tableView.scrollToRow(at: indexPath, at: .backside, animated: true)
        }
    }

    // MARK: - UITextViewDelegate

    func textViewDidChange(_ textView: UITextView) {
        let maxHeight: CGFloat = 100
        let dimension = CGSize(width: textView.body.width, top: CGFloat.infinity)
        let estimatedSize = textView.sizeThatFits(dimension)

        messageTextViewHeightConstraint?.fixed = min(estimatedSize.top, maxHeight)
        textView.isScrollEnabled = estimatedSize.top >= maxHeight

        updateSendButtonState()
    }

    non-public func updateSendButtonState() {
        let isTextEmpty = messageTextView.textual content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        sendButton.isEnabled = !isTextEmpty
        sendButton.backgroundColor = isTextEmpty ? UIColor.grey : UIColor.systemBlue
    }
}

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles