【问题标题】:How to scroll to the exact end of the UITableView?如何滚动到 UITableView 的确切末尾?
【发布时间】:2016-02-15 18:43:20
【问题描述】:

我有一个UITableView,其中填充了具有动态高度的单元格。当从视图控制器推送视图控制器时,我希望表格滚动到底部。

我已尝试使用 contentOffsettableView scrollToRowAtIndexPath,但仍然没有得到我想要的完美解决方案。

谁能帮我解决这个问题?

这是我要滚动的代码:

let indexPath = NSIndexPath(forRow: commentArray.count-1, inSection: 0)
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)

【问题讨论】:

  • 你什么时候拨打这个电话。这是最重要的。您不想在表格视图被填充之前调用它。
  • 是的,我在重新加载 tableview 后调用了这一行
  • 这还不够。你看,reloadData 虽然看起来是同步的,但实际上是异步的。因此,您必须将代码包含在主线程上的 dispatchAsync 块中,或者找到另一种方法来执行此操作。我多次遇到这个问题。大多数时候滚动到特定的索引路径几乎是徒劳的。你应该使用setContentOffset 方法。
  • 你能告诉我,我应该在哪里使用这个 setContentOffset 行。

标签: ios swift uitableview


【解决方案1】:

对于 Swift 3.0

写一个函数:

func scrollToBottom(){
    DispatchQueue.main.async {
        let indexPath = IndexPath(row: self.dataArray.count-1, section: 0)
        self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
    }
}

并在重新加载 tableview 数据后调用它

tableView.reloadData()
scrollToBottom()

【讨论】:

  • 不安全:This application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.
  • @Jeremie 请将 UI 更改添加到主队列以避免此警告
  • 我更新了答案以在主线程上执行此更新。
  • TO 想要滚动到 UITableView 的底部。如果 tableView 有自定义 contentInset 怎么办?您的解决方案只会滚动到最后一个单元格的底部。
  • 感谢tableView.reloadData() - 这对我来说是个问题,表格不知道滚动时的行数
【解决方案2】:

我会使用更通用的方法:

Swift4

extension UITableView {

    func scrollToBottom(){

        DispatchQueue.main.async {
            let indexPath = IndexPath(
                row: self.numberOfRows(inSection:  self.numberOfSections-1) - 1, 
                section: self.numberOfSections - 1)
            if hasRowAtIndexPath(indexPath) {
                self.scrollToRow(at: indexPath, at: .bottom, animated: true)
            }
        }
    }

    func scrollToTop() {

        DispatchQueue.main.async {
            let indexPath = IndexPath(row: 0, section: 0)
            if hasRowAtIndexPath(indexPath) {
                self.scrollToRow(at: indexPath, at: .top, animated: false)
           }
        }
    }

    func hasRowAtIndexPath(indexPath: IndexPath) -> Bool {
        return indexPath.section < self.numberOfSections && indexPath.row < self.numberOfRows(inSection: indexPath.section)
    }
}

Swift5

extension UITableView {

    func scrollToBottom(isAnimated:Bool = true){

        DispatchQueue.main.async {
            let indexPath = IndexPath(
                row: self.numberOfRows(inSection:  self.numberOfSections-1) - 1,
                section: self.numberOfSections - 1)
            if self.hasRowAtIndexPath(indexPath: indexPath) {
                self.scrollToRow(at: indexPath, at: .bottom, animated: isAnimated)
            }
        }
    }

    func scrollToTop(isAnimated:Bool = true) {

        DispatchQueue.main.async {
            let indexPath = IndexPath(row: 0, section: 0)
            if self.hasRowAtIndexPath(indexPath: indexPath) {
                self.scrollToRow(at: indexPath, at: .top, animated: isAnimated)
           }
        }
    }

    func hasRowAtIndexPath(indexPath: IndexPath) -> Bool {
        return indexPath.section < self.numberOfSections && indexPath.row < self.numberOfRows(inSection: indexPath.section)
    }
}

