UIScrollView contentOffset 在方向更改后损坏

Posted

技术标签:

【中文标题】UIScrollView contentOffset 在方向更改后损坏【英文标题】:UIScrollView contentOffset corrupted after orientation change 【发布时间】:2020-10-23 17:59:11 【问题描述】:

我有一个 UITableView 并打开了分页,所以我可以像页面一样翻阅每个单元格(这是整个 tableView 的高度)。这很棒

直到我强制改变方向并返回。

现在它在单元格之间分页。所以两个细胞的一部分都是可见的。它继续这样翻页,直到我将 tableView 一直滚动到顶部。然后它会重置,您可以继续正确向下滚动。

考虑到这些信息,我意识到我只需将 contentOffset 设置为之前的值。

如果我手动将内容偏移量设置为 3000(假设滚动视图为 1000 点高)

应该滚动到索引 3

但是如果我这样做,在旋转之后,它会滚动到喜欢索引 16。但是当我从 scrollViewDidScroll 打印时,它仍然说偏移量是 3000

当我向上滚动时,内容偏移量在 scrollView 重新计算并确定它还不能是负偏移量并在偏移量上再增加 1000 个点之前几乎为零。每次我向上滚动它都会变为零,然后将 1000 重新添加到偏移量。 最终它会重复自己,直到您实际上位于顶部的偏移量 0 处,然后您可以滚动并且一切正常。

以下是正在发生的事情的示例:

我创建了一个示例项目来让它工作:

class TestViewController: UIViewController 
        
    var isLandscape = false
    
    var tableView: UITableView!
    var buttonRotate: UIButton!
    var labelOffset: UILabel!
    let numberOfItems = 30

    override func viewDidLoad() 
        super.viewDidLoad()
        
        tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)
        tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
        tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
        tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true

        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.dataSource = self
        tableView.delegate = self
        tableView.isPagingEnabled = true
        
        
        buttonRotate = UIButton()
        buttonRotate.backgroundColor = .lightGray
        buttonRotate.addTarget(self, action: #selector(clickedRotate(_:)), for: .touchUpInside)
        buttonRotate.translatesAutoresizingMaskIntoConstraints = false
        buttonRotate.setTitle("Rotate", for: .normal)
        view.addSubview(buttonRotate)
        buttonRotate.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
        buttonRotate.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true

        labelOffset = UILabel()
        labelOffset.translatesAutoresizingMaskIntoConstraints = false
        labelOffset.text = "Offset: \(tableView.contentOffset.y)"
        view.addSubview(labelOffset)
        labelOffset.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
        labelOffset.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true

    

    @IBAction func clickedRotate(_ sender: Any) 
        
        self.isLandscape = !self.isLandscape

        if self.isLandscape 
            let value = UIInterfaceOrientation.landscapeRight.rawValue
            UIDevice.current.setValue(value, forKey: "orientation")
         else 
            let value = UIInterfaceOrientation.portrait.rawValue
            UIDevice.current.setValue(value, forKey: "orientation")
        
    
    
    //This fixes it by temporarily bringing the offset to zero, which is the same as scrolling to the top. After this it scrolls back to the correct place. It needs to be separated by 0.1 seconds to work
//    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 
//        coordinator.animate(alongsideTransition:  context in
//        )  (context) in
//            self.printScrollValues()
//            self.tableView.contentOffset.y = 0
//            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) 
//                self.resetContentOffset()
//            
//        
//
//    
    
    
    func resetContentOffset() 
        let size = tableView.frame.size
        let index = 3
        let offset = size.height * CGFloat(index)
        print("\n\noffset: \(offset)")
        tableView.contentOffset.y = offset
    


extension TestViewController: UITableViewDelegate, UITableViewDataSource 
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat 
        return tableView.frame.size.height
    
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
        return numberOfItems
    
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
        cell.textLabel?.text = indexPath.row.description
        cell.textLabel?.font = .systemFont(ofSize: 40)
        let index = indexPath.row
        if index % 2 == 0 
            cell.backgroundColor = .yellow
         else 
            cell.backgroundColor = .blue
        
        return cell
    
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) 
        print(scrollView.contentOffset.y)
        labelOffset.text = "Offset: \(Int(scrollView.contentOffset.y))"
    


【问题讨论】:

【参考方案1】:

你需要一些东西......

首先,我们需要给 tableView 一个估计的行高。我们可以在这里这样做:

override func viewDidLayoutSubviews() 
    super.viewDidLayoutSubviews()
    // only need to call this if tableView size has changed
    if tableView.estimatedRowHeight != tableView.frame.size.height 
        tableView.estimatedRowHeight = tableView.frame.size.height
    

接下来,在旋转之后,我们需要告诉 tableView 重新计算它的布局。处理“设备旋转”操作的正确位置在这里,因为 tableView 可以由于其他因素(不在此测试控制器中,但一般情况下)改变大小:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 
    super.viewWillTransition(to: size, with: coordinator)
    coordinator.animate(alongsideTransition: nil, completion: 
        _ in
        // tell the tableView to recalculate its layout
        self.tableView.performBatchUpdates(nil, completion: nil)
        self.labelOffset.text = "Offset: \(Int(self.tableView.contentOffset.y)) ContentSize: \(Int(self.tableView.contentSize.height))"
    )

这是您的完整示例,包含这两个功能和少量编辑(删除未使用的代码,更改了 labelOffset.text 等...请参阅代码中的 cmets):

class TestRotateViewController: UIViewController 
    
    var isLandscape = false
    
    var tableView: UITableView!
    var buttonRotate: UIButton!
    var labelOffset: UILabel!
    let numberOfItems = 30
    
    override func viewDidLoad() 
        super.viewDidLoad()
        
        tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)
        tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
        tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
        tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
        
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.dataSource = self
        tableView.delegate = self
        tableView.isPagingEnabled = true
        
        
        buttonRotate = UIButton()
        buttonRotate.backgroundColor = .lightGray
        buttonRotate.addTarget(self, action: #selector(clickedRotate(_:)), for: .touchUpInside)
        buttonRotate.translatesAutoresizingMaskIntoConstraints = false
        buttonRotate.setTitle("Rotate", for: .normal)
        view.addSubview(buttonRotate)
        buttonRotate.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
        buttonRotate.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
        
        labelOffset = UILabel()
        labelOffset.translatesAutoresizingMaskIntoConstraints = false
        labelOffset.text = "Offset: \(tableView.contentOffset.y)"
        view.addSubview(labelOffset)
        labelOffset.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
        labelOffset.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
        
    
    
    override func viewDidLayoutSubviews() 
        super.viewDidLayoutSubviews()
        // only need to call this if tableView size has changed
        if tableView.estimatedRowHeight != tableView.frame.size.height 
            tableView.estimatedRowHeight = tableView.frame.size.height
        
    
    
    // viewDidAppear implemented ONLY to update the labelOffset text
    //  this is NOT needed for functionality
    override func viewDidAppear(_ animated: Bool) 
        super.viewDidAppear(animated)
        labelOffset.text = "Offset: \(Int(tableView.contentOffset.y)) ContentSize: \(Int(tableView.contentSize.height))"
    
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: nil, completion: 
            _ in
            // tell the tableView to recalculate its layout
            self.tableView.performBatchUpdates(nil, completion: nil)
            self.labelOffset.text = "Offset: \(Int(self.tableView.contentOffset.y)) ContentSize: \(Int(self.tableView.contentSize.height))"
        )
    
    
    @IBAction func clickedRotate(_ sender: Any) 
        
        self.isLandscape = !self.isLandscape
        
        if self.isLandscape 
            let value = UIInterfaceOrientation.landscapeRight.rawValue
            UIDevice.current.setValue(value, forKey: "orientation")
         else 
            let value = UIInterfaceOrientation.portrait.rawValue
            UIDevice.current.setValue(value, forKey: "orientation")
        
    
    


extension TestRotateViewController: UITableViewDelegate, UITableViewDataSource 
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat 
        return tableView.frame.size.height
    
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
        return numberOfItems
    
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = indexPath.row.description
        cell.textLabel?.font = .systemFont(ofSize: 40)
        let index = indexPath.row
        if index % 2 == 0 
            cell.backgroundColor = .yellow
         else 
            // light-blue to make it easier to read the black label text
            cell.backgroundColor = UIColor(red: 0.0, green: 0.75, blue: 1.0, alpha: 1.0)
        
        return cell
    
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) 
        labelOffset.text = "Offset: \(Int(scrollView.contentOffset.y)) ContentSize: \(Int(scrollView.contentSize.height))"
    
    

【讨论】:

以上是关于UIScrollView contentOffset 在方向更改后损坏的主要内容,如果未能解决你的问题,请参考以下文章

UIScrollView 内 UIScrollView 内 UIScrollView

UIScrollView 内的 UIScrollView 时滚动

UIScrollView里面的UIScrollView:滚动

iOS开发-KVO的奥秘

问题:UIScrollview 弹跳使父 UIScrollview 弹跳

UIScrollView 嵌入其他 UIScrollView