diff --git a/DittoToolsApp/DittoToolsApp/Views/MenuListItem.swift b/DittoToolsApp/DittoToolsApp/Views/MenuListItem.swift index 7b5d58d..ba97742 100644 --- a/DittoToolsApp/DittoToolsApp/Views/MenuListItem.swift +++ b/DittoToolsApp/DittoToolsApp/Views/MenuListItem.swift @@ -53,7 +53,6 @@ struct MenuListItem_Previews: PreviewProvider { } Section("Exports") { MenuListItem(title: "Export Logs", systemImage: "square.and.arrow.up", color: .green) - MenuListItem(title: "Export Logs", systemImage: "square.and.arrow.up", color: .green) } } .listStyle(GroupedListStyle()) diff --git a/Img/diskUsage.png b/Img/diskUsage.png index a950618..ea1c3a4 100644 Binary files a/Img/diskUsage.png and b/Img/diskUsage.png differ diff --git a/README.md b/README.md index dd9203d..12f2f24 100644 --- a/README.md +++ b/README.md @@ -261,54 +261,6 @@ Allows you to export a file of the logs from your applcation as a zip file. First, make sure the "DittoExportLogs" is added to your Target. Then, use `import DittoExportLogs` to import the Export Logs. -**Important** - -Before calling `ditto.startSync()` we need to set the `DittoLogger.setLogFileURL()`. This registers a file path where logs will be written to, whenever Ditto wants to issue a log (on top of emitting the log to the console). Use the `LogFileConfig` struct: - -``` -struct LogFileConfig { - static let logsDirectoryName = "debug-logs" - static let logFileName = "logs.txt" - static let zippedLogFileName = "logs.zip" - - static var logsDirectory: URL! = { - let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! - return directory.appendingPathComponent(logsDirectoryName, isDirectory: true) - }() - - static var logFileURL: URL! = { - return Self.logsDirectory.appendingPathComponent(logFileName) - }() - - static var zippedLogsURL: URL! = { - let directory = FileManager.default.temporaryDirectory - return directory.appendingPathComponent(zippedLogFileName) - }() - - public static func createLogFileURL() -> URL? { - do { - try FileManager().createDirectory(at: self.logsDirectory, - withIntermediateDirectories: true) - } catch let error { - assertionFailure("Failed to create logs directory: \(error)") - return nil - } - - return self.logFileURL - } -} -``` - -and then before calling `ditto.startSync()` set the log file url with: - -``` -if let logFileURL = LogFileConfig.createLogFileURL() { - DittoLogger.setLogFileURL(logFileURL) -} -``` - -Now we can call `ExportLogs()`. - **SwiftUI** Use `ExportLogs()` to export the logs. It is recommended to call `ExportLogs` from within a [sheet](https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)). diff --git a/Sources/DittoExportLogs/DittoLogManager.swift b/Sources/DittoExportLogs/DittoLogManager.swift index 6892b82..72c1651 100644 --- a/Sources/DittoExportLogs/DittoLogManager.swift +++ b/Sources/DittoExportLogs/DittoLogManager.swift @@ -1,55 +1,56 @@ // -// DittoLogManager.swift -// -// -// Created by Walker Erekson on 1/13/23. +// Copyright © 2021 DittoLive Incorporated. All rights reserved. // import Foundation +#if canImport(MessageUI) +import MessageUI +#endif +import UIKit +import DittoSwift private struct Config { static let logsDirectoryName = "debug-logs" - static let zippedLogFileName = "DittoLogs.zip" + static let logFileName = "logs.txt" + static let zippedLogFileName = "ditto.jsonl.gz" + /// Directory into which debug logs are to be stored. We use a dedicated + /// directory to keep logs grouped (in the event that we begin generating + /// more than one log - either from multiple sub-systems or due to log + /// rotation). static var logsDirectory: URL! = { let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! return directory.appendingPathComponent(logsDirectoryName, isDirectory: true) }() + /// A temporary location into which we can store zipped logs before sharing + /// them via a share sheet. static var zippedLogsURL: URL! = { let directory = FileManager.default.temporaryDirectory return directory.appendingPathComponent(Config.zippedLogFileName) }() - } -public struct DittoLogManager { - public static let shared = DittoLogManager() +/// LogManager acts as a thin interface over our stored log files and +/// offers functionality to share zipped logs with an iOS share sheet. +struct LogManager { - private init() {} + // MARK: - Singleton - public func createLogsZip() -> URL? { - try? FileManager().removeItem(at: Config.zippedLogsURL) + public static let shared = LogManager() - let coordinator = NSFileCoordinator() - var nsError: NSError? + // MARK: - Initialization - // Runs synchronously, so no need to co-ordinate multiple callers - coordinator.coordinate(readingItemAt: Config.logsDirectory, - options: [.forUploading], error: &nsError) { tempURL in - do { - try FileManager().moveItem(at: tempURL, to: Config.zippedLogsURL) - } catch let error { - assertionFailure("Failed to move zipped logs into location: \(error)") - } - } + private init() { + // Private singleton constructor + } - if let error = nsError { - assertionFailure("Failed to zip logs: \(error)") - return nil - } + // MARK: - Functions + /// Zips all contents in our log directory, placing an updated zip file at URL returned. + public func exportLogs() async throws -> URL { + try? FileManager().removeItem(at: Config.zippedLogsURL) + try await DittoLogger.export(to: Config.zippedLogsURL) return Config.zippedLogsURL } - } diff --git a/Sources/DittoExportLogs/ExportLogs.swift b/Sources/DittoExportLogs/ExportLogs.swift index 5ab8b05..f032c93 100644 --- a/Sources/DittoExportLogs/ExportLogs.swift +++ b/Sources/DittoExportLogs/ExportLogs.swift @@ -11,31 +11,45 @@ import UIKit @available(tvOS, unavailable) public struct ExportLogs: UIViewControllerRepresentable { - public init() {} + @Binding var activityViewController: UIActivityViewController? + + public init(activityViewController: Binding) { + self._activityViewController = activityViewController + } - public func makeUIViewController(context: Context) -> UIActivityViewController { - - let zippedLogs = getZippedLogs() + public func makeUIViewController(context: Context) -> UIViewController { + // Create a dummy UIViewController to host the UIActivityViewController later + let viewController = UIViewController() - let avc = UIActivityViewController(activityItems: [zippedLogs as Any], applicationActivities: nil) - avc.excludedActivityTypes = [.postToVimeo, .postToWeibo, .postToFlickr, .postToTwitter, .postToFacebook, .postToTencentWeibo, .addToReadingList, .assignToContact, .openInIBooks] + Task { + if let zippedLogs = await getZippedLogs() { + let avc = UIActivityViewController(activityItems: [zippedLogs], applicationActivities: nil) + avc.excludedActivityTypes = [.postToVimeo, .postToWeibo, .postToFlickr, .postToTwitter, .postToFacebook, .postToTencentWeibo, .addToReadingList, .assignToContact, .openInIBooks] + + DispatchQueue.main.async { + self.activityViewController = avc + } + } + } - return avc + return viewController } - - public func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { + public func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + // Present the activity view controller if it’s available + if let avc = activityViewController, uiViewController.presentedViewController == nil { + uiViewController.present(avc, animated: true) + } } - - func getZippedLogs() -> URL? { - - guard let zippedLogs = DittoLogManager.shared.createLogsZip() else { - assertionFailure(); return nil + func getZippedLogs() async -> URL? { + do { + return try await LogManager.shared.exportLogs() + } catch { + print("Error exporting logs") + return nil } - - return zippedLogs } - } + diff --git a/Sources/DittoExportLogs/LoggingDetailsView.swift b/Sources/DittoExportLogs/LoggingDetailsView.swift index 727068b..038267b 100644 --- a/Sources/DittoExportLogs/LoggingDetailsView.swift +++ b/Sources/DittoExportLogs/LoggingDetailsView.swift @@ -9,6 +9,8 @@ import Combine import DittoSwift import SwiftUI +import UIKit + public struct LoggingDetailsView: View { @Environment(\.colorScheme) private var colorScheme @@ -16,6 +18,8 @@ public struct LoggingDetailsView: View { @State private var presentExportLogsAlert: Bool = false @Binding var selectedLoggingOption: DittoLogger.LoggingOptions + @State private var activityViewController: UIActivityViewController? + public init(loggingOption: Binding) { self._selectedLoggingOption = loggingOption } @@ -42,6 +46,7 @@ public struct LoggingDetailsView: View { // Export Logs Button(action: { self.presentExportLogsAlert.toggle() + print(self.presentExportLogsAlert) }) { HStack { Text("Export Logs") @@ -52,40 +57,56 @@ public struct LoggingDetailsView: View { .foregroundColor(textColor) .frame(maxWidth: .infinity, maxHeight: .infinity) #if !os(tvOS) - .sheet(isPresented: $presentExportLogsShare) { - ExportLogs() - } + .sheet(isPresented: $presentExportLogsShare) { + if let activityVC = activityViewController { + // Use a wrapper UIViewController to present the activity controller + ActivityViewControllerWrapper(activityViewController: activityVC) + } else { + // Pass the binding for the `UIActivityViewController?` + ExportLogs(activityViewController: $activityViewController) + } + } #endif } + .alert(isPresented: $presentExportLogsAlert) { + #if os(tvOS) + Alert(title: Text("Export Logs"), + message: Text("Exporting logs is not supported on tvOS at this time."), + dismissButton: .cancel() + ) + #else + Alert(title: Text("Export Logs"), + message: Text("Compressing the logs may take a few seconds."), + primaryButton: .default( + Text("Export"), + action: { + presentExportLogsShare = true + }), + secondaryButton: .cancel() + ) + #endif + } } #if os(tvOS) .listStyle(GroupedListStyle()) #else .listStyle(InsetGroupedListStyle()) #endif - .alert(isPresented: $presentExportLogsAlert) { -#if os(tvOS) - Alert(title: Text("Export Logs"), - message: Text("Exporting logs is not supported on tvOS at this time."), - dismissButton: .cancel() - ) -#else - Alert(title: Text("Export Logs"), - message: Text("Compressing the logs may take a few seconds."), - primaryButton: .default( - Text("Export"), - action: { - presentExportLogsShare = true - }), - secondaryButton: .cancel() - ) -#endif - } } } -struct LoggingDetailsView_Previews: PreviewProvider { - static var previews: some View { - LoggingDetailsView(loggingOption: .constant(.debug)) +struct ActivityViewControllerWrapper: UIViewControllerRepresentable { + let activityViewController: UIActivityViewController + + func makeUIViewController(context: Context) -> UIViewController { + let viewController = UIViewController() + DispatchQueue.main.async { + viewController.present(activityViewController, animated: true) + } + return viewController + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + // No need to update the view controller here } }