From ac9d0dfbe58f6ed6dc30af380456d6cd46b228ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Andr=C3=A9s=20Mesa=20Ni=C3=B1o?= Date: Sat, 16 May 2026 23:57:20 -0500 Subject: [PATCH] Cache expensive allocations and simplify collection idioms - Hoist NSRegularExpression, JSONEncoder, and DateFormatter instances to module scope so they are not re-created on every call. - Replace filter().count, filter().isEmpty, and count > 0 patterns with count(where:), contains(where:), and !isEmpty respectively to avoid allocating intermediate arrays on hot paths (recipient field input, sidebar selection, mailbox list cell rendering, composer attachment counting). --- .../Sources/UI/Screens/Sidebar/SidebarModel.swift | 2 +- .../MailboxItemListView/MailboxItemsListView.swift | 7 ++++--- .../UI/Views/TrackersInfo/TrackersUIModel.swift | 4 ++-- .../UIKitComponents/RecipientsFieldController.swift | 2 +- .../RecipientsFieldEditingController.swift | 2 +- .../Views/DraftActionBarViewController.swift | 12 ++++++++---- .../Sources/Composer/Utils/HtmlSanitizer.swift | 4 +++- .../Composer/Views/ComposerView/ComposerModel.swift | 5 ++--- Modules/InboxWebView/Sources/preProcessedHTML.swift | 6 +++--- 9 files changed, 25 insertions(+), 19 deletions(-) diff --git a/Modules/App/Sources/UI/Screens/Sidebar/SidebarModel.swift b/Modules/App/Sources/UI/Screens/Sidebar/SidebarModel.swift index d0649f43d6..f20916393c 100644 --- a/Modules/App/Sources/UI/Screens/Sidebar/SidebarModel.swift +++ b/Modules/App/Sources/UI/Screens/Sidebar/SidebarModel.swift @@ -218,7 +218,7 @@ final class SidebarModel: Sendable, ObservableObject { } private func selectFirstSystemItemIfNeeded() { - if state.items.filter(\.isSelected).isEmpty, let first = state.system.first { + if !state.items.contains(where: \.isSelected), let first = state.system.first { select(item: .system(first)) } } diff --git a/Modules/App/Sources/UI/Views/MailboxItemListView/MailboxItemsListView.swift b/Modules/App/Sources/UI/Views/MailboxItemListView/MailboxItemsListView.swift index 991e0219c9..e9cafff95b 100644 --- a/Modules/App/Sources/UI/Views/MailboxItemListView/MailboxItemsListView.swift +++ b/Modules/App/Sources/UI/Views/MailboxItemListView/MailboxItemsListView.swift @@ -175,10 +175,11 @@ struct MailboxItemsListView String { let unread = item.isRead ? "" : L10n.Mailbox.VoiceOver.unread.string let expiration = item.expirationDate?.toExpirationDateUIModel?.text.string ?? "" + let previewables = item.attachments.previewables let attachments = - item.attachments.previewables.count > 0 - ? L10n.Mailbox.VoiceOver.attachments(count: item.attachments.previewables.count).string - : "" + previewables.isEmpty + ? "" + : L10n.Mailbox.VoiceOver.attachments(count: previewables.count).string let value: String = """ \(unread) \(item.emails). diff --git a/Modules/App/Sources/UI/Views/TrackersInfo/TrackersUIModel.swift b/Modules/App/Sources/UI/Views/TrackersInfo/TrackersUIModel.swift index 645f9b7ff5..37e5ff5fb9 100644 --- a/Modules/App/Sources/UI/Views/TrackersInfo/TrackersUIModel.swift +++ b/Modules/App/Sources/UI/Views/TrackersInfo/TrackersUIModel.swift @@ -36,11 +36,11 @@ struct TrackersUIModel: Identifiable, Hashable { } var areTrackersPresented: Bool { - blockedTrackers.count > 0 || (blockedTrackers.isEmpty && cleanedLinks.isEmpty) + !blockedTrackers.isEmpty || (blockedTrackers.isEmpty && cleanedLinks.isEmpty) } var areLinksPresented: Bool { - cleanedLinks.count > 0 + !cleanedLinks.isEmpty } static var empty: TrackersUIModel { diff --git a/Modules/InboxComposer/Sources/Composer/UIKitComponents/RecipientsFieldController.swift b/Modules/InboxComposer/Sources/Composer/UIKitComponents/RecipientsFieldController.swift index d2a18c6374..fc0e2e07b1 100644 --- a/Modules/InboxComposer/Sources/Composer/UIKitComponents/RecipientsFieldController.swift +++ b/Modules/InboxComposer/Sources/Composer/UIKitComponents/RecipientsFieldController.swift @@ -140,7 +140,7 @@ final class RecipientsFieldController: UIViewController { updateStateInExpandedAndEditingViews(state) case .editing: updateStateInExpandedAndEditingViews(state) - if state.recipients.filter(\.isSelected).isEmpty { + if !state.recipients.contains(where: \.isSelected) { editingController.setFocus() } case .contactPicker: diff --git a/Modules/InboxComposer/Sources/Composer/UIKitComponents/RecipientsFieldEditingController.swift b/Modules/InboxComposer/Sources/Composer/UIKitComponents/RecipientsFieldEditingController.swift index 8822b9fa63..2d91e03e66 100644 --- a/Modules/InboxComposer/Sources/Composer/UIKitComponents/RecipientsFieldEditingController.swift +++ b/Modules/InboxComposer/Sources/Composer/UIKitComponents/RecipientsFieldEditingController.swift @@ -216,7 +216,7 @@ extension RecipientsFieldEditingController: UICollectionViewDataSource { case .onTextChanged(let text): onEvent?(.onInputChange(text: text)) case .onDeleteKeyPressedOnEmptyTextField: - if !cellUIModels.filter(\.isRecipient).isEmpty { removeFocusFromCursor() } + if cellUIModels.contains(where: \.isRecipient) { removeFocusFromCursor() } onEvent?(.onDeleteKeyPressedInsideEmptyInputField) case .onReturnKeyPressed: onEvent?(.onReturnKeyPressed) diff --git a/Modules/InboxComposer/Sources/Composer/UIKitComponents/Views/DraftActionBarViewController.swift b/Modules/InboxComposer/Sources/Composer/UIKitComponents/Views/DraftActionBarViewController.swift index 89e37d9b12..bbda7f11f9 100644 --- a/Modules/InboxComposer/Sources/Composer/UIKitComponents/Views/DraftActionBarViewController.swift +++ b/Modules/InboxComposer/Sources/Composer/UIKitComponents/Views/DraftActionBarViewController.swift @@ -302,13 +302,17 @@ extension DraftActionBarViewController { } } +private let customExpirationDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + return formatter +}() + private extension DraftExpirationTime { var customDateString: String? { if let customDate { - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeStyle = .short - return formatter.string(from: customDate) + return customExpirationDateFormatter.string(from: customDate) } return nil } diff --git a/Modules/InboxComposer/Sources/Composer/Utils/HtmlSanitizer.swift b/Modules/InboxComposer/Sources/Composer/Utils/HtmlSanitizer.swift index 0fb438b9fe..6f36da7037 100644 --- a/Modules/InboxComposer/Sources/Composer/Utils/HtmlSanitizer.swift +++ b/Modules/InboxComposer/Sources/Composer/Utils/HtmlSanitizer.swift @@ -18,11 +18,13 @@ import Foundation struct HtmlSanitizer { + private static let encoder = JSONEncoder() + /// Escapes characters that can trigger WebKit JS SyntaxError static func applyStringLiteralEscapingRules(html: String) -> String { // We use JSONEncoder as a trick to ensure all problematic // characters (quotes, backslashes, control chars) are properly escaped. - let jsonEncodedText = try! JSONEncoder().encode(html) + let jsonEncodedText = try! encoder.encode(html) let sanitized = String(data: jsonEncodedText, encoding: .utf8)! return sanitized } diff --git a/Modules/InboxComposer/Sources/Composer/Views/ComposerView/ComposerModel.swift b/Modules/InboxComposer/Sources/Composer/Views/ComposerView/ComposerModel.swift index 0b585cf6d9..8de43fca96 100644 --- a/Modules/InboxComposer/Sources/Composer/Views/ComposerView/ComposerModel.swift +++ b/Modules/InboxComposer/Sources/Composer/Views/ComposerView/ComposerModel.swift @@ -621,9 +621,8 @@ extension ComposerModel { private func updateStateAttachmentUIModels() async { do { let draftAttachments = try await draft.attachmentList().attachments().get() - let dispositions = draftAttachments.map(\.attachment.disposition) - let inlineCount = dispositions.filter { $0 == .inline }.count - let attachmentCount = dispositions.filter { $0 == .attachment }.count + let inlineCount = draftAttachments.count(where: { $0.attachment.disposition == .inline }) + let attachmentCount = draftAttachments.count(where: { $0.attachment.disposition == .attachment }) AppLogger.log(message: "Attachments update: inline: \(inlineCount), attachment: \(attachmentCount)", category: .composer) state.attachments = draftAttachments.toDraftAttachmentUIModels() attachmentAlertState.enqueueAlertsForFailedAttachmentUploads(attachments: draftAttachments) diff --git a/Modules/InboxWebView/Sources/preProcessedHTML.swift b/Modules/InboxWebView/Sources/preProcessedHTML.swift index 2193d2adec..e738c065a8 100644 --- a/Modules/InboxWebView/Sources/preProcessedHTML.swift +++ b/Modules/InboxWebView/Sources/preProcessedHTML.swift @@ -1,15 +1,15 @@ import Foundation +private let httpsURLRegex = try! NSRegularExpression(pattern: #"https://[^"'\s<>)]+"#, options: []) + /// Processes raw HTML by replacing https:// URLs with proton-https:// scheme /// and extracting only filenames from full URLs. public func preProcessedHTML(rawHTML: String) -> String { // FIXME: This function should use Rust implmentation - it's only temporary solution var modifiedHTML = rawHTML - let httpsURLPattern = #"https://[^"'\s<>)]+"# - let regex = try! NSRegularExpression(pattern: httpsURLPattern, options: []) let nsString = modifiedHTML as NSString - let matches = regex.matches(in: modifiedHTML, options: [], range: NSRange(location: 0, length: nsString.length)) + let matches = httpsURLRegex.matches(in: modifiedHTML, options: [], range: NSRange(location: 0, length: nsString.length)) for match in matches.reversed() { let urlRange = match.range(at: 0)