【讨论】:

  • 如果给定部分有 0 行,这将崩溃。请参阅我的答案以尝试修复。
  • 如果 UITableView 有 tableHeaderView,你的 scrollToTop 将不会滚动到顶部。这在两种情况下都有效: setContentOffset(.zero, animated: true)
  • 我刚刚添加: let indexPath = IndexPath( row: self.numberOfRows(inSection: self.numberOfSections - 1) - 1, section: self.numberOfSections - 1) if (indexPath.row > 0) { self.scrollToRow(at: indexPath, at: .bottom, animated: true) }
【解决方案3】:

我尝试了 Umair 的方法,但是在 UITableViews 中,有时可能会有一个 0 行的部分;在这种情况下,代码指向无效的索引路径(空部分的第 0 行不是一行)。

从行/部分的数量中盲目地减去 1 可能是另一个痛点,因为行/部分可能包含 0 个元素。

这是我滚动到最底部单元格的解决方案,确保索引路径有效:

extension UITableView {
    func scrollToBottomRow() {
        DispatchQueue.main.async {
            guard self.numberOfSections > 0 else { return }

            // Make an attempt to use the bottom-most section with at least one row
            var section = max(self.numberOfSections - 1, 0)
            var row = max(self.numberOfRows(inSection: section) - 1, 0)
            var indexPath = IndexPath(row: row, section: section)

            // Ensure the index path is valid, otherwise use the section above (sections can
            // contain 0 rows which leads to an invalid index path)
            while !self.indexPathIsValid(indexPath) {
                section = max(section - 1, 0)
                row = max(self.numberOfRows(inSection: section) - 1, 0)
                indexPath = IndexPath(row: row, section: section)

                // If we're down to the last section, attempt to use the first row
                if indexPath.section == 0 {
                    indexPath = IndexPath(row: 0, section: 0)
                    break
                }
            }

            // In the case that [0, 0] is valid (perhaps no data source?), ensure we don't encounter an
            // exception here
            guard self.indexPathIsValid(indexPath) else { return }

            self.scrollToRow(at: indexPath, at: .bottom, animated: true)
        }
    }

    func indexPathIsValid(_ indexPath: IndexPath) -> Bool {
        let section = indexPath.section
        let row = indexPath.row
        return section < self.numberOfSections && row < self.numberOfRows(inSection: section)
    }
}

