Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F10455592
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
18 KB
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Jan 31, 2:56 PM (10 h, 14 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3145823
Attached To
rWFSUI WriteFreely SwiftUI
Event Timeline
Log In to Comment