From 242a2c0e80fdb393275f0159a0f443c32dda46a1 Mon Sep 17 00:00:00 2001 From: Caio Zullo Date: Thu, 2 Feb 2023 13:54:07 +0100 Subject: [PATCH] Release cell reference on `prepareForReuse` as a fix for iOS 15+ cell lifecycle changes. On iOS 15+, for performance reasons, the table view data source may create cells ahead of time using the `cellForRow` method. So the cell may be created but never go through the whole `willDisplayCell/didEndDisplaying` lifecycle callbacks as it may never be displayed. However, we start loading the cell image on `cellForRow` and only cancel the request on `didEndDisplaying`. In such cases, there can be a race condition when reusing a cell that was never displayed because the request would carry on and potentially load the wrong image at the wrong index path. --- .../FeedUIIntegrationTests.swift | 15 +++++++++++++++ .../Controllers/FeedImageCellController.swift | 3 +++ .../Feed UI/Views/FeedImageCell.swift | 7 +++++++ 3 files changed, 25 insertions(+) diff --git a/EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift b/EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift index e31abdc4..18dff504 100644 --- a/EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift +++ b/EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift @@ -492,6 +492,21 @@ class FeedUIIntegrationTests: XCTestCase { XCTAssertEqual(view0?.isShowingImageLoadingIndicator, false, "Expected no loading indicator when image loads successfully after view becomes visible again") } + func test_feedImageView_doesNotShowDataFromPreviousRequestWhenCellIsReused() throws { + let (sut, loader) = makeSUT() + + sut.loadViewIfNeeded() + loader.completeFeedLoading(with: [makeImage(), makeImage()]) + + let view0 = try XCTUnwrap(sut.simulateFeedImageViewVisible(at: 0)) + view0.prepareForReuse() + + let imageData0 = UIImage.make(withColor: .red).pngData()! + loader.completeImageLoading(with: imageData0, at: 0) + + XCTAssertEqual(view0.renderedImage, .none, "Expected no image state change for reused view once image loading completes successfully") + } + func test_feedImageView_doesNotRenderLoadedImageWhenNotVisibleAnymore() { let (sut, loader) = makeSUT() sut.loadViewIfNeeded() diff --git a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedImageCellController.swift b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedImageCellController.swift index fa43143a..7fff93a1 100644 --- a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedImageCellController.swift +++ b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedImageCellController.swift @@ -42,6 +42,9 @@ extension FeedImageCellController: UITableViewDataSource, UITableViewDelegate, U cell?.onRetry = { [weak self] in self?.delegate.didRequestImage() } + cell?.onReuse = { [weak self] in + self?.releaseCellForReuse() + } delegate.didRequestImage() return cell! } diff --git a/EssentialFeed/EssentialFeediOS/Feed UI/Views/FeedImageCell.swift b/EssentialFeed/EssentialFeediOS/Feed UI/Views/FeedImageCell.swift index c018a838..f8465492 100644 --- a/EssentialFeed/EssentialFeediOS/Feed UI/Views/FeedImageCell.swift +++ b/EssentialFeed/EssentialFeediOS/Feed UI/Views/FeedImageCell.swift @@ -13,8 +13,15 @@ public final class FeedImageCell: UITableViewCell { @IBOutlet private(set) public var descriptionLabel: UILabel! var onRetry: (() -> Void)? + var onReuse: (() -> Void)? @IBAction private func retryButtonTapped() { onRetry?() } + + public override func prepareForReuse() { + super.prepareForReuse() + + onReuse?() + } }