reloadData () của UITableView với chiều cao ô động gây ra cuộn nhanh


142

Tôi cảm thấy như đây có thể là một vấn đề phổ biến và tự hỏi liệu có giải pháp chung nào cho nó không.

Về cơ bản, UITableView của tôi có chiều cao ô động cho mọi ô. Nếu tôi không ở trên cùng của UITableView và tôi tableView.reloadData(), việc cuộn lên sẽ trở nên khó khăn.

Tôi tin rằng điều này là do thực tế là vì tôi đã tải lại dữ liệu, khi tôi cuộn lên, UITableView đang tính toán lại chiều cao cho mỗi ô hiển thị. Làm cách nào để giảm thiểu điều đó hoặc làm cách nào tôi chỉ tải lạiData từ một IndexPath nhất định đến hết UITableView?

Hơn nữa, khi tôi xoay xở để di chuyển từ trên xuống, tôi có thể cuộn xuống và sau đó lên, không có vấn đề gì khi không nhảy. Điều này rất có thể vì độ cao của UITableViewCell đã được tính toán.


Một vài điều ... (1) Có, bạn chắc chắn có thể tải lại một số hàng nhất định bằng cách sử dụng reloadRowsAtIndexPaths. Nhưng (2) bạn có ý nghĩa gì khi "tăng vọt" và (3) bạn đã đặt chiều cao hàng ước tính? (Chỉ cần cố gắng tìm hiểu xem có giải pháp nào tốt hơn cho phép bạn cập nhật bảng một cách linh hoạt không.)
Lyndsey Scott

@LyndseyScott, vâng, tôi đã đặt chiều cao hàng ước tính. Bằng cách tăng vọt tôi có nghĩa là khi tôi cuộn lên, các hàng đang dịch chuyển lên trên. Tôi tin rằng điều này là do tôi đã đặt chiều cao hàng ước tính là 128 và sau đó khi tôi cuộn lên, tất cả các bài đăng của tôi ở trên trong UITableView đều nhỏ hơn, do đó nó thu nhỏ chiều cao, khiến bảng của tôi nhảy lên. Tôi đang nghĩ đến việc thực hiện reloadRowsAtIndexPaths từ hàng này xsang hàng cuối cùng trong TableView của tôi ... nhưng vì tôi đang chèn các hàng mới, nó sẽ không hoạt động, tôi không thể biết phần cuối của phần xem bảng của mình sẽ là gì trước khi tôi tải lại dữ liệu.
David

2
@LyndseyScott tôi vẫn không thể giải quyết vấn đề, có giải pháp nào tốt không?
rad

1
Bạn đã bao giờ tìm thấy một giải pháp cho vấn đề này? Tôi đang gặp vấn đề chính xác giống như trong video của bạn.
dùng344977

1
Không có câu trả lời dưới đây làm việc cho tôi.
Srujan Simha

Câu trả lời:


220

Để tránh nhảy, bạn nên lưu chiều cao của các ô khi chúng tải và đưa ra giá trị chính xác trong tableView:estimatedHeightForRowAtIndexPath:

Nhanh:

var cellHeights = [IndexPath: CGFloat]()

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return cellHeights[indexPath] ?? UITableView.automaticDimension
}

Mục tiêu C:

// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary = @{}.mutableCopy;

// declare table dynamic row height and create correct constraints in cells
tableView.rowHeight = UITableViewAutomaticDimension;

// save height
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    [cellHeightsDictionary setObject:@(cell.frame.size.height) forKey:indexPath];
}

// give exact height value
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = [cellHeightsDictionary objectForKey:indexPath];
    if (height) return height.doubleValue;
    return UITableViewAutomaticDimension;
}

1
Cảm ơn, bạn thực sự tiết kiệm được ngày của tôi :) Hoạt động trong objc quá
Artem Z.

3
Đừng quên khởi tạo cellHeightsDictionary: cellHeightsDictionary = [NSMutableDictionary dictionary];
Gerharbo 5/11/2016

1
estimatedHeightForRowAtIndexPath:trả về giá trị gấp đôi có thể gây ra *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]lỗi. Để sửa nó, return floorf(height.floatValue);thay vào đó.
liushuaikobe

Xin chào @lgor, tôi đang gặp vấn đề tương tự & đang cố gắng thực hiện giải pháp của bạn. Vấn đề tôi đang gặp phải là ước tínhHeightForRowAtIndexPath được gọi trước willDisplayCell, do đó, chiều cao của ô không được tính khi ước tính Có ai giúp đỡ không?
Madhuri

