Skip to content

Prevent crash on non-unique diffable data source item identifiers - #1585

Open
Matthias-Huehne-Kiteworks wants to merge 1 commit into
masterfrom
fix/collectionview-duplicate-identifiers
Open

Matthias-Huehne-Kiteworks wants to merge 1 commit into
masterfrom
fix/collectionview-duplicate-identifiers

Conversation

@Matthias-Huehne-Kiteworks

Copy link
Copy Markdown
Contributor

Problem

The app could crash with a fatal NSInternalInconsistencyException"supplied item identifiers are not unique" — raised by UICollectionViewDiffableDataSource when a snapshot was applied. CollectionViewSection appended all wrapped references from a section's data source to the snapshot unconditionally, so if the same identifier appeared twice, the apply crashed the app.

Why it happens / how it relates to the SDK fix

A file's diffable identifier is its OCItem.localID (OCItem+OCDataItem.mdataItemReference returns self.localID). So the crash means two entries resolving to the same localID landed in one snapshot.

This is the render-layer symptom of a sync-engine anomaly fixed at the source in owncloud/ios-sdk#172 (endless upload loop for removed placeholder items):

  • Removed placeholder items could get stuck carrying active upload / dangling sync records.
  • The item-list merge only suppresses a removed item from query results while it is in the process of deletion (OCItemSyncActivityDeleting) — not while stuck uploading.
  • So such a removed placeholder could linger in / re-appear in a query alongside a re-merged instance sharing the same localID → duplicate identifier → crash.

The two changes are complementary:

  • ios-sdk#172 removes the cause (no more stuck removed-placeholders with active upload records).
  • This PR is the defensive backstop at the UI layer, so a single stray duplicate can never crash the UI again — and is surfaced via a log warning.

Change

In CollectionViewSection snapshot population, deduplicate wrapped items before appending them:

  • The seen-set is seeded from the identifiers already in the snapshot, so uniqueness holds across the entire snapshot, not just within one section (diffable data sources require global uniqueness).
  • Duplicates are dropped and a Log.warning is emitted for observability.

Why dropping is correct and lossless:

  • An ItemRef is only an identity token; the cell content is resolved lazily by reference (dataSource.record(forItemRef:)). The surviving reference therefore resolves to the exact same item — no displayable content is lost, only a duplicate, content-identical phantom row.
  • Uniqueness is a hard UIKit requirement — "keep both" is not an option; it is a guaranteed crash.
  • The check keys on (localID, section) via WrappedItem equality, so the same item legitimately shown in two different sections (e.g. a folder listing and a "recents"-style section) is preserved.

The authoritative fix for duplicate identifiers stays at the data source / SDK level; this is intentionally a robustness net, not a place to "merge" or pick a winner.

Dependency

Bumps the ios-sdk submodule to 715758b3 (fix/stuck-sync-loop), which carries the corresponding sync-engine fix. Once owncloud/ios-sdk#172 is merged, the submodule pointer should be updated to the merged SDK commit.

Validation

Built and deployed to a physical device that was reproducing the stuck-sync state. After resolving the recurring issue dialog once, the sync backlog drains and the UI no longer crashes. The dedup path additionally protects against any future stray duplicate.

…e diffable item identifiers

CollectionViewSection populated the diffable data source snapshot by appending
all wrapped references from a section's data source unconditionally. If the same
item identifier appeared twice, UICollectionViewDiffableDataSource raised a fatal
NSInternalInconsistencyException ("supplied item identifiers are not unique") and
crashed the app.

An item's diffable identifier is its OCItem.localID (OCItem+OCDataItem.m:
dataItemReference returns self.localID). So the crash means two entries resolving
to the same localID landed in one snapshot. This is the render-layer symptom of a
sync-engine anomaly: removed placeholder items that stayed stuck with active
upload / dangling sync records (fixed at the source in ios-sdk#172). Because such
removed items were only suppressed from query results while in the process of
*deletion* (OCItemSyncActivityDeleting) - not while stuck uploading - a removed
placeholder could linger in / re-appear in a query alongside a re-merged instance
sharing the same localID, producing a duplicate identifier.

Fix (defensive backstop at the UI layer):
- Deduplicate wrapped items before appending them to the snapshot, seeding the
  seen-set from the identifiers already in the snapshot so uniqueness holds across
  the entire snapshot (not just within one section). Duplicates are dropped and a
  warning is logged. The drop is lossless: the surviving reference resolves to the
  exact same item (content is looked up lazily by reference), and the check keys on
  (localID, section) via WrappedItem equality, so the same item legitimately shown
  in two different sections is preserved.

This complements the SDK fix rather than replacing it: ios-sdk#172 removes the
source of the duplicate (no more stuck removed-placeholders with active upload
records), while this change keeps a single stray duplicate from ever crashing the
UI and surfaces it via a log warning.

Also bumps the ios-sdk submodule to 715758b3 (fix/stuck-sync-loop), which carries
the corresponding sync-engine fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant