ios: core data thread safety crashes fix

This commit is contained in:
mamadnazar 2025-03-02 17:38:28 +05:00
parent f25019b8f8
commit 3d9ebab8c2
12 changed files with 435 additions and 350 deletions

View file

@ -301,7 +301,6 @@
527D5E7F2C60E69C00736A85 /* Layouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527D5E7E2C60E69C00736A85 /* Layouting.swift */; };
527D5E822C60EFEE00736A85 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527D5E812C60EFEE00736A85 /* UIViewExtensions.swift */; };
528D72A12C5BBBF700D53210 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 528D72A02C5BBBF700D53210 /* Colors.xcassets */; };
529A5F162C8595BB004FE4A1 /* PersonalData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F112C859535004FE4A1 /* PersonalData.xcdatamodeld */; };
529A5F192C85BFF0004FE4A1 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F182C85BFF0004FE4A1 /* ToastView.swift */; };
529A5F1E2C86DDE5004FE4A1 /* PlaceDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F1D2C86DDE5004FE4A1 /* PlaceDTO.swift */; };
529A5F202C86DE14004FE4A1 /* CoordinatesDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F1F2C86DE14004FE4A1 /* CoordinatesDTO.swift */; };
@ -369,7 +368,6 @@
52ECA81A2C8A25D800F213B3 /* PlaceTopBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52ECA8192C8A25D800F213B3 /* PlaceTopBar.swift */; };
52ED919D2C71F639000EE25B /* SimpleResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52ED919C2C71F639000EE25B /* SimpleResponse.swift */; };
52ED919F2C71F718000EE25B /* SignUpRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52ED919E2C71F718000EE25B /* SignUpRequestDTO.swift */; };
52ED91A32C7200C4000EE25B /* Currency.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 52ED91A12C7200C4000EE25B /* Currency.xcdatamodeld */; };
52ED91A52C72C50F000EE25B /* CurrencyRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52ED91A42C72C50F000EE25B /* CurrencyRepositoryImpl.swift */; };
52ED91A72C72C58A000EE25B /* CurrencyPersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52ED91A62C72C58A000EE25B /* CurrencyPersistenceController.swift */; };
52ED91A92C73020A000EE25B /* CurrencyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52ED91A82C73020A000EE25B /* CurrencyService.swift */; };
@ -608,13 +606,14 @@
CED0E0372C902532008C61CA /* ThemeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0362C902532008C61CA /* ThemeDTO.swift */; };
CED0E0392C904868008C61CA /* NavigationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0382C904868008C61CA /* NavigationUtils.swift */; };
CED0E03B2C904A06008C61CA /* FavoritesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E03A2C904A06008C61CA /* FavoritesViewModel.swift */; };
CED0E03E2C905140008C61CA /* Place.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CED0E03C2C905140008C61CA /* Place.xcdatamodeld */; };
CED0E0422C9077D3008C61CA /* HashesPersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0412C9077D3008C61CA /* HashesPersistenceController.swift */; };
CED0E0452C918ED4008C61CA /* Hash.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0442C918ED4008C61CA /* Hash.swift */; };
CED0E0472C919F44008C61CA /* PlacesPersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0462C919F44008C61CA /* PlacesPersistenceController.swift */; };
CED0E04A2C91A2A9008C61CA /* DBUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0492C91A2A9008C61CA /* DBUtils.swift */; };
CED0E04C2C91A6A3008C61CA /* CoordinatesEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E04B2C91A6A3008C61CA /* CoordinatesEntity.swift */; };
CED0E04E2C91A702008C61CA /* UserEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E04D2C91A702008C61CA /* UserEntity.swift */; };
E11202462D744BFA001B3B24 /* TourismDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E11202442D744BFA001B3B24 /* TourismDB.xcdatamodeld */; };
E112024A2D7454F3001B3B24 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11202492D7454EF001B3B24 /* CoreDataManager.swift */; };
ED0B1C312BC2951F00FB8EDD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = ED0B1C302BC2951F00FB8EDD /* PrivacyInfo.xcprivacy */; };
ED1080A72B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1080A62B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift */; };
ED1263AB2B6F99F900AD99F3 /* UIView+AddSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1263AA2B6F99F900AD99F3 /* UIView+AddSeparator.swift */; };
@ -1370,7 +1369,6 @@
527D5E7E2C60E69C00736A85 /* Layouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layouting.swift; sourceTree = "<group>"; };
527D5E812C60EFEE00736A85 /* UIViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = "<group>"; };
528D72A02C5BBBF700D53210 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = "<group>"; };
529A5F122C859535004FE4A1 /* PersonalData.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = PersonalData.xcdatamodel; sourceTree = "<group>"; };
529A5F182C85BFF0004FE4A1 /* ToastView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
529A5F1D2C86DDE5004FE4A1 /* PlaceDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceDTO.swift; sourceTree = "<group>"; };
529A5F1F2C86DE14004FE4A1 /* CoordinatesDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesDTO.swift; sourceTree = "<group>"; };
@ -1438,7 +1436,6 @@
52ECA8192C8A25D800F213B3 /* PlaceTopBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceTopBar.swift; sourceTree = "<group>"; };
52ED919C2C71F639000EE25B /* SimpleResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleResponse.swift; sourceTree = "<group>"; };
52ED919E2C71F718000EE25B /* SignUpRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpRequestDTO.swift; sourceTree = "<group>"; };
52ED91A22C7200C4000EE25B /* CurrencyRates.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CurrencyRates.xcdatamodel; sourceTree = "<group>"; };
52ED91A42C72C50F000EE25B /* CurrencyRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyRepositoryImpl.swift; sourceTree = "<group>"; };
52ED91A62C72C58A000EE25B /* CurrencyPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyPersistenceController.swift; sourceTree = "<group>"; };
52ED91A82C73020A000EE25B /* CurrencyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyService.swift; sourceTree = "<group>"; };
@ -1661,13 +1658,14 @@
CED0E0362C902532008C61CA /* ThemeDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeDTO.swift; sourceTree = "<group>"; };
CED0E0382C904868008C61CA /* NavigationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationUtils.swift; sourceTree = "<group>"; };
CED0E03A2C904A06008C61CA /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = "<group>"; };
CED0E03D2C905140008C61CA /* Place.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Place.xcdatamodel; sourceTree = "<group>"; };
CED0E0412C9077D3008C61CA /* HashesPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashesPersistenceController.swift; sourceTree = "<group>"; };
CED0E0442C918ED4008C61CA /* Hash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hash.swift; sourceTree = "<group>"; };
CED0E0462C919F44008C61CA /* PlacesPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacesPersistenceController.swift; sourceTree = "<group>"; };
CED0E0492C91A2A9008C61CA /* DBUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBUtils.swift; sourceTree = "<group>"; };
CED0E04B2C91A6A3008C61CA /* CoordinatesEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesEntity.swift; sourceTree = "<group>"; };
CED0E04D2C91A702008C61CA /* UserEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntity.swift; sourceTree = "<group>"; };
E11202452D744BFA001B3B24 /* TourismDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TourismDB.xcdatamodel; sourceTree = "<group>"; };
E11202492D7454EF001B3B24 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = "<group>"; };
ED097E762BB80C320006ED01 /* OMapsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OMapsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
ED0B1C302BC2951F00FB8EDD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
ED1080A62B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialMediaCollectionViewHeader.swift; sourceTree = "<group>"; };
@ -3120,6 +3118,7 @@
5260D3C92C64F58A00C673B4 /* Db */ = {
isa = PBXGroup;
children = (
E11202492D7454EF001B3B24 /* CoreDataManager.swift */,
CED0E0492C91A2A9008C61CA /* DBUtils.swift */,
CED0E0402C9077B7008C61CA /* PersistenceControllers */,
3D2D79B82C7C4FC30062BC3D /* ControllerTemplates */,
@ -3502,11 +3501,9 @@
52ED91A02C72007C000EE25B /* DataModels */ = {
isa = PBXGroup;
children = (
529A5F112C859535004FE4A1 /* PersonalData.xcdatamodeld */,
52ED91A12C7200C4000EE25B /* Currency.xcdatamodeld */,
CED0E03C2C905140008C61CA /* Place.xcdatamodeld */,
CED0E04B2C91A6A3008C61CA /* CoordinatesEntity.swift */,
CED0E04D2C91A702008C61CA /* UserEntity.swift */,
E11202442D744BFA001B3B24 /* TourismDB.xcdatamodeld */,
);
path = DataModels;
sourceTree = "<group>";
@ -5120,6 +5117,7 @@
47E8163323B17734008FD836 /* MWMStorage+UI.m in Sources */,
3D2D79BA2C7C508E0062BC3D /* SingleEntityCoreDataController.swift in Sources */,
993DF11123F6BDB100AC231A /* UILabelRenderer.swift in Sources */,
E112024A2D7454F3001B3B24 /* CoreDataManager.swift in Sources */,
52A48AE12C882FEE0081E522 /* SearchViewModel.swift in Sources */,
34AB66471FC5AA330078E451 /* RouteManagerTableView.swift in Sources */,
52D588BA2C5CE2E800AB96B3 /* Font.swift in Sources */,
@ -5162,7 +5160,6 @@
F6E2FEE51E097BA00083EBEC /* MWMSearchNoResults.m in Sources */,
3DBD7BE42425015C00ED9FE8 /* ParntersStyleSheet.swift in Sources */,
F6E2FF631E097BA00083EBEC /* MWMTTSLanguageViewController.mm in Sources */,
52ED91A32C7200C4000EE25B /* Currency.xcdatamodeld in Sources */,
52522F462C6DFE060015709C /* AppTopBar.swift in Sources */,
52E2D3A42C59F9CE00A8843A /* WelcomeViewController.swift in Sources */,
4715273524907F8200E91BBA /* BookmarkColorViewController.swift in Sources */,
@ -5171,7 +5168,6 @@
34AB667D1FC5AA330078E451 /* MWMRoutePreview.mm in Sources */,
529A5F3F2C86E09B004FE4A1 /* HashDTO.swift in Sources */,
993DF11B23F6BDB100AC231A /* UIViewRenderer.swift in Sources */,
529A5F162C8595BB004FE4A1 /* PersonalData.xcdatamodeld in Sources */,
99C964302428C27A00E41723 /* PlacePageHeaderView.swift in Sources */,
9989273A2449E60200260CE2 /* BottomMenuViewController.swift in Sources */,
47E3C72D2111E6A2008B3B27 /* FadeTransitioning.swift in Sources */,
@ -5262,6 +5258,7 @@
34C9BD031C6DB693000DC38D /* MWMTableViewController.m in Sources */,
52E95F0D2C6C797B00A3FE2E /* ProfileViewController.swift in Sources */,
CE2D27F82CA2C49F00094565 /* BackButtonWithText.m in Sources */,
E11202462D744BFA001B3B24 /* TourismDB.xcdatamodeld in Sources */,
F6E2FD8C1E097BA00083EBEC /* MWMNoMapsView.m in Sources */,
529A5F312C86DF61004FE4A1 /* Review Models.swift in Sources */,
34D3B0361E389D05004100F9 /* MWMEditorSelectTableViewCell.m in Sources */,
@ -5359,7 +5356,6 @@
993DF10223F6BDB100AC231A /* Colors.swift in Sources */,
34AB66201FC5AA330078E451 /* RouteStartButton.swift in Sources */,
AC79C8922A65AB9500594C24 /* UIColor+hexString.swift in Sources */,
CED0E03E2C905140008C61CA /* Place.xcdatamodeld in Sources */,
99DEF9D723E420F6006BFD21 /* ElevationProfileDescriptionCell.swift in Sources */,
CED0E04A2C91A2A9008C61CA /* DBUtils.swift in Sources */,
993DF11623F6BDB100AC231A /* UIWindowRenderer.swift in Sources */,
@ -6037,34 +6033,13 @@
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */
529A5F112C859535004FE4A1 /* PersonalData.xcdatamodeld */ = {
E11202442D744BFA001B3B24 /* TourismDB.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
529A5F122C859535004FE4A1 /* PersonalData.xcdatamodel */,
E11202452D744BFA001B3B24 /* TourismDB.xcdatamodel */,
);
currentVersion = 529A5F122C859535004FE4A1 /* PersonalData.xcdatamodel */;
path = PersonalData.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
52ED91A12C7200C4000EE25B /* Currency.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
52ED91A22C7200C4000EE25B /* CurrencyRates.xcdatamodel */,
);
currentVersion = 52ED91A22C7200C4000EE25B /* CurrencyRates.xcdatamodel */;
path = Currency.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
CED0E03C2C905140008C61CA /* Place.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
CED0E03D2C905140008C61CA /* Place.xcdatamodel */,
);
currentVersion = CED0E03D2C905140008C61CA /* Place.xcdatamodel */;
name = Place.xcdatamodeld;
path = /Users/user/Projects/Tourism/iphone/Maps/Tourism/Data/Db/DataModels/Place.xcdatamodeld;
currentVersion = E11202452D744BFA001B3B24 /* TourismDB.xcdatamodel */;
path = TourismDB.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};

