As in iMessage for example, when keyboard appears, I want to push messages up also. Please help
I’ve tried put tableView bottomAnchor to backdrop top. But it’s not as I want. Messages should be behind backdrop and keyboard. It means tableView bottom anchored to view bottom. Because I need blur effect.
import UIKit
import CoreData
import FirebaseAuth
import FirebaseFirestore
class ChatViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var titleText: String?
var country: String?
var category: String?
var messageType: String = "general"
var messages: [Message] = []
private var tableView: UITableView!
private let backdrop: UIVisualEffectView = {
let blurEffect = UIBlurEffect(style: .prominent)
let view = UIVisualEffectView(effect: blurEffect)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let textField: UITextField = {
let textField = UITextField()
textField.borderStyle = .none
textField.placeholder = "Enter message..."
textField.translatesAutoresizingMaskIntoConstraints = false
textField.layer.borderColor = UIColor.systemGray4.cgColor
textField.layer.cornerRadius = 17
textField.layer.borderWidth = 1.0
textField.layer.masksToBounds = true
textField.backgroundColor = .clear
textField.setLeftPaddingPoints(10)
textField.setRightPaddingPoints(10)
return textField
}()
private lazy var sendButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "paperplane.fill"), for: .normal)
button.addTarget(self, action: #selector(sendButtonTapped), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupTableView()
setupInputComponents()
setupConstraints()
if let titleText = titleText {
self.title = titleText
}
fetchMessages()
listenForMessages()
}
private func setupTableView() {
tableView = UITableView(frame: .zero, style: .plain)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.backgroundColor = .clear
tableView.delegate = self
tableView.dataSource = self
tableView.register(MessageCell.self, forCellReuseIdentifier: MessageCell.identifier)
tableView.keyboardDismissMode = .interactive
tableView.separatorStyle = .none
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 55, right: 0)
view.addSubview(tableView)
}
private func setupInputComponents() {
view.addSubview(backdrop)
backdrop.contentView.addSubview(textField)
backdrop.contentView.addSubview(sendButton)
}
private func setupConstraints() {
view.keyboardLayoutGuide.usesBottomSafeArea = false
view.keyboardLayoutGuide.keyboardDismissPadding = 50
NSLayoutConstraint.activate([
view.keyboardLayoutGuide.topAnchor.constraint(greaterThanOrEqualToSystemSpacingBelow: textField.bottomAnchor, multiplier: 1.0),
view.keyboardLayoutGuide.topAnchor.constraint(equalTo: backdrop.bottomAnchor),
view.safeAreaLayoutGuide.bottomAnchor.constraint(greaterThanOrEqualTo: textField.bottomAnchor),
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
backdrop.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backdrop.trailingAnchor.constraint(equalTo: view.trailingAnchor),
backdrop.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
textField.leadingAnchor.constraint(equalTo: backdrop.leadingAnchor, constant: 8),
textField.bottomAnchor.constraint(equalTo: backdrop.keyboardLayoutGuide.topAnchor, constant: -8),
textField.topAnchor.constraint(equalToSystemSpacingBelow: backdrop.topAnchor, multiplier: 1.0),
textField.heightAnchor.constraint(equalToConstant: 36),
sendButton.leadingAnchor.constraint(equalTo: textField.trailingAnchor, constant: 8),
sendButton.trailingAnchor.constraint(equalTo: backdrop.trailingAnchor, constant: -8),
sendButton.centerYAnchor.constraint(equalTo: textField.centerYAnchor),
sendButton.widthAnchor.constraint(equalToConstant: 30),
sendButton.heightAnchor.constraint(equalToConstant: 30),
])
}
@objc private func sendButtonTapped() {
guard let text = textField.text, !text.isEmpty else { return }
guard let currentUser = Auth.auth().currentUser else {
print("No user is currently logged in")
return
}
let senderId = currentUser.uid
let receiverId: String? = messageType == "private" ? "specialistId" : nil
let messageId = UUID().uuidString
let timestamp = Date()
// Fetch user details using UserProfileManager
UserProfileManager.shared.fetchUserProfile(uid: senderId) { [weak self] user in
guard let self = self else { return }
// guard let user = user else {
// print("Failed to fetch user details from Core Data or Firestore")
// return
// }
// Save to Firestore
let db = Firestore.firestore()
let messageData: [String: Any] = [
"id": messageId,
"senderId": senderId,
"receiverId": receiverId ?? "",
"text": text,
"messageType": self.messageType,
"country": self.country ?? "",
"category": self.category ?? "",
"timestamp": timestamp
]
db.collection("messages").document(messageId).setData(messageData) { error in
if let error = error {
print("Error saving message to Firestore: (error)")
} else {
print("Message saved to Firestore")
}
}
// Save to Core Data
CoreDataHelper.shared.saveMessage(
id: messageId,
senderId: senderId,
receiverId: receiverId,
text: text,
messageType: self.messageType,
country: self.country,
category: self.category,
timestamp: timestamp
)
self.textField.text = ""
self.fetchMessages()
self.scrollToBottom(animated: true)
}
}
private func fetchMessages() {
messages = CoreDataHelper.shared.fetchMessages(country: country, category: category)
tableView.reloadData()
scrollToBottom(animated: false)
}
private func scrollToBottom(animated: Bool) {
guard messages.count > 0 else { return }
let indexPath = IndexPath(row: messages.count - 1, section: 0)
tableView.scrollToRow(at: indexPath, at: .bottom, animated: animated)
}
private func listenForMessages() {
let db = Firestore.firestore()
db.collection("messages").addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("No documents in snapshot")
return
}
var newMessages: [Message] = []
for document in documents {
let data = document.data()
let id = data["id"] as! String
if !CoreDataHelper.shared.messageExists(id: id) {
let senderId = data["senderId"] as! String
let receiverId = data["receiverId"] as? String
let text = data["text"] as! String
let messageType = data["messageType"] as! String
let country = data["country"] as? String
let category = data["category"] as? String
let timestamp = (data["timestamp"] as! Timestamp).dateValue()
let message = Message(context: CoreDataHelper.shared.context)
message.id = id
message.senderId = senderId
message.receiverId = receiverId
message.text = text
message.messageType = messageType
message.country = country
message.category = category
message.timestamp = timestamp
newMessages.append(message)
}
}
CoreDataHelper.shared.syncMessages(with: newMessages)
self.fetchMessages()
self.scrollToBottom(animated: true)
}
}
// UITableViewDataSource methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: MessageCell.identifier, for: indexPath) as! MessageCell
let message = messages[indexPath.row]
guard let currentUser = Auth.auth().currentUser else {
return cell
}
let isCurrentUser = message.senderId == currentUser.uid
cell.configure(with: message, isCurrentUser: isCurrentUser)
return cell
}
// UITableViewDelegate methods
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let message = messages[indexPath.row]
let size = CGSize(width: tableView.frame.width * 0.75, height: CGFloat.greatestFiniteMagnitude)
let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 16)]
let estimatedFrame = NSString(string: message.text ?? "").boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
let height = estimatedFrame.height + 40 // Padding and name label height
return height
}
}
extension UITextField {
func setLeftPaddingPoints(_ amount:CGFloat){
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: amount, height: self.frame.size.height))
self.leftView = paddingView
self.leftViewMode = .always
}
func setRightPaddingPoints(_ amount:CGFloat) {
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: amount, height: self.frame.size.height))
self.rightView = paddingView
self.rightViewMode = .always
}
}