1
Độ cao hiệu quả của @Madhuri nên được tính bằng "heightForRowAtIndexPath", được gọi cho mọi ô trên màn hình ngay trước willDisplayCell, sẽ đặt chiều cao trong từ điển để sử dụng sau này trong ước tínhRowHeight (trên bảng tải lại).
Donnit

108

Swift 3 phiên bản của câu trả lời được chấp nhận.

var cellHeights: [IndexPath : CGFloat] = [:]


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return cellHeights[indexPath] ?? 70.0 
}

Cảm ơn điều này đã làm việc tuyệt vời! trong thực tế tôi đã có thể loại bỏ việc thực hiện của mình func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {, điều này xử lý tất cả các phép tính chiều cao tôi cần.
Natalia

Sau khi vật lộn nhiều giờ với việc nhảy liên tục, tôi phát hiện ra rằng tôi đã quên thêm UITableViewDelegatevào lớp học của mình. Việc tuân thủ giao thức đó là không cần thiết bởi vì nó chứa willDisplaychức năng được hiển thị ở trên . Tôi hy vọng tôi có thể cứu ai đó cùng một cuộc đấu tranh.
MJQZ1347

Cảm ơn bạn đã trả lời Swift. Trong trường hợp của tôi, tôi đã có một số hành vi kỳ lạ của các tế bào sắp hết trật tự khi tải lại khi chế độ xem bảng được cuộn xuống / gần phía dưới. Tôi sẽ sử dụng cái này từ bây giờ bất cứ khi nào tôi có các ô tự kích cỡ.
Trev14

Hoạt động hoàn hảo trong Swift 4.2
Adam S.

Một người cứu rỗi cuộc sống. Vì vậy, hữu ích khi cố gắng thêm nhiều mục trong nguồn dữ liệu. Ngăn chặn việc nhảy các ô mới được thêm vào giữa màn hình.
Philip Borbon

38

Nhảy là do chiều cao ước tính xấu. Ước tínhRowHeight càng khác so với chiều cao thực tế, bảng có thể nhảy càng nhiều khi được tải lại, đặc biệt là càng di chuyển xuống dưới. Điều này là do kích thước ước tính của bảng khác hoàn toàn với kích thước thực tế của nó, buộc bảng phải điều chỉnh kích thước nội dung và độ lệch của nó. Vì vậy, chiều cao ước tính không nên là một giá trị ngẫu nhiên mà gần với chiều cao bạn nghĩ là chiều cao sẽ có. Tôi cũng đã có kinh nghiệm khi tôi đặt UITableViewAutomaticDimension nếu các ô của bạn cùng loại thì

func viewDidLoad() {
     super.viewDidLoad()
     tableView.estimatedRowHeight = 100//close to your cell height
}

Nếu bạn có nhiều loại tế bào trong các phần khác nhau thì tôi nghĩ nơi tốt hơn là

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
     //return different sizes for different cells if you need to
     return 100
}

2
cảm ơn bạn, đó chính xác là lý do tại sao bảngView của tôi rất tăng vọt.
Louis de Decker

1
Một câu trả lời cũ, nhưng nó vẫn thực tế kể từ năm 2018. Không giống như tất cả các câu trả lời khác, câu trả lời này gợi ý thiết lập ước tínhRowHeigh một lần trong viewDidLoad, giúp khi các ô có cùng chiều cao hoặc rất giống nhau. Thanx. BTW, xen kẽ esimatedRowHeight có thể được đặt qua Trình tạo giao diện trong Trình kiểm tra kích thước> Chế độ xem bảng> Ước tính.
Vitalii

cung cấp một chiều cao ước tính chính xác hơn đã giúp tôi. Tôi cũng có một kiểu xem bảng được nhóm nhiều phần và phải thực hiệntableView(_:estimatedHeightForHeaderInSection:)
nteissler

25

Câu trả lời @Igor đang hoạt động tốt trong trường hợp này,Swift-4mã của nó.

// declaration & initialization  
var cellHeightsDictionary: [IndexPath: CGFloat] = [:]  

trong các phương pháp sau UITableViewDelegate

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  // print("Cell height: \(cell.frame.size.height)")
  self.cellHeightsDictionary[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
  if let height =  self.cellHeightsDictionary[indexPath] {
    return height
  }
  return UITableView.automaticDimension
}

