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.