Skip to content

Commit

Permalink
Convert to models after NSFetchedResultsController finishes reporting…
Browse files Browse the repository at this point in the history
… changes for avoiding update cycle triggered by fetch requests in item creator (#3508)
  • Loading branch information
laevandus authored Nov 27, 2024
1 parent c056225 commit 82b28ef
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 11 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

# Upcoming

### 🔄 Changed
## StreamChat
### 🐞 Fixed
- Fix a rare infinite loop triggering a crash when handling database changes [#3508](https://github.com/GetStream/stream-chat-swift/pull/3508)

# [4.67.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.67.0)
_November 25, 2024_
Expand Down
40 changes: 30 additions & 10 deletions Sources/StreamChat/Controllers/DatabaseObserver/ListChange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,8 @@ class ListChangeAggregator<DTO: NSManagedObject, Item>: NSObject, NSFetchedResul
/// Called with the aggregated changes after `FetchResultsController` calls controllerDidChangeContent` on its delegate.
var onDidChange: (([ListChange<Item>]) -> Void)?

/// An array of changes in the current update. It gets reset every time `controllerWillChangeContent` is called, and
/// published to the observer when `controllerDidChangeContent` is called.
private var currentChanges: [ListChange<Item>] = []
/// An array of changes in the current update.
private var currentChanges: [ListChange<DTO>] = []

/// Creates a new `ChangeAggregator`.
///
Expand All @@ -178,8 +177,10 @@ class ListChangeAggregator<DTO: NSManagedObject, Item>: NSObject, NSFetchedResul
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?
) {
guard let dto = anObject as? DTO, let item = try? itemCreator(dto) else {
log.debug("Skipping the update from DB because the DTO can't be converted to the model object.")
// Model conversions must happen in `controllerDidChangeContent`. Otherwise, it can trigger a loop where
// this delegate method is called again when additional fetch requests in `asModel()` are triggered.
guard let dto = anObject as? DTO else {
log.debug("Skipping the update from DB because the DTO has invalid type: \(anObject)")
return
}

Expand All @@ -189,35 +190,54 @@ class ListChangeAggregator<DTO: NSManagedObject, Item>: NSObject, NSFetchedResul
log.warning("Skipping the update from DB because `newIndexPath` is missing for `.insert` change.")
return
}
currentChanges.append(.insert(item, index: index))
currentChanges.append(.insert(dto, index: index))

case .move:
guard let fromIndex = indexPath, let toIndex = newIndexPath else {
log.warning("Skipping the update from DB because `indexPath` or `newIndexPath` are missing for `.move` change.")
return
}
currentChanges.append(.move(item, fromIndex: fromIndex, toIndex: toIndex))
currentChanges.append(.move(dto, fromIndex: fromIndex, toIndex: toIndex))

case .update:
guard let index = indexPath else {
log.warning("Skipping the update from DB because `indexPath` is missing for `.update` change.")
return
}
currentChanges.append(.update(item, index: index))
currentChanges.append(.update(dto, index: index))

case .delete:
guard let index = indexPath else {
log.warning("Skipping the update from DB because `indexPath` is missing for `.delete` change.")
return
}
currentChanges.append(.remove(item, index: index))
currentChanges.append(.remove(dto, index: index))

default:
break
}
}

func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
onDidChange?(currentChanges)
// Model conversion is safe when all the changes have been processed (Core Data's _processRecentChanges can be called if conversion triggers additional fetch requests).
let itemChanges = currentChanges.compactMap { dtoChange in
do {
switch dtoChange {
case .update(let dto, index: let indexPath):
return try ListChange.update(itemCreator(dto), index: indexPath)
case .insert(let dto, index: let indexPath):
return try ListChange.insert(itemCreator(dto), index: indexPath)
case .move(let dto, fromIndex: let fromIndex, toIndex: let toIndex):
return try ListChange.move(itemCreator(dto), fromIndex: fromIndex, toIndex: toIndex)
case .remove(let dto, index: let indexPath):
return try ListChange.remove(itemCreator(dto), index: indexPath)
}
} catch {
log.debug("Skipping the update from DB because the DTO can't be converted to the model object: \(error)")
return nil
}
}
onDidChange?(itemChanges)
currentChanges.removeAll()
}
}

0 comments on commit 82b28ef

Please sign in to comment.