6
Làm thế nào để đối phó với chèn / xóa hàng bằng giải pháp này? TableView nhảy, vì dữ liệu từ điển không thực tế.
Alexey Chekanov

1
làm việc tuyệt vời đặc biệt là trên ô cuối cùng khi tải lại hàng.
Ning

19

Tôi đã thử tất cả các cách giải quyết ở trên, nhưng không có gì hiệu quả.

Sau khi dành hàng giờ và trải qua tất cả những thất vọng có thể, tìm ra cách để khắc phục điều này. Giải pháp này là một vị cứu tinh! Làm việc như người ở!

Swift 4

let lastContentOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastContentOffset, animated: false)

Tôi đã thêm nó dưới dạng một phần mở rộng, để làm cho mã trông gọn gàng hơn và tránh viết tất cả các dòng này mỗi khi tôi muốn tải lại.

extension UITableView {

    func reloadWithoutAnimation() {
        let lastScrollOffset = contentOffset
        beginUpdates()
        endUpdates()
        layer.removeAllAnimations()
        setContentOffset(lastScrollOffset, animated: false)
    }
}

cuối cùng ..

tableView.reloadWithoutAnimation()

HOẶC bạn thực sự có thể thêm các dòng này trong UITableViewCell awakeFromNib()phương thức của bạn

layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale

và làm bình thường reloadData()


1
Làm thế nào để làm bất kỳ tải lại? Bạn gọireloadWithoutAnimationnhưng reloadphần đó ở đâu?
matt

@matt bạn có thể gọi tableView.reloadData()trước và sau đó tableView.reloadWithoutAnimation(), nó vẫn hoạt động.
Srujan Simha

Tuyệt quá! Không ai ở trên không làm việc cho tôi cả. Ngay cả tất cả các chiều cao và chiều cao ước tính là hoàn toàn giống nhau. Hấp dẫn.
TY Kucuk

1
Đừng làm việc cho tôi. Đó là sự cố tại tableView.endUpdates (). Ai đó có thể giúp tôi!
Kakashi

12

Tôi sử dụng nhiều cách hơn để khắc phục nó:

Đối với bộ điều khiển xem:

var cellHeights: [IndexPath : CGFloat] = [:]


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return cellHeights[indexPath] ?? 70.0 
}

làm phần mở rộng cho UITableView

extension UITableView {
  func reloadSectionWithouAnimation(section: Int) {
      UIView.performWithoutAnimation {
          let offset = self.contentOffset
          self.reloadSections(IndexSet(integer: section), with: .none)
          self.contentOffset = offset
      }
  }
}

Kết quả là

tableView.reloadSectionWithouAnimation(section: indexPath.section)

1
Chìa khóa cho tôi là triển khai tiện ích mở rộng UITableView của mình tại đây. Rất thông minh. Cảm ơn rastislv
BennyTheNerd

Hoạt động hoàn hảo nhưng nó chỉ có một nhược điểm, bạn mất hình ảnh động khi chèn tiêu đề, chân trang hoặc hàng.
Soufian Hossam

Trường hợp tải lạiSectionWithouAnimation sẽ được gọi ở đâu? Vì vậy, ví dụ, người dùng có thể đăng một hình ảnh trong ứng dụng của tôi (như Instagram); Tôi có thể lấy các hình ảnh để thay đổi kích thước, nhưng trong hầu hết các trường hợp, tôi phải cuộn ô bảng ra khỏi màn hình để điều đó xảy ra. Tôi muốn ô có kích thước chính xác khi bảng đi qua reloadData.
Luke Irvin

11

Tôi chạy vào đây hôm nay và quan sát:

  1. Đó chỉ là iOS 8.
  2. Overridding cellForRowAtIndexPathkhông giúp đỡ.

Cách khắc phục thực sự khá đơn giản:

Ghi đè estimatedHeightForRowAtIndexPathvà đảm bảo nó trả về các giá trị chính xác.

Với điều này, tất cả sự lộn xộn kỳ lạ và nhảy lung tung trong UITableViews của tôi đã dừng lại.

LƯU Ý: Tôi thực sự biết kích thước của các tế bào của tôi. Chỉ có hai giá trị có thể. Nếu các ô của bạn thực sự có kích thước thay đổi, thì bạn có thể muốn lưu trữ bộ đệm cell.bounds.size.heighttừtableView:willDisplayCell:forRowAtIndexPath:


2
Đã sửa lỗi nó ghi đè phương thức ước tínhHeightForRowAtIndexPath với giá trị cao, ví dụ 300f
Flappy

