diff --git a/Proton/Sources/Swift/Table/TableCell.swift b/Proton/Sources/Swift/Table/TableCell.swift
index 3e66aafc..bbcfd388 100644
--- a/Proton/Sources/Swift/Table/TableCell.swift
+++ b/Proton/Sources/Swift/Table/TableCell.swift
@@ -49,6 +49,14 @@ public class TableCell {
 
     public var onEditorInitialized: ((TableCell, EditorView) -> Void)?
 
+    @MainActor
+    private var retainCount = 0
+    /// Returns `true` if the cell has one or more retain invocations.
+    @MainActor
+    public var isRetained: Bool {
+        retainCount > 0
+    }
+
     /// Additional attributes that can be stored on Cell to identify various aspects like Header, Numbered etc.
     public var additionalAttributes: [String: Any] = [:]
 
@@ -184,6 +192,22 @@ public class TableCell {
         contentView?.removeFocus()
     }
 
+    /// Retains the cell to prevent it from getting recycled when viewport changes and cell is scrolled off-screen
+    /// Cell with focus is automatically retained and released.
+    /// - Note: A cell may be retained as many time as needed but needs to have a corresponding release for every retain.
+    /// Failing to release would mean that the cell will never participate in virtualization and will always be kept alive even when off-screen.
+    /// - Important: It is responsibility of consumer to ensure that the cells are correctly released after being retained.
+    @MainActor
+    public func retain() {
+        retainCount += 1
+    }
+
+    /// Releases a retained cell. Calling this function on a non-retained cell is a no-op.
+    @MainActor
+    public func release() {
+        retainCount = max(0, retainCount - 1)
+    }
+
     func hideEditor() {
         contentView?.hideEditor()
     }
@@ -192,6 +216,7 @@ public class TableCell {
         contentView?.showEditor()
     }
 
+
     func performWithoutChangingFirstResponder(_ closure: () -> Void) {
         editor?.disableFirstResponder()
         closure()
diff --git a/Proton/Sources/Swift/Table/TableView.swift b/Proton/Sources/Swift/Table/TableView.swift
index bbf756e4..5829c70d 100644
--- a/Proton/Sources/Swift/Table/TableView.swift
+++ b/Proton/Sources/Swift/Table/TableView.swift
@@ -412,8 +412,12 @@ public class TableView: UIView {
         removeScrollObserver()
     }
 
+    private var retainedCells = Set<TableCell>()
+
     var cellsInViewport: [TableCell] = [] {
         didSet {
+            reclaimReleasedCells()
+
             guard oldValue != cellsInViewport else { return }
 
             let oldCells = Set(oldValue)
@@ -422,16 +426,19 @@ public class TableView: UIView {
             let toReclaim = oldCells.subtracting(newCells)
 
             toReclaim.forEach { [weak self] cell in
-                // Ignore reclaiming the cell if it has focus
-                if cell.containsFirstResponder == false {
+                // Ignore reclaiming the cell if these are retained
+                // Cell having focus is always retained and released on lost focus
+                if cell.isRetained == false {
                     self?.repository.enqueue(cell: cell)
+                } else {
+                    self?.retainedCells.insert(cell)
                 }
             }
 
             toGenerate.forEach { [weak self] cell in
-                // Ignore generating the cell if it has focus. The focussed cell is not reclaimed, hence need not be regenerated.
-                // In absence of this check, there may be cases where the focussed cell gets duplicated
-                if cell.containsFirstResponder == false {
+                // Ignore generating the cell if it is already retained. The retained cell is not reclaimed, hence need not be regenerated.
+                // In absence of this check, there may be cases where the retained cell gets duplicated
+                if cell.isRetained == false {
                     self?.repository.dequeue(for: cell)
                 }
             }
@@ -489,6 +496,15 @@ public class TableView: UIView {
             .intersects(adjustedViewport) }
     }
 
+    private func reclaimReleasedCells() {
+        retainedCells.forEach {
+            if $0.isRetained == false {
+                self.repository.enqueue(cell: $0)
+                retainedCells.remove($0)
+            }
+        }
+    }
+
     func cellBelow(_ cell: TableCell) -> TableCell? {
         guard let row = cell.rowSpan.max(),
               let column = cell.columnSpan.min() else {
@@ -854,11 +870,13 @@ extension TableView: TableContentViewDelegate {
     }
 
     func tableContentView(_ tableContentView: TableContentView, didReceiveFocusAt range: NSRange, in cell: TableCell) {
+        cell.retain()
         resetColumnResizingHandles(selectedCell: cell)
         delegate?.tableView(self, didReceiveFocusAt: range, in: cell)
     }
 
     func tableContentView(_ tableContentView: TableContentView, didLoseFocusFrom range: NSRange, in cell: TableCell) {
+        cell.release()
         removeSelectionBorders()
         delegate?.tableView(self, didLoseFocusFrom: range, in: cell)
     }
diff --git a/Proton/Tests/Table/TableViewAttachmentSnapshotTests.swift b/Proton/Tests/Table/TableViewAttachmentSnapshotTests.swift
index 3d4ebb5d..1766e8aa 100644
--- a/Proton/Tests/Table/TableViewAttachmentSnapshotTests.swift
+++ b/Proton/Tests/Table/TableViewAttachmentSnapshotTests.swift
@@ -1094,6 +1094,46 @@ class TableViewAttachmentSnapshotTests: SnapshotTestCase {
         assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
     }
 
+    func FLAKY_testRecyclesRetainedCell() {
+        var viewport = CGRect(x: 0, y: 100, width: 350, height: 200)
+        delegate.viewport = viewport
+
+        Utility.drawRect(rect: viewport, color: .red, in: editor)
+
+        let attachment = AttachmentGenerator.makeTableViewAttachment(id: 1, numRows: 20, numColumns: 5)
+        attachment.view.delegate = delegate
+        editor.replaceCharacters(in: .zero, with: "Some text in editor")
+        editor.insertAttachment(in: editor.textEndRange, attachment: attachment)
+        editor.replaceCharacters(in: editor.textEndRange, with: "Text after grid")
+
+        XCTAssertEqual(attachment.view.containerAttachment, attachment)
+
+        viewController.render(size: CGSize(width: 400, height: 700))
+        assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
+
+        let cell10 = attachment.view.cellAt(rowIndex: 1, columnIndex: 0)
+        cell10?.editor?.setFocus()
+
+        viewport = CGRect(x: 0, y: 300, width: 350, height: 200)
+        delegate.viewport = viewport
+        attachment.view.scrollViewDidScroll(editor.scrollView)
+
+        Utility.drawRect(rect: viewport, color: .red, in: editor)
+        viewController.render(size: CGSize(width: 400, height: 700))
+        assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
+
+        let cell40 = attachment.view.cellAt(rowIndex: 4, columnIndex: 0)
+        cell40?.editor?.setFocus()
+
+        viewport = CGRect(x: 0, y: 320, width: 350, height: 200)
+        delegate.viewport = viewport
+        attachment.view.scrollViewDidScroll(editor.scrollView)
+
+        Utility.drawRect(rect: viewport, color: .red, in: editor)
+        viewController.render(size: CGSize(width: 400, height: 700))
+        assertSnapshot(matching: viewController.view, as: .image, record: recordMode)
+    }
+
     func FLAKY_testPreventsRecyclingNestedEditorFocussedCell() {
         var viewport = CGRect(x: 0, y: 100, width: 350, height: 200)
         delegate.viewport = viewport
diff --git a/Proton/Tests/Table/__Snapshots__/TableViewAttachmentSnapshotTests/testRecyclesRetainedCell.1.png b/Proton/Tests/Table/__Snapshots__/TableViewAttachmentSnapshotTests/testRecyclesRetainedCell.1.png
new file mode 100644
index 00000000..922babf3
Binary files /dev/null and b/Proton/Tests/Table/__Snapshots__/TableViewAttachmentSnapshotTests/testRecyclesRetainedCell.1.png differ
diff --git a/Proton/Tests/Table/__Snapshots__/TableViewAttachmentSnapshotTests/testRecyclesRetainedCell.2.png b/Proton/Tests/Table/__Snapshots__/TableViewAttachmentSnapshotTests/testRecyclesRetainedCell.2.png
new file mode 100644
index 00000000..01398581
Binary files /dev/null and b/Proton/Tests/Table/__Snapshots__/TableViewAttachmentSnapshotTests/testRecyclesRetainedCell.2.png differ
diff --git a/Proton/Tests/Table/__Snapshots__/TableViewAttachmentSnapshotTests/testRecyclesRetainedCell.3.png b/Proton/Tests/Table/__Snapshots__/TableViewAttachmentSnapshotTests/testRecyclesRetainedCell.3.png
new file mode 100644
index 00000000..af5ed60c
Binary files /dev/null and b/Proton/Tests/Table/__Snapshots__/TableViewAttachmentSnapshotTests/testRecyclesRetainedCell.3.png differ