@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
- [Mac] Added a menu item for toggling the toolbar. | |||
- [Mac] In a post with unpublished changes (i.e., with "local" or "edited" status), the post is autosaved after a one-second pause in typing. | |||
- [iOS/Mac] Added a context-menu item to delete local posts from the post list. | |||
- [iOS/Mac] Added methods to fetch device logs. | |||
- [iOS] Added a settings option to generate a log file as a new local draft (iOS 15+). | |||
### Changed | |||
@@ -1,9 +1,4 @@ | |||
// | |||
// Logging.swift | |||
// WriteFreely-MultiPlatform | |||
// | |||
// Created by Angelo Stavrow on 2022-06-25. | |||
// | |||
// Credit for much of this class: https://steipete.com/posts/logging-in-swift/ | |||
import Foundation | |||
import os | |||
@@ -14,6 +9,11 @@ protocol LogWriter { | |||
func logCrashAndSetFlag(error: Error) | |||
} | |||
@available(iOS 15, *) | |||
protocol LogReader { | |||
func fetchLogs() -> [String] | |||
} | |||
final class Logging { | |||
private let logger: Logger | |||
@@ -48,3 +48,57 @@ extension Logging: LogWriter { | |||
} | |||
} | |||
extension Logging: LogReader { | |||
@available(iOS 15, *) | |||
func fetchLogs() -> [String] { | |||
var logs: [String] = [] | |||
do { | |||
let osLog = try getLogEntries() | |||
for logEntry in osLog { | |||
let formattedEntry = formatEntry(logEntry) | |||
logs.append(formattedEntry) | |||
} | |||
} catch { | |||
logs.append("Could not fetch logs") | |||
} | |||
return logs | |||
} | |||
@available(iOS 15, *) | |||
private func getLogEntries() throws -> [OSLogEntryLog] { | |||
let logStore = try OSLogStore(scope: .currentProcessIdentifier) | |||
let oneHourAgo = logStore.position(date: Date().addingTimeInterval(-3600)) | |||
let allEntries = try Array(logStore.__entriesEnumerator(position: oneHourAgo, predicate: nil)) | |||
return allEntries | |||
.compactMap { $0 as? OSLogEntryLog } | |||
.filter { $0.subsystem == subsystem } | |||
} | |||
@available(iOS 15, *) | |||
private func formatEntry(_ logEntry: OSLogEntryLog) -> String { | |||
/// The desired format is: | |||
/// `date [process/category] LEVEL: composedMessage (threadIdentifier)` | |||
var level: String = "" | |||
switch logEntry.level { | |||
case .debug: | |||
level = "DEBUG" | |||
case .info: | |||
level = "INFO" | |||
case .notice: | |||
level = "NOTICE" | |||
case .error: | |||
level = "ERROR" | |||
case .fault: | |||
level = "FAULT" | |||
default: | |||
level = "UNDEFINED" | |||
} | |||
// swiftlint:disable:next line_length | |||
return "\(logEntry.date) [\(logEntry.process)/\(logEntry.category)] \(level): \(logEntry.composedMessage) (\(logEntry.threadIdentifier))" | |||
} | |||
} |
@@ -24,6 +24,8 @@ struct CheckForDebugModifier { | |||
struct WriteFreely_MultiPlatformApp: App { | |||
@StateObject private var model = WriteFreelyModel.shared | |||
private let logger = Logging(for: String(describing: WriteFreely_MultiPlatformApp.self)) | |||
#if os(macOS) | |||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate | |||
@StateObject var updaterViewModel = MacUpdatesViewModel() | |||
@@ -68,6 +70,11 @@ struct WriteFreely_MultiPlatformApp: App { | |||
) | |||
) | |||
} | |||
.onAppear { | |||
if #available(iOS 15, *) { | |||
if didCrash { generateCrashLogPost() } | |||
} | |||
} | |||
.withErrorHandling() | |||
.environmentObject(model) | |||
.environment(\.managedObjectContext, LocalStorageManager.standard.container.viewContext) | |||
@@ -167,6 +174,33 @@ struct WriteFreely_MultiPlatformApp: App { | |||
} | |||
} | |||
@available(iOS 15, *) | |||
private func generateCrashLogPost() { | |||
logger.log("Generating local log post...") | |||
DispatchQueue.main.asyncAfter(deadline: .now()) { | |||
// Unset selected post and collection and navigate to local drafts. | |||
self.model.selectedPost = nil | |||
self.model.selectedCollection = nil | |||
self.model.showAllPosts = false | |||
// Create the new log post. | |||
let newLogPost = model.editor.generateNewLocalPost(withFont: 2) | |||
newLogPost.title = "Logs For Support" | |||
var postBody: [String] = [ | |||
"WriteFreely-Multiplatform v\(Bundle.main.appMarketingVersion) (\(Bundle.main.appBuildVersion))", | |||
"Generated \(Date())", | |||
"" | |||
] | |||
postBody.append(contentsOf: logger.fetchLogs()) | |||
newLogPost.body = postBody.joined(separator: "\n") | |||
self.model.selectedPost = newLogPost | |||
} | |||
logger.log("Generated local log post.") | |||
} | |||
private func resetCrashFlags() { | |||
UserDefaults.shared.set(false, forKey: WFDefaults.didHaveFatalError) | |||
UserDefaults.shared.removeObject(forKey: WFDefaults.fatalErrorDescription) | |||
@@ -1050,7 +1050,7 @@ | |||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; | |||
CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements"; | |||
CODE_SIGN_STYLE = Automatic; | |||
CURRENT_PROJECT_VERSION = 690; | |||
CURRENT_PROJECT_VERSION = 691; | |||
DEVELOPMENT_TEAM = TPPAB4YBA6; | |||
GENERATE_INFOPLIST_FILE = YES; | |||
INFOPLIST_FILE = "ActionExtension-iOS/Info.plist"; | |||
@@ -1081,7 +1081,7 @@ | |||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; | |||
CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements"; | |||
CODE_SIGN_STYLE = Automatic; | |||
CURRENT_PROJECT_VERSION = 690; | |||
CURRENT_PROJECT_VERSION = 691; | |||
DEVELOPMENT_TEAM = TPPAB4YBA6; | |||
GENERATE_INFOPLIST_FILE = YES; | |||
INFOPLIST_FILE = "ActionExtension-iOS/Info.plist"; | |||
@@ -1224,7 +1224,7 @@ | |||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | |||
CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; | |||
CODE_SIGN_STYLE = Automatic; | |||
CURRENT_PROJECT_VERSION = 690; | |||
CURRENT_PROJECT_VERSION = 691; | |||
DEVELOPMENT_TEAM = TPPAB4YBA6; | |||
ENABLE_PREVIEWS = YES; | |||
INFOPLIST_FILE = iOS/Info.plist; | |||
@@ -1250,7 +1250,7 @@ | |||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | |||
CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; | |||
CODE_SIGN_STYLE = Automatic; | |||
CURRENT_PROJECT_VERSION = 690; | |||
CURRENT_PROJECT_VERSION = 691; | |||
DEVELOPMENT_TEAM = TPPAB4YBA6; | |||
ENABLE_PREVIEWS = YES; | |||
INFOPLIST_FILE = iOS/Info.plist; | |||
@@ -1,7 +1,11 @@ | |||
import SwiftUI | |||
struct SettingsView: View { | |||
@EnvironmentObject var model: WriteFreelyModel | |||
@State private var isShowingAlert = false | |||
private let logger = Logging(for: String(describing: SettingsView.self)) | |||
var body: some View { | |||
VStack { | |||
@@ -14,21 +18,22 @@ struct SettingsView: View { | |||
Section(header: Text("Appearance")) { | |||
PreferencesView(preferences: model.preferences) | |||
} | |||
Section(header: Text("External Links")) { | |||
HStack { | |||
Spacer() | |||
Link("View the Guide", destination: model.howToURL) | |||
Spacer() | |||
} | |||
HStack { | |||
Spacer() | |||
Link("Visit the Help Forum", destination: model.helpURL) | |||
Spacer() | |||
} | |||
HStack { | |||
Spacer() | |||
Link("Write a Review on the App Store", destination: model.reviewURL) | |||
Spacer() | |||
Section(header: Text("Help and Support")) { | |||
Link("View the Guide", destination: model.howToURL) | |||
Link("Visit the Help Forum", destination: model.helpURL) | |||
Link("Write a Review on the App Store", destination: model.reviewURL) | |||
if #available(iOS 15.0, *) { | |||
VStack(alignment: .leading, spacing: 8) { | |||
Button( | |||
action: didTapGenerateLogPostButton, | |||
label: { | |||
Text("Create Log Post") | |||
} | |||
) | |||
Text("Generates a local post using recent logs. You can share this for troubleshooting.") | |||
.font(.footnote) | |||
.foregroundColor(.secondary) | |||
} | |||
} | |||
} | |||
Section(header: Text("Acknowledgements")) { | |||
@@ -55,8 +60,41 @@ struct SettingsView: View { | |||
} | |||
} | |||
} | |||
.alert(isPresented: $isShowingAlert) { | |||
Alert( | |||
title: Text("Log Post Created"), | |||
message: Text("Check your local drafts for app logs from the past 24 hours.") | |||
) | |||
} | |||
// .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info. | |||
} | |||
@available(iOS 15, *) | |||
private func didTapGenerateLogPostButton() { | |||
logger.log("Generating local log post...") | |||
DispatchQueue.main.asyncAfter(deadline: .now()) { | |||
// Unset selected post and collection and navigate to local drafts. | |||
self.model.selectedPost = nil | |||
self.model.selectedCollection = nil | |||
self.model.showAllPosts = false | |||
// Create the new log post. | |||
let newLogPost = model.editor.generateNewLocalPost(withFont: 2) | |||
newLogPost.title = "Logs For Support" | |||
var postBody: [String] = [ | |||
"WriteFreely-Multiplatform v\(Bundle.main.appMarketingVersion) (\(Bundle.main.appBuildVersion))", | |||
"Generated \(Date())", | |||
"" | |||
] | |||
postBody.append(contentsOf: logger.fetchLogs()) | |||
newLogPost.body = postBody.joined(separator: "\n") | |||
self.isShowingAlert = true | |||
} | |||
logger.log("Generated local log post.") | |||
} | |||
} | |||
struct SettingsView_Previews: PreviewProvider { | |||