1
@Flappy thật thú vị khi giải pháp do bạn cung cấp hoạt động và ngắn hơn các kỹ thuật được đề xuất khác. Hãy xem xét đăng nó như là một câu trả lời.
Rohan Sanap

8

Thực tế bạn chỉ có thể tải lại một số hàng nhất định bằng cách sử dụng reloadRowsAtIndexPaths, ví dụ:

tableView.reloadRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)

Nhưng, nói chung, bạn cũng có thể làm động các thay đổi chiều cao của ô bảng như vậy:

tableView.beginUpdates()
tableView.endUpdates()

Tôi đã thử phương thức BeginUpdates / endUpdates, nhưng điều đó chỉ ảnh hưởng đến các hàng hiển thị của bảng của tôi. Tôi vẫn có vấn đề khi tôi cuộn lên.
David

@David Có lẽ vì bạn đang sử dụng chiều cao hàng ước tính.
Lyndsey Scott

Tôi có nên thoát khỏi Ước tính của mình không, và thay vào đó thay thế bằng BeginUpdates và endUpdates?
David

@David Bạn sẽ không "thay thế" bất cứ điều gì, nhưng nó thực sự phụ thuộc vào hành vi mong muốn ... Nếu bạn muốn sử dụng chiều cao hàng ước tính và chỉ cần tải lại các chỉ mục bên dưới phần hiển thị hiện tại của bảng, bạn có thể làm như vậy Tôi đã nói sử dụng reloadRowsAtIndexPaths
Lyndsey Scott

Một trong những vấn đề của tôi khi thử phương thức reladRowsAtIndexPaths là tôi đang thực hiện cuộn vô hạn, vì vậy khi tôi tải lạiData thì đó là vì tôi chỉ thêm 15 hàng vào dataSource. Điều này có nghĩa là indexPath cho các hàng đó chưa tồn tại trong UITableView
David

3

Đây là phiên bản ngắn hơn một chút:

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return self.cellHeightsDictionary[indexPath] ?? UITableViewAutomaticDimension
}

3

Ghi đè phương thức ước tínhHeightForRowAtIndexPath với giá trị cao, ví dụ 300f

Điều này sẽ khắc phục vấn đề :)


2

Có một lỗi mà tôi tin rằng đã được giới thiệu trong iOS11.

Đó là khi bạn thực hiện một reloadbảngView contentOffSetbị thay đổi bất ngờ. Trong thực tế contentOffsetkhông nên thay đổi sau khi tải lại. Nó có xu hướng xảy ra do tính toán saiUITableViewAutomaticDimension

Bạn phải lưu của bạn contentOffSetvà đặt nó trở lại giá trị đã lưu của bạn sau khi tải lại xong.

func reloadTableOnMain(with offset: CGPoint = CGPoint.zero){

    DispatchQueue.main.async { [weak self] () in

        self?.tableView.reloadData()
        self?.tableView.layoutIfNeeded()
        self?.tableView.contentOffset = offset
    }
}

Làm thế nào bạn sử dụng nó?

someFunctionThatMakesChangesToYourDatasource()
let offset = tableview.contentOffset
reloadTableOnMain(with: offset)

Câu trả lời này được bắt nguồn từ đây


2

Cái này hoạt động với tôi trong Swift4:

extension UITableView {

    func reloadWithoutAnimation() {
        let lastScrollOffset = contentOffset
        reloadData()
        layoutIfNeeded()
        setContentOffset(lastScrollOffset, animated: false)
    }
}

1

Không có giải pháp nào trong số này làm việc cho tôi. Đây là những gì tôi đã làm với Swift 4 & Xcode 10.1 ...

Trong viewDidLoad (), khai báo chiều cao hàng động của bảng và tạo các ràng buộc chính xác trong các ô ...

tableView.rowHeight = UITableView.automaticDimension

Ngoài ra trong viewDidLoad (), hãy đăng ký tất cả các ngòi tế bào trong bảng của bạn để xem bảng như thế này:

tableView.register(UINib(nibName: "YourTableViewCell", bundle: nil), forCellReuseIdentifier: "YourTableViewCell")
tableView.register(UINib(nibName: "YourSecondTableViewCell", bundle: nil), forCellReuseIdentifier: "YourSecondTableViewCell")
tableView.register(UINib(nibName: "YourThirdTableViewCell", bundle: nil), forCellReuseIdentifier: "YourThirdTableViewCell")