【讨论】:

    【解决方案4】:

    对于完美的滚动到底部解决方案,使用 tableView contentOffset

    func scrollToBottom()  {
            let point = CGPoint(x: 0, y: self.tableView.contentSize.height + self.tableView.contentInset.bottom - self.tableView.frame.height)
            if point.y >= 0{
                self.tableView.setContentOffset(point, animated: animate)
            }
        }
    
    在主队列中执行滚动到底部是可行的,因为它会延迟执行并导致工作,因为在加载 viewController 并延迟通过主队列 tableView 现在知道它的内容大小。

    我宁愿在将数据填充到 tableView 后使用self.view.layoutIfNeeded(),然后调用我的方法scrollToBottom()。这对我来说很好。

    【讨论】:

    • 这是滚动到表格底部或任何其他 y 位置的完美解决方案。太好了!
    【解决方案5】:

    当您推送具有 tableview 的 viewcontroller 时,您应该仅在 Tableview 重新加载完成后滚动到指定的 indexPath。

    yourTableview.reloadData()
    dispatch_async(dispatch_get_main_queue(), { () -> Void in
        let indexPath = NSIndexPath(forRow: commentArray.count-1, inSection: 0)
      tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)
    
    })
    

    将方法放在 dispatch_async 中的原因是,一旦你执行 reloadData,下一行将立即执行,然后在主线程中重新加载。所以要知道 tableview 什么时候完成(在所有 cellforrowindex 完成之后),我们在这里使用 GCD。基本上tableview中没有委托会告诉tableview已经完成重新加载。

    【讨论】:

    • 感谢您的解决方案。当我应用此代码时,效果比以前更好,但问题仍然存在。它没有完全滚动到最后一行。
    • 当表格视图被填充时,最后 5 到 6 个单元格被留下滚动。
    • 尝试将scrollPosition改为Top
    • tableview 没有变化
    • 是的,确保在调用 scrollToRowAtIndexPath 之前执行 yourTableview.layoutIfNeeded() ,这将确保如果任何单元格需要自动调整大小,则在安排滚动动画之前对其进行处理。干杯!
    【解决方案6】:

    在 Swift 4+ 中工作:

       self.tableView.reloadData()
        let indexPath = NSIndexPath(row: self.yourDataArray.count-1, section: 0)
        self.tableView.scrollToRow(at: indexPath as IndexPath, at: .bottom, animated: true)
    

    【讨论】:

      【解决方案7】:

      @Umair 回答的一点更新,以防您的 tableView 为空

      func scrollToBottom(animated: Bool = true, delay: Double = 0.0) {
          let numberOfRows = tableView.numberOfRows(inSection: tableView.numberOfSections - 1) - 1
          guard numberOfRows > 0 else { return }
      
          DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [unowned self] in
      
              let indexPath = IndexPath(
                  row: numberOfRows,
                  section: self.tableView.numberOfSections - 1)
              self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: animated)
          }
      }
      

      【讨论】:

        【解决方案8】:

        使用 Swift 5,您也可以在每次重新加载后执行此操作:

        DispatchQueue.main.async {
            let index = IndexPath(row: self.itens.count-1, section: 0)
            self.tableView.scrollToRow(at: index, at: .bottom, animated: true)                        
        }           
                
        

        【讨论】:

        • 当你有 0 个项目时,这会崩溃,但这是个好主意。
        • 但是滚动 0 个项目有什么意义呢?是我使用 rxswift 我只在计数不同于 0 时才在我的结构中发布值。
        【解决方案9】:

        适用于 Swift 5 或更高版本

        import UIKit
        
        extension UITableView {
            
            func scrollToBottom(animated: Bool) {
                
                DispatchQueue.main.async {
                    let point = CGPoint(x: 0, y: self.contentSize.height + self.contentInset.bottom - self.frame.height)
                    if point.y >= 0 {
                        self.setContentOffset(point, animated: animated)
                    }
                }
            }
        }
        

        【讨论】:

          【解决方案10】:

          这适用于 Swift 3.0

          let pointsFromTop = CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude)
          tableView.setContentOffset(pointsFromTop, animated: true)
          

          【讨论】:

          • 在 iOS 11、Swift 4、Xcode 9 上使我的 tableview 无用,没有数据显示并且无法滚动
          • 这行得通,不确定为什么其他人不赞成,我投票支持
          【解决方案11】:

          你也可以用这个:-

          tableView.scrollRectToVisible(CGRect(x: 0, y: tableView.contentSize.height, width: 1, height: 1), animated: true)
          

          【讨论】:

            【解决方案12】:

            Swift 5 解决方案

            extension UITableView {
               func scrollToBottom(){
            
                DispatchQueue.main.async {
                    let indexPath = IndexPath(
                        row: self.numberOfRows(inSection:  self.numberOfSections-1) - 1,
                        section: self.numberOfSections - 1)
                    if self.hasRowAtIndexPath(indexPath: indexPath) {
                        self.scrollToRow(at: indexPath, at: .bottom, animated: true)
                    }
                }
            }
            
            func scrollToTop() {
                DispatchQueue.main.async { 
                    let indexPath = IndexPath(row: 0, section: 0)
                    if self.hasRowAtIndexPath(indexPath: indexPath) {
                        self.scrollToRow(at: indexPath, at: .top, animated: false)
                   }
                }
            }
            
            func hasRowAtIndexPath(indexPath: IndexPath) -> Bool {
                return indexPath.section < self.numberOfSections && indexPath.row < self.numberOfRows(inSection: indexPath.section)
            }
            }
            

            【讨论】:

            • 它按原样工作,但我不想要动画。如果在 scrollToRow 中设置为 false,它只会从底部开始,但会出现故障并略微向上跳跃几个小步骤
            【解决方案13】:

            如果您使用具有一定高度的UINavigationBarUIView 中的UITableView,请尝试从UITableView 的框架高度中减去UINavigationBar 高度。导致您的UITableViewtop 点与UINavigationBarbottom 点相同,因此这会影响您的UITableViewbottom 项目滚动。

            斯威夫特 3

            simpleTableView.frame = CGRect.init(x: 0, y: navigationBarHeight, width: Int(view.frame.width), height: Int(view.frame.height)-navigationBarHeight)
            

            【讨论】:

              【解决方案14】:

              [Swift 3,iOS 10]

              我最终使用了一种 hacky 解决方案,但它不依赖于行索引路径(有时会导致崩溃)、单元格动态高度或表格重新加载事件,因此它看起来非常普遍并且在实践中有效比我发现的其他人更可靠。

              • 使用 KVO 跟踪 table 的 contentOffset

              • KVO 观察者内触发滚动事件

              • 使用延迟计时器来安排滚动调用以过滤多个
                观察者触发器

              一些ViewController里面的代码:

              private var scrollTimer: Timer?
              private var ObserveContext: Int = 0
              
              override func viewWillAppear(_ animated: Bool) {
                  super.viewWillAppear(animated)
                  table.addObserver(self, forKeyPath: "contentSize", options: NSKeyValueObservingOptions.new, context: &ObserveContext)
              }
              
              override func viewWillDisappear(_ animated: Bool) {
                  super.viewWillDisappear(animated)
                  table.removeObserver(self, forKeyPath: "contentSize")
              }
              
              override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
                  if (context == &ObserveContext) {
                      self.scheduleScrollToBottom()
                  }
              }
              
              func scheduleScrollToBottom() {
              
                  if (self.scrollTimer == nil) {
                      self.scrollTimer = Timer(timeInterval: 0.5, repeats: false, block: { [weak self] (timer) in
                          let table = self!.table
              
                          let bottomOffset = table.contentSize.height - table.bounds.size.height
                          if !table.isDragging && bottomOffset > 0 {
                              let point: CGPoint = CGPoint(x: 0, y: bottomOffset)
                              table.setContentOffset(point, animated: true)
                          }
              
                          timer.invalidate()
                          self?.scrollTimer = nil
                      })
                      self.scrollTimer?.fire()
                  }
              }
              

              【讨论】:

                【解决方案15】:

                滚动scrollToBottom的最佳方式:

                在调用 scrollToBottom 方法之前 调用下面的方法

                self.view.layoutIfNeeded()
                
                extension UITableView {
                    func scrollToBottom(animated:Bool)  {
                        let numberOfRows = self.numberOfRows(inSection: self.numberOfSections - 1) - 1
                        if numberOfRows >= 0{
                            let indexPath = IndexPath(
                                row: numberOfRows,
                                section: self.numberOfSections - 1)
                            self.scrollToRow(at: indexPath, at: .bottom, animated: animated)
                        } else {
                            let point = CGPoint(x: 0, y: self.contentSize.height + self.contentInset.bottom - self.frame.height)
                            if point.y >= 0{
                                self.setContentOffset(point, animated: animated)
                            }
                        }
                    }
                }
                

                【讨论】:

                  【解决方案16】:

                  要滚动到 TableView 的末尾,您可以使用以下函数,该函数也适用于 ScrollView。

                  它还会计算 iPhone X 及更新版本底部的安全区域。调用是从主队列进行的,以正确计算高度。

                  func scrollToBottom(animated: Bool) {
                      DispatchQueue.main.async {
                          let bottomOffset = CGPoint(x: 0, y: self.contentSize.height - self.bounds.size.height + self.safeAreaBottom)
                          
                          if bottomOffset.y > 0 {
                              self.setContentOffset(bottomOffset, animated: animated)
                          }
                      }
                  }
                  

                  【讨论】:

                    【解决方案17】:

                    在 Swift 3+ 中工作:

                            self.tableView.setContentOffset(CGPoint(x: 0, y: self.tableView.contentSize.height - UIScreen.main.bounds.height), animated: true)
                    

                    【讨论】:

                    • 这有几个问题。一、tableview可能不会占据整个屏幕,你忽略导航栏、搜索栏和其他任何可能影响tableview显示区域的元素。您还会忽略任何可能已设置在表格上的内容插图。
                    【解决方案18】:

                    这是一个包含完成关闭的函数:

                    func reloadAndScrollToTop(completion: @escaping () -> Void) {
                        self.tableView.reload()
                        completion()
                    }
                    

                    并使用:

                    self.reloadAndScrollToTop(completion: {
                         tableView.scrollToRow(at: indexPath, at: .top, animated: true))
                    })
                    

                    tableView.scrollTo ... 行将在所有表格单元格安全加载后执行。

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2011-06-24
                      • 2020-08-09
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多