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
}
}