简体   繁体   中英

UITableView.indexPathsForVisibleRows not returning correct values when table view content is offset to avoid visible keyboard

I have a chat view and when messages come in, if the last cell was visible, then the table view is supposed to scroll to the end so that the new message is visible. This behavior works properly when the keyboard is hidden. But when the keyboard is visible, I offset the table view content by keyboard's height, so that the last few messages that were visible before will still be visible. But now when a new message comes in, the list of indexPaths that are visible is completely wrong. And hence the scroll to end condition is not triggered.

let offset = -1 * endFrame.size.height
self.discussionChatView.discussionTableView.contentOffset.y -= offset

In this case, the scrolling worked properly. It scrolled to the end when the last cell was visible and didn't when the last cell was not visible.

Console Logs:

Visible paths:  [[2, 6], [2, 7], [2, 8], [2, 9], [2, 10]]
Sections:  2
Row:  10
Last cell visible true
Visible paths:  [[1, 14], [1, 15], [1, 16]]
Sections:  2
Row:  11
Last cell visible false

But when the keyboard is visible, it does not work the same. The final chat view is shown in the last image (after manually scrolling down).

Logs from keyboard frame change handling code:

Shifted visible paths:  Optional([[2, 8], [2, 9], [2, 10], [2, 11], [2, 12]])
Shifted visible paths:  Optional([[2, 11], [2, 12]])
Shifted visible paths:  Optional([])

Logs from the chats tableview (in reality the I HAD scrolled to the last cell)

Visible paths:  [[2, 3], [2, 4], [2, 5], [2, 6], [2, 7], [2, 8]]
Sections:  2
Row:  12
Last cell visible false

ViewController Code:

class DiscussionsViewController: UIViewController {
    
    static let discussionVC = DiscussionsViewController()
    let interactor = Interactor()
    let sideNavVC = SideNavVC()
    
    let headerContainer = UIView()
    let countryCountView = UserCountryUIView()
    let discussionsMessageBox = DiscussionsMessageBox()
    let discussionChatView = DiscussionChatView()
    let userProfileButton = UIButton()
    
    var discussionsMessageBoxBottomAnchor: NSLayoutConstraint = NSLayoutConstraint()
    var isKeyboardFullyVisible = false
    let keyboard = KeyboardObserver()
        
    let postLoginInfoMessage =  "This is a chatroom created to help students discuss topics with each other and get advice. Use it to ask questions, get tips, etc. "
    var preLoginInfoMessage = "You will have to login with your gmail account to send messages."

    
    override func viewDidLoad() {
        
        view.backgroundColor = UIColor.black
        addSlideGesture()
        addHeader()
        addCountryCountTableView()
        addDiscussionsMessageBox()
        addDiscussionChatView()
        
        preLoginInfoMessage = postLoginInfoMessage + preLoginInfoMessage
        GIDSignIn.sharedInstance().delegate = self
        GIDSignIn.sharedInstance()?.presentingViewController = self

        keyboard.observe { [weak self] (event) -> Void in
            guard let self = self else { return }
            switch event.type {
            case .willChangeFrame:
                self.handleKeyboardWillChangeFrame(keyboardEvent: event)
            default:
                break
            }
        }

        
    }
    
