Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/ThreemaFramework/Message/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 30 kB image not shown  

Quelle  MessageProvider.swift   Sprache: JAVA

 

import CocoaLumberjackSwift
import Combine
import CoreData
import OSLog
import PromiseKit
import UIKit

/// Variables to try different message loading configurations
///
/// This can probably be removed before the public release
private protocol Configuration {
    // MARK: Optimizations configuration
    
    var limitEnabled: Bool { get }
    var isFixedWindowEnabled: Bool { get }
    // TODO: Maybe try an expanding window until a maximum size
    
    var windowSize: Int { get }
    
    var backgroundFetching: Bool { get }
}

extension MessageProvider {
    fileprivate struct TestConfiguration: Configuration {
        fileprivate let limitEnabled = true
        fileprivate let isFixedWindowEnabled = false
        
        fileprivate let windowSize = 150
        
        fileprivate let backgroundFetching = true
    }
    
    fileprivate struct ProductionConfiguration: Configuration {
        fileprivate let limitEnabled = true
        fileprivate let isFixedWindowEnabled = false // Makes the scroll bar less jumpy
        
        fileprivate let windowSize = 1000 // TODO: (IOS-2014) Measure what the sweet spot might be
        
        fileprivate let backgroundFetching = true
    }
}

/// Load messages of conversation and notify observer about changes
public final class MessageProvider: NSObject {
    
    public struct FetchRequestResultIdentifier {
        public let firstMessage: NSManagedObjectID
        public let lastMessage: NSManagedObjectID
        public let messageCount: Int
    }
    
    /// A snapshot of the currently loaded messages and an indication if the previous snapshot contained the most recent
    /// messages
    public struct MessagesSnapshot {
        /// Snapshot to be used by the data source
        public let snapshot: NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
        /// Did the previous snapshot contain the most recent messages?
        public let previouslyNewestMessagesLoaded: Bool
    }
    
    /// Current snapshot of messages (provided by the underlying FRC)
    ///
    /// The updates will always arrive on the same queue, but it might not be the main queue.
    @Published public var currentMessages: MessagesSnapshot?
    
    // MARK: - Private properties
    
    private let fetchedResultsController: NSFetchedResultsController<BaseMessageEntity>
    
    private static let configuration = TestConfiguration()
    
    // Keep track if the previous snapshot contained the newest messages
    // This is needed by the chat view to decide if we scrolled all the way to the bottom
    private var newestMessagesLoaded = false
    private var previouslyNewestMessagesLoaded = false
    
    private var oldestMessagesLoaded = true
    
    // TODO: (IOS-2468) Do we need to update this value more often?
    // - How about big insertions? How do we track them?
    // - Maybe we just ensure it runs on a background queue and update it on every query adjustments
    /// Number of messages updates at creation of provider and when batch deletes happen
    private var numberOfMessages: Int
    
    private var lastWillResignActiveNotification: Date?
    
    // If this is bigger than the available number of messages no messages will be loaded (but it won't crash)
    private var currentOffset = 0 {
        didSet {
            // This value should always be positive
            guard currentOffset >= 0 else {
                currentOffset = 0
                return
            }
            
            oldestMessagesLoaded = currentOffset == 0
        }
    }
    
    // TODO: (IOS-2014) Do we have to ensure that current window size always increases?
    // - How does this hold if we delete (all) messages?
    // - Does this help improving the problems while scrolling?
    // - Should probably be limited to `numberOfMessages` and some change
    private var currentWindowSize = 100
    
    private let conversationObjectID: NSManagedObjectID
    private let entityManager: EntityManager
    private let backgroundEntityManager: EntityManager
    
    private let messageFetcher: MessageFetcher
    
    // MARK: - Lifecycle
    