View file

@ -2,30 +2,34 @@ import CoreData
import Combine
class SingleEntityCoreDataController<Entity: NSManagedObject>: NSObject, NSFetchedResultsControllerDelegate {
private let container: NSPersistentContainer
private var fetchedResultsController: NSFetchedResultsController<Entity>?
var entitySubject = PassthroughSubject<Entity?, ResourceError>()
init(modelName: String) {
container = NSPersistentContainer(name: modelName)
super.init()
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
}
}
private let viewContext = CoreDataManager.shared.viewContext
private let backgroundContext = CoreDataManager.shared.backgroundContext
var context: NSManagedObjectContext {
return container.viewContext
}
// init(modelName: String) {
// container = NSPersistentContainer(name: modelName)
// super.init()
// container.loadPersistentStores { (description, error) in
// if let error = error {
// fatalError("Failed to load Core Data stack: \(error)")
// }
// }
// container.viewContext.automaticallyMergesChangesFromParent = true
// }
// var context: NSManagedObjectContext {
// return container.viewContext
// }
func observeEntity(fetchRequest: NSFetchRequest<Entity>, sortDescriptor: NSSortDescriptor) {
fetchRequest.sortDescriptors = [sortDescriptor]
fetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: context,
managedObjectContext: viewContext,
sectionNameKeyPath: nil,
cacheName: nil
)
@ -47,9 +51,9 @@ class SingleEntityCoreDataController<Entity: NSManagedObject>: NSObject, NSFetch
func updateEntity(updateBlock: @escaping (Entity) -> Void, fetchRequest: NSFetchRequest<Entity>) -> AnyPublisher<Void, ResourceError> {
Future { promise in
do {
let entityToUpdate = try self.context.fetch(fetchRequest).first ?? Entity(context: self.context)
let entityToUpdate = try self.viewContext.fetch(fetchRequest).first ?? Entity(context: self.viewContext)
updateBlock(entityToUpdate)
try self.context.save()
try self.viewContext.save()
promise(.success(()))
} catch {

View file

@ -0,0 +1,54 @@
import CoreData
final class CoreDataManager: NSObject {
static let shared = CoreDataManager()
let persistentContainer: NSPersistentContainer
var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
var backgroundContext: NSManagedObjectContext {
return persistentContainer.newBackgroundContext()
}
private override init() {
persistentContainer = NSPersistentContainer(name: "TourismDB")
persistentContainer.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
}
// lazy var persistentContainer: NSPersistentContainer = {
// let container = NSPersistentContainer(name: "TourismDB")
// container.loadPersistentStores { (storeDescription, error) in
// if let error = error as NSError? {
// fatalError("Unresolved error \(error), \(error.userInfo)")
// }
// }
// return container
// }()
func saveContext() {
let context = viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="21G646" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="CurrencyRatesEntity" representedClassName="CurrencyRatesEntity" syncable="YES" codeGenerationType="class">
<attribute name="eur" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rub" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="usd" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
</entity>
</model>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="PersonalDataEntity" representedClassName="PersonalDataEntity" syncable="YES" codeGenerationType="class">
<attribute name="country" attributeType="String"/>
<attribute name="email" attributeType="String"/>
<attribute name="fullName" attributeType="String"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="language" optional="YES" attributeType="String"/>
<attribute name="pfpUrl" optional="YES" attributeType="String"/>
<attribute name="theme" optional="YES" attributeType="String"/>
</entity>
</model>

View file

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="24A335" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="FavoriteSyncEntity" representedClassName="FavoriteSyncEntity" syncable="YES" codeGenerationType="class">
<attribute name="isFavorite" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="placeId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
<entity name="HashEntity" representedClassName="HashEntity" syncable="YES" codeGenerationType="class">
<attribute name="categoryId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="value" attributeType="String"/>
</entity>
<entity name="PlaceEntity" representedClassName="PlaceEntity" syncable="YES" codeGenerationType="class">
<attribute name="categoryId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="coordinatesJson" optional="YES" attributeType="String"/>
<attribute name="cover" attributeType="String"/>
<attribute name="descr" attributeType="String"/>
<attribute name="excerpt" attributeType="String"/>
<attribute name="galleryJson" attributeType="String"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isFavorite" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="rating" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
</entity>
<entity name="ReviewEntity" representedClassName="ReviewEntity" syncable="YES" codeGenerationType="class">
<attribute name="comment" optional="YES" attributeType="String"/>
<attribute name="date" attributeType="String"/>
<attribute name="deletionPlanned" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="picsUrlsJson" attributeType="String"/>
<attribute name="placeId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rating" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="userJson" attributeType="String"/>
</entity>
<entity name="ReviewPlannedToPostEntity" representedClassName="ReviewPlannedToPostEntity" syncable="YES" codeGenerationType="class">
<attribute name="comment" attributeType="String"/>
<attribute name="imagesJson" attributeType="String"/>
<attribute name="placeId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rating" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
</model>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="24A348" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="CurrencyRatesEntity" representedClassName="CurrencyRatesEntity" syncable="YES" codeGenerationType="class">
<attribute name="eur" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rub" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="usd" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
</entity>
<entity name="FavoriteSyncEntity" representedClassName="FavoriteSyncEntity" syncable="YES" codeGenerationType="class">
<attribute name="isFavorite" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="placeId" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
<entity name="HashEntity" representedClassName="HashEntity" syncable="YES" codeGenerationType="class">
<attribute name="categoryId" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="value" optional="YES" attributeType="String"/>
</entity>
<entity name="PersonalDataEntity" representedClassName="PersonalDataEntity" syncable="YES" codeGenerationType="class">
<attribute name="country" optional="YES" attributeType="String"/>
<attribute name="email" optional="YES" attributeType="String"/>
<attribute name="fullName" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="language" optional="YES" attributeType="String"/>
<attribute name="pfpUrl" optional="YES" attributeType="String"/>
<attribute name="theme" optional="YES" attributeType="String"/>
</entity>
<entity name="PlaceEntity" representedClassName="PlaceEntity" syncable="YES" codeGenerationType="class">
<attribute name="categoryId" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="coordinatesJson" optional="YES" attributeType="String"/>
<attribute name="cover" optional="YES" attributeType="String"/>
<attribute name="descr" optional="YES" attributeType="String"/>
<attribute name="excerpt" optional="YES" attributeType="String"/>
<attribute name="galleryJson" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isFavorite" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="rating" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
</entity>
<entity name="ReviewEntity" representedClassName="ReviewEntity" syncable="YES" codeGenerationType="class">
<attribute name="comment" optional="YES" attributeType="String"/>
<attribute name="date" optional="YES" attributeType="String"/>
<attribute name="deletionPlanned" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="picsUrlsJson" optional="YES" attributeType="String"/>
<attribute name="placeId" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rating" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="userJson" optional="YES" attributeType="String"/>
</entity>
<entity name="ReviewPlannedToPostEntity" representedClassName="ReviewPlannedToPostEntity" syncable="YES" codeGenerationType="class">
<attribute name="comment" optional="YES" attributeType="String"/>
<attribute name="imagesJson" optional="YES" attributeType="String"/>
<attribute name="placeId" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rating" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
</model>

View file

@ -6,7 +6,7 @@ class CurrencyPersistenceController {
private let coreDataController: SingleEntityCoreDataController<CurrencyRatesEntity>
private init() {
coreDataController = SingleEntityCoreDataController(modelName: "Currency")
coreDataController = SingleEntityCoreDataController()
}
var currencyRatesSubject: PassthroughSubject<CurrencyRatesEntity?, ResourceError> {

View file

@ -4,44 +4,35 @@ import CoreData
class HashesPersistenceController {
static let shared = HashesPersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Place")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
private let viewContext = CoreDataManager.shared.viewContext
// MARK: - CRUD Operations
func insertHashes(hashes: [Hash]) {
let context = container.viewContext
do {
for hash in hashes {
let newHash = HashEntity(context: context)
newHash.categoryId = hash.categoryId
newHash.value = hash.value
let backgroundContext = CoreDataManager.shared.backgroundContext
backgroundContext.perform {
do {
for hash in hashes {
let newHash = HashEntity(context: backgroundContext)
newHash.categoryId = hash.categoryId
newHash.value = hash.value
}
try backgroundContext.save()
} catch {
print("Failed to save context: \(error)")
}
try context.save()
} catch {
print("Failed to save context: \(error)")
}
}
func getHash(categoryId: Int64) -> Hash? {
let context = container.viewContext
let fetchRequest: NSFetchRequest<HashEntity> = HashEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "categoryId == %lld", categoryId)
fetchRequest.fetchLimit = 1
do {
let result = try context.fetch(fetchRequest).first
let result = try viewContext.fetch(fetchRequest).first
if let result = result {
return Hash(categoryId: result.categoryId, value: result.value!)
} else {
@ -54,11 +45,10 @@ class HashesPersistenceController {
}
func getHashes() -> [Hash] {
let context = container.viewContext
let fetchRequest: NSFetchRequest<HashEntity> = HashEntity.fetchRequest()
do {
let result = try context.fetch(fetchRequest)
let result = try viewContext.fetch(fetchRequest)
let hashes = result.map { hashEntity in
Hash(categoryId: hashEntity.categoryId, value: hashEntity.value!)
}
@ -70,18 +60,23 @@ class HashesPersistenceController {
}
func deleteHash(hash: Hash) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<HashEntity> = HashEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "categoryId == %lld", hash.categoryId)
do {
if let hash = try context.fetch(fetchRequest).first {
context.delete(hash)
try context.save()
let backgroundContext = CoreDataManager.shared.backgroundContext
backgroundContext.perform {
let fetchRequest: NSFetchRequest<HashEntity> = HashEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "categoryId == %lld", hash.categoryId)
do {
if let hash = try backgroundContext.fetch(fetchRequest).first {
backgroundContext.delete(hash)
try backgroundContext.save()
}
} catch {
print(error)
print("Failed to delete review: \(error)")
}
} catch {
print(error)
print("Failed to delete review: \(error)")
}
}
}

View file

@ -6,7 +6,7 @@ class PersonalDataPersistenceController {
private let coreDataController: SingleEntityCoreDataController<PersonalDataEntity>
private init() {
coreDataController = SingleEntityCoreDataController(modelName: "PersonalData")
coreDataController = SingleEntityCoreDataController()
}
var personalDataSubject: PassthroughSubject<PersonalDataEntity?, ResourceError> {

View file

@ -4,7 +4,7 @@ import Combine
class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate {
static let shared = PlacesPersistenceController()
let container: NSPersistentContainer
private let viewContext = CoreDataManager.shared.viewContext
private var searchFetchedResultsController: NSFetchedResultsController<PlaceEntity>?
private var placesByCatFetchedResultsController: NSFetchedResultsController<PlaceEntity>?
@ -21,33 +21,38 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
let favoritePlacesSubject = PassthroughSubject<[PlaceShort], ResourceError>()
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Place")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
super.init()
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
// init(inMemory: Bool = false) {
// container = NSPersistentContainer(name: "Place")
// if inMemory {
// container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
// }
// super.init()
// container.loadPersistentStores { (storeDescription, error) in
// if let error = error as NSError? {
// fatalError("Unresolved error \(error), \(error.userInfo)")
// }
// }
// container.viewContext.automaticallyMergesChangesFromParent = true
// }
// MARK: Places
func insertPlaces(_ places: [PlaceFull], categoryId: Int64) {
let context = container.viewContext
do {
for place in places {
let newPlace = PlaceEntity(context: context)
newPlace.id = place.id
updatePlace(newPlace, with: place, categoryId: categoryId)
let backgroundContext = CoreDataManager.shared.backgroundContext
backgroundContext.perform { [weak self] in
do {
for place in places {
let newPlace = PlaceEntity(context: backgroundContext)
newPlace.id = place.id
self?.updatePlace(newPlace, with: place, categoryId: categoryId)
}
try backgroundContext.save()
} catch {
print("Failed to save context: \(error)")
}
try context.save()
} catch {
print("Failed to save context: \(error)")
}
}
private func updatePlace(_ placeEntity: PlaceEntity, with place: PlaceFull, categoryId: Int64) {
@ -63,47 +68,57 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
}
func deleteAllPlaces() {
let context = container.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = PlaceEntity.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
// Configure the request to return the IDs of the deleted objects
deleteRequest.resultType = .resultTypeObjectIDs
let backgroundContext = CoreDataManager.shared.backgroundContext
do {
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
backgroundContext.perform {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = PlaceEntity.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
try context.save()
} catch {
print("Failed to delete all places: \(error)")
// Configure the request to return the IDs of the deleted objects
deleteRequest.resultType = .resultTypeObjectIDs
do {
let result = try backgroundContext.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [backgroundContext])
try backgroundContext.save()
} catch {
print("Failed to delete all places: \(error)")
}
}
}
func deleteAllPlacesByCategory(categoryId: Int64) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = PlaceEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "categoryId == %d", categoryId)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
let backgroundContext = CoreDataManager.shared.backgroundContext
// Configure the request to return the IDs of the deleted objects
deleteRequest.resultType = .resultTypeObjectIDs
do {
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
backgroundContext.perform {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = PlaceEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "categoryId == %d", categoryId)
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
try context.save()
} catch {
print("Failed to delete places by category \(categoryId): \(error)")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
// Configure the request to return the IDs of the deleted objects
deleteRequest.resultType = .resultTypeObjectIDs
do {
let result = try backgroundContext.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [backgroundContext])
try backgroundContext.save()
} catch {
print("Failed to delete places by category \(categoryId): \(error)")
}
}
}
// MARK: Observe places
@ -114,7 +129,7 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
fetchRequest.predicate = NSPredicate(format: "name CONTAINS[cd] %@", query)
}
searchFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
searchFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: viewContext, sectionNameKeyPath: nil, cacheName: nil)
searchFetchedResultsController?.delegate = self
@ -134,7 +149,7 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
fetchRequest.predicate = NSPredicate(format: "categoryId == %d", categoryId)
placesByCatFetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest, managedObjectContext: container.viewContext, sectionNameKeyPath: nil, cacheName: nil
fetchRequest: fetchRequest, managedObjectContext: viewContext, sectionNameKeyPath: nil, cacheName: nil
)
placesByCatFetchedResultsController?.delegate = self
@ -156,7 +171,7 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
fetchRequest.fetchLimit = 15
topSightsFetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest, managedObjectContext: container.viewContext, sectionNameKeyPath: nil, cacheName: nil
fetchRequest: fetchRequest, managedObjectContext: viewContext, sectionNameKeyPath: nil, cacheName: nil
)
topSightsFetchedResultsController?.delegate = self
@ -178,7 +193,7 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
fetchRequest.fetchLimit = 15
topRestaurantsFetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest, managedObjectContext: container.viewContext, sectionNameKeyPath: nil, cacheName: nil
fetchRequest: fetchRequest, managedObjectContext: viewContext, sectionNameKeyPath: nil, cacheName: nil
)
topRestaurantsFetchedResultsController?.delegate = self
@ -200,7 +215,7 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
fetchRequest.fetchLimit = 1
singlePlaceFetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest, managedObjectContext: container.viewContext, sectionNameKeyPath: nil, cacheName: nil
fetchRequest: fetchRequest, managedObjectContext: viewContext, sectionNameKeyPath: nil, cacheName: nil
)
singlePlaceFetchedResultsController?.delegate = self
@ -230,7 +245,7 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
favoritePlacesFetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest, managedObjectContext: container.viewContext, sectionNameKeyPath: nil, cacheName: nil
fetchRequest: fetchRequest, managedObjectContext: viewContext, sectionNameKeyPath: nil, cacheName: nil
)
favoritePlacesFetchedResultsController?.delegate = self
@ -246,7 +261,6 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
}
func getFavoritePlaces(query: String = "") -> [PlaceEntity] {
let context = container.viewContext
let fetchRequest: NSFetchRequest<PlaceEntity> = PlaceEntity.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
var predicates = [
@ -258,7 +272,7 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
do {
return try context.fetch(fetchRequest)
return try viewContext.fetch(fetchRequest)
} catch {
print("Failed to fetch favorite places: \(error)")
return []
@ -266,60 +280,74 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
}
func setFavorite(placeId: Int64, isFavorite: Bool) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<PlaceEntity> = PlaceEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %lld", placeId)
do {
if let place = try context.fetch(fetchRequest).first {
place.isFavorite = isFavorite
try context.save()
let backgroundContext = CoreDataManager.shared.backgroundContext
backgroundContext.perform {
let fetchRequest: NSFetchRequest<PlaceEntity> = PlaceEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %lld", placeId)
do {
if let place = try backgroundContext.fetch(fetchRequest).first {
place.isFavorite = isFavorite
try backgroundContext.save()
}
} catch {
print("Failed to set favorite status: \(error)")
}
} catch {
print("Failed to set favorite status: \(error)")
}
}
func addFavoritingRecordForSync(placeId: Int64, isFavorite: Bool) {
let context = container.viewContext
let favoriteSyncEntity = FavoriteSyncEntity(context: context)
favoriteSyncEntity.placeId = placeId
favoriteSyncEntity.isFavorite = isFavorite
do {
try context.save()
} catch {
print("Failed to add favorite sync: \(error)")
let backgroundContext = CoreDataManager.shared.backgroundContext
backgroundContext.perform {
let favoriteSyncEntity = FavoriteSyncEntity(context: backgroundContext)
favoriteSyncEntity.placeId = placeId
favoriteSyncEntity.isFavorite = isFavorite
do {
try backgroundContext.save()
} catch {
print("Failed to add favorite sync: \(error)")
}
}
}
func removeFavoritingRecordsForSync(placeIds: [Int64]) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = FavoriteSyncEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "placeId IN %@", placeIds)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs
let backgroundContext = CoreDataManager.shared.backgroundContext
do {
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
backgroundContext.perform {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = FavoriteSyncEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "placeId IN %@", placeIds)
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
try context.save()
} catch {
print("Failed to remove favoriting records for sync: \(error)")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs
do {
let result = try backgroundContext.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [backgroundContext])
try backgroundContext.save()
} catch {
print("Failed to remove favoriting records for sync: \(error)")
}
}
}
func getFavoritingRecordsForSync() -> [FavoriteSyncEntity] {
let context = container.viewContext
let fetchRequest: NSFetchRequest<FavoriteSyncEntity> = FavoriteSyncEntity.fetchRequest()
do {
return try context.fetch(fetchRequest)
return try viewContext.fetch(fetchRequest)
} catch {
print("Failed to fetch favorite sync data: \(error)")
return []

View file

@ -5,7 +5,7 @@ import Combine
class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate {
static let shared = ReviewsPersistenceController()
let container: NSPersistentContainer
private let viewContext = CoreDataManager.shared.viewContext
private var reviewsForPlaceFetchedResultsController: NSFetchedResultsController<ReviewEntity>?
private var reviewsPlannedToPostFetchedResultsController: NSFetchedResultsController<ReviewPlannedToPostEntity>?
@ -13,31 +13,36 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
let reviewsForPlaceSubject = PassthroughSubject<[Review], ResourceError>()
let reviewsPlannedToPostSubject = PassthroughSubject<[ReviewPlannedToPostEntity], ResourceError>()
override init() {
container = NSPersistentContainer(name: "Place")
super.init()
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
// override init() {
// container = NSPersistentContainer(name: "Place")
// super.init()
// container.loadPersistentStores { (storeDescription, error) in
// if let error = error as NSError? {
// fatalError("Unresolved error \(error), \(error.userInfo)")
// }
// }
// container.viewContext.automaticallyMergesChangesFromParent = true
// }
// MARK: - Review Operations
func insertReviews(_ reviews: [Review]) {
let context = container.viewContext
let backgroundContext = CoreDataManager.shared.backgroundContext
do {
for review in reviews {
let newReview = ReviewEntity(context: context)
newReview.id = review.id
updateReviewEntity(newReview, with: review)
backgroundContext.perform {
do {
for review in reviews {
let newReview = ReviewEntity(context: backgroundContext)
newReview.id = review.id
self.updateReviewEntity(newReview, with: review)
}
try backgroundContext.save()
} catch {
print(error)
print("Failed to insert/update reviews: \(error)")
}
try context.save()
} catch {
print(error)
print("Failed to insert/update reviews: \(error)")
}
}
private func updateReviewEntity(_ entity: ReviewEntity, with review: Review) {
@ -51,88 +56,110 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
}
func deleteReview(id: Int64) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %lld", id)
do {
let reviews = try context.fetch(fetchRequest)
for review in reviews {
context.delete(review)
let backgroundContext = CoreDataManager.shared.backgroundContext
backgroundContext.perform {
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %lld", id)
do {
let reviews = try backgroundContext.fetch(fetchRequest)
for review in reviews {
backgroundContext.delete(review)
}
try backgroundContext.save()
} catch {
print(error)
print("Failed to delete review: \(error)")
}
try context.save()
} catch {
print(error)
print("Failed to delete review: \(error)")
}
}
func deleteReviews(ids: [Int64]) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
let backgroundContext = CoreDataManager.shared.backgroundContext
do {
let reviews = try context.fetch(fetchRequest)
for review in reviews {
context.delete(review)
backgroundContext.perform {
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
do {
let reviews = try backgroundContext.fetch(fetchRequest)
for review in reviews {
backgroundContext.delete(review)
}
try backgroundContext.save()
} catch {
print(error)
print("Failed to delete reviews: \(error)")
}
try context.save()
} catch {
print(error)
print("Failed to delete reviews: \(error)")
}
}
func deleteAllReviews() {
let context = container.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ReviewEntity.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs
do {
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
let backgroundContext = CoreDataManager.shared.backgroundContext
backgroundContext.perform {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ReviewEntity.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
try context.save()
} catch {
print(error)
print("Failed to delete all places: \(error)")
do {
let result = try backgroundContext.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [backgroundContext])
try backgroundContext.save()
} catch {
print(error)
print("Failed to delete all places: \(error)")
}
}
}
func deleteAllPlaceReviews(placeId: Int64) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ReviewEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs
do {
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
let backgroundContext = CoreDataManager.shared.backgroundContext
backgroundContext.perform {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ReviewEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
try context.save()
} catch {
print(error)
print("Failed to delete place reviews: \(error)")
do {
let result = try backgroundContext.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [backgroundContext])
try backgroundContext.save()
} catch {
print(error)
print("Failed to delete place reviews: \(error)")
}
}
}
func observeReviewsForPlace(placeId: Int64) {
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
reviewsForPlaceFetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: container.viewContext,
managedObjectContext: viewContext,
sectionNameKeyPath: nil,
cacheName: nil
)
@ -153,15 +180,14 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
}
func markReviewForDeletion(id: Int64, deletionPlanned: Bool = true) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %lld", id)
do {
let reviews = try context.fetch(fetchRequest)
let reviews = try viewContext.fetch(fetchRequest)
if let review = reviews.first {
review.deletionPlanned = deletionPlanned
try context.save()
try viewContext.save()
}
} catch {
print(error)
@ -170,12 +196,11 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
}
func getReviewsPlannedForDeletion() -> [ReviewEntity] {
let context = container.viewContext
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "deletionPlanned == YES")
do {
return try context.fetch(fetchRequest)
return try viewContext.fetch(fetchRequest)
} catch {
print(error)
print("Failed to fetch reviews planned for deletion: \(error)")
@ -186,50 +211,60 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
// // MARK: - Planned Review Operations
func insertReviewPlannedToPost(_ review: ReviewToPost) {
let context = container.viewContext
let newReview = ReviewPlannedToPostEntity(context: context)
newReview.placeId = review.placeId
newReview.comment = review.comment
newReview.rating = Int32(review.rating)
let imagesJson = DBUtils.encodeToJsonString(review.images)
newReview.imagesJson = imagesJson
do {
try context.save()
} catch {
print(error)
print("Failed to insert planned review: \(error)")
let backgroundContext = CoreDataManager.shared.backgroundContext
backgroundContext.perform {
let newReview = ReviewPlannedToPostEntity(context: backgroundContext)
newReview.placeId = review.placeId
newReview.comment = review.comment
newReview.rating = Int32(review.rating)
let imagesJson = DBUtils.encodeToJsonString(review.images)
newReview.imagesJson = imagesJson
do {
try backgroundContext.save()
} catch {
print(error)
print("Failed to insert planned review: \(error)")
}
}
}
func deleteReviewPlannedToPost(placeId: Int64) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ReviewPlannedToPostEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs
let backgroundContext = CoreDataManager.shared.backgroundContext
do {
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
backgroundContext.perform {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ReviewPlannedToPostEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId)
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
try context.save()
} catch {
print(error)
print("Failed to delete planned review: \(error)")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs
do {
let result = try backgroundContext.execute(deleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [backgroundContext])
try backgroundContext.save()
} catch {
print(error)
print("Failed to delete planned review: \(error)")
}
}
}
func getReviewsPlannedToPost() -> [ReviewPlannedToPostEntity] {
let context = container.viewContext
let fetchRequest: NSFetchRequest<ReviewPlannedToPostEntity> = ReviewPlannedToPostEntity.fetchRequest()
do {
return try context.fetch(fetchRequest)
return try viewContext.fetch(fetchRequest)
} catch {
print(error)
print("Failed to fetch planned reviews: \(error)")