    deinit {
        //        NotificationCenter.default.removeObserver(self)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        discussionChatView.scrollTableViewToEnd(animated: true)
    }
    
    
    func addHeader() {
        
        headerContainer.translatesAutoresizingMaskIntoConstraints = false
        let discussionsTitleLbl = UILabel()
        discussionsTitleLbl.translatesAutoresizingMaskIntoConstraints = false
        discussionsTitleLbl.text = "Discussions"
        discussionsTitleLbl.textColor = .white
        discussionsTitleLbl.font = UIFont(name: "HelveticaNeue-Bold", size: 20)!
        
        let hamburgerBtn = UIButton()
        hamburgerBtn.translatesAutoresizingMaskIntoConstraints = false
        hamburgerBtn.setImage(sideNavIcon.withRenderingMode(.alwaysTemplate), for: .normal)
        
        hamburgerBtn.tintColor = accentColor
        setBtnImgProp(button: hamburgerBtn, topPadding: 45/4, leftPadding: 5)
        hamburgerBtn.addTarget(self, action: #selector(displaySideNavTapped), for: .touchUpInside)
        hamburgerBtn.contentMode = .scaleAspectFit
        
        
        userProfileButton.translatesAutoresizingMaskIntoConstraints = false
        userProfileButton.setImage(userPlaceholder.withRenderingMode(.alwaysOriginal), for: .normal)
        userProfileButton.imageView?.contentMode = .scaleToFill
//        userProfileButton.tintColor = accentColor
        userProfileButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        userProfileButton.addTarget(self, action: #selector(displayInfoTapped), for: .touchUpInside)
        userProfileButton.clipsToBounds = true
        userProfileButton.layer.cornerRadius = 20
        userProfileButton.layer.borderWidth = 1
        userProfileButton.layer.borderColor = UIColor.white.cgColor
        setUserProfileImage()
        
        headerContainer.addSubview(hamburgerBtn)
        headerContainer.addSubview(discussionsTitleLbl)
        headerContainer.addSubview(userProfileButton)
        view.addSubview(headerContainer)
        
        NSLayoutConstraint.activate([
            hamburgerBtn.leadingAnchor.constraint(equalTo: headerContainer.leadingAnchor),
            hamburgerBtn.topAnchor.constraint(equalTo: headerContainer.topAnchor),
            hamburgerBtn.heightAnchor.constraint(equalToConstant: 35),
            hamburgerBtn.widthAnchor.constraint(equalToConstant: 35),
            
            discussionsTitleLbl.centerXAnchor.constraint(equalTo: headerContainer.centerXAnchor),
            discussionsTitleLbl.centerYAnchor.constraint(equalTo: headerContainer.centerYAnchor),
            discussionsTitleLbl.heightAnchor.constraint(equalToConstant: 50),
            
            userProfileButton.trailingAnchor.constraint(equalTo: headerContainer.trailingAnchor, constant: -4),
            userProfileButton.topAnchor.constraint(equalTo: headerContainer.topAnchor),
            userProfileButton.heightAnchor.constraint(equalToConstant: 40),
            userProfileButton.widthAnchor.constraint(equalToConstant: 40),
        
            headerContainer.heightAnchor.constraint(equalToConstant: 50),
            headerContainer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 4),
            headerContainer.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 4),
            headerContainer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -4),
        ])
    }
    
    func addCountryCountTableView() {
        countryCountView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(countryCountView)
        
        NSLayoutConstraint.activate([
            countryCountView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0),
            countryCountView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            countryCountView.topAnchor.constraint(equalTo: headerContainer.bottomAnchor),
            countryCountView.heightAnchor.constraint(equalToConstant: 60)
        ])
    }
    
    func addDiscussionsMessageBox() {
        view.addSubview(discussionsMessageBox)
        discussionsMessageBox.translatesAutoresizingMaskIntoConstraints = false
        
        
        discussionsMessageBoxBottomAnchor = discussionsMessageBox.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0)
        
        NSLayoutConstraint.activate([
            discussionsMessageBox.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
            discussionsMessageBox.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
            discussionsMessageBoxBottomAnchor,
        ])
        
    }
    
    func addDiscussionChatView() {
        self.view.addSubview(discussionChatView)
        discussionChatView.translatesAutoresizingMaskIntoConstraints = false
        
        
        NSLayoutConstraint.activate([
            discussionChatView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
            discussionChatView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
            discussionChatView.topAnchor.constraint(equalTo: countryCountView.bottomAnchor  , constant: 10),
            discussionChatView.bottomAnchor.constraint(equalTo: discussionsMessageBox.topAnchor, constant: -10),
        ])
    }
    
    func addSlideGesture() {
        
        let edgeSlide = UIPanGestureRecognizer(target: self, action: #selector(presentSideNav(sender:)))
        view.addGestureRecognizer(edgeSlide)
    }
}


//MARK:- All Actions

extension DiscussionsViewController {
    @objc func displaySideNavTapped(_ sender: Any) {
        Analytics.logEvent(AnalyticsEvent.ShowSideNav.rawValue, parameters: nil)
        sideNavVC.transitioningDelegate = self
        sideNavVC.modalPresentationStyle = .custom
        sideNavVC.interactor = interactor
        sideNavVC.calledFromVC = DiscussionsViewController.discussionVC
        self.present(sideNavVC, animated: true, completion: nil)
        
    }
    
