Merge pull request #195 from writefreely/migrate-store-to-group-container

Migrate persistent store to App Group
This commit is contained in:
Angelo Stavrow 2021-10-22 16:23:16 -04:00 committed by GitHub
commit 6693a83bd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 155 additions and 82 deletions

View File

@ -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.container.viewContext.fetch(request)
if editedPosts.count == 1 {
editedPostsWarningString = "You'll lose unpublished changes to \(editedPosts.count) edited post. "
}

View File

@ -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.container.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.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.persistentContainer.viewContext)
let managedPost = WFAPost(context: LocalStorageManager.standard.container.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.container.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.container.viewContext.rollback()
}
print(error)
}

View File

@ -6,19 +6,100 @@ 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)")
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 {
try container.viewContext.save()
} catch {
print("Error saving context: \(error)")
}
}
return container
}()
}
init() {
func purgeUserCollections() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "WFACollection")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try container.viewContext.executeAndMergeChanges(using: deleteRequest)
} catch {
print("Error: Failed to purge cached collections.")
}
}
}
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,
options: nil,
withType: NSSQLiteStoreType)
} catch {
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)")
}
}
func registerObservers() {
let center = NotificationCenter.default
#if os(iOS)
@ -35,30 +116,8 @@ class LocalStorageManager {
center.addObserver(forName: notification, object: nil, queue: nil, using: self.saveContextOnResignActive)
}
func saveContext() {
if LocalStorageManager.persistentContainer.viewContext.hasChanges {
do {
try LocalStorageManager.persistentContainer.viewContext.save()
} catch {
print("Error saving context: \(error)")
}
}
}
func purgeUserCollections() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "WFACollection")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try LocalStorageManager.persistentContainer.viewContext.executeAndMergeChanges(using: deleteRequest)
} catch {
print("Error: Failed to purge cached collections.")
}
}
}
private extension LocalStorageManager {
func saveContextOnResignActive(_ notification: Notification) {
saveContext()
}
}

View File

@ -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.container.viewContext
let model = WriteFreelyModel()
return ContentView()

View File

@ -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.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.persistentContainer.viewContext
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
return CollectionListView()

View File

@ -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.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.persistentContainer.persistentStoreCoordinator
let coordinator = LocalStorageManager.standard.container.persistentStoreCoordinator
guard let managedObjectID = coordinator.managedObjectID(forURIRepresentation: objectURL) else { return nil }
let object = LocalStorageManager.persistentContainer.viewContext.object(with: managedObjectID)
let object = LocalStorageManager.standard.container.viewContext.object(with: managedObjectID)
return object
}
}

View File

@ -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.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.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.persistentContainer.viewContext
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
let deletedPost = WFAPost(context: context)
deletedPost.status = PostStatus.published.rawValue

View File

@ -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.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.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.persistentContainer.viewContext
let context = LocalStorageManager.standard.container.viewContext
let testPost = WFAPost(context: context)
testPost.title = ""
testPost.body = "Here's some cool sample body text."

View File

@ -4,8 +4,8 @@ import CoreData
class PostListModel: ObservableObject {
func remove(_ post: WFAPost) {
withAnimation {
LocalStorageManager.persistentContainer.viewContext.delete(post)
LocalStorageManager().saveContext()
LocalStorageManager.standard.container.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.container.viewContext.executeAndMergeChanges(using: deleteRequest)
} catch {
print("Error: Failed to purge cached posts.")
}

View File

@ -169,7 +169,7 @@ struct PostListView: View {
struct PostListView_Previews: PreviewProvider {
static var previews: some View {
let context = LocalStorageManager.persistentContainer.viewContext
let context = LocalStorageManager.standard.container.viewContext
let model = WriteFreelyModel()
return PostListView(showAllPosts: true)

View File

@ -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.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.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.persistentContainer.viewContext
let context = LocalStorageManager.standard.container.viewContext
let testPost = WFAPost(context: context)
testPost.status = PostStatus.published.rawValue

View File

@ -56,7 +56,7 @@ struct WriteFreely_MultiPlatformApp: App {
// }
})
.environmentObject(model)
.environment(\.managedObjectContext, LocalStorageManager.persistentContainer.viewContext)
.environment(\.managedObjectContext, LocalStorageManager.standard.container.viewContext)
// .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info.
}
.commands {

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.abunchtell.writefreely</string>
</array>
</dict>
</plist>

View File

@ -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 = "<group>"; };
1780F6EE25895EDB00FE45FF /* PostCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCommands.swift; sourceTree = "<group>"; };
17A355D3271A052C007C7A47 /* WriteFreely-MultiPlatform (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "WriteFreely-MultiPlatform (iOS).entitlements"; sourceTree = "<group>"; };
17A4FEDF25924E810037E96B /* MacSoftwareUpdater.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = MacSoftwareUpdater.md; sourceTree = "<group>"; };
17A4FEEC25927E730037E96B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
17A5388724DDA31F00DEFF9A /* MacAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAccountView.swift; sourceTree = "<group>"; };
@ -368,6 +369,7 @@
17DF327B24C87D3300BCE2E3 = {
isa = PBXGroup;
children = (
17A355D3271A052C007C7A47 /* WriteFreely-MultiPlatform (iOS).entitlements */,
17DF32C624C884FF00BCE2E3 /* README.md */,
17DF32C924C8855E00BCE2E3 /* LICENSE.md */,
17DF32CA24C8856C00BCE2E3 /* CHANGELOG.md */,
@ -974,8 +976,9 @@
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;
CURRENT_PROJECT_VERSION = 631;
DEVELOPMENT_TEAM = TPPAB4YBA6;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = iOS/Info.plist;
@ -984,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;
@ -998,8 +1001,9 @@
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;
CURRENT_PROJECT_VERSION = 631;
DEVELOPMENT_TEAM = TPPAB4YBA6;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = iOS/Info.plist;
@ -1008,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;

View File

@ -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.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.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."

View File

@ -129,7 +129,7 @@ struct ActivePostToolbarView: View {
return
}
DispatchQueue.main.async {
LocalStorageManager().saveContext()
LocalStorageManager.standard.saveContext()
model.publish(post: post)
}
}

View File

@ -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."

View File

@ -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