Page MenuHomeMusing Studio

No OneTemporary

diff --git a/Shared/Extensions/UserDefaults+Extensions.swift b/Shared/Extensions/UserDefaults+Extensions.swift
index b010fdc..dcf0267 100644
--- a/Shared/Extensions/UserDefaults+Extensions.swift
+++ b/Shared/Extensions/UserDefaults+Extensions.swift
@@ -1,57 +1,59 @@
import Foundation
enum WFDefaults {
static let isLoggedIn = "isLoggedIn"
static let showAllPostsFlag = "showAllPostsFlag"
static let selectedCollectionURL = "selectedCollectionURL"
static let lastDraftURL = "lastDraftURL"
static let colorSchemeIntegerKey = "colorSchemeIntegerKey"
static let defaultFontIntegerKey = "defaultFontIntegerKey"
static let usernameStringKey = "usernameStringKey"
static let serverStringKey = "serverStringKey"
#if os(macOS)
static let automaticallyChecksForUpdates = "automaticallyChecksForUpdates"
static let subscribeToBetaUpdates = "subscribeToBetaUpdates"
#endif
+ static let didHaveFatalError = "didHaveFatalError"
+ static let fatalErrorDescription = "fatalErrorDescription"
}
extension UserDefaults {
private static let appGroupName: String = "group.com.abunchtell.writefreely"
private static let didMigrateDefaultsToAppGroup: String = "didMigrateDefaultsToAppGroup"
private static let didRemoveStandardDefaults: String = "didRemoveStandardDefaults"
static var shared: UserDefaults {
if let groupDefaults = UserDefaults(suiteName: UserDefaults.appGroupName),
groupDefaults.bool(forKey: UserDefaults.didMigrateDefaultsToAppGroup) {
return groupDefaults
} else {
do {
let groupDefaults = try UserDefaults.standard.migrateDefaultsToAppGroup()
return groupDefaults
} catch {
return UserDefaults.standard
}
}
}
private func migrateDefaultsToAppGroup() throws -> UserDefaults {
let userDefaults = UserDefaults.standard
let groupDefaults = UserDefaults(suiteName: UserDefaults.appGroupName)
if let groupDefaults = groupDefaults {
if groupDefaults.bool(forKey: UserDefaults.didMigrateDefaultsToAppGroup) {
return groupDefaults
}
for (key, value) in userDefaults.dictionaryRepresentation() {
groupDefaults.set(value, forKey: key)
}
groupDefaults.set(true, forKey: UserDefaults.didMigrateDefaultsToAppGroup)
return groupDefaults
} else {
throw UserDefaultsError.couldNotMigrateStandardDefaults
}
}
}
diff --git a/Shared/LocalStorageManager.swift b/Shared/LocalStorageManager.swift
index ae074b4..759bbf0 100644
--- a/Shared/LocalStorageManager.swift
+++ b/Shared/LocalStorageManager.swift
@@ -1,125 +1,152 @@
import CoreData
+import os
#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
final class LocalStorageManager {
public static var standard = LocalStorageManager()
public let container: NSPersistentContainer
private let containerName = "LocalStorageModel"
private init() {
container = NSPersistentContainer(name: containerName)
setupStore(in: container)
registerObservers()
}
func saveContext() {
if container.viewContext.hasChanges {
do {
+ Self.logger.info("Saving context to local store started...")
try container.viewContext.save()
+ Self.logger.notice("Context saved to local store.")
} catch {
- fatalError(LocalStoreError.couldNotSaveContext.localizedDescription)
+ logCrashAndSetFlag(error: LocalStoreError.couldNotSaveContext)
}
}
}
func purgeUserCollections() throws {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "WFACollection")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
+ Self.logger.info("Purging user collections from local store...")
try container.viewContext.executeAndMergeChanges(using: deleteRequest)
+ Self.logger.notice("User collections purged from local store.")
} catch {
+ Self.logger.error("\(LocalStoreError.couldNotPurgeCollections.localizedDescription)")
throw LocalStoreError.couldNotPurgeCollections
}
}
}
private extension LocalStorageManager {
var oldStoreURL: URL {
let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
return appSupport.appendingPathComponent("LocalStorageModel.sqlite")
}
var sharedStoreURL: URL {
let id = "group.com.abunchtell.writefreely"
let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: id)!
return groupContainer.appendingPathComponent("LocalStorageModel.sqlite")
}
func setupStore(in container: NSPersistentContainer) {
if !FileManager.default.fileExists(atPath: oldStoreURL.path) {
container.persistentStoreDescriptions.first!.url = sharedStoreURL
}
container.loadPersistentStores { _, error in
+ Self.logger.info("Loading local store...")
if let error = error {
- fatalError(LocalStoreError.couldNotLoadStore(error.localizedDescription).localizedDescription)
+ self.logCrashAndSetFlag(error: LocalStoreError.couldNotLoadStore(error.localizedDescription))
}
+ Self.logger.notice("Loaded local store.")
}
migrateStore(for: container)
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
func migrateStore(for container: NSPersistentContainer) {
// Check if the shared store exists before attempting a migration — for example, in case we've already attempted
// and successfully completed a migration, but the deletion of the old store failed for some reason.
guard !FileManager.default.fileExists(atPath: sharedStoreURL.path) else { return }
let coordinator = container.persistentStoreCoordinator
// Get a reference to the old store.
guard let oldStore = coordinator.persistentStore(for: oldStoreURL) else {
return
}
// Attempt to migrate the old store over to the shared store URL.
do {
+ Self.logger.info("Migrating local store to shared store...")
try coordinator.migratePersistentStore(oldStore,
to: sharedStoreURL,
options: nil,
withType: NSSQLiteStoreType)
+ Self.logger.notice("Migrated local store to shared store.")
} catch {
- fatalError(LocalStoreError.couldNotMigrateStore(error.localizedDescription).localizedDescription)
+ logCrashAndSetFlag(error: LocalStoreError.couldNotMigrateStore(error.localizedDescription))
}
// Attempt to delete the old store.
do {
+ Self.logger.info("Deleting migrated local store...")
try FileManager.default.removeItem(at: oldStoreURL)
+ Self.logger.notice("Deleted migrated local store.")
} catch {
- fatalError(
- LocalStoreError.couldNotDeleteStoreAfterMigration(error.localizedDescription).localizedDescription
- )
+ logCrashAndSetFlag(error: LocalStoreError.couldNotDeleteStoreAfterMigration(error.localizedDescription))
}
}
func registerObservers() {
let center = NotificationCenter.default
#if os(iOS)
let notification = UIApplication.willResignActiveNotification
#elseif os(macOS)
let notification = NSApplication.willResignActiveNotification
#endif
// We don't need to worry about removing this observer because we're targeting iOS 9+ / macOS 10.11+; the
// system will clean this up the next time it would be posted to.
// See: https://developer.apple.com/documentation/foundation/notificationcenter/1413994-removeobserver
// And: https://developer.apple.com/documentation/foundation/notificationcenter/1407263-removeobserver
// swiftlint:disable:next discarded_notification_center_observer
center.addObserver(forName: notification, object: nil, queue: nil, using: self.saveContextOnResignActive)
}
func saveContextOnResignActive(_ notification: Notification) {
saveContext()
}
}
+
+private extension LocalStorageManager {
+
+ private static let logger = Logger(
+ subsystem: Bundle.main.bundleIdentifier!,
+ category: String(describing: LocalStorageManager.self)
+ )
+
+ private func logCrashAndSetFlag(error: Error) {
+ let errorDescription = error.localizedDescription
+ UserDefaults.shared.set(true, forKey: WFDefaults.didHaveFatalError)
+ UserDefaults.shared.set(errorDescription, forKey: WFDefaults.fatalErrorDescription)
+ Self.logger.critical("\(errorDescription)")
+ fatalError(errorDescription)
+ }
+
+}
diff --git a/Shared/PostCollection/CollectionListModel.swift b/Shared/PostCollection/CollectionListModel.swift
index b2ac884..d1cd2bb 100644
--- a/Shared/PostCollection/CollectionListModel.swift
+++ b/Shared/PostCollection/CollectionListModel.swift
@@ -1,41 +1,58 @@
import SwiftUI
import CoreData
+import os
class CollectionListModel: NSObject, ObservableObject {
@Published var list: [WFACollection] = []
private let collectionsController: NSFetchedResultsController<WFACollection>
init(managedObjectContext: NSManagedObjectContext) {
collectionsController = NSFetchedResultsController(fetchRequest: WFACollection.collectionsFetchRequest,
managedObjectContext: managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil)
super.init()
collectionsController.delegate = self
do {
+ Self.logger.info("Fetching collections from local store...")
try collectionsController.performFetch()
list = collectionsController.fetchedObjects ?? []
+ Self.logger.notice("Fetched collections from local store.")
} catch {
- // FIXME: Errors cannot be thrown out of the CollectionListView property initializer
- fatalError(LocalStoreError.couldNotFetchCollections.localizedDescription)
+ logCrashAndSetFlag(error: LocalStoreError.couldNotFetchCollections)
}
}
}
extension CollectionListModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let collections = controller.fetchedObjects as? [WFACollection] else { return }
self.list = collections
}
}
+extension CollectionListModel {
+ private static let logger = Logger(
+ subsystem: Bundle.main.bundleIdentifier!,
+ category: String(describing: CollectionListModel.self)
+ )
+
+ private func logCrashAndSetFlag(error: Error) {
+ let errorDescription = error.localizedDescription
+ UserDefaults.shared.set(true, forKey: WFDefaults.didHaveFatalError)
+ UserDefaults.shared.set(errorDescription, forKey: WFDefaults.fatalErrorDescription)
+ Self.logger.critical("\(errorDescription)")
+ fatalError(errorDescription)
+ }
+}
+
extension WFACollection {
static var collectionsFetchRequest: NSFetchRequest<WFACollection> {
let request: NSFetchRequest<WFACollection> = WFACollection.createFetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)]
return request
}
}
diff --git a/Shared/WriteFreely_MultiPlatformApp.swift b/Shared/WriteFreely_MultiPlatformApp.swift
index d17c965..feedb83 100644
--- a/Shared/WriteFreely_MultiPlatformApp.swift
+++ b/Shared/WriteFreely_MultiPlatformApp.swift
@@ -1,148 +1,166 @@
import SwiftUI
#if os(macOS)
import Sparkle
#endif
@main
struct CheckForDebugModifier {
static func main() {
#if os(macOS)
if NSEvent.modifierFlags.contains(.shift) {
// Clear the launch-to-last-draft values to load a new draft.
UserDefaults.shared.setValue(false, forKey: WFDefaults.showAllPostsFlag)
UserDefaults.shared.setValue(nil, forKey: WFDefaults.selectedCollectionURL)
UserDefaults.shared.setValue(nil, forKey: WFDefaults.lastDraftURL)
} else {
// No-op
}
#endif
WriteFreely_MultiPlatformApp.main()
}
}
struct WriteFreely_MultiPlatformApp: App {
@StateObject private var model = WriteFreelyModel.shared
#if os(macOS)
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject var updaterViewModel = MacUpdatesViewModel()
@State private var selectedTab = 0
#endif
+ @State private var didCrash = UserDefaults.shared.bool(forKey: WFDefaults.didHaveFatalError)
+
var body: some Scene {
WindowGroup {
ContentView()
.onAppear(perform: {
if model.editor.showAllPostsFlag {
DispatchQueue.main.async {
self.model.selectedCollection = nil
self.model.showAllPosts = true
showLastDraftOrCreateNewLocalPost()
}
} else {
DispatchQueue.main.async {
self.model.selectedCollection = model.editor.fetchSelectedCollectionFromAppStorage()
self.model.showAllPosts = false
showLastDraftOrCreateNewLocalPost()
}
}
})
+ .alert(isPresented: $didCrash) {
+ // TODO: - Confirm copy for this alert
+ Alert(
+ title: Text("Crash Detected"),
+ message: Text(
+ UserDefaults.shared.object(forKey: WFDefaults.fatalErrorDescription) as? String ??
+ "Something went horribly wrong!"
+ ),
+ dismissButton: .default(
+ Text("Dismiss"), action: {
+ UserDefaults.shared.set(false, forKey: WFDefaults.didHaveFatalError)
+ UserDefaults.shared.removeObject(forKey: WFDefaults.fatalErrorDescription)
+ }
+ )
+ )
+ }
.withErrorHandling()
.environmentObject(model)
.environment(\.managedObjectContext, LocalStorageManager.standard.container.viewContext)
// .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info.
}
.commands {
#if os(macOS)
CommandGroup(after: .appInfo) {
CheckForUpdatesView(updaterViewModel: updaterViewModel)
}
#endif
CommandGroup(replacing: .newItem, addition: {
Button("New Post") {
createNewLocalPost()
}
.keyboardShortcut("n", modifiers: [.command])
})
CommandGroup(after: .newItem) {
Button("Refresh Posts") {
DispatchQueue.main.async {
model.fetchUserCollections()
model.fetchUserPosts()
}
}
.disabled(!model.account.isLoggedIn)
.keyboardShortcut("r", modifiers: [.command])
}
SidebarCommands()
#if os(macOS)
PostCommands(model: model)
#endif
CommandGroup(after: .help) {
Button("Visit Support Forum") {
#if os(macOS)
NSWorkspace().open(model.helpURL)
#else
UIApplication.shared.open(model.helpURL)
#endif
}
}
ToolbarCommands()
TextEditingCommands()
}
#if os(macOS)
Settings {
TabView(selection: $selectedTab) {
MacAccountView()
.environmentObject(model)
.tabItem {
Image(systemName: "person.crop.circle")
Text("Account")
}
.tag(0)
MacPreferencesView(preferences: model.preferences)
.tabItem {
Image(systemName: "gear")
Text("Preferences")
}
.tag(1)
MacUpdatesView(updaterViewModel: updaterViewModel)
.tabItem {
Image(systemName: "arrow.down.circle")
Text("Updates")
}
.tag(2)
}
.withErrorHandling()
.frame(minWidth: 500, maxWidth: 500, minHeight: 200)
.padding()
// .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info.
}
#endif
}
private func showLastDraftOrCreateNewLocalPost() {
if model.editor.lastDraftURL != nil {
self.model.selectedPost = model.editor.fetchLastDraftFromAppStorage()
} else {
createNewLocalPost()
}
}
private func createNewLocalPost() {
withAnimation {
// Un-set the currently selected post
self.model.selectedPost = nil
}
// Create the new-post managed object
let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font)
withAnimation {
// Set it as the selectedPost
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.model.selectedPost = managedPost
}
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Jan 31, 2:56 PM (12 h, 52 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3145823

Event Timeline