    @objc func displayInfoTapped(_ sender: UIButton) {
        
        if GIDSignIn.sharedInstance()?.currentUser == nil {
            let preSignInAlert = UIAlertController(title: "Discussions", message: preLoginInfoMessage, preferredStyle: .alert)
            let dismissAction = UIAlertAction(title: "Okay", style: .cancel) { _ in }
            let loginAction = UIAlertAction(title: "Login", style: .default) { (alert) in
                GIDSignIn.sharedInstance()?.signIn()
            }
            preSignInAlert.addAction(dismissAction)
            preSignInAlert.addAction(loginAction)
            present(preSignInAlert, animated: true, completion: nil)
        } else {
            let postSignInAlert = UIAlertController(title: "Discussions", message: postLoginInfoMessage, preferredStyle: .alert)
            let dismissAction = UIAlertAction(title: "Okay", style: .cancel) { _ in }
            postSignInAlert.addAction(dismissAction)
            present(postSignInAlert, animated: true, completion: nil)
        }
    }
    
    @objc func presentSideNav(sender: UIPanGestureRecognizer) {
        
        let translation = sender.translation(in: view)
        let progress = MenuHelper.calculateProgress(translationInView: translation, viewBounds: view.bounds, direction: .Right)
        
        MenuHelper.mapGestureStateToInteractor(gestureState: sender.state, progress: progress, interactor: interactor) {
            
            sideNavVC.transitioningDelegate = self
            sideNavVC.modalPresentationStyle = .custom
            sideNavVC.interactor = interactor
            sideNavVC.calledFromVC = DiscussionsViewController.discussionVC
            self.present(sideNavVC, animated: true, completion: nil)
            
        }
    }
}


//MARK:- Transition Delegate

extension DiscussionsViewController: UIViewControllerTransitioningDelegate {
    
    func animationController(forPresented presented: UIViewController,
                             presenting: UIViewController,
                             source: UIViewController)
    -> UIViewControllerAnimatedTransitioning?
    {
        if presenting == self && presented == sideNavVC {
            return RevealSideNav()
        }
        return nil
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        if dismissed == sideNavVC {
            return HideSideNav(vcPresent: true)
        }
        return nil
    }
    
    func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactor.hasStarted ? interactor : nil
    }
    
    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactor.hasStarted ? interactor : nil
    }
}

//MARK:- Keyboard handler

extension DiscussionsViewController {
    
    @objc func keyboardWillShow(notification: Notification) {
        if let keyboardFrame: NSValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
            let keyboardRectangle = keyboardFrame.cgRectValue
            let keyboardHeight = keyboardRectangle.height
            print("Keyboard Height:", keyboardHeight)
        }
    }
    
    func keyboardWillShow(keyboarEvent: KeyboardEvent ) {
        let keyboardFrame = keyboarEvent.keyboardFrameEnd
        let keyboardHeight = keyboardFrame.height
        print("Keyboard Height from observer:", keyboardHeight)
    }
    
    
    func handleKeyboardWillChangeFrame(keyboardEvent: KeyboardEvent) {
        
        
        let uiScreenHeight = UIScreen.main.bounds.size.height
        let endFrame = keyboardEvent.keyboardFrameEnd
        
        let endFrameY = endFrame.origin.y
        
        let offset = -1 * endFrame.size.height
        
        if endFrameY >= uiScreenHeight {
            self.discussionsMessageBoxBottomAnchor.constant = 0.0
            self.discussionChatView.discussionTableView.contentOffset.y += 2 * offset
        } else {
            self.discussionsMessageBoxBottomAnchor.constant = offset
            self.discussionChatView.discussionTableView.contentOffset.y -= offset
            print("Shifted visible paths: ", self.discussionChatView.discussionTableView.indexPathsForVisibleRows)
        }
        
        UIView.animate(
            withDuration: keyboardEvent.duration,
            delay: TimeInterval(0),
            options: keyboardEvent.options,
            animations: {
                self.view.layoutIfNeeded()
                
            },
            completion: nil)
    }
}

