diff --git a/Sources/Pulse/LoggerStore/LoggerStore+Entities.swift b/Sources/Pulse/LoggerStore/LoggerStore+Entities.swift index 42fb5a519..ba11d7364 100644 --- a/Sources/Pulse/LoggerStore/LoggerStore+Entities.swift +++ b/Sources/Pulse/LoggerStore/LoggerStore+Entities.swift @@ -11,6 +11,19 @@ private let timestampFormatter: DateFormatter = { return f }() +/// Formats the entity's timestamp for display. +/// +/// Reads `createdAt` via KVC so a NULL stored value (which Core Data reports for +/// every attribute of a deleted object once the deletion is merged into the +/// context) is surfaced as `nil` instead of trapping the ObjC->Swift `Date` +/// bridge. +private func makeFormattedTimestamp(for object: NSManagedObject) -> String { + guard let createdAt = object.value(forKey: "createdAt") as? Date else { + return "–" + } + return timestampFormatter.string(from: createdAt) +} + public final class LoggerSessionEntity: NSManagedObject { @NSManaged public var createdAt: Date @NSManaged public var id: UUID @@ -32,7 +45,7 @@ public final class LoggerMessageEntity: NSManagedObject { @NSManaged public var task: NetworkTaskEntity? public lazy var metadata = { KeyValueEncoding.decodeKeyValuePairs(rawMetadata) }() - public lazy var formattedTimestamp: String = timestampFormatter.string(from: createdAt) + public lazy var formattedTimestamp: String = makeFormattedTimestamp(for: self) } public final class NetworkTaskEntity: NSManagedObject { @@ -112,7 +125,7 @@ public final class NetworkTaskEntity: NSManagedObject { // MARK: Helpers public lazy var metadata = { rawMetadata.map(KeyValueEncoding.decodeKeyValuePairs) }() - public lazy var formattedTimestamp: String = timestampFormatter.string(from: createdAt) + public lazy var formattedTimestamp: String = makeFormattedTimestamp(for: self) public lazy var parsedURLComponents: URLComponents? = url.flatMap { URLComponents(string: $0) } // View-layer formatting caches populated by PulseUI. Keyed on the input diff --git a/Sources/PulseUI/Features/Console/Views/ConsoleEntityCell.swift b/Sources/PulseUI/Features/Console/Views/ConsoleEntityCell.swift index de40ed5b4..9ecdcee35 100644 --- a/Sources/PulseUI/Features/Console/Views/ConsoleEntityCell.swift +++ b/Sources/PulseUI/Features/Console/Views/ConsoleEntityCell.swift @@ -11,7 +11,13 @@ import CoreData @available(iOS 18, tvOS 18, macOS 15, watchOS 11, visionOS 1, *) struct ConsoleEntityCell: View { - let entity: NSManagedObject + /// Observed so that the `isDeleted` check below is re-evaluated when the + /// entity is deleted from under the list. The console keeps rendering the + /// entities it last fetched (`ConsoleListViewModel.visibleEntities` is not + /// refreshed while the list is scrolled away from the top), so without the + /// observation this view keeps a deleted entity on screen and the cell + /// traps reading its non-optional attributes. + @ObservedObject var entity: NSManagedObject var urlMatch: ConsoleSearchMatch? init(entity: NSManagedObject) {