From febd0a6c84c8483aa8d3f1b123e5ad9f4def0de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 15 Sep 2026 19:50:05 +0200 Subject: [PATCH] Zip log files before sharing Share Logs now bundles the notice file and the log files into a single zip archive before opening the share sheet. The archive is created with NSFileCoordinator, so no new dependency is needed. If zipping fails the files are shared uncompressed as before. --- LoopFollow/Log/LogArchiver.swift | 36 +++++++++++++++++++ LoopFollow/ViewControllers/MoreMenuView.swift | 30 +++++++++++----- 2 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 LoopFollow/Log/LogArchiver.swift diff --git a/LoopFollow/Log/LogArchiver.swift b/LoopFollow/Log/LogArchiver.swift new file mode 100644 index 000000000..a68d61f89 --- /dev/null +++ b/LoopFollow/Log/LogArchiver.swift @@ -0,0 +1,36 @@ +// LoopFollow +// LogArchiver.swift + +import Foundation + +enum LogArchiver { + /// Copies `files` into a folder named `archiveName` and returns a zip archive + /// of that folder in the temporary directory. + static func zip(files: [URL], archiveName: String) throws -> URL { + let fileManager = FileManager.default + let staging = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + let folder = staging.appendingPathComponent(archiveName, isDirectory: true) + try fileManager.createDirectory(at: folder, withIntermediateDirectories: true) + defer { try? fileManager.removeItem(at: staging) } + + for file in files { + try fileManager.copyItem(at: file, to: folder.appendingPathComponent(file.lastPathComponent)) + } + + let destination = fileManager.temporaryDirectory.appendingPathComponent("\(archiveName).zip") + try? fileManager.removeItem(at: destination) + + var coordinationError: NSError? + var copyError: Error? + NSFileCoordinator().coordinate(readingItemAt: folder, options: .forUploading, error: &coordinationError) { zipURL in + do { + try fileManager.copyItem(at: zipURL, to: destination) + } catch { + copyError = error + } + } + if let coordinationError { throw coordinationError } + if let copyError { throw copyError } + return destination + } +} diff --git a/LoopFollow/ViewControllers/MoreMenuView.swift b/LoopFollow/ViewControllers/MoreMenuView.swift index 31f3b3915..ed1c8f360 100644 --- a/LoopFollow/ViewControllers/MoreMenuView.swift +++ b/LoopFollow/ViewControllers/MoreMenuView.swift @@ -287,19 +287,31 @@ struct MoreMenuView: View { } private func presentLogShareSheet(noticeText: String, logFiles: [URL]) { - var items: [Any] = logFiles - if let noticeURL = writeShareNoticeFile(text: noticeText) { - items.insert(noticeURL, at: 0) - } - let avc = UIActivityViewController(activityItems: items, applicationActivities: nil) - UIApplication.shared.topMost?.present(avc, animated: true) - } - - private func writeShareNoticeFile(text: String) -> URL? { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd_HHmm" let timestamp = formatter.string(from: Date()) + var files = logFiles + if let noticeURL = writeShareNoticeFile(text: noticeText, timestamp: timestamp) { + files.insert(noticeURL, at: 0) + } + + DispatchQueue.global(qos: .userInitiated).async { + let items: [Any] + do { + items = try [LogArchiver.zip(files: files, archiveName: "LoopFollow Logs \(timestamp)")] + } catch { + LogManager.shared.log(category: .general, message: "Failed to zip log files, sharing them uncompressed: \(error)") + items = files + } + DispatchQueue.main.async { + let avc = UIActivityViewController(activityItems: items, applicationActivities: nil) + UIApplication.shared.topMost?.present(avc, animated: true) + } + } + } + + private func writeShareNoticeFile(text: String, timestamp: String) -> URL? { let version = AppVersionManager().version() let branchAndSha = BuildDetails.default.branchAndSha