//MARK:- Login Handler

extension DiscussionsViewController: GIDSignInDelegate {
    func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error?) {
          // ...
          if let error = error {
              // ...
            print("Error signing in")
            print(error)
              return
          }
                  
          guard let authentication = user.authentication else { return }
          let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken,
                                                         accessToken: authentication.accessToken)
          
          Auth.auth().signIn(with: credential) { (authResult, error) in
              if let error = error {
                  print("authentication error \(error.localizedDescription)")
              }
          }
        setUserProfileImage()
      }
      
      func sign(_ signIn: GIDSignIn!, didDisconnectWith user: GIDGoogleUser!, withError error: Error!) {
          // Perform any operations when the user disconnects from app here.
          // ...
      }
    
    func setUserProfileImage() {
        discussionChatView.saveUserEmail()
        guard let googleUser = GIDSignIn.sharedInstance()?.currentUser else { return }
        guard let userImageUrl = googleUser.profile.imageURL(withDimension: 40) else { return }
        URLSession.shared.dataTask(with: userImageUrl) { (data, response, error) in
            
            guard let data = data, error == nil else { return }
            DispatchQueue.main.async() { [weak self] in
               let userImage = UIImage(data: data)
               self?.userProfileButton.setImage(userImage, for: .normal)
           }
        }.resume()
    }
}

ChatView Code:

class DiscussionChatView: UIView {
    
    let discussionChatId = "DiscussionChatID"
    let discussionTableView: UITableView
    var messages: [String: [DiscussionMessage]]  = [:]
    var messageSendDates: [String] = []
    
    var userEmail = "UserNotLoggedIn"
    var first = true
    
    override init(frame: CGRect) {
        discussionTableView = UITableView()
        
        super.init(frame: frame)
        
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sendTapNotification))
        discussionTableView.addGestureRecognizer(tapRecognizer)
        
        //        saveUserEmail()
        
        discussionTableView.register(DiscussionChatMessageCell.self, forCellReuseIdentifier: discussionChatId)
        discussionTableView.delegate = self
        discussionTableView.dataSource = self
        
        discussionTableView.estimatedRowHeight = 30
        discussionTableView.rowHeight = UITableViewAutomaticDimension
        
        self.addSubview(discussionTableView)
        discussionTableView.translatesAutoresizingMaskIntoConstraints = false
        discussionTableView.backgroundColor = .clear
        discussionTableView.allowsSelection = false
        discussionTableView.separatorStyle = .none
        
        NSLayoutConstraint.activate([
            discussionTableView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            discussionTableView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            discussionTableView.topAnchor.constraint(equalTo: self.topAnchor),
            discussionTableView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
        ])
        
        loadInitialMessages()
        appendNewMessages()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func loadInitialMessages() {
        messagesReference.queryOrderedByKey().observeSingleEvent(of: .value) { (snapshot) in
            
            guard let value = snapshot.value as? [String: Any] else {return}
            do {
                let data = try JSONSerialization.data(withJSONObject: value, options: .prettyPrinted)
                let messages = try JSONDecoder().decode([String: DiscussionMessage].self, from: data)
                var messagesList = messages.map { $0.1 }
                messagesList = messagesList.sorted(by: {
                    $0.messageTimestamp < $1.messageTimestamp
                })
                
                for message in messagesList {
                    
                    let dateString = self.getDateString(from: message.messageTimestamp)
                    
                    if !self.messageSendDates.contains(dateString) {
                        self.messageSendDates.append(dateString)
                    }
                    
                    self.messages[dateString, default: [DiscussionMessage]()].append(message)
                }
                
                self.discussionTableView.reloadData()
            } catch {
                print(error)
            }
        }
    }
    
    func appendNewMessages() {
        messagesReference.queryLimited(toLast: 1).observe(.childAdded) { (snapshot) in
            
            if self.first {
                self.first = false
                return
            }
            
            self.saveUserEmail()
            
            if  let value = snapshot.value {
                do {
                    
                    var lastCellWasVisible: Bool = false
                    if let visiblePaths = self.discussionTableView.indexPathsForVisibleRows {
                        print("Visible paths: ", visiblePaths) 
                        
                        print("Sections: ", self.messageSendDates.count - 1)
                        print("Row: ", self.messages[self.messageSendDates.last ?? "", default: [DiscussionMessage]()].count - 1)
                        
                        lastCellWasVisible = visiblePaths.contains([self.messageSendDates.count - 1, self.messages[self.messageSendDates.last ?? "", default: [DiscussionMessage]()].count - 1])
                    }
                    
                    let data = try JSONSerialization.data(withJSONObject: value, options: .prettyPrinted)
                    let message = try JSONDecoder().decode(DiscussionMessage.self, from: data)
                    
                    let dateString = self.getDateString(from: message.messageTimestamp)
                    
                    if !self.messageSendDates.contains(dateString) {
                        self.messageSendDates.append(dateString)
                        let indexSet = IndexSet(integer: self.messageSendDates.count - 1)
                        self.discussionTableView.performBatchUpdates({
                            self.discussionTableView.insertSections(indexSet, with: .automatic)
                        }) { (update) in
                            print("Update Success")
                            print("Last cell visible", lastCellWasVisible)
                            self.insertMessage(dateString: dateString, message: message)
                        }
                    } else {
                        print("Last cell visible", lastCellWasVisible)
                        self.insertMessage(dateString: dateString, message: message)
                    }
                    
                    if lastCellWasVisible {
                        self.scrollTableViewToEnd()
                        // This is not working
                        // This is not working
                        // This is not working
                    } else {
                        Toast.show(message: "New Message", type: .Info)
                    }
                } catch {
                    print(error)
                }
            }
        }
    }
    
    func insertMessage(dateString: String, message: DiscussionMessage) {
        messages[dateString, default: [DiscussionMessage]()].append(message)
        let indexPath = IndexPath(row:(self.messages[dateString, default: [DiscussionMessage]()].count - 1), section: self.messageSendDates.index(of: dateString) ?? 0)
        
        self.discussionTableView.insertRows(at: [indexPath], with: .automatic)
    }
}

