From 11ad3bc2ff84eab9911183f096dd488b1a27b81c Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Fri, 8 Oct 2021 17:07:06 -0400 Subject: [PATCH 1/7] Use LocalStorageManager 'standard' singleton --- Shared/Account/AccountLogoutView.swift | 2 +- .../WriteFreelyModel+APIHandlers.swift | 24 +-- Shared/LocalStorageManager.swift | 29 +-- Shared/Navigation/ContentView.swift | 2 +- .../PostCollection/CollectionListView.swift | 4 +- Shared/PostEditor/PostEditorModel.swift | 6 +- .../PostEditorStatusToolbarView.swift | 6 +- Shared/PostList/PostCellView.swift | 6 +- Shared/PostList/PostListModel.swift | 6 +- Shared/PostList/PostListView.swift | 2 +- Shared/PostList/PostListView.swift.orig | 191 ++++++++++++++++++ Shared/PostList/PostStatusBadgeView.swift | 6 +- Shared/WriteFreely_MultiPlatformApp.swift | 2 +- iOS/PostEditor/PostEditorView.swift | 10 +- macOS/Navigation/ActivePostToolbarView.swift | 2 +- macOS/PostEditor/PostEditorView.swift | 8 +- macOS/PostEditor/PostTextEditingView.swift | 4 +- 17 files changed, 251 insertions(+), 59 deletions(-) create mode 100644 Shared/PostList/PostListView.swift.orig diff --git a/Shared/Account/AccountLogoutView.swift b/Shared/Account/AccountLogoutView.swift index 0ff1c77..66aa1b1 100644 --- a/Shared/Account/AccountLogoutView.swift +++ b/Shared/Account/AccountLogoutView.swift @@ -58,7 +58,7 @@ struct AccountLogoutView: View { let request = WFAPost.createFetchRequest() request.predicate = NSPredicate(format: "status == %i", 1) do { - let editedPosts = try LocalStorageManager.persistentContainer.viewContext.fetch(request) + let editedPosts = try LocalStorageManager.standard.persistentContainer.viewContext.fetch(request) if editedPosts.count == 1 { editedPostsWarningString = "You'll lose unpublished changes to \(editedPosts.count) edited post. " } diff --git a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift index b4d24a6..029e675 100644 --- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift +++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift @@ -55,7 +55,7 @@ extension WriteFreelyModel { client = nil DispatchQueue.main.async { self.account.logout() - LocalStorageManager().purgeUserCollections() + LocalStorageManager.standard.purgeUserCollections() self.posts.purgePublishedPosts() } } catch { @@ -70,7 +70,7 @@ extension WriteFreelyModel { client = nil DispatchQueue.main.async { self.account.logout() - LocalStorageManager().purgeUserCollections() + LocalStorageManager.standard.purgeUserCollections() self.posts.purgePublishedPosts() } } catch { @@ -99,7 +99,7 @@ extension WriteFreelyModel { let fetchedCollections = try result.get() for fetchedCollection in fetchedCollections { DispatchQueue.main.async { - let localCollection = WFACollection(context: LocalStorageManager.persistentContainer.viewContext) + let localCollection = WFACollection(context: LocalStorageManager.standard.persistentContainer.viewContext) localCollection.alias = fetchedCollection.alias localCollection.blogDescription = fetchedCollection.description localCollection.email = fetchedCollection.email @@ -110,7 +110,7 @@ extension WriteFreelyModel { } } DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() } } catch WFError.unauthorized { DispatchQueue.main.async { @@ -130,7 +130,7 @@ extension WriteFreelyModel { } let request = WFAPost.createFetchRequest() do { - let locallyCachedPosts = try LocalStorageManager.persistentContainer.viewContext.fetch(request) + let locallyCachedPosts = try LocalStorageManager.standard.persistentContainer.viewContext.fetch(request) do { var postsToDelete = locallyCachedPosts.filter { $0.status != PostStatus.local.rawValue } let fetchedPosts = try result.get() @@ -146,7 +146,7 @@ extension WriteFreelyModel { } } else { DispatchQueue.main.async { - let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext) + let managedPost = WFAPost(context: LocalStorageManager.standard.persistentContainer.viewContext) managedPost.postId = fetchedPost.postId managedPost.slug = fetchedPost.slug managedPost.appearance = fetchedPost.appearance @@ -164,7 +164,7 @@ extension WriteFreelyModel { } DispatchQueue.main.async { for post in postsToDelete { post.wasDeletedFromServer = true } - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() } } catch { print(error) @@ -204,7 +204,7 @@ extension WriteFreelyModel { updatingPost.title = fetchedPost.title ?? "" updatingPost.updatedDate = fetchedPost.updatedDate DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() } } else { // Otherwise if it's a newly-published post, find it in the local store. @@ -222,7 +222,7 @@ extension WriteFreelyModel { request.predicate = matchBodyPredicate } do { - let cachedPostsResults = try LocalStorageManager.persistentContainer.viewContext.fetch(request) + let cachedPostsResults = try LocalStorageManager.standard.persistentContainer.viewContext.fetch(request) guard let cachedPost = cachedPostsResults.first else { return } cachedPost.appearance = fetchedPost.appearance cachedPost.body = fetchedPost.body @@ -235,7 +235,7 @@ extension WriteFreelyModel { cachedPost.title = fetchedPost.title ?? "" cachedPost.updatedDate = fetchedPost.updatedDate DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() } } catch { print("Error: Failed to fetch cached posts") @@ -270,7 +270,7 @@ extension WriteFreelyModel { cachedPost.updatedDate = fetchedPost.updatedDate cachedPost.hasNewerRemoteCopy = false DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() } } catch { print(error) @@ -293,7 +293,7 @@ extension WriteFreelyModel { } } catch { DispatchQueue.main.async { - LocalStorageManager.persistentContainer.viewContext.rollback() + LocalStorageManager.standard.persistentContainer.viewContext.rollback() } print(error) } diff --git a/Shared/LocalStorageManager.swift b/Shared/LocalStorageManager.swift index aa3e52f..d5ebe42 100644 --- a/Shared/LocalStorageManager.swift +++ b/Shared/LocalStorageManager.swift @@ -6,19 +6,20 @@ import UIKit import AppKit #endif -class LocalStorageManager { - static let persistentContainer: NSPersistentContainer = { - let container = NSPersistentContainer(name: "LocalStorageModel") - container.loadPersistentStores { _, error in - container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - if let error = error { - fatalError("Unresolved error loading persistent store: \(error)") - } - } - return container - }() +final class LocalStorageManager { + public static var standard = LocalStorageManager() + public let persistentContainer: NSPersistentContainer init() { + // Set up the persistent container. + persistentContainer = NSPersistentContainer(name: "LocalStorageModel") + persistentContainer.loadPersistentStores { description, error in + if let error = error { + fatalError("Core Data store failed to load with error: \(error)") + } + } + persistentContainer.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + let center = NotificationCenter.default #if os(iOS) @@ -36,9 +37,9 @@ class LocalStorageManager { } func saveContext() { - if LocalStorageManager.persistentContainer.viewContext.hasChanges { + if persistentContainer.viewContext.hasChanges { do { - try LocalStorageManager.persistentContainer.viewContext.save() + try persistentContainer.viewContext.save() } catch { print("Error saving context: \(error)") } @@ -50,7 +51,7 @@ class LocalStorageManager { let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) do { - try LocalStorageManager.persistentContainer.viewContext.executeAndMergeChanges(using: deleteRequest) + try persistentContainer.viewContext.executeAndMergeChanges(using: deleteRequest) } catch { print("Error: Failed to purge cached collections.") } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 2052aad..1e320f6 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -61,7 +61,7 @@ struct ContentView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let model = WriteFreelyModel() return ContentView() diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift index 4a4583d..bf0595c 100644 --- a/Shared/PostCollection/CollectionListView.swift +++ b/Shared/PostCollection/CollectionListView.swift @@ -2,7 +2,7 @@ import SwiftUI struct CollectionListView: View { @EnvironmentObject var model: WriteFreelyModel - @ObservedObject var collections = CollectionListModel(managedObjectContext: LocalStorageManager.persistentContainer.viewContext) + @ObservedObject var collections = CollectionListModel(managedObjectContext: LocalStorageManager.standard.persistentContainer.viewContext) @State var selectedCollection: WFACollection? var body: some View { @@ -43,7 +43,7 @@ struct CollectionListView: View { struct CollectionListView_LoggedOutPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let model = WriteFreelyModel() return CollectionListView() diff --git a/Shared/PostEditor/PostEditorModel.swift b/Shared/PostEditor/PostEditorModel.swift index 8219ead..9c2d2d3 100644 --- a/Shared/PostEditor/PostEditorModel.swift +++ b/Shared/PostEditor/PostEditorModel.swift @@ -27,7 +27,7 @@ struct PostEditorModel { } func generateNewLocalPost(withFont appearance: Int) -> WFAPost { - let managedPost = WFAPost(context: LocalStorageManager.persistentContainer.viewContext) + let managedPost = WFAPost(context: LocalStorageManager.standard.persistentContainer.viewContext) managedPost.createdDate = Date() managedPost.title = "" managedPost.body = "" @@ -55,9 +55,9 @@ struct PostEditorModel { } private func fetchManagedObject(from objectURL: URL) -> NSManagedObject? { - let coordinator = LocalStorageManager.persistentContainer.persistentStoreCoordinator + let coordinator = LocalStorageManager.standard.persistentContainer.persistentStoreCoordinator guard let managedObjectID = coordinator.managedObjectID(forURIRepresentation: objectURL) else { return nil } - let object = LocalStorageManager.persistentContainer.viewContext.object(with: managedObjectID) + let object = LocalStorageManager.standard.persistentContainer.viewContext.object(with: managedObjectID) return object } } diff --git a/Shared/PostEditor/PostEditorStatusToolbarView.swift b/Shared/PostEditor/PostEditorStatusToolbarView.swift index cc02858..7b2e1bb 100644 --- a/Shared/PostEditor/PostEditorStatusToolbarView.swift +++ b/Shared/PostEditor/PostEditorStatusToolbarView.swift @@ -65,7 +65,7 @@ struct PostEditorStatusToolbarView: View { struct PESTView_StandardPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let model = WriteFreelyModel() let testPost = WFAPost(context: context) testPost.status = PostStatus.published.rawValue @@ -77,7 +77,7 @@ struct PESTView_StandardPreviews: PreviewProvider { struct PESTView_OutdatedLocalCopyPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let model = WriteFreelyModel() let updatedPost = WFAPost(context: context) updatedPost.status = PostStatus.published.rawValue @@ -90,7 +90,7 @@ struct PESTView_OutdatedLocalCopyPreviews: PreviewProvider { struct PESTView_DeletedRemoteCopyPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let model = WriteFreelyModel() let deletedPost = WFAPost(context: context) deletedPost.status = PostStatus.published.rawValue diff --git a/Shared/PostList/PostCellView.swift b/Shared/PostList/PostCellView.swift index 1522141..f7e430a 100644 --- a/Shared/PostList/PostCellView.swift +++ b/Shared/PostList/PostCellView.swift @@ -46,7 +46,7 @@ struct PostCellView: View { struct PostCell_AllPostsPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." @@ -59,7 +59,7 @@ struct PostCell_AllPostsPreviews: PreviewProvider { struct PostCell_NormalPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." @@ -73,7 +73,7 @@ struct PostCell_NormalPreviews: PreviewProvider { struct PostCell_NoTitlePreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.title = "" testPost.body = "Here's some cool sample body text." diff --git a/Shared/PostList/PostListModel.swift b/Shared/PostList/PostListModel.swift index c7ada24..69a0cf8 100644 --- a/Shared/PostList/PostListModel.swift +++ b/Shared/PostList/PostListModel.swift @@ -4,8 +4,8 @@ import CoreData class PostListModel: ObservableObject { func remove(_ post: WFAPost) { withAnimation { - LocalStorageManager.persistentContainer.viewContext.delete(post) - LocalStorageManager().saveContext() + LocalStorageManager.standard.persistentContainer.viewContext.delete(post) + LocalStorageManager.standard.saveContext() } } @@ -15,7 +15,7 @@ class PostListModel: ObservableObject { let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) do { - try LocalStorageManager.persistentContainer.viewContext.executeAndMergeChanges(using: deleteRequest) + try LocalStorageManager.standard.persistentContainer.viewContext.executeAndMergeChanges(using: deleteRequest) } catch { print("Error: Failed to purge cached posts.") } diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index a7ee1a1..b52c99f 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -165,7 +165,7 @@ struct PostListView: View { struct PostListView_Previews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let model = WriteFreelyModel() return PostListView(showAllPosts: true) diff --git a/Shared/PostList/PostListView.swift.orig b/Shared/PostList/PostListView.swift.orig new file mode 100644 index 0000000..f058c26 --- /dev/null +++ b/Shared/PostList/PostListView.swift.orig @@ -0,0 +1,191 @@ +import SwiftUI +import Combine + +struct PostListView: View { + @EnvironmentObject var model: WriteFreelyModel + @Environment(\.managedObjectContext) var managedObjectContext + + @State private var postCount: Int = 0 + @State private var filteredListViewId: Int = 0 + + var selectedCollection: WFACollection? + var showAllPosts: Bool + + #if os(iOS) + private var frameHeight: CGFloat { + var height: CGFloat = 50 + let bottom = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0 + height += bottom + return height + } + #endif + + var body: some View { + #if os(iOS) + ZStack(alignment: .bottom) { + PostListFilteredView( + collection: selectedCollection, + showAllPosts: showAllPosts, + postCount: $postCount + ) +<<<<<<< HEAD + .navigationTitle( + showAllPosts ? "All Posts" : selectedCollection?.title ?? ( + model.account.server == "https://write.as" ? "Anonymous" : "Drafts" + ) +======= + .id(self.filteredListViewId) + .navigationTitle( + model.showAllPosts ? "All Posts" : model.selectedCollection?.title ?? ( + model.account.server == "https://write.as" ? "Anonymous" : "Drafts" +>>>>>>> c9322d1 (Invalidate PostListView on didBecomeActive) + ) + ) + .toolbar { + ToolbarItem(placement: .primaryAction) { + // We have to add a Spacer as a sibling view to the Button in some kind of Stack, so that any + // a11y modifiers are applied as expected: bug report filed as FB8956392. + ZStack { + Spacer() + Button(action: { + let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font) + withAnimation { + self.model.showAllPosts = false + self.model.selectedPost = managedPost + } + }, label: { + ZStack { + Image("does.not.exist") + .accessibilityHidden(true) + Image(systemName: "square.and.pencil") + .accessibilityHidden(true) + .imageScale(.large) // These modifiers compensate for the resizing + .padding(.vertical, 12) // done to the Image (and the button tap target) + .padding(.leading, 12) // by the SwiftUI layout system from adding a + .padding(.trailing, 8) // Spacer in this ZStack (FB8956392). + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + }) + .accessibilityLabel(Text("Compose")) + .accessibilityHint(Text("Compose a new local draft")) + } + } + } + VStack { + HStack(spacing: 0) { + Button(action: { + model.isPresentingSettingsView = true + }, label: { + Image(systemName: "gear") + .padding(.vertical, 4) + .padding(.horizontal, 8) + }) + .accessibilityLabel(Text("Settings")) + .accessibilityHint(Text("Open the Settings sheet")) + .sheet( + isPresented: $model.isPresentingSettingsView, + onDismiss: { model.isPresentingSettingsView = false }, + content: { + SettingsView() + .environmentObject(model) + } + ) + Spacer() + Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts") + .foregroundColor(.secondary) + .alert(isPresented: $model.isPresentingNetworkErrorAlert, content: { + Alert( + title: Text("Connection Error"), + message: Text(""" + There is no internet connection at the moment. Please reconnect or try again later. + """), + dismissButton: .default(Text("OK"), action: { + model.isPresentingNetworkErrorAlert = false + }) + ) + }) + Spacer() + if model.isProcessingRequest { + ProgressView() + .padding(.vertical, 4) + .padding(.horizontal, 8) + } else { + Button(action: { + DispatchQueue.main.async { + model.fetchUserCollections() + model.fetchUserPosts() + } + }, label: { + Image(systemName: "arrow.clockwise") + .padding(.vertical, 4) + .padding(.horizontal, 8) + }) + .accessibilityLabel(Text("Refresh Posts")) + .accessibilityHint(Text("Fetch changes from the server")) + .disabled(!model.account.isLoggedIn) + } + } + .padding(.top, 8) + .padding(.horizontal, 8) + Spacer() + } + .frame(height: frameHeight) + .background(Color(UIColor.systemGray5)) + .overlay(Divider(), alignment: .top) + } + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in + // We use this to invalidate and refresh the view, to make sure we show any new posts that were created + // in an extension, for example. + withAnimation { + self.filteredListViewId += 1 + } + } + .ignoresSafeArea() + .onAppear { + model.selectedCollection = selectedCollection + model.showAllPosts = showAllPosts + } + #else + PostListFilteredView( + collection: selectedCollection, + showAllPosts: showAllPosts, + postCount: $postCount + ) + .toolbar { + ToolbarItemGroup(placement: .primaryAction) { + if model.selectedPost != nil { + ActivePostToolbarView(activePost: model.selectedPost!) + .alert(isPresented: $model.isPresentingNetworkErrorAlert, content: { + Alert( + title: Text("Connection Error"), + message: Text(""" + There is no internet connection at the moment. \ + Please reconnect or try again later. + """), + dismissButton: .default(Text("OK"), action: { + model.isPresentingNetworkErrorAlert = false + }) + ) + }) + } + } + } + .navigationTitle( + showAllPosts ? "All Posts" : selectedCollection?.title ?? ( + model.account.server == "https://write.as" ? "Anonymous" : "Drafts" + ) + ) + #endif + } +} + +struct PostListView_Previews: PreviewProvider { + static var previews: some View { + let context = LocalStorageManager.persistentContainer.viewContext + let model = WriteFreelyModel() + + return PostListView(showAllPosts: true) + .environment(\.managedObjectContext, context) + .environmentObject(model) + } +} diff --git a/Shared/PostList/PostStatusBadgeView.swift b/Shared/PostList/PostStatusBadgeView.swift index fa691b4..03e367b 100644 --- a/Shared/PostList/PostStatusBadgeView.swift +++ b/Shared/PostList/PostStatusBadgeView.swift @@ -38,7 +38,7 @@ struct PostStatusBadgeView: View { struct PostStatusBadge_LocalDraftPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.status = PostStatus.local.rawValue @@ -49,7 +49,7 @@ struct PostStatusBadge_LocalDraftPreviews: PreviewProvider { struct PostStatusBadge_EditedPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.status = PostStatus.edited.rawValue @@ -60,7 +60,7 @@ struct PostStatusBadge_EditedPreviews: PreviewProvider { struct PostStatusBadge_PublishedPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.status = PostStatus.published.rawValue diff --git a/Shared/WriteFreely_MultiPlatformApp.swift b/Shared/WriteFreely_MultiPlatformApp.swift index 53bd758..bd51ae3 100644 --- a/Shared/WriteFreely_MultiPlatformApp.swift +++ b/Shared/WriteFreely_MultiPlatformApp.swift @@ -56,7 +56,7 @@ struct WriteFreely_MultiPlatformApp: App { // } }) .environmentObject(model) - .environment(\.managedObjectContext, LocalStorageManager.persistentContainer.viewContext) + .environment(\.managedObjectContext, LocalStorageManager.standard.persistentContainer.viewContext) // .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info. } .commands { diff --git a/iOS/PostEditor/PostEditorView.swift b/iOS/PostEditor/PostEditorView.swift index 92c4e06..a2a0194 100644 --- a/iOS/PostEditor/PostEditorView.swift +++ b/iOS/PostEditor/PostEditorView.swift @@ -158,7 +158,7 @@ struct PostEditorView: View { self.model.editor.clearLastDraft() } DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() } }) .onAppear(perform: { @@ -183,7 +183,7 @@ struct PostEditorView: View { } } else if post.status != PostStatus.published.rawValue { DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() } } }) @@ -191,7 +191,7 @@ struct PostEditorView: View { private func publishPost() { DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() model.publish(post: post) } #if os(iOS) @@ -236,7 +236,7 @@ struct PostEditorView: View { struct PostEditorView_EmptyPostPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.createdDate = Date() testPost.appearance = "norm" @@ -251,7 +251,7 @@ struct PostEditorView_EmptyPostPreviews: PreviewProvider { struct PostEditorView_ExistingPostPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." diff --git a/macOS/Navigation/ActivePostToolbarView.swift b/macOS/Navigation/ActivePostToolbarView.swift index 1319400..15955f2 100644 --- a/macOS/Navigation/ActivePostToolbarView.swift +++ b/macOS/Navigation/ActivePostToolbarView.swift @@ -129,7 +129,7 @@ struct ActivePostToolbarView: View { return } DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() model.publish(post: post) } } diff --git a/macOS/PostEditor/PostEditorView.swift b/macOS/PostEditor/PostEditorView.swift index 4b7ab68..a20bd41 100644 --- a/macOS/PostEditor/PostEditorView.swift +++ b/macOS/PostEditor/PostEditorView.swift @@ -35,7 +35,7 @@ struct PostEditorView: View { self.model.editor.clearLastDraft() } DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() } }) .onDisappear(perform: { @@ -52,7 +52,7 @@ struct PostEditorView: View { } } else if post.status != PostStatus.published.rawValue { DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() } } }) @@ -61,7 +61,7 @@ struct PostEditorView: View { struct PostEditorView_EmptyPostPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.createdDate = Date() testPost.appearance = "norm" @@ -76,7 +76,7 @@ struct PostEditorView_EmptyPostPreviews: PreviewProvider { struct PostEditorView_ExistingPostPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext + let context = LocalStorageManager.standard.persistentContainer.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." diff --git a/macOS/PostEditor/PostTextEditingView.swift b/macOS/PostEditor/PostTextEditingView.swift index fabae24..5e60002 100644 --- a/macOS/PostEditor/PostTextEditingView.swift +++ b/macOS/PostEditor/PostTextEditingView.swift @@ -60,7 +60,7 @@ struct PostTextEditingView: View { .onReceive(timer) { _ in if !post.body.isEmpty && hasBeenEdited { DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() hasBeenEdited = false } } @@ -87,7 +87,7 @@ struct PostTextEditingView: View { private func onCommit() { if !post.body.isEmpty && hasBeenEdited { DispatchQueue.main.async { - LocalStorageManager().saveContext() + LocalStorageManager.standard.saveContext() } } hasBeenEdited = false From 2f1b895df505d892b0c0065feb5568fa1bef5f64 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Fri, 8 Oct 2021 17:15:38 -0400 Subject: [PATCH 2/7] Rename 'persistentContainer' to 'container' --- Shared/Account/AccountLogoutView.swift | 2 +- .../Extensions/WriteFreelyModel+APIHandlers.swift | 10 +++++----- Shared/LocalStorageManager.swift | 14 +++++++------- Shared/Navigation/ContentView.swift | 2 +- Shared/PostCollection/CollectionListView.swift | 4 ++-- Shared/PostEditor/PostEditorModel.swift | 6 +++--- .../PostEditor/PostEditorStatusToolbarView.swift | 6 +++--- Shared/PostList/PostCellView.swift | 6 +++--- Shared/PostList/PostListModel.swift | 4 ++-- Shared/PostList/PostListView.swift | 2 +- Shared/PostList/PostStatusBadgeView.swift | 6 +++--- Shared/WriteFreely_MultiPlatformApp.swift | 2 +- iOS/PostEditor/PostEditorView.swift | 4 ++-- 13 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Shared/Account/AccountLogoutView.swift b/Shared/Account/AccountLogoutView.swift index 66aa1b1..22df2da 100644 --- a/Shared/Account/AccountLogoutView.swift +++ b/Shared/Account/AccountLogoutView.swift @@ -58,7 +58,7 @@ struct AccountLogoutView: View { let request = WFAPost.createFetchRequest() request.predicate = NSPredicate(format: "status == %i", 1) do { - let editedPosts = try LocalStorageManager.standard.persistentContainer.viewContext.fetch(request) + let editedPosts = try LocalStorageManager.standard.container.viewContext.fetch(request) if editedPosts.count == 1 { editedPostsWarningString = "You'll lose unpublished changes to \(editedPosts.count) edited post. " } diff --git a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift index 029e675..9a7447e 100644 --- a/Shared/Extensions/WriteFreelyModel+APIHandlers.swift +++ b/Shared/Extensions/WriteFreelyModel+APIHandlers.swift @@ -99,7 +99,7 @@ extension WriteFreelyModel { let fetchedCollections = try result.get() for fetchedCollection in fetchedCollections { DispatchQueue.main.async { - let localCollection = WFACollection(context: LocalStorageManager.standard.persistentContainer.viewContext) + let localCollection = WFACollection(context: LocalStorageManager.standard.container.viewContext) localCollection.alias = fetchedCollection.alias localCollection.blogDescription = fetchedCollection.description localCollection.email = fetchedCollection.email @@ -130,7 +130,7 @@ extension WriteFreelyModel { } let request = WFAPost.createFetchRequest() do { - let locallyCachedPosts = try LocalStorageManager.standard.persistentContainer.viewContext.fetch(request) + let locallyCachedPosts = try LocalStorageManager.standard.container.viewContext.fetch(request) do { var postsToDelete = locallyCachedPosts.filter { $0.status != PostStatus.local.rawValue } let fetchedPosts = try result.get() @@ -146,7 +146,7 @@ extension WriteFreelyModel { } } else { DispatchQueue.main.async { - let managedPost = WFAPost(context: LocalStorageManager.standard.persistentContainer.viewContext) + let managedPost = WFAPost(context: LocalStorageManager.standard.container.viewContext) managedPost.postId = fetchedPost.postId managedPost.slug = fetchedPost.slug managedPost.appearance = fetchedPost.appearance @@ -222,7 +222,7 @@ extension WriteFreelyModel { request.predicate = matchBodyPredicate } do { - let cachedPostsResults = try LocalStorageManager.standard.persistentContainer.viewContext.fetch(request) + let cachedPostsResults = try LocalStorageManager.standard.container.viewContext.fetch(request) guard let cachedPost = cachedPostsResults.first else { return } cachedPost.appearance = fetchedPost.appearance cachedPost.body = fetchedPost.body @@ -293,7 +293,7 @@ extension WriteFreelyModel { } } catch { DispatchQueue.main.async { - LocalStorageManager.standard.persistentContainer.viewContext.rollback() + LocalStorageManager.standard.container.viewContext.rollback() } print(error) } diff --git a/Shared/LocalStorageManager.swift b/Shared/LocalStorageManager.swift index d5ebe42..739ae8d 100644 --- a/Shared/LocalStorageManager.swift +++ b/Shared/LocalStorageManager.swift @@ -8,17 +8,17 @@ import AppKit final class LocalStorageManager { public static var standard = LocalStorageManager() - public let persistentContainer: NSPersistentContainer + public let container: NSPersistentContainer init() { // Set up the persistent container. - persistentContainer = NSPersistentContainer(name: "LocalStorageModel") - persistentContainer.loadPersistentStores { description, error in + container = NSPersistentContainer(name: "LocalStorageModel") + container.loadPersistentStores { description, error in if let error = error { fatalError("Core Data store failed to load with error: \(error)") } } - persistentContainer.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy let center = NotificationCenter.default @@ -37,9 +37,9 @@ final class LocalStorageManager { } func saveContext() { - if persistentContainer.viewContext.hasChanges { + if container.viewContext.hasChanges { do { - try persistentContainer.viewContext.save() + try container.viewContext.save() } catch { print("Error saving context: \(error)") } @@ -51,7 +51,7 @@ final class LocalStorageManager { let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) do { - try persistentContainer.viewContext.executeAndMergeChanges(using: deleteRequest) + try container.viewContext.executeAndMergeChanges(using: deleteRequest) } catch { print("Error: Failed to purge cached collections.") } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 1e320f6..7fe7566 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -61,7 +61,7 @@ struct ContentView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let model = WriteFreelyModel() return ContentView() diff --git a/Shared/PostCollection/CollectionListView.swift b/Shared/PostCollection/CollectionListView.swift index bf0595c..da5dcda 100644 --- a/Shared/PostCollection/CollectionListView.swift +++ b/Shared/PostCollection/CollectionListView.swift @@ -2,7 +2,7 @@ import SwiftUI struct CollectionListView: View { @EnvironmentObject var model: WriteFreelyModel - @ObservedObject var collections = CollectionListModel(managedObjectContext: LocalStorageManager.standard.persistentContainer.viewContext) + @ObservedObject var collections = CollectionListModel(managedObjectContext: LocalStorageManager.standard.container.viewContext) @State var selectedCollection: WFACollection? var body: some View { @@ -43,7 +43,7 @@ struct CollectionListView: View { struct CollectionListView_LoggedOutPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let model = WriteFreelyModel() return CollectionListView() diff --git a/Shared/PostEditor/PostEditorModel.swift b/Shared/PostEditor/PostEditorModel.swift index 9c2d2d3..6970949 100644 --- a/Shared/PostEditor/PostEditorModel.swift +++ b/Shared/PostEditor/PostEditorModel.swift @@ -27,7 +27,7 @@ struct PostEditorModel { } func generateNewLocalPost(withFont appearance: Int) -> WFAPost { - let managedPost = WFAPost(context: LocalStorageManager.standard.persistentContainer.viewContext) + let managedPost = WFAPost(context: LocalStorageManager.standard.container.viewContext) managedPost.createdDate = Date() managedPost.title = "" managedPost.body = "" @@ -55,9 +55,9 @@ struct PostEditorModel { } private func fetchManagedObject(from objectURL: URL) -> NSManagedObject? { - let coordinator = LocalStorageManager.standard.persistentContainer.persistentStoreCoordinator + let coordinator = LocalStorageManager.standard.container.persistentStoreCoordinator guard let managedObjectID = coordinator.managedObjectID(forURIRepresentation: objectURL) else { return nil } - let object = LocalStorageManager.standard.persistentContainer.viewContext.object(with: managedObjectID) + let object = LocalStorageManager.standard.container.viewContext.object(with: managedObjectID) return object } } diff --git a/Shared/PostEditor/PostEditorStatusToolbarView.swift b/Shared/PostEditor/PostEditorStatusToolbarView.swift index 7b2e1bb..be49544 100644 --- a/Shared/PostEditor/PostEditorStatusToolbarView.swift +++ b/Shared/PostEditor/PostEditorStatusToolbarView.swift @@ -65,7 +65,7 @@ struct PostEditorStatusToolbarView: View { struct PESTView_StandardPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let model = WriteFreelyModel() let testPost = WFAPost(context: context) testPost.status = PostStatus.published.rawValue @@ -77,7 +77,7 @@ struct PESTView_StandardPreviews: PreviewProvider { struct PESTView_OutdatedLocalCopyPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let model = WriteFreelyModel() let updatedPost = WFAPost(context: context) updatedPost.status = PostStatus.published.rawValue @@ -90,7 +90,7 @@ struct PESTView_OutdatedLocalCopyPreviews: PreviewProvider { struct PESTView_DeletedRemoteCopyPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let model = WriteFreelyModel() let deletedPost = WFAPost(context: context) deletedPost.status = PostStatus.published.rawValue diff --git a/Shared/PostList/PostCellView.swift b/Shared/PostList/PostCellView.swift index f7e430a..6787b00 100644 --- a/Shared/PostList/PostCellView.swift +++ b/Shared/PostList/PostCellView.swift @@ -46,7 +46,7 @@ struct PostCellView: View { struct PostCell_AllPostsPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." @@ -59,7 +59,7 @@ struct PostCell_AllPostsPreviews: PreviewProvider { struct PostCell_NormalPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." @@ -73,7 +73,7 @@ struct PostCell_NormalPreviews: PreviewProvider { struct PostCell_NoTitlePreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.title = "" testPost.body = "Here's some cool sample body text." diff --git a/Shared/PostList/PostListModel.swift b/Shared/PostList/PostListModel.swift index 69a0cf8..db0ff4a 100644 --- a/Shared/PostList/PostListModel.swift +++ b/Shared/PostList/PostListModel.swift @@ -4,7 +4,7 @@ import CoreData class PostListModel: ObservableObject { func remove(_ post: WFAPost) { withAnimation { - LocalStorageManager.standard.persistentContainer.viewContext.delete(post) + LocalStorageManager.standard.container.viewContext.delete(post) LocalStorageManager.standard.saveContext() } } @@ -15,7 +15,7 @@ class PostListModel: ObservableObject { let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) do { - try LocalStorageManager.standard.persistentContainer.viewContext.executeAndMergeChanges(using: deleteRequest) + try LocalStorageManager.standard.container.viewContext.executeAndMergeChanges(using: deleteRequest) } catch { print("Error: Failed to purge cached posts.") } diff --git a/Shared/PostList/PostListView.swift b/Shared/PostList/PostListView.swift index b52c99f..6e0f898 100644 --- a/Shared/PostList/PostListView.swift +++ b/Shared/PostList/PostListView.swift @@ -165,7 +165,7 @@ struct PostListView: View { struct PostListView_Previews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let model = WriteFreelyModel() return PostListView(showAllPosts: true) diff --git a/Shared/PostList/PostStatusBadgeView.swift b/Shared/PostList/PostStatusBadgeView.swift index 03e367b..07696c2 100644 --- a/Shared/PostList/PostStatusBadgeView.swift +++ b/Shared/PostList/PostStatusBadgeView.swift @@ -38,7 +38,7 @@ struct PostStatusBadgeView: View { struct PostStatusBadge_LocalDraftPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.status = PostStatus.local.rawValue @@ -49,7 +49,7 @@ struct PostStatusBadge_LocalDraftPreviews: PreviewProvider { struct PostStatusBadge_EditedPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.status = PostStatus.edited.rawValue @@ -60,7 +60,7 @@ struct PostStatusBadge_EditedPreviews: PreviewProvider { struct PostStatusBadge_PublishedPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.status = PostStatus.published.rawValue diff --git a/Shared/WriteFreely_MultiPlatformApp.swift b/Shared/WriteFreely_MultiPlatformApp.swift index bd51ae3..3ba051d 100644 --- a/Shared/WriteFreely_MultiPlatformApp.swift +++ b/Shared/WriteFreely_MultiPlatformApp.swift @@ -56,7 +56,7 @@ struct WriteFreely_MultiPlatformApp: App { // } }) .environmentObject(model) - .environment(\.managedObjectContext, LocalStorageManager.standard.persistentContainer.viewContext) + .environment(\.managedObjectContext, LocalStorageManager.standard.container.viewContext) // .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info. } .commands { diff --git a/iOS/PostEditor/PostEditorView.swift b/iOS/PostEditor/PostEditorView.swift index a2a0194..7a78ad6 100644 --- a/iOS/PostEditor/PostEditorView.swift +++ b/iOS/PostEditor/PostEditorView.swift @@ -236,7 +236,7 @@ struct PostEditorView: View { struct PostEditorView_EmptyPostPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.createdDate = Date() testPost.appearance = "norm" @@ -251,7 +251,7 @@ struct PostEditorView_EmptyPostPreviews: PreviewProvider { struct PostEditorView_ExistingPostPreviews: PreviewProvider { static var previews: some View { - let context = LocalStorageManager.standard.persistentContainer.viewContext + let context = LocalStorageManager.standard.container.viewContext let testPost = WFAPost(context: context) testPost.title = "Test Post Title" testPost.body = "Here's some cool sample body text." From f55ae3c621305eb47f69d591ceb2c99568a24554 Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Fri, 8 Oct 2021 17:16:01 -0400 Subject: [PATCH 3/7] Implement initial store-migration functionality --- Shared/LocalStorageManager.swift | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Shared/LocalStorageManager.swift b/Shared/LocalStorageManager.swift index 739ae8d..b938fdf 100644 --- a/Shared/LocalStorageManager.swift +++ b/Shared/LocalStorageManager.swift @@ -10,6 +10,17 @@ final class LocalStorageManager { public static var standard = LocalStorageManager() public let container: NSPersistentContainer + private var oldStoreURL: URL { + let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + return appSupport.appendingPathComponent("LocalStorageModel.sqlite") + } + + private var sharedStoreURL: URL { + let id = "group.com.abunchtell.writefreely" + let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: id)! + return groupContainer.appendingPathComponent("LocalStorageModel.sqlite") + } + init() { // Set up the persistent container. container = NSPersistentContainer(name: "LocalStorageModel") @@ -56,6 +67,29 @@ final class LocalStorageManager { print("Error: Failed to purge cached collections.") } } + + func migrateStore(for container: NSPersistentContainer) { + let coordinator = container.persistentStoreCoordinator + + guard let oldStore = coordinator.persistentStore(for: oldStoreURL) else { + return + } + + do { + try coordinator.migratePersistentStore(oldStore, + to: sharedStoreURL, + options: nil, + withType: NSSQLiteStoreType) + } catch { + fatalError("Something went wrong migrating the store: \(error)") + } + + do { + try FileManager.default.removeItem(at: oldStoreURL) + } catch { + fatalError("Something went wrong while deleting the old store: \(error)") + } + } } private extension LocalStorageManager { From 8ba016d91795c7e8bd3ee8ee76c2f6d883c155cc Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Fri, 15 Oct 2021 15:05:51 -0400 Subject: [PATCH 4/7] Add App Group entitlement --- WriteFreely-MultiPlatform (iOS).entitlements | 10 ++++++++++ WriteFreely-MultiPlatform.xcodeproj/project.pbxproj | 4 ++++ 2 files changed, 14 insertions(+) create mode 100644 WriteFreely-MultiPlatform (iOS).entitlements diff --git a/WriteFreely-MultiPlatform (iOS).entitlements b/WriteFreely-MultiPlatform (iOS).entitlements new file mode 100644 index 0000000..a592bed --- /dev/null +++ b/WriteFreely-MultiPlatform (iOS).entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.abunchtell.writefreely + + + diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index 4a9380d..e7c7d74 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 1756DC0024FEE18400207AB8 /* WFACollection+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WFACollection+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; }; 17681E402519410E00D394AE /* UINavigationController+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Appearance.swift"; sourceTree = ""; }; 1780F6EE25895EDB00FE45FF /* PostCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCommands.swift; sourceTree = ""; }; + 17A355D3271A052C007C7A47 /* WriteFreely-MultiPlatform (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "WriteFreely-MultiPlatform (iOS).entitlements"; sourceTree = ""; }; 17A4FEDF25924E810037E96B /* MacSoftwareUpdater.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = MacSoftwareUpdater.md; sourceTree = ""; }; 17A4FEEC25927E730037E96B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 17A5388724DDA31F00DEFF9A /* MacAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAccountView.swift; sourceTree = ""; }; @@ -368,6 +369,7 @@ 17DF327B24C87D3300BCE2E3 = { isa = PBXGroup; children = ( + 17A355D3271A052C007C7A47 /* WriteFreely-MultiPlatform (iOS).entitlements */, 17DF32C624C884FF00BCE2E3 /* README.md */, 17DF32C924C8855E00BCE2E3 /* LICENSE.md */, 17DF32CA24C8856C00BCE2E3 /* CHANGELOG.md */, @@ -974,6 +976,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 625; DEVELOPMENT_TEAM = TPPAB4YBA6; @@ -998,6 +1001,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 625; DEVELOPMENT_TEAM = TPPAB4YBA6; From 8a3a835d445c7aa903c77161259791d8ac7d057f Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Fri, 15 Oct 2021 15:06:35 -0400 Subject: [PATCH 5/7] Refactor LocalStorageManager and add migration of persistent store to App Group --- Shared/LocalStorageManager.swift | 98 ++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/Shared/LocalStorageManager.swift b/Shared/LocalStorageManager.swift index b938fdf..63bdd88 100644 --- a/Shared/LocalStorageManager.swift +++ b/Shared/LocalStorageManager.swift @@ -7,44 +7,15 @@ import AppKit #endif final class LocalStorageManager { + public static var standard = LocalStorageManager() public let container: NSPersistentContainer + private let containerName = "LocalStorageModel" - private var oldStoreURL: URL { - let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - return appSupport.appendingPathComponent("LocalStorageModel.sqlite") - } - - private var sharedStoreURL: URL { - let id = "group.com.abunchtell.writefreely" - let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: id)! - return groupContainer.appendingPathComponent("LocalStorageModel.sqlite") - } - - init() { - // Set up the persistent container. - container = NSPersistentContainer(name: "LocalStorageModel") - container.loadPersistentStores { description, error in - if let error = error { - fatalError("Core Data store failed to load with error: \(error)") - } - } - container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - - 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) + private init() { + container = NSPersistentContainer(name: containerName) + setupStore(in: container) + registerObservers() } func saveContext() { @@ -68,13 +39,49 @@ final class LocalStorageManager { } } +} + +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 { description, error in + if let error = error { + fatalError("Core Data store failed to load with error: \(error)") + } + } + 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 { try coordinator.migratePersistentStore(oldStore, to: sharedStoreURL, @@ -84,16 +91,33 @@ final class LocalStorageManager { fatalError("Something went wrong migrating the store: \(error)") } + // Attempt to delete the old store. do { try FileManager.default.removeItem(at: oldStoreURL) } catch { fatalError("Something went wrong while deleting the old store: \(error)") } } -} -private extension LocalStorageManager { + 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() } + } From d2ed73ca517b73a964a7ec0b1c7dca8de57e76ec Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Fri, 15 Oct 2021 15:08:08 -0400 Subject: [PATCH 6/7] Bump build and version number for internal TestFlight release --- WriteFreely-MultiPlatform.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj index e7c7d74..6e32322 100644 --- a/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj +++ b/WriteFreely-MultiPlatform.xcodeproj/project.pbxproj @@ -978,7 +978,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 625; + CURRENT_PROJECT_VERSION = 631; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; @@ -987,7 +987,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.7; + MARKETING_VERSION = 1.0.8; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = iphoneos; @@ -1003,7 +1003,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 625; + CURRENT_PROJECT_VERSION = 631; DEVELOPMENT_TEAM = TPPAB4YBA6; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = iOS/Info.plist; @@ -1012,7 +1012,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.7; + MARKETING_VERSION = 1.0.8; PRODUCT_BUNDLE_IDENTIFIER = "com.abunchtell.WriteFreely-MultiPlatform"; PRODUCT_NAME = "WriteFreely-MultiPlatform"; SDKROOT = iphoneos; From c0e0b6184f910303c0ec05ba90ea26c7f383c27e Mon Sep 17 00:00:00 2001 From: Angelo Stavrow Date: Fri, 22 Oct 2021 16:23:01 -0400 Subject: [PATCH 7/7] Remove diff tool backup file --- Shared/PostList/PostListView.swift.orig | 191 ------------------------ 1 file changed, 191 deletions(-) delete mode 100644 Shared/PostList/PostListView.swift.orig diff --git a/Shared/PostList/PostListView.swift.orig b/Shared/PostList/PostListView.swift.orig deleted file mode 100644 index f058c26..0000000 --- a/Shared/PostList/PostListView.swift.orig +++ /dev/null @@ -1,191 +0,0 @@ -import SwiftUI -import Combine - -struct PostListView: View { - @EnvironmentObject var model: WriteFreelyModel - @Environment(\.managedObjectContext) var managedObjectContext - - @State private var postCount: Int = 0 - @State private var filteredListViewId: Int = 0 - - var selectedCollection: WFACollection? - var showAllPosts: Bool - - #if os(iOS) - private var frameHeight: CGFloat { - var height: CGFloat = 50 - let bottom = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0 - height += bottom - return height - } - #endif - - var body: some View { - #if os(iOS) - ZStack(alignment: .bottom) { - PostListFilteredView( - collection: selectedCollection, - showAllPosts: showAllPosts, - postCount: $postCount - ) -<<<<<<< HEAD - .navigationTitle( - showAllPosts ? "All Posts" : selectedCollection?.title ?? ( - model.account.server == "https://write.as" ? "Anonymous" : "Drafts" - ) -======= - .id(self.filteredListViewId) - .navigationTitle( - model.showAllPosts ? "All Posts" : model.selectedCollection?.title ?? ( - model.account.server == "https://write.as" ? "Anonymous" : "Drafts" ->>>>>>> c9322d1 (Invalidate PostListView on didBecomeActive) - ) - ) - .toolbar { - ToolbarItem(placement: .primaryAction) { - // We have to add a Spacer as a sibling view to the Button in some kind of Stack, so that any - // a11y modifiers are applied as expected: bug report filed as FB8956392. - ZStack { - Spacer() - Button(action: { - let managedPost = model.editor.generateNewLocalPost(withFont: model.preferences.font) - withAnimation { - self.model.showAllPosts = false - self.model.selectedPost = managedPost - } - }, label: { - ZStack { - Image("does.not.exist") - .accessibilityHidden(true) - Image(systemName: "square.and.pencil") - .accessibilityHidden(true) - .imageScale(.large) // These modifiers compensate for the resizing - .padding(.vertical, 12) // done to the Image (and the button tap target) - .padding(.leading, 12) // by the SwiftUI layout system from adding a - .padding(.trailing, 8) // Spacer in this ZStack (FB8956392). - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - }) - .accessibilityLabel(Text("Compose")) - .accessibilityHint(Text("Compose a new local draft")) - } - } - } - VStack { - HStack(spacing: 0) { - Button(action: { - model.isPresentingSettingsView = true - }, label: { - Image(systemName: "gear") - .padding(.vertical, 4) - .padding(.horizontal, 8) - }) - .accessibilityLabel(Text("Settings")) - .accessibilityHint(Text("Open the Settings sheet")) - .sheet( - isPresented: $model.isPresentingSettingsView, - onDismiss: { model.isPresentingSettingsView = false }, - content: { - SettingsView() - .environmentObject(model) - } - ) - Spacer() - Text(postCount == 1 ? "\(postCount) post" : "\(postCount) posts") - .foregroundColor(.secondary) - .alert(isPresented: $model.isPresentingNetworkErrorAlert, content: { - Alert( - title: Text("Connection Error"), - message: Text(""" - There is no internet connection at the moment. Please reconnect or try again later. - """), - dismissButton: .default(Text("OK"), action: { - model.isPresentingNetworkErrorAlert = false - }) - ) - }) - Spacer() - if model.isProcessingRequest { - ProgressView() - .padding(.vertical, 4) - .padding(.horizontal, 8) - } else { - Button(action: { - DispatchQueue.main.async { - model.fetchUserCollections() - model.fetchUserPosts() - } - }, label: { - Image(systemName: "arrow.clockwise") - .padding(.vertical, 4) - .padding(.horizontal, 8) - }) - .accessibilityLabel(Text("Refresh Posts")) - .accessibilityHint(Text("Fetch changes from the server")) - .disabled(!model.account.isLoggedIn) - } - } - .padding(.top, 8) - .padding(.horizontal, 8) - Spacer() - } - .frame(height: frameHeight) - .background(Color(UIColor.systemGray5)) - .overlay(Divider(), alignment: .top) - } - .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in - // We use this to invalidate and refresh the view, to make sure we show any new posts that were created - // in an extension, for example. - withAnimation { - self.filteredListViewId += 1 - } - } - .ignoresSafeArea() - .onAppear { - model.selectedCollection = selectedCollection - model.showAllPosts = showAllPosts - } - #else - PostListFilteredView( - collection: selectedCollection, - showAllPosts: showAllPosts, - postCount: $postCount - ) - .toolbar { - ToolbarItemGroup(placement: .primaryAction) { - if model.selectedPost != nil { - ActivePostToolbarView(activePost: model.selectedPost!) - .alert(isPresented: $model.isPresentingNetworkErrorAlert, content: { - Alert( - title: Text("Connection Error"), - message: Text(""" - There is no internet connection at the moment. \ - Please reconnect or try again later. - """), - dismissButton: .default(Text("OK"), action: { - model.isPresentingNetworkErrorAlert = false - }) - ) - }) - } - } - } - .navigationTitle( - showAllPosts ? "All Posts" : selectedCollection?.title ?? ( - model.account.server == "https://write.as" ? "Anonymous" : "Drafts" - ) - ) - #endif - } -} - -struct PostListView_Previews: PreviewProvider { - static var previews: some View { - let context = LocalStorageManager.persistentContainer.viewContext - let model = WriteFreelyModel() - - return PostListView(showAllPosts: true) - .environment(\.managedObjectContext, context) - .environmentObject(model) - } -}