    /// Create a new messages provider
    ///
    /// This abstracts away the loading of messages from Core Data by providing an updating snapshot of the current set
    /// of messages. To load more messages below or above use the provided methods for that.
    ///
    /// - Parameters:
    ///   - conversation: Messages from this conversation will be fetched and provided
    ///   - date: Date to do initial fetch around. If `nil` newest messages will be fetched.
    ///   - entityManager: Main context to fetch data used in UI
    ///   - backgroundEntityManager: Background context to fetch meta data information on
    public convenience init(
        for conversation: ConversationEntity,
        around date: Date?,
        entityManager: EntityManager = PersistenceManager(
            appGroupID: AppGroup.groupID(),
            userDefaults: AppGroup.userDefaults(),
            remoteSecretManager: RemoteSecretProvider.remoteSecretManager
        ).entityManager,
        backgroundEntityManager: EntityManager = PersistenceManager(
            appGroupID: AppGroup.groupID(),
            userDefaults: AppGroup.userDefaults(),
            remoteSecretManager: RemoteSecretProvider.remoteSecretManager
        ).backgroundEntityManager
    ) {
        var context: ThreemaManagedObjectContext
        do {
            let persistenceManager = PersistenceManager(
                appGroupID: AppGroup.groupID(),
                userDefaults: AppGroup.userDefaults(),
                remoteSecretManager: RemoteSecretProvider.remoteSecretManager
            )

            context =
                if MessageProvider.configuration.backgroundFetching {
                    try DatabaseContext.directBackgroundContext(
                        with: persistenceManager.databaseManager.persistentStoreCoordinator
                    )
                }
                else {
                    persistenceManager.databaseManager.databaseContext().main
                }
        }
        catch {
            fatalError("\(error)")
        }

        self.init(
            for: conversation,
            around: date,
            entityManager: entityManager,
            backgroundEntityManager: backgroundEntityManager,
            context: context
        )
    }
    
    /// Convenience init should be used everywhere except in tests.
    /// See the comment for convenience init above for more information.
    /// `context` is the context on which the `NSFetchedResultsController` is run. This is necessary due to how db
    /// access is currently setup where most requests run on the main thread.
    public init(
        for conversation: ConversationEntity,
        around date: Date?,
        entityManager: EntityManager,
        backgroundEntityManager: EntityManager,
        context: ThreemaManagedObjectContext
    ) {
        self.conversationObjectID = conversation.objectID
        self.entityManager = entityManager
        self.backgroundEntityManager = backgroundEntityManager
        
        let localMessageFetcher: MessageFetcher
        if MessageProvider.configuration.backgroundFetching {
            localMessageFetcher = MessageFetcher(for: conversation, with: backgroundEntityManager)
            self.messageFetcher = localMessageFetcher
        }
        else {
            localMessageFetcher = MessageFetcher(for: conversation, with: entityManager)
            self.messageFetcher = localMessageFetcher
        }
        
        self.fetchedResultsController = NSFetchedResultsController(
            fetchRequest: localMessageFetcher.messagesFetchRequest,
            managedObjectContext: context,
            sectionNameKeyPath: #keyPath(BaseMessageEntity.sectionDateString),
            cacheName: nil
        )
        
        // TODO: (IOS-2014)
        // - In my analysis this was not called on a background context on an iPhone 7 with iOS 13. Why?
        // - Can we improve speed if we create a derived property for it? (Maybe only possible if we drop iOS 12
        //   support)
        self.numberOfMessages = localMessageFetcher.count()
        
        super.init()
        
        fetchedResultsController.delegate = self
        
        configureChangeObservation(for: context)
        
        configureInitialFetchRequest(around: date)
        
        fetch()
    }

    @available(*, unavailable)
    override init() {
        fatalError("Not available")
    }
    
    deinit {
        DatabaseContext.removeDirectBackgroundContext(self.fetchedResultsController.managedObjectContext)
    }

    // MARK: - Configuration
    