extension DiscussionChatView: UITableViewDelegate, UITableViewDataSource {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return messageSendDates.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return messages[messageSendDates[section], default: [DiscussionMessage]()] .count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let discussionChatMessageCell = tableView.dequeueReusableCell(withIdentifier: discussionChatId, for: indexPath) as? DiscussionChatMessageCell else { return UITableViewCell()}
        
        
        let message = messages[messageSendDates[indexPath.section], default: [DiscussionMessage]()][indexPath.row]
        discussionChatMessageCell.configureCell(message:message, isSender: message.userEmailAddress == userEmail)
        
        return discussionChatMessageCell
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        
        let headerLabelView = UILabel(frame: CGRect(x: 0, y: 0, width: discussionTableView.frame.size.width, height: 60))
        let headerLabel = UILabel(frame: CGRect(x: (discussionTableView.frame.size.width-100)/2, y: 20, width: 100, height: 40))
        
        headerLabel.adjustsFontSizeToFitWidth = true
        headerLabel.font = UIFont(name: "Helvetica Neue", size: 13)!
        headerLabel.backgroundColor = UIColor.white
        headerLabel.textAlignment = .center
        headerLabel.textColor = UIColor.black
        
        headerLabelView.addSubview(headerLabel)
        headerLabel.clipsToBounds = true
        headerLabel.layer.cornerRadius = 10
        
        headerLabel.text = getDateStringForHeaderText(dateString: messageSendDates[section])
        
        return headerLabelView
        
    }
    
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        cell.backgroundColor = .clear
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 60
    }
    
    //    func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
    //        return true
    //    }
    //
    //    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
    //        return true
    //    }
    
}

//MARK:- Utility Functions

extension DiscussionChatView {
    
    func saveUserEmail() {
        if userEmail != "UserNotLoggedIn" { return }
        if let currentUser = GIDSignIn.sharedInstance().currentUser {
            userEmail = currentUser.profile.email
            print("Email: ", userEmail)
            discussionTableView.reloadData()
            scrollTableViewToEnd(animated: false)
        }
    }
    
    func getDateFormatter() -> DateFormatter {
        let dateFormatter = DateFormatter()
        dateFormatter.timeZone = .current
        dateFormatter.dateFormat = "dd MMM yyyy"
        return dateFormatter
    }
    