Trong bảngView heightForRowAt, trả về chiều cao bằng với chiều cao của mỗi ô tại indexPath.row ...

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {

    if indexPath.row == 0 {
        let cell = Bundle.main.loadNibNamed("YourTableViewCell", owner: self, options: nil)?.first as! YourTableViewCell
        return cell.layer.frame.height
    } else if indexPath.row == 1 {
        let cell = Bundle.main.loadNibNamed("YourSecondTableViewCell", owner: self, options: nil)?.first as! YourSecondTableViewCell
        return cell.layer.frame.height
    } else {
        let cell = Bundle.main.loadNibNamed("YourThirdTableViewCell", owner: self, options: nil)?.first as! YourThirdTableViewCell
        return cell.layer.frame.height
    } 

}

Bây giờ, đưa ra chiều cao hàng ước tính cho mỗi ô trong bảngView ước tínhHeightForRowAt. Hãy chính xác như bạn có thể ...

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {

    if indexPath.row == 0 {
        return 400 // or whatever YourTableViewCell's height is
    } else if indexPath.row == 1 {
        return 231 // or whatever YourSecondTableViewCell's height is
    } else {
        return 216 // or whatever YourThirdTableViewCell's height is
    } 

}

Cần làm việc...

Tôi không cần lưu và đặt nội dung Offerset khi gọi bảngView.reloadData ()


1

Tôi có 2 chiều cao tế bào khác nhau.

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
        return Helper.makeDeviceSpecificCommonSize(cellHeight)
    }

Sau khi tôi thêm ước tínhHeightForRowAt , không còn nhảy nữa.

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
    return Helper.makeDeviceSpecificCommonSize(cellHeight)
}

0

Cố gắng gọi cell.layoutSubviews()trước khi trả lại tế bào func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. Đó là lỗi đã biết trong iOS8.


0

Bạn có thể sử dụng như sau trong ViewDidLoad()

tableView.estimatedRowHeight = 0     // if have just tableViewCells <br/>

// use this if you have tableview Header/footer <br/>
tableView.estimatedSectionFooterHeight = 0 <br/>
tableView.estimatedSectionHeaderHeight = 0

0

Tôi đã có hành vi nhảy này và ban đầu tôi có thể giảm thiểu nó bằng cách đặt chiều cao tiêu đề ước tính chính xác (vì tôi chỉ có 1 chế độ xem tiêu đề có thể), tuy nhiên các bước nhảy sau đó bắt đầu xảy ra bên trong các tiêu đề, không ảnh hưởng đến toàn bộ bảng nữa.

Theo dõi các câu trả lời ở đây, tôi có manh mối rằng nó có liên quan đến hoạt hình, vì vậy tôi thấy rằng chế độ xem bảng nằm trong chế độ xem ngăn xếp và đôi khi chúng tôi gọi stackView.layoutIfNeeded()bên trong một khối hoạt hình. Giải pháp cuối cùng của tôi là đảm bảo cuộc gọi này không xảy ra trừ khi "thực sự" cần thiết, bởi vì bố cục "nếu cần" có hành vi trực quan trong bối cảnh đó ngay cả khi "không cần thiết".


0

Tôi gặp vấn đề tương tự. Tôi đã phân trang và tải lại dữ liệu mà không có hình ảnh động nhưng nó không giúp cuộn cuộn không bị nhảy. Tôi có kích thước khác nhau của iPhone, cuộn không tăng vọt trên iphone8 nhưng nó lại tăng vọt trên iphone7 +

Tôi đã áp dụng các thay đổi sau trên chức năng viewDidLoad :

    self.myTableView.estimatedRowHeight = 0.0
    self.myTableView.estimatedSectionFooterHeight = 0
    self.myTableView.estimatedSectionHeaderHeight = 0

và vấn đề của tôi đã được giải quyết. Tôi hy vọng nó cũng giúp bạn.


0

Một trong những cách tiếp cận để giải quyết vấn đề này mà tôi tìm thấy là

CATransaction.begin()
UIView.setAnimationsEnabled(false)
CATransaction.setCompletionBlock {
   UIView.setAnimationsEnabled(true)
}
tableView.reloadSections([indexPath.section], with: .none)
CATransaction.commit()

-2

Trên thực tế tôi tìm thấy nếu bạn sử dụng reloadRowsgây ra một vấn đề nhảy. Sau đó, bạn nên cố gắng sử dụng reloadSectionsnhư thế này:

UIView.performWithoutAnimation {
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.