    private func configureChangeObservation(for context: NSManagedObjectContext) {
        // This implementation should be streamlined in the future by using persistence history tracking (IOS-2354)
        
        // Observe all changes that go through a normal Core Data save call
        context.automaticallyMergesChangesFromParent = true
        
        // Custom notifications for batch actions
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(batchDeletedAllMessagesInAConversation(notification:)),
            name: DatabaseContext.batchDeletedAllConversationMessages,
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(batchDeletedOldMessages),
            name: DatabaseContext.batchDeletedOldMessages,
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(resetAndReload),
            name: UIApplication.didBecomeActiveNotification,
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(willResignActiveNotification),
            name: UIApplication.willResignActiveNotification,
            object: nil
        )
    }
    
    @objc private func willResignActiveNotification() {
        lastWillResignActiveNotification = Date()
    }
    
    /// Workaround when using a direct background context which might miss updates through the NSE
    @objc private func resetAndReload() {
        guard let lastWillResignActiveNotification else {
            return
        }
        
        backgroundEntityManager.perform { [weak self] in
            guard let conversationObjectID = self?.conversationObjectID,
                  let conversation = self?.backgroundEntityManager.entityFetcher
                  .existingObject(with: conversationObjectID) as? ConversationEntity,
                  let lastUpdate = conversation.lastUpdate else {
                return
            }
            if lastUpdate > lastWillResignActiveNotification {
                self?.fetchedResultsController.managedObjectContext.performAndWait {
                    self?.fetchedResultsController.managedObjectContext.reset()
                }
                self?.refetch()
            }
        }
    }
    
    private func configureInitialFetchRequest(around date: Date?) {
        // Needed for sectioning (BaseMessageEntity.sectionDateString)
        fetchedResultsController.fetchRequest.propertiesToFetch = BaseMessageEntity.sectioningKeyPaths
        
        if let date {
            configureLoadingMessages(around: date)
        }
        else {
            configureLoadingMessagesAtBottom()
        }
    }
    
    private func configureLoadingMessages(around date: Date) {
        let numberOfMessagesAfterDate: Int = messageFetcher.numberOfMessages(after: date)
        currentOffset = numberOfMessages - numberOfMessagesAfterDate - (MessageProvider.configuration.windowSize / 2)
    }
    
    private func configureLoadingMessagesAtBottom() {
        if MessageProvider.configuration.isFixedWindowEnabled {
            currentOffset = numberOfMessages - MessageProvider.configuration.windowSize
        }
        else {
            currentOffset = numberOfMessages - currentWindowSize
        }
    }
    
    private func configureLoadingMessagesAtTop() {
        currentOffset = 0
    }
    
    // MARK: - Initial fetch with a new configuration
    
    @discardableResult private func fetch() -> Guarantee<NSManagedObjectID?> {
        DDLogVerbose("Fetch...")
        if MessageProvider.configuration.limitEnabled {
            limitFetchRequest()
        }
        
        if MessageProvider.configuration.backgroundFetching {
            return Guarantee { seal in
                // As this is probably happening on a background context we need to dispatch the fetch correctly (is
                // this true?)
                fetchedResultsController.managedObjectContext.perform {
                    do {
                        let prevFetchedObjs = self.fetchedResultsController.fetchedObjects
                        
                        let startTime = CFAbsoluteTimeGetCurrent()
                        DDLogVerbose("Start fetch")
                        try self.fetchedResultsController.performFetch()
                        DDLogVerbose("End fetch")
                        
                        let endTime = CFAbsoluteTimeGetCurrent()
                        DDLogVerbose("fetchRequest duration \(endTime - startTime)s")
                
                        guard let fetchedObjs = self.fetchedResultsController.fetchedObjects,
                              !fetchedObjs.isEmpty,
                              let arr = fetchedObjs as NSArray?,
                              let lastObj = arr[max(0, arr.count - 1)] as? BaseMessageEntity,
                              let firstObj = arr[0] as? BaseMessageEntity else {
                            return seal(nil)
                        }
                        
                        if let prevFetchedObjs {
                            guard !prevFetchedObjs.isEmpty,
                                  let prevArr = prevFetchedObjs as NSArray?,
                                  let prevLastObj = prevArr[max(0, prevArr.count - 1)] as? BaseMessageEntity,
                                  let prevFirstObj = prevArr[0] as? BaseMessageEntity else {
                                DDLogError("Couldn't access previous state")
                                return seal(nil)
                            }
                            if lastObj == prevLastObj, firstObj == prevFirstObj, prevArr.count == arr.count {
                                return seal(nil)
                            }
                        }
                        
                        seal(lastObj.objectID)
                    }
                    catch {
                        // TODO: Fail more gracefully
                        fatalError("Initial fetch of messages failed: \(error.localizedDescription)")
                    }
                }
            }
        }
        else {
            do {
                DDLogVerbose("Start fetch")
                try fetchedResultsController.performFetch()
                DDLogVerbose("End fetch")
            }
            catch {
                // TODO: Fail more gracefully
                fatalError("Initial fetch of messages failed: \(error.localizedDescription)")
            }
            guard let fetchedObjs = fetchedResultsController.fetchedObjects,
                  !fetchedObjs.isEmpty,
                  let arr = fetchedObjs as NSArray?,
                  let lastObj = arr[max(0, arr.count - 1)] as? BaseMessageEntity else {
                return Guarantee { $0(nil) }
            }
            
            return Guarantee { $0(lastObj.objectID) }
        }
    }
    
    private func limitFetchRequest() {
        // Set window size
        if MessageProvider.configuration.isFixedWindowEnabled {
            if currentOffset + MessageProvider.configuration.windowSize >= numberOfMessages {
                fetchedResultsController.fetchRequest.fetchLimit = 0
                newestMessagesLoaded = true
            }
            else {
                fetchedResultsController.fetchRequest.fetchLimit = MessageProvider.configuration.windowSize
                newestMessagesLoaded = false
            }
        }
        else {
            if currentOffset + currentWindowSize >= numberOfMessages {
                fetchedResultsController.fetchRequest.fetchLimit = 0
                newestMessagesLoaded = true
            }
            else {
                fetchedResultsController.fetchRequest.fetchLimit = currentWindowSize
                newestMessagesLoaded = false
            }
        }
        
        // Set offset
        fetchedResultsController.fetchRequest.fetchOffset = currentOffset
    }
    
    // MARK: - Public methods
    
    // TODO: (IOS-2393) Add a reload that reset the complete FRC so the section title are update (or maybe other things)
    
    /// Load more message at the top if there are any
    ///
    /// Fetching happens asynchronously.
    ///
    /// - Returns: Guarantee that is full-filled when initial fetch of loading completes or no new fetch is needed
    public func loadMessagesAtTop() -> Guarantee<NSManagedObjectID?> {
        guard currentOffset > 0 else {
            // We're at the top
            return Guarantee { $0(nil) }
        }
        
        if MessageProvider.configuration.isFixedWindowEnabled {
            currentOffset = currentOffset - (2 * MessageProvider.configuration.windowSize / 3)
        }
        else {
            currentWindowSize += MessageProvider.configuration.windowSize
            currentOffset -= MessageProvider.configuration.windowSize
        }
        
        return fetch()
    }
    
    public func refetch() {
        DDLogVerbose("\(#function)")
        do {
            try fetchedResultsController.managedObjectContext.performAndWait {
                try self.fetchedResultsController.performFetch()
            }
        }
        catch {
            DDLogError("An error occurred when refetching the current snapshot.")
        }
    }
    
    /// Load more message at the bottom if there are any
    ///
    /// Fetching happens asynchronously.
    ///
    /// - Returns: Guarantee that is full-filled when initial fetch of loading completes or no new fetch is needed
    public func loadMessagesAtBottom() -> Guarantee<NSManagedObjectID?> {
        guard !newestMessagesLoaded else {
            return Guarantee { $0(nil) }
        }
        
        if MessageProvider.configuration.isFixedWindowEnabled {
            currentOffset = min(
                currentOffset + (3 * MessageProvider.configuration.windowSize / 4),
                numberOfMessages - MessageProvider.configuration.windowSize
            )
        }
        else {
            currentWindowSize += MessageProvider.configuration.windowSize
        }
        
        return fetch()
    }
    
    /// Load message around `date`
    ///
    /// Fetching happens asynchronously and no fetch happens if messages around `date` are already loaded.
    ///
    /// - Parameter date: Date to load messages around
    /// - Returns: Guarantee that is full-filled when initial fetch of loading completes or no new fetch is needed
    public func loadMessages(around date: Date) -> Guarantee<NSManagedObjectID?> {
        Guarantee { seal in
            messageFetcher.numberOfMessages(after: date) { numberOfMessagesAfterDate in

                let firstMessageAfterDateOffset = self.numberOfMessages - numberOfMessagesAfterDate
                // Don't do new fetch if messages around `date` are already loaded

                // Note that the top (oldest) message has number 0

                let topLoadedLimit = self.currentOffset
                let bottomLoadedLimit = self.currentOffset + self.currentWindowSize
                let refetchThreshold = MessageProvider.configuration.windowSize / 4

                DDLogVerbose(
                    "\(#function) \(self.numberOfMessages) messages total; Loaded messages from \(topLoadedLimit) until \(bottomLoadedLimit); Loading around \(firstMessageAfterDateOffset)"
                )

                // If all messages at the top are loaded we don't need to set any threshold to load more at the top
                let topRefetchLimit: Int =
                    if topLoadedLimit <= 0 {
                        0
                    }
                    else {
                        topLoadedLimit + refetchThreshold
                    }

                // If all messages at the bottom are loaded we don't need to set any threshold to load more at the
                // bottom
                let bottomRefetchLimit: Int =
                    if self.numberOfMessages <= bottomLoadedLimit {
                        bottomLoadedLimit
                    }
                    else {
                        bottomLoadedLimit - refetchThreshold
                    }

                let shouldRefetchTop = firstMessageAfterDateOffset < topRefetchLimit
                let shouldRefetchBottom = firstMessageAfterDateOffset > bottomRefetchLimit

                DDLogVerbose(
                    "\(#function) Refetch if first message after date offset is not in \(topRefetchLimit)..<\(bottomRefetchLimit). First message is \(firstMessageAfterDateOffset); Refetching \(shouldRefetchTop || shouldRefetchBottom)"
                )

                guard shouldRefetchTop || shouldRefetchBottom else {
                    seal(nil)
                    return
                }

                self.currentOffset = self
                    .numberOfMessages - numberOfMessagesAfterDate - (MessageProvider.configuration.windowSize / 2)
                self.currentWindowSize = MessageProvider.configuration.windowSize

                self.fetch().done { objectID in
                    seal(objectID)
                }
            }
        }
    }
    
    /// Load newest messages
    /// - Returns: Guarantee that is full-filled when initial fetch of loading completes or no new fetch is needed
    public func loadNewestMessages() -> Guarantee<NSManagedObjectID?> {

        guard !newestMessagesLoaded else {
            return Guarantee { $0(nil) }
        }
        
        configureLoadingMessagesAtBottom()
        
        return fetch()
    }
    
    /// Load oldest messages
    /// - Returns: Guarantee that is full-filled when initial fetch of loading completes or no new fetch is needed
    public func loadOldestMessages() -> Guarantee<NSManagedObjectID?> {

        guard !oldestMessagesLoaded else {
            return Guarantee { $0(nil) }
        }
        
        configureLoadingMessagesAtTop()
        
        return fetch()
    }
    
    /// Get `BaseMessage` for id on main queue
    /// - Parameter objectID: Object to load
    /// - Returns: `BaseMessage` if one is found
    public func message(for objectID: NSManagedObjectID) -> BaseMessageEntity? {
        // TODO: (IOS-2014) Is there a way to optimize this by using a fetch request with batch sizes?
        guard let message = entityManager.entityFetcher.existingObject(with: objectID) as? BaseMessageEntity else {
            DDLogVerbose("Object not found or unable to cast to BaseMessage")
            return nil
        }
        
        return message
    }

    // MARK: - Notifications
    
    @objc private func batchDeletedAllMessagesInAConversation(notification: Notification) {
        guard let userInfo = notification.userInfo as? [String: Any],
              let conversationObjectIDOfMessageDeletion =
              userInfo[DatabaseContext.conversationObjectIDKey] as? NSManagedObjectID
        else {
            return
        }
        
        if conversationObjectIDOfMessageDeletion == conversationObjectID {
            if currentOffset > 0 {
                configureLoadingMessagesAtBottom()
            }
            fetch()
        }
    }
    
    @objc private func batchDeletedOldMessages() {
        // Count on the context of the fetched results controller, not on `messageFetcher`s own one.
        // Counting on the context of the fetched results controller also keeps this number consistent with the snapshot
        // that is published below.
        numberOfMessages = messageFetcher.count(using: fetchedResultsController.managedObjectContext)

        if currentOffset > numberOfMessages {
            configureLoadingMessagesAtBottom()
        }
        fetch()
    }
}