    func getDate(from dateString: String) -> Date? {
        //        print("Date String: ", dateString)
        let dateFormatter = getDateFormatter()
        return dateFormatter.date(from: dateString) ?? nil
    }
    
    func getDateString(from timestamp: Double) -> String {
        let dateFormatter = getDateFormatter()
        let date = Date(timeIntervalSince1970: timestamp)
        let dateString = dateFormatter.string(from: date)
        return dateString
    }
    
    func getDateStringForHeaderText(dateString: String) -> String {
        guard let date = getDate(from: dateString) else {
            //            print("Could not get date for generting header string")
            return dateString
        }
        //        print("Date: ", date.description(with: .current))
        if Calendar.current.isDateInToday(date) { return "Today"}
        if Calendar.current.isDateInYesterday(date) {return "Yesterday"}
        return dateString
    }
    
    func scrollTableViewToEnd(animated: Bool = true) {
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now(), execute: {
            let indexPath = IndexPath(row: self.messages[self.messageSendDates.last ?? "", default: [DiscussionMessage]()].count - 1, section: self.messageSendDates.count - 1)
            if self.discussionTableView.isValid(indexPath: indexPath) {
                self.discussionTableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.bottom, animated: animated)
            }
        })
    }
}

//MARK:- Actions

extension DiscussionChatView {
    @objc func sendTapNotification() {
        NotificationCenter.default.post(name: NSNotification.Name(chatViewTappedNotificationName), object: nil)
    }
}

Related Question: Smoothly scrolling tableview up by fixed constant when keyboard appears so last cells are still visible

Instead of adjusting the tableView's offset it's better to modified the contentInset / adjustedContentInset.

You could also try to set automaticallyAdjustsScrollIndicatorInsets to true , so you don't need to manually change the offset at all.

You still may need to use scrollToRow(at:at:animated:) to keep the latest row visible.

Edit:

I've put together a little sample app that might clarify this a bit.

//
//  ViewController.swift
//  TableViewTest
//
//  Created by Dirk Mika on 21.01.21.
//

import UIKit

class ViewController: UIViewController
{
    let keyboardObserver = KeyboardObserver()
    var tableView: UITableView!
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        
        tableView = UITableView(frame: CGRect.zero, style: .grouped)
        tableView.delegate = self
        tableView.dataSource = self
        tableView.translatesAutoresizingMaskIntoConstraints = false
        let textField = UITextField(frame: CGRect(x: 0.0, y: 0.0, width: self.view.bounds.size.width, height: 44.0))
        textField.borderStyle = .roundedRect
        tableView.tableFooterView = textField
        view.addSubview(tableView)
        NSLayoutConstraint.activate([
            tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
            tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
        ])
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        
        keyboardObserver.observe { [weak self] (event) -> Void in
            guard let self = self else { return }
            switch event.type {
            case .willChangeFrame:
                self.handleKeyboardWillChangeFrame(keyboardEvent: event)
            default:
                break
            }
        }
    }
    
    override func viewDidAppear(_ animated: Bool)
    {
        super.viewDidAppear(animated)
        tableView.scrollToRow(at: IndexPath(row: 19, section: 0), at: .bottom, animated: true)
    }
    
    func handleKeyboardWillChangeFrame(keyboardEvent: KeyboardEvent)
    {
        let keyboardFrame = keyboardEvent.keyboardFrameEnd
        
        let keyboardWindowFrame = self.view.window!.convert(keyboardFrame, from: nil)
        let relativeFrame = self.view.convert(keyboardWindowFrame, from: nil)
        var bottomOffset = tableView.frame.origin.y + tableView.frame.size.height - relativeFrame.origin.y - self.view.safeAreaInsets.bottom;
        if (bottomOffset < 0.0)
        {
            bottomOffset = 0.0;
        }
        
        var insets = tableView.contentInset
        insets.bottom = bottomOffset
        tableView.contentInset = insets
        tableView.scrollIndicatorInsets  = insets
    }
}


extension ViewController: UITableViewDelegate, UITableViewDataSource
{
    func numberOfSections(in tableView: UITableView) -> Int
    {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    {
        return 20
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel!.text = "\(indexPath.row)"
        return cell
    }
}

Dirk

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM