ios - 当表格视图内容偏移以避免可见键盘时,UITableView.indexPathsForVisibleRows 未返回正确的值
问题描述
我有一个聊天视图,当消息进入时,如果最后一个单元格可见,那么表格视图应该滚动到最后,以便新消息可见。当键盘隐藏时,此行为正常工作。但是当键盘可见时,我将表格视图内容偏移了键盘的高度,这样之前可见的最后几条消息仍然可见。但是现在当有新消息进来时,可见的 indexPaths 列表是完全错误的。因此不会触发滚动到结束条件。
let offset = -1 * endFrame.size.height
self.discussionChatView.discussionTableView.contentOffset.y -= offset
在这种情况下,滚动工作正常。当最后一个单元格可见时它会滚动到末尾,而当最后一个单元格不可见时它不会滚动到末尾。
控制台日志:
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
但是当键盘可见时,它的工作方式就不一样了。最终的聊天视图显示在最后一张图片中(手动向下滚动后)。
来自键盘帧更改处理代码的日志:
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([])
来自聊天表视图的日志(实际上我已经滚动到最后一个单元格)
Visible paths: [[2, 3], [2, 4], [2, 5], [2, 6], [2, 7], [2, 8]]
Sections: 2
Row: 12
Last cell visible false
视图控制器代码:
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()
}
}
聊天查看代码:
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)
}
}
解决方案
与其调整 tableView 的偏移量,不如修改 contentInset /adjustedContentInset。
您也可以尝试设置automaticallyAdjustsScrollIndicatorInsets
为true
,因此您根本不需要手动更改偏移量。
您仍然可能需要使用scrollToRow(at:at:animated:)
来保持最新的行可见。
编辑:
我整理了一个小示例应用程序,可以稍微澄清一下。
//
// 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
}
}
短剑
推荐阅读
- jquery - 如何刷新列表用户(表)中的单选按钮
- openshift - 让`oc`根据目录跟随一个集群
- javascript - 如何从标题更改html的背景颜色
- python-3.x - 调用 z:org.apache.spark.api.python.PythonRDD.collectAndServe 时发生 py4j.protocol.Py4JJavaError
- sql - SQL - 更新具有特定条件的所有行
- apache-spark - Spark Structured Stream Executors奇怪的行为
- php - 注意:尝试在第 298 行的 /wp-includes/post-template.php 中获取非对象的属性
- mongoose - 错误:超过 2000 毫秒的超时。如果返回 Promise,请确保它已解决。即使用 return 替换 done() 仍然不起作用
- solr - 被多值字段上的 solr 查询阻塞
- javascript - 有没有办法将两个javascript合并在一起?