// MARK: - NSFetchedResultsControllerDelegate

extension MessageProvider: NSFetchedResultsControllerDelegate {
    // As we use the diffable data source only one delegate method is needed
    public func controller(
        _ controller: NSFetchedResultsController<NSFetchRequestResult>,
        didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
    ) {
        DDLogNotice("New snapshot...")
        let newSnapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
        let convertedSnapshot = convert(newSnapshot)
        
        // Publish new snapshot
        currentMessages = MessagesSnapshot(
            snapshot: convertedSnapshot,
            previouslyNewestMessagesLoaded: previouslyNewestMessagesLoaded
        )
        
        DDLogNotice("New snapshot published")
        
        // The new state is the new previous state
        previouslyNewestMessagesLoaded = newestMessagesLoaded
    }
    
    /// Convert and filter the snapshot
    ///
    /// We never want to reload cells because otherwise we might reuse a cell with different content when just updating
    /// the delivery state of our message causing weird flickering. Reconfigure always dequeues the *exact* same cell
    /// which means no weird flickering ��
    private func convert(
        _ snapshot: NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
    ) -> NSDiffableDataSourceSnapshot<String, NSManagedObjectID> {
        
        var newSnapshot = NSDiffableDataSourceSnapshot<String, NSManagedObjectID>()
        
        // Because messages are saved really early in processing for the first time file message might have no MIME
        // type. Leading them to be rendered as file messages. Because they might change to another type later on, on
        // reconfiguring we crash (as the same cell (type)) is expected. Thus we filter all file messages that have no
        // MIME type.
        // Fetching should not happen on the main context. Otherwise we might deadlock if a chat is open and
        // `mergeChanges(fromRemoteContextSave:into:)` is called in `refreshDirtyObjects(:_)`.
        let fileMessageWithNoMIMETypeObjectIDs = Set(messageFetcher.fileMessagesWithNoMIMEType(
            using: fetchedResultsController.managedObjectContext
        ))
        DDLogNotice("\(fileMessageWithNoMIMETypeObjectIDs.count) messages with no MIME type are filtered")

        // Go over all sections & rows and remove file messages with no MIME type (and sections if they are now empty)
        for sectionIdentifier in snapshot.sectionIdentifiers {
            newSnapshot.appendSections([sectionIdentifier])
            
            for itemIdentifier in snapshot.itemIdentifiers(inSection: sectionIdentifier) {
                guard !fileMessageWithNoMIMETypeObjectIDs.contains(itemIdentifier) else {
                    continue
                }
                
                newSnapshot.appendItems([itemIdentifier], toSection: sectionIdentifier)
            }
            
            if newSnapshot.itemIdentifiers(inSection: sectionIdentifier).isEmpty {
                newSnapshot.deleteSections([sectionIdentifier])
            }
        }
        
        // Get all deleted messages in this conversation, to reload new cell identifiers for deleted messages
        let messageWithDeletedMessageObjectIDs = Set(messageFetcher.messagesWithDeletedMessages(
            using: fetchedResultsController.managedObjectContext
        ))
        DDLogNotice("\(messageWithDeletedMessageObjectIDs.count) deleted messages are filtered")

        var isInactiveGroup = false
        if let groupEntity = messageFetcher.group(
            for: conversationObjectID,
            using: fetchedResultsController.managedObjectContext
        ) {
            isInactiveGroup = groupEntity.state.intValue != 0
        }

        // Identify the cells neighboring the reloaded cells. Neighbors must be reloaded as well as they may be grouped
        // together.
        // If a new message is added, but not marked as reloaded (maybe due to an update of the new message) neighboring
        // updates will not happen until the provider is recreated. Before iOS 18 this could happen especially when
        // sending message while being offline. This is kind of fixed because starting with iOS 18 updating
        // `lastMessage` on `Conversation` also leads to a reload of the previous assigned message which is, by
        // accident, normally the message that needs the neighboring update.
        for reloadedItemIdentifier in snapshot.reloadedItemIdentifiers {

            guard let index = newSnapshot.indexOfItem(reloadedItemIdentifier) else {
                continue
            }

            // We may not insert the same identifier twice in the same call to reloadItems otherwise we get a crash.
            // This is most likely not relevant for performance but it might still be fun to optimize this (e.g. by
            // collecting all the item to reconfigure in a set and apply it once with `reconfigureItems(_:)`)
            // We use reconfigure to get the exact same cell back from the cell provider, this avoids flickering cells.
            //
            // If shown message changed to deleted then a new cell type should be shown, in this case `reloadItems` must
            // be called to add replace the old cell with new cell having a new identifier.
            //
            // If the chat is an inactive group, all cells are reloaded to prevent a crash due to the reconfiguration of
            // the cells if the conversation is deleted.
            func reloadOrReconfigureItems(for identifier: NSManagedObjectID) {
                if messageWithDeletedMessageObjectIDs.contains(identifier) || isInactiveGroup {
                    newSnapshot.reloadItems([identifier])
                }
                else {
                    newSnapshot.reconfigureItems([identifier])
                }
            }

            if index - 1 >= 0 {
                reloadOrReconfigureItems(for: newSnapshot.itemIdentifiers[index - 1])
            }

            reloadOrReconfigureItems(for: newSnapshot.itemIdentifiers[index])

            if index + 1 < newSnapshot.itemIdentifiers.count {
                reloadOrReconfigureItems(for: newSnapshot.itemIdentifiers[index + 1])
            }
        }

        // If we try to reconfigure an item that's not in the snapshot it crashes thus we need to filter them out, too.
        // Because we cannot delete items from `reconfiguredItemIdentifiers` we need to filter the original array first
        // and then mark them as reconfigured.
        let filteredReconfiguredItems = snapshot.reconfiguredItemIdentifiers.filter {
            !fileMessageWithNoMIMETypeObjectIDs.contains($0) &&
                !messageWithDeletedMessageObjectIDs.contains($0) &&
                !snapshot.reloadedItemIdentifiers.contains($0) &&
                !newSnapshot.reloadedItemIdentifiers.contains($0)
        }
        newSnapshot.reconfigureItems(filteredReconfiguredItems)

        return newSnapshot
    }
}

Messung
CHZG
859510093

¤ Dauer der Verarbeitung: 0.3 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.