This commit is contained in:
Emin 2024-09-16 11:21:24 +05:00
parent bcf18422b5
commit 52bf2acb91
43 changed files with 1615 additions and 553 deletions

View file

@ -4093,6 +4093,8 @@
"plz_wait_dowloading" = "Please, wait, data being downloaded";
"download_failed" = "Download failed";
"no_content" = "Empty";
"empty_list" = "Empty";

View file

@ -4093,6 +4093,8 @@
"plz_wait_dowloading" = "Please, wait, data being downloaded";
"download_failed" = "Download failed";
"no_content" = "Empty";
"empty_list" = "Empty";

View file

@ -4093,6 +4093,8 @@
"plz_wait_dowloading" = "Пожалуйста подождите данные скачиваются";
"download_failed" = "Загрузка данных провалена";
"no_content" = "Пусто";
"empty_list" = "Пусто";

View file

@ -586,6 +586,10 @@
CDCA27842245090900167D87 /* ListenerContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCA27832245090900167D87 /* ListenerContainer.swift */; };
CDCA278622451F5000167D87 /* RouteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCA278522451F5000167D87 /* RouteInfo.swift */; };
CDCA278E2248F34C00167D87 /* MWMRoutingManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = CDCA278B2248F34C00167D87 /* MWMRoutingManager.mm */; };
CE64501B2C93F5840075A59B /* PlacePersistenceControllerTesterBro.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE64501A2C93F5840075A59B /* PlacePersistenceControllerTesterBro.swift */; };
CE64501D2C93F8350075A59B /* ReviewsPersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE64501C2C93F8350075A59B /* ReviewsPersistenceController.swift */; };
CE6450202C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */; };
CE6450242C9772310075A59B /* DownloadProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6450232C9772310075A59B /* DownloadProgress.swift */; };
CED0E00E2C8ACBCA008C61CA /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = CED0E00D2C8ACBCA008C61CA /* SDWebImageSwiftUI */; };
CED0E0112C8ACBE1008C61CA /* CountryPickerView in Frameworks */ = {isa = PBXBuildFile; productRef = CED0E0102C8ACBE1008C61CA /* CountryPickerView */; };
CED0E0172C8ACF0D008C61CA /* RoundedCornerShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0162C8ACF0D008C61CA /* RoundedCornerShape.swift */; };
@ -604,9 +608,9 @@
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 /* HashPersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0412C9077D3008C61CA /* HashPersistenceController.swift */; };
CED0E0422C9077D3008C61CA /* HashesPersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0412C9077D3008C61CA /* HashesPersistenceController.swift */; };
CED0E0452C918ED4008C61CA /* Hash.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0442C918ED4008C61CA /* Hash.swift */; };
CED0E0472C919F44008C61CA /* PlacePersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0462C919F44008C61CA /* PlacePersistenceController.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 */; };
@ -1635,6 +1639,10 @@
CDCA278C2248F34C00167D87 /* MWMRouterResultCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MWMRouterResultCode.h; sourceTree = "<group>"; };
CDCA278F2248F3B800167D87 /* MWMLocationModeListener.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMLocationModeListener.h; sourceTree = "<group>"; };
CDE0F3AD225B8D45008BA5C3 /* MWMSpeedCameraManagerMode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMSpeedCameraManagerMode.h; sourceTree = "<group>"; };
CE64501A2C93F5840075A59B /* PlacePersistenceControllerTesterBro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacePersistenceControllerTesterBro.swift; sourceTree = "<group>"; };
CE64501C2C93F8350075A59B /* ReviewsPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsPersistenceController.swift; sourceTree = "<group>"; };
CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsPersistenceControllerTesterBro.swift; sourceTree = "<group>"; };
CE6450232C9772310075A59B /* DownloadProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgress.swift; sourceTree = "<group>"; };
CED0E0162C8ACF0D008C61CA /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = "<group>"; };
CED0E0182C8AD57C008C61CA /* EmptyUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUI.swift; sourceTree = "<group>"; };
CED0E01A2C8B048C008C61CA /* AllPicsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPicsScreen.swift; sourceTree = "<group>"; };
@ -1651,9 +1659,9 @@
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 /* HashPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashPersistenceController.swift; 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 /* PlacePersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacePersistenceController.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>"; };
@ -3080,6 +3088,7 @@
5260D3E12C66287D00C673B4 /* Auth */,
52ED919C2C71F639000EE25B /* SimpleResponse.swift */,
529A5F342C86DF99004FE4A1 /* PlaceLocation.swift */,
CE6450232C9772310075A59B /* DownloadProgress.swift */,
);
path = Models;
sourceTree = "<group>";
@ -3859,6 +3868,15 @@
path = Location;
sourceTree = "<group>";
};
CE6450192C93F56E0075A59B /* Testers */ = {
isa = PBXGroup;
children = (
CE64501A2C93F5840075A59B /* PlacePersistenceControllerTesterBro.swift */,
CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */,
);
path = Testers;
sourceTree = "<group>";
};
CED0E0202C8B22BD008C61CA /* Components */ = {
isa = PBXGroup;
children = (
@ -3871,10 +3889,12 @@
CED0E0402C9077B7008C61CA /* PersistenceControllers */ = {
isa = PBXGroup;
children = (
CE6450192C93F56E0075A59B /* Testers */,
52ED91A62C72C58A000EE25B /* CurrencyPersistenceController.swift */,
3D2D79C22C7C80E60062BC3D /* PersonalDataPersistenceController.swift */,
CED0E0412C9077D3008C61CA /* HashPersistenceController.swift */,
CED0E0462C919F44008C61CA /* PlacePersistenceController.swift */,
CED0E0412C9077D3008C61CA /* HashesPersistenceController.swift */,
CED0E0462C919F44008C61CA /* PlacesPersistenceController.swift */,
CE64501C2C93F8350075A59B /* ReviewsPersistenceController.swift */,
);
path = PersistenceControllers;
sourceTree = "<group>";
@ -5034,13 +5054,14 @@
CDCA273F2238087700167D87 /* MWMCarPlaySearchService.mm in Sources */,
529A5F392C86E048004FE4A1 /* CategoryDTO.swift in Sources */,
529A5F1E2C86DDE5004FE4A1 /* PlaceDTO.swift in Sources */,
CE64501D2C93F8350075A59B /* ReviewsPersistenceController.swift in Sources */,
348B926D1FF3B5E100379009 /* UIView+Animation.swift in Sources */,
F6E2FDE91E097BA00083EBEC /* MWMObjectsCategorySelectorController.mm in Sources */,
34AB665F1FC5AA330078E451 /* TransportTransitIntermediatePoint.swift in Sources */,
34B846A82029E8110081ECCD /* BMCDefaultViewModel.swift in Sources */,
993DF12123F6BDB100AC231A /* UIViewControllerRenderer.swift in Sources */,
529A5F192C85BFF0004FE4A1 /* ToastView.swift in Sources */,
CED0E0422C9077D3008C61CA /* HashPersistenceController.swift in Sources */,
CED0E0422C9077D3008C61CA /* HashesPersistenceController.swift in Sources */,
34D3AFF61E37A36A004100F9 /* UICollectionView+Cells.swift in Sources */,
4767CDA420AAF66B00BD8166 /* NSAttributedString+HTML.swift in Sources */,
6741A9A91BF340DE002C974C /* MWMDefaultAlert.mm in Sources */,
@ -5083,7 +5104,7 @@
993DF12C23F6BDB100AC231A /* Theme.swift in Sources */,
47CA68D8250044C500671019 /* BookmarksListRouter.swift in Sources */,
52ED91AB2C7302A7000EE25B /* CurrencyRatesDTO.swift in Sources */,
CED0E0472C919F44008C61CA /* PlacePersistenceController.swift in Sources */,
CED0E0472C919F44008C61CA /* PlacesPersistenceController.swift in Sources */,
34D3B0421E389D05004100F9 /* MWMEditorTextTableViewCell.m in Sources */,
99F3EB1223F418C900C713F8 /* PlacePageInteractor.swift in Sources */,
52522F3B2C6DDA750015709C /* ThemeViewModel.swift in Sources */,
@ -5170,6 +5191,7 @@
99A906F323FA95AB0005872B /* PlacePageStyleSheet.swift in Sources */,
529A5F6C2C870D45004FE4A1 /* HorizontalPlaces.swift in Sources */,
52522F2E2C6C9E070015709C /* UserPreferences.swift in Sources */,
CE6450202C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift in Sources */,
6741A9CF1BF340DE002C974C /* MWMLocationAlert.m in Sources */,
F6E2FDA11E097BA00083EBEC /* MWMEditorAdditionalNamesTableViewController.mm in Sources */,
4767CDA620AB1F6200BD8166 /* LeftAlignedIconButton.swift in Sources */,
@ -5252,6 +5274,7 @@
529A5F202C86DE14004FE4A1 /* CoordinatesDTO.swift in Sources */,
CDB4D4E1222D70DF00104869 /* CarPlayMapViewController.swift in Sources */,
471AB98923AA8A3500F56D49 /* IDownloaderDataSource.swift in Sources */,
CE64501B2C93F5840075A59B /* PlacePersistenceControllerTesterBro.swift in Sources */,
52ED91B02C73030D000EE25B /* PersonalDataDTO.swift in Sources */,
EDE243E72B6D55610057369B /* InfoView.swift in Sources */,
F692F3831EA0FAF5001E82EB /* MWMAutoupdateController.mm in Sources */,
@ -5388,6 +5411,7 @@
5260D3E52C66290500C673B4 /* AuthResponse.swift in Sources */,
47E3C7252111E41B008B3B27 /* DimmedModalPresentationController.swift in Sources */,
52B573F22C61E8980047FAC9 /* SignUpViewController.swift in Sources */,
CE6450242C9772310075A59B /* DownloadProgress.swift in Sources */,
3472B5CB200F43EF00DC6CD5 /* BackgroundFetchScheduler.swift in Sources */,
34FE5A6F1F18F30F00BCA729 /* TrafficButtonArea.swift in Sources */,
993DF10D23F6BDB100AC231A /* UIPageControlRenderer.swift in Sources */,

View file

@ -6,6 +6,7 @@ class DBUtils {
let encoded = try encoder.encode(body)
return convertDataToString(encoded)
} catch {
print(error)
return nil
}
}
@ -22,6 +23,7 @@ class DBUtils {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
print(error)
return nil
}
}

View file

@ -25,12 +25,12 @@
<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="imagesJson" attributeType="String"/>
<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="ReviewToPublishEntity" representedClassName="ReviewToPublishEntity" syncable="YES" codeGenerationType="class">
<entity name="ReviewPlannedToPostEntity" representedClassName="ReviewPlannedToPostEntity" syncable="YES" codeGenerationType="class">
<attribute name="comment" attributeType="String"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="imagesJson" attributeType="String"/>

View file

@ -1,4 +1,4 @@
struct UserEntity {
struct UserEntity: Encodable {
let userId: Int64
let fullName: String
let avatar: String

View file

@ -17,3 +17,67 @@ extension PersonalDataEntity {
)
}
}
extension PlaceEntity {
func toPlaceShort() -> PlaceShort {
return PlaceShort(
id: self.id,
name: self.name ?? "",
cover: self.cover,
rating: self.rating,
excerpt: self.excerpt,
isFavorite: self.isFavorite
)
}
func toPlaceFull() -> PlaceFull {
let placeLocation =
DBUtils.decodeFromJsonString(self.coordinatesJson ?? "", to: CoordinatesEntity.self)?.toPlaceLocation(name: self.name ?? "")
let pics = DBUtils.decodeFromJsonString(self.galleryJson ?? "", to: [String].self) ?? []
return PlaceFull(
id: self.id,
name: self.name ?? "",
rating: self.rating,
excerpt: self.excerpt ?? "",
description: self.descr ?? "",
placeLocation: placeLocation,
cover: self.cover ?? "",
pics: pics,
// we have different table for reviews
reviews: nil,
isFavorite: self.isFavorite
)
}
}
extension [PlaceEntity] {
func toFullPlaces() -> [PlaceFull] {
return self.map { placeEntity in
placeEntity.toPlaceFull()
}
}
func toShortPlaces() -> [PlaceShort] {
return self.map { placeEntity in
placeEntity.toPlaceShort()
}
}
}
extension CoordinatesEntity {
func toPlaceLocation(name: String) -> PlaceLocation {
return PlaceLocation(name: name, lat: self.latitude, lon: self.longitude)
}
}
extension UserEntity {
func toUser() -> User {
return User(
id: self.userId,
name: self.fullName,
pfpUrl: self.avatar,
countryCodeName: self.country
)
}
}

View file

@ -1,8 +1,8 @@
import Foundation
import CoreData
class HashPersistenceController {
static let shared = HashPersistenceController()
class HashesPersistenceController {
static let shared = HashesPersistenceController()
let container: NSPersistentContainer
@ -62,26 +62,26 @@ class HashPersistenceController {
}
}
func getHash(id: Int64) -> Result<Hash?, ResourceError> {
func getHash(categoryId: Int64) -> Hash? {
let context = container.viewContext
let fetchRequest: NSFetchRequest<HashEntity> = HashEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "categoryId == %lld", id)
fetchRequest.predicate = NSPredicate(format: "categoryId == %lld", categoryId)
fetchRequest.fetchLimit = 1
do {
let result = try context.fetch(fetchRequest).first
if let result = result {
return .success(Hash(categoryId: result.categoryId, value: result.value!))
return Hash(categoryId: result.categoryId, value: result.value!)
} else {
return .success(nil)
return nil
}
} catch {
print("Failed to fetch hash: \(error)")
return .failure(.cacheError)
return nil
}
}
func getHashes() -> Result<[Hash], ResourceError> {
func getHashes() -> [Hash] {
let context = container.viewContext
let fetchRequest: NSFetchRequest<HashEntity> = HashEntity.fetchRequest()
@ -90,10 +90,10 @@ class HashPersistenceController {
let hashes = result.map { hashEntity in
Hash(categoryId: hashEntity.categoryId, value: hashEntity.value!)
}
return .success(hashes)
return hashes
} catch {
print("Failed to fetch hashes: \(error)")
return .failure(.cacheError)
return []
}
}
}

View file

@ -2,22 +2,24 @@ import Foundation
import CoreData
import Combine
class PlacePersistenceController: NSObject, NSFetchedResultsControllerDelegate {
static let shared = PlacePersistenceController()
class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate {
static let shared = PlacesPersistenceController()
let container: NSPersistentContainer
private var searchFetchedResultsController: NSFetchedResultsController<PlaceEntity>?
private var placesByCatFetchedResultsController: NSFetchedResultsController<PlaceEntity>?
private var topPlacesFetchedResultsController: NSFetchedResultsController<PlaceEntity>?
private var topSightsFetchedResultsController: NSFetchedResultsController<PlaceEntity>?
private var topRestaurantsFetchedResultsController: NSFetchedResultsController<PlaceEntity>?
private var singlePlaceFetchedResultsController: NSFetchedResultsController<PlaceEntity>?
private var favoritePlacesFetchedResultsController: NSFetchedResultsController<PlaceEntity>?
let searchSubject = PassthroughSubject<[PlaceEntity], ResourceError>()
let placesByCatSubject = PassthroughSubject<[PlaceEntity], ResourceError>()
let topPlacesSubject = PassthroughSubject<[PlaceEntity], ResourceError>()
let singlePlaceSubject = PassthroughSubject<PlaceEntity?, ResourceError>()
let favoritePlacesSubject = PassthroughSubject<[PlaceEntity], ResourceError>()
let searchSubject = PassthroughSubject<[PlaceShort], ResourceError>()
let placesByCatSubject = PassthroughSubject<[PlaceShort], ResourceError>()
let topSightsSubject = PassthroughSubject<[PlaceShort], ResourceError>()
let topRestaurantsSubject = PassthroughSubject<[PlaceShort], ResourceError>()
let singlePlaceSubject = PassthroughSubject<PlaceFull, ResourceError>()
let favoritePlacesSubject = PassthroughSubject<[PlaceShort], ResourceError>()
init(inMemory: Bool = false) {
@ -56,39 +58,30 @@ class PlacePersistenceController: NSObject, NSFetchedResultsControllerDelegate {
do {
if let existingPlace = try context.fetch(fetchRequest).first {
// Update the existing place
existingPlace.categoryId = categoryId
existingPlace.name = place.name
existingPlace.excerpt = place.excerpt
existingPlace.descr = place.description
existingPlace.cover = place.cover
let galleryJson = DBUtils.encodeToJsonString(place.pics)
existingPlace.galleryJson = galleryJson
let coordinatesJson = DBUtils.encodeToJsonString(place.placeLocation?.toCoordinatesEntity())
existingPlace.coordinatesJson = coordinatesJson
existingPlace.rating = place.rating
existingPlace.isFavorite = place.isFavorite
updatePlace(existingPlace, with: place, categoryId: categoryId)
} else {
// Insert a new place
let newPlace = PlaceEntity(context: context)
newPlace.id = place.id
newPlace.categoryId = categoryId
newPlace.name = place.name
newPlace.excerpt = place.excerpt
newPlace.descr = place.description
newPlace.cover = place.cover
let galleryJson = DBUtils.encodeToJsonString(place.pics)
newPlace.galleryJson = galleryJson
let coordinatesJson = DBUtils.encodeToJsonString(place.placeLocation?.toCoordinatesEntity())
newPlace.coordinatesJson = coordinatesJson
newPlace.rating = place.rating
newPlace.isFavorite = place.isFavorite
updatePlace(newPlace, with: place, categoryId: categoryId)
}
} catch {
print("Failed to insert or update place: \(error)")
}
}
private func updatePlace(_ placeEntity: PlaceEntity, with place: PlaceFull, categoryId: Int64) {
placeEntity.categoryId = categoryId
placeEntity.name = place.name
placeEntity.excerpt = place.excerpt
placeEntity.descr = place.description
placeEntity.cover = place.cover
placeEntity.galleryJson = DBUtils.encodeToJsonString(place.pics)
placeEntity.coordinatesJson = DBUtils.encodeToJsonString(place.placeLocation?.toCoordinatesEntity())
placeEntity.rating = place.rating
placeEntity.isFavorite = place.isFavorite
}
func deleteAllPlaces() {
let context = container.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = PlaceEntity.fetchRequest()
@ -137,7 +130,9 @@ class PlacePersistenceController: NSObject, NSFetchedResultsControllerDelegate {
func observeSearch(query: String) {
let fetchRequest: NSFetchRequest<PlaceEntity> = PlaceEntity.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
fetchRequest.predicate = NSPredicate(format: "name CONTAINS[cd] %@", query)
if !query.isEmpty {
fetchRequest.predicate = NSPredicate(format: "name CONTAINS[cd] %@", query)
}
searchFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
@ -146,7 +141,7 @@ class PlacePersistenceController: NSObject, NSFetchedResultsControllerDelegate {
do {
try searchFetchedResultsController!.performFetch()
if let results = searchFetchedResultsController!.fetchedObjects {
searchSubject.send(results)
searchSubject.send(results.toShortPlaces())
}
} catch {
searchSubject.send(completion: .failure(.cacheError))
@ -167,32 +162,54 @@ class PlacePersistenceController: NSObject, NSFetchedResultsControllerDelegate {
do {
try placesByCatFetchedResultsController!.performFetch()
if let results = placesByCatFetchedResultsController!.fetchedObjects {
placesByCatSubject.send(results)
placesByCatSubject.send(results.toShortPlaces())
}
} catch {
placesByCatSubject.send(completion: .failure(.cacheError))
}
}
func observeTopPlacesByCategoryId(categoryId: Int64) {
func observeTopSights() {
let fetchRequest: NSFetchRequest<PlaceEntity> = PlaceEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "categoryId == %lld", categoryId)
fetchRequest.predicate = NSPredicate(format: "categoryId == %lld", PlaceCategory.sights.id)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "rating", ascending: false)]
fetchRequest.fetchLimit = 15
topPlacesFetchedResultsController = NSFetchedResultsController(
topSightsFetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest, managedObjectContext: container.viewContext, sectionNameKeyPath: nil, cacheName: nil
)
topPlacesFetchedResultsController?.delegate = self
topSightsFetchedResultsController?.delegate = self
do {
try topPlacesFetchedResultsController!.performFetch()
if let results = topPlacesFetchedResultsController!.fetchedObjects {
topPlacesSubject.send(results)
try topSightsFetchedResultsController!.performFetch()
if let results = topSightsFetchedResultsController!.fetchedObjects {
topSightsSubject.send(results.toShortPlaces())
}
} catch {
topPlacesSubject.send(completion: .failure(.cacheError))
topSightsSubject.send(completion: .failure(.cacheError))
}
}
func observeTopRestaurants() {
let fetchRequest: NSFetchRequest<PlaceEntity> = PlaceEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "categoryId == %lld", PlaceCategory.restaurants.id)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "rating", ascending: false)]
fetchRequest.fetchLimit = 15
topRestaurantsFetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest, managedObjectContext: container.viewContext, sectionNameKeyPath: nil, cacheName: nil
)
topRestaurantsFetchedResultsController?.delegate = self
do {
try topRestaurantsFetchedResultsController!.performFetch()
if let results = topRestaurantsFetchedResultsController!.fetchedObjects {
topRestaurantsSubject.send(results.toShortPlaces())
}
} catch {
topRestaurantsSubject.send(completion: .failure(.cacheError))
}
}
@ -211,7 +228,9 @@ class PlacePersistenceController: NSObject, NSFetchedResultsControllerDelegate {
do {
try singlePlaceFetchedResultsController!.performFetch()
if let results = singlePlaceFetchedResultsController!.fetchedObjects {
singlePlaceSubject.send(results.first)
if let place = results.first {
singlePlaceSubject.send(place.toPlaceFull())
}
}
} catch {
singlePlaceSubject.send(completion: .failure(.cacheError))
@ -222,10 +241,12 @@ class PlacePersistenceController: NSObject, NSFetchedResultsControllerDelegate {
func observeFavoritePlaces(query: String = "") {
let fetchRequest: NSFetchRequest<PlaceEntity> = PlaceEntity.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
let predicates = [
var predicates = [
NSPredicate(format: "isFavorite == YES"),
NSPredicate(format: "name CONTAINS[cd] %@", query)
]
if !query.isEmpty {
predicates.append(NSPredicate(format: "name CONTAINS[cd] %@", query))
}
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
favoritePlacesFetchedResultsController = NSFetchedResultsController(
@ -237,7 +258,7 @@ class PlacePersistenceController: NSObject, NSFetchedResultsControllerDelegate {
do {
try favoritePlacesFetchedResultsController!.performFetch()
if let results = favoritePlacesFetchedResultsController!.fetchedObjects {
favoritePlacesSubject.send(results)
favoritePlacesSubject.send(results.toShortPlaces())
}
} catch {
favoritePlacesSubject.send(completion: .failure(.cacheError))
@ -248,10 +269,12 @@ class PlacePersistenceController: NSObject, NSFetchedResultsControllerDelegate {
let context = container.viewContext
let fetchRequest: NSFetchRequest<PlaceEntity> = PlaceEntity.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
let predicates = [
var predicates = [
NSPredicate(format: "isFavorite == YES"),
NSPredicate(format: "name CONTAINS[cd] %@", query)
]
if !query.isEmpty {
predicates.append(NSPredicate(format: "name CONTAINS[cd] %@", query))
}
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
do {
@ -326,15 +349,19 @@ class PlacePersistenceController: NSObject, NSFetchedResultsControllerDelegate {
switch controller {
case searchFetchedResultsController:
searchSubject.send(fetchedObjects)
searchSubject.send(fetchedObjects.toShortPlaces())
case placesByCatFetchedResultsController:
placesByCatSubject.send(fetchedObjects)
case topPlacesFetchedResultsController:
topPlacesSubject.send(fetchedObjects)
placesByCatSubject.send(fetchedObjects.toShortPlaces())
case topSightsFetchedResultsController:
topSightsSubject.send(fetchedObjects.toShortPlaces())
case topRestaurantsFetchedResultsController:
topRestaurantsSubject.send(fetchedObjects.toShortPlaces())
case singlePlaceFetchedResultsController:
singlePlaceSubject.send(fetchedObjects.first)
if let place = fetchedObjects.first {
singlePlaceSubject.send(place.toPlaceFull())
}
case favoritePlacesFetchedResultsController:
favoritePlacesSubject.send(fetchedObjects)
favoritePlacesSubject.send(fetchedObjects.toShortPlaces())
default:
break
}

View file

@ -0,0 +1,298 @@
import Foundation
import CoreData
import Combine
class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate {
static let shared = ReviewsPersistenceController()
let container: NSPersistentContainer
private var reviewsForPlaceFetchedResultsController: NSFetchedResultsController<ReviewEntity>?
private var reviewsPlannedToPostFetchedResultsController: NSFetchedResultsController<ReviewPlannedToPostEntity>?
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)")
}
}
}
// MARK: - Review Operations
func putReview(_ review: Review) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %lld", review.id)
do {
let results = try context.fetch(fetchRequest)
let reviewEntity: ReviewEntity
if let existingReview = results.first {
// Update existing review
updateReviewEntity(existingReview, with: review)
} else {
let newReview = ReviewEntity(context: context)
newReview.id = review.id
updateReviewEntity(newReview, with: review)
}
try context.save()
} catch {
print(error)
print("Failed to insert/update review: \(error)")
}
}
func putReviews(_ reviews: [Review]) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
do {
for review in reviews {
fetchRequest.predicate = NSPredicate(format: "id == %lld", review.id)
let results = try context.fetch(fetchRequest)
let reviewEntity: ReviewEntity
if let existingReview = results.first {
// Update existing review
updateReviewEntity(existingReview, with: review)
} else {
let newReview = ReviewEntity(context: context)
newReview.id = review.id
updateReviewEntity(newReview, with: review)
}
}
try context.save()
} catch {
print(error)
print("Failed to insert/update reviews: \(error)")
}
}
private func updateReviewEntity(_ entity: ReviewEntity, with review: Review) {
entity.placeId = review.placeId
entity.rating = Int16(review.rating)
entity.userJson = DBUtils.encodeToJsonString(review.user.toUserEntity())
entity.date = review.date
entity.comment = review.comment
entity.picsUrlsJson = DBUtils.encodeToJsonString(review.picsUrls)
entity.deletionPlanned = review.deletionPlanned
}
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)
}
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)
do {
let reviews = try context.fetch(fetchRequest)
for review in reviews {
context.delete(review)
}
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)
// 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] ?? []
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
try context.save()
} catch {
print(error)
print("Failed to delete all places: \(error)")
}
}
func deleteAllPlaceReviews(placeId: Int64) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId)
do {
let reviews = try context.fetch(fetchRequest)
for review in reviews {
context.delete(review)
}
try context.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: "id", ascending: true)]
reviewsForPlaceFetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: container.viewContext,
sectionNameKeyPath: nil,
cacheName: nil
)
reviewsForPlaceFetchedResultsController?.delegate = self
do {
try reviewsForPlaceFetchedResultsController?.performFetch()
if let results = reviewsForPlaceFetchedResultsController?.fetchedObjects {
reviewsForPlaceSubject.send(results)
}
} catch {
print(error)
reviewsForPlaceSubject.send(completion: .failure(ResourceError.cacheError))
}
}
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)
if let review = reviews.first {
review.deletionPlanned = deletionPlanned
try context.save()
}
} catch {
print(error)
print("Failed to mark review for deletion: \(error)")
}
}
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)
} catch {
print(error)
print("Failed to fetch reviews planned for deletion: \(error)")
return []
}
}
// // MARK: - Planned Review Operations
func insertReviewPlannedToPost(_ review: ReviewPlannedToPostEntity) {
let context = container.viewContext
let newReview = ReviewPlannedToPostEntity(context: context)
// Set properties of newReview based on the input review
do {
try context.save()
} catch {
print(error)
print("Failed to insert planned review: \(error)")
}
}
func deleteReviewPlannedToPost(placeId: Int64) {
let context = container.viewContext
let fetchRequest: NSFetchRequest<ReviewPlannedToPostEntity> = ReviewPlannedToPostEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId)
do {
let reviews = try context.fetch(fetchRequest)
for review in reviews {
context.delete(review)
}
try context.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)
} catch {
print(error)
print("Failed to fetch planned reviews: \(error)")
return []
}
}
func observeReviewsPlannedToPost() {
let fetchRequest: NSFetchRequest<ReviewPlannedToPostEntity> = ReviewPlannedToPostEntity.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "placeId", ascending: true)]
reviewsPlannedToPostFetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: container.viewContext,
sectionNameKeyPath: nil,
cacheName: nil
)
reviewsPlannedToPostFetchedResultsController?.delegate = self
do {
try reviewsPlannedToPostFetchedResultsController?.performFetch()
if let results = reviewsPlannedToPostFetchedResultsController?.fetchedObjects {
reviewsPlannedToPostSubject.send(results)
}
} catch {
print(error)
reviewsPlannedToPostSubject.send(completion: .failure(ResourceError.cacheError))
}
}
// MARK: - NSFetchedResultsControllerDelegate
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let fetchedObjects = controller.fetchedObjects else {
return
}
switch controller {
case reviewsForPlaceFetchedResultsController:
reviewsForPlaceSubject.send(fetchedObjects as! [Review])
case reviewsPlannedToPostFetchedResultsController:
reviewsPlannedToPostSubject.send(fetchedObjects as! [ReviewPlannedToPostEntity])
default:
break
}
}
}

View file

@ -0,0 +1,192 @@
//import Combine
//
//class PlacePersistenceControllerTesterBro {
// private static var cancellables = Set<AnyCancellable>()
// private static let persistenceController = PlacesPersistenceController.shared
// private static let searchQuery = "place"
//
// static func testAllPlaces() {
// testSearchOperation()
// testPlacesByCatFetchOperation()
// testTopPlacesFetchOperation()
// testSinglePlaceFetchOperation()
// testFavoritePlacesFetchOperation()
// testCRUDOperations()
// }
//
// private static func testCRUDOperations() {
// print("Testing CRUD Operations...")
//
// // Example PlaceFull object
// let place = PlaceFull(
// id: 1,
// name: "Test Place",
// rating: 5,
// excerpt: "A great place",
// description: "Detailed description",
// placeLocation: nil,
// cover: Constants.imageUrlExample,
// pics: [Constants.imageUrlExample, Constants.imageUrlExample, Constants.anotherImageExample],
// reviews: nil,
// isFavorite: true
// )
//
// let place2 = PlaceFull(
// id: 2,
// name: "Test Place 2222",
// rating: 4.9,
// excerpt: "A great place",
// description: "Detailed description",
// placeLocation: nil,
// cover: Constants.imageUrlExample,
// pics: [Constants.imageUrlExample, Constants.imageUrlExample, Constants.anotherImageExample],
// reviews: nil,
// isFavorite: true
// )
//
// let place3 = PlaceFull(
// id: 3,
// name: "Test Place 3",
// rating: 5,
// excerpt: "A great place",
// description: "Detailed description",
// placeLocation: nil,
// cover: Constants.imageUrlExample,
// pics: [Constants.imageUrlExample, Constants.imageUrlExample, Constants.anotherImageExample],
// reviews: nil,
// isFavorite: false
// )
//
// var place4 = PlaceFull(
// id: 4,
// name: "Test Place 4",
// rating: 4,
// excerpt: "A great place",
// description: "Detailed description",
// placeLocation: nil,
// cover: Constants.imageUrlExample,
// pics: [Constants.imageUrlExample, Constants.imageUrlExample, Constants.anotherImageExample],
// reviews: nil,
// isFavorite: false
// )
//
// // Insert or update place
// DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// self.persistenceController.putPlaces([place], categoryId: 1)
// print("Inserted/Updated places with ID: \(place.id)")
// }
// DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// self.persistenceController.putPlaces([place2, place3], categoryId: 2)
// print("Inserted/Updated places with ID: \(place2.id), \(place3.id)")
// }
// DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
// self.persistenceController.putPlaces([place4], categoryId: 3)
// print("Inserted/Updated places with ID: \(place4.id)")
// }
// DispatchQueue.main.asyncAfter(deadline: .now() + 9) {
// place4.isFavorite = !place4.isFavorite
// self.persistenceController.putPlaces([place4], categoryId: 3)
// print("Inserted/Updated places with ID: \(place4.id)")
// }
// // Delete all
// DispatchQueue.main.asyncAfter(deadline: .now() + 0) {
// self.persistenceController.deleteAllPlaces()
// print("Deleted all places")
// }
// // Delete places by category (assuming `categoryId` is available)
// DispatchQueue.main.asyncAfter(deadline: .now() + 7) {
// self.persistenceController.deleteAllPlacesByCategory(categoryId: 3)
// print("Deleted places with category ID: 2")
// }
// }
//
// private static func testSearchOperation() {
// print("Testing Search Operation...")
// persistenceController.searchSubject
// .sink(receiveCompletion: { completion in
// if case .failure(let error) = completion {
// print("Search failed with error: \(error)")
// }
// }, receiveValue: { places in
// print("Search Results:")
// places.forEach { place in
// print("ID: \(place.id), Name: \(place.name ?? ""), Excerpt: \(place.excerpt ?? "No excerpt")")
// }
// })
// .store(in: &cancellables)
//
// persistenceController.observeSearch(query: searchQuery)
// }
//
// private static func testPlacesByCatFetchOperation() {
// print("Testing PlacesByCat Operation...")
// persistenceController.placesByCatSubject
// .sink(receiveCompletion: { completion in
// if case .failure(let error) = completion {
// print("PlacesByCat failed with error: \(error)")
// }
// }, receiveValue: { places in
// print("PlacesByCat Results:")
// places.forEach { place in
// print("ID: \(place.id), Name: \(place.name ?? ""), Excerpt: \(place.excerpt ?? "No excerpt")")
// }
// })
// .store(in: &cancellables)
//
// persistenceController.observePlacesByCategoryId(categoryId: 3)
// }
//
// private static func testTopPlacesFetchOperation() {
// print("Testing TopPlaces Operation...")
// persistenceController.topPlacesSubject
// .sink(receiveCompletion: { completion in
// if case .failure(let error) = completion {
// print("TopPlaces failed with error: \(error)")
// }
// }, receiveValue: { places in
// print("TopPlaces Results:")
// places.forEach { place in
// print("ID: \(place.id), Name: \(place.name ?? ""), Excerpt: \(place.excerpt ?? "No excerpt")")
// }
// })
// .store(in: &cancellables)
//
// persistenceController.observeTopPlacesByCategoryId(categoryId: 2)
// }
//
// private static func testSinglePlaceFetchOperation() {
// print("Testing SinglePlace Operation...")
// persistenceController.singlePlaceSubject
// .sink(receiveCompletion: { completion in
// if case .failure(let error) = completion {
// print("SinglePlace failed with error: \(error)")
// }
// }, receiveValue: { place in
// print("SinglePlace Results:")
// if let place = place {
// print("ID: \(place.id), Name: \(place.name ?? ""), Excerpt: \(place.excerpt ?? "No excerpt")")
// }
// })
// .store(in: &cancellables)
//
// persistenceController.observePlaceById(placeId: 1)
// }
//
// private static func testFavoritePlacesFetchOperation() {
// print("Testing FavoritePlaces Operation...")
// persistenceController.favoritePlacesSubject
// .sink(receiveCompletion: { completion in
// if case .failure(let error) = completion {
// print("FavoritePlaces failed with error: \(error)")
// }
// }, receiveValue: { places in
// print("FavoritePlaces Results:")
// places.forEach { place in
// print("ID: \(place.id), Name: \(place.name ?? ""), Excerpt: \(place.excerpt ?? "No excerpt")")
// }
// })
// .store(in: &cancellables)
//
// persistenceController.observeFavoritePlaces()
// }
//}

View file

@ -0,0 +1,144 @@
import Combine
import Foundation
class ReviewsPersistenceControllerTesterBro {
private static var cancellables = Set<AnyCancellable>()
private static let persistenceController = ReviewsPersistenceController.shared
static func testAllReviews() {
testCRUDOperations()
testObserveReviewsForPlace()
testMarkReviewForDeletion()
testGetReviewsPlannedForDeletion()
}
private static func testCRUDOperations() {
print("Testing CRUD Operations...")
// Example Review objects
let review1 = Review(
id: 1,
placeId: 1,
rating: 5,
user: User(id: 1, name: "John Doe", pfpUrl: Constants.imageUrlExample, countryCodeName: "us"),
date: "01-01-2000",
comment: "Great place!",
picsUrls: [Constants.imageUrlExample,Constants.imageUrlExample,Constants.anotherImageExample],
deletionPlanned: false
)
let review2 = Review(
id: 2,
placeId: 1,
rating: 4,
user: User(id: 1, name: "John Doe", pfpUrl: Constants.anotherImageExample, countryCodeName: "us"),
date: "01-01-2000",
comment: "Nice atmosphere",
picsUrls: [Constants.imageUrlExample],
deletionPlanned: false
)
let review3 = Review(
id: 3,
placeId: 1,
rating: 4,
user: User(id: 1, name: "John Doe", pfpUrl: Constants.anotherImageExample, countryCodeName: "us"),
date: "01-01-2000",
comment: "Nice atmosphere",
picsUrls: [Constants.imageUrlExample],
deletionPlanned: false
)
// Insert reviews
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.persistenceController.putReview(review1)
print("Inserted review with ID: \(review1.id)")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.persistenceController.putReviews([review1, review2])
print("Inserted reviews with IDs: \(review1.id), \(review2.id)")
}
// Delete review
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
self.persistenceController.deleteReview(id: review1.id)
print("Deleted review with ID: \(review1.id)")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.persistenceController.putReviews([review1, review2])
print("Inserted reviews with IDs: \(review1.id), \(review2.id)")
}
// Delete multiple reviews
DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
self.persistenceController.deleteReviews(ids: [review1.id, review2.id])
print("Deleted reviews with IDs: \(review1.id), \(review2.id)")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 7) {
self.persistenceController.putReviews([review1, review2, review3])
print("Inserted reviews with IDs: \(review1.id), \(review2.id), \(review3.id)")
}
// Delete all reviews for a place
DispatchQueue.main.asyncAfter(deadline: .now() + 8) {
self.persistenceController.deleteAllPlaceReviews(placeId: 1)
print("Deleted all reviews for place with ID: 1")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 9) {
self.persistenceController.putReviews([review1, review2, review3])
print("Inserted reviews with IDs: \(review1.id), \(review2.id), \(review3.id)")
}
// Delete all reviews
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
self.persistenceController.deleteAllReviews()
print("Deleted all reviews")
}
}
private static func testObserveReviewsForPlace() {
print("Testing Observe Reviews for Place...")
persistenceController.reviewsForPlaceSubject
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Observe reviews failed with error: \(error)")
}
}, receiveValue: { reviews in
print("Reviews for Place Results:")
reviews.forEach { review in
print("ID: \(review.id), PlaceID: \(review.placeId), Rating: \(review.rating), Comment: \(review.comment ?? "No comment"), deletionPlanned: \(review.deletionPlanned)")
}
})
.store(in: &cancellables)
persistenceController.observeReviewsForPlace(placeId: 1)
}
private static func testMarkReviewForDeletion() {
print("Testing Mark Review for Deletion...")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.persistenceController.markReviewForDeletion(id: 1, deletionPlanned: true)
print("Marked review with ID 1 for deletion")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.persistenceController.markReviewForDeletion(id: 1, deletionPlanned: false)
print("Unmarked review with ID 1 for deletion")
}
}
private static func testGetReviewsPlannedForDeletion() {
print("Testing Get Reviews Planned for Deletion...")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
let reviewsPlannedForDeletion = self.persistenceController.getReviewsPlannedForDeletion()
print("Reviews planned for deletion:")
reviewsPlannedForDeletion.forEach { review in
print("ID: \(review.id), PlaceID: \(review.placeId), Rating: \(review.rating), Comment: \(review.comment ?? "No comment"), deletionPlanned: \(review.deletionPlanned)")
}
}
}
}

View file

@ -13,8 +13,8 @@ struct APIEndpoints {
static let updateThemeUrl = "\(BASE_URL)profile/theme"
// MARK: - Places
static func getPlacesByCategoryUrl(id: Int64) -> String {
return "\(BASE_URL)marks/\(id)"
static func getPlacesByCategoryUrl(id: Int64, hash: String) -> String {
return "\(BASE_URL)marks/\(id)?hash=\(hash)"
}
static let getAllPlacesUrl = "\(BASE_URL)marks/all"

View file

@ -1,28 +1,61 @@
import Foundation
struct PlaceDTO: Codable {
let id: Int64
let name: String
let coordinates: CoordinatesDTO?
let cover: String
let feedbacks: [ReviewDTO]?
let gallery: [String]
let rating: String
let shortDescription: String
let longDescription: String
func toPlaceFull(isFavorite: Bool) -> PlaceFull {
return PlaceFull(
id: id,
name: name,
rating: Double(rating) ?? 0.0,
excerpt: shortDescription,
description: longDescription,
placeLocation: coordinates?.toPlaceLocation(name: name),
cover: cover,
pics: gallery,
reviews: feedbacks?.map { $0.toReview() } ?? [],
isFavorite: isFavorite
)
}
let id: Int64
let name: String
let coordinates: CoordinatesDTO?
let cover: String
let feedbacks: [ReviewDTO]?
let gallery: [String]
let rating: Double
let shortDescription: String
let longDescription: String
enum CodingKeys: String, CodingKey {
case id, name, coordinates, cover, feedbacks, gallery, rating, shortDescription, longDescription
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int64.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
coordinates = try container.decodeIfPresent(CoordinatesDTO.self, forKey: .coordinates)
cover = try container.decode(String.self, forKey: .cover)
feedbacks = try container.decodeIfPresent([ReviewDTO].self, forKey: .feedbacks)
gallery = try container.decode([String].self, forKey: .gallery)
rating = try container.decode(FlexibleDouble.self, forKey: .rating).value
shortDescription = try container.decode(String.self, forKey: .shortDescription)
longDescription = try container.decode(String.self, forKey: .longDescription)
}
func toPlaceFull(isFavorite: Bool) -> PlaceFull {
return PlaceFull(
id: id,
name: name,
rating: rating,
excerpt: shortDescription,
description: longDescription,
placeLocation: coordinates?.toPlaceLocation(name: name),
cover: cover,
pics: gallery,
reviews: feedbacks?.map { $0.toReview() } ?? [],
isFavorite: isFavorite
)
}
}
struct FlexibleDouble: Codable {
let value: Double
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let doubleValue = try? container.decode(Double.self) {
value = doubleValue
} else if let stringValue = try? container.decode(String.self),
let doubleValue = Double(stringValue) {
value = doubleValue
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unable to decode double value")
}
}
}

View file

@ -1,6 +1,32 @@
import Combine
protocol PlacesService {
func getPlacesByCategory()
func getPlacesByCategory(id: Int64, hash: String) async throws -> CategoryDTO
func getAllPlaces() async throws -> AllDataDTO
func getFavorites() async throws -> FavoritesDTO
func addFavorites(ids: FavoritesIdsDTO) async throws -> SimpleResponse
func removeFromFavorites(ids: FavoritesIdsDTO) async throws -> SimpleResponse
}
class PlacesServiceImpl : PlacesService {
func getPlacesByCategory(id: Int64, hash: String) async throws -> CategoryDTO {
return try await AppNetworkHelper.get(path: APIEndpoints.getPlacesByCategoryUrl(id: id, hash: hash))
}
func getAllPlaces() async throws -> AllDataDTO {
return try await AppNetworkHelper.get(path: APIEndpoints.getAllPlacesUrl)
}
func getFavorites() async throws -> FavoritesDTO {
return try await AppNetworkHelper.get(path: APIEndpoints.getFavoritesUrl)
}
func addFavorites(ids: FavoritesIdsDTO) async throws -> SimpleResponse {
return try await AppNetworkHelper.post(path: APIEndpoints.addFavoritesUrl, body: ids)
}
func removeFromFavorites(ids: FavoritesIdsDTO) async throws -> SimpleResponse {
return try await AppNetworkHelper.delete(path: APIEndpoints.removeFromFavoritesUrl, body: ids)
}
}

View file

@ -114,19 +114,27 @@ class ProfileServiceImpl: ProfileService {
func updateLanguage(code: String) {
Task {
await AppNetworkHelper.put(
path: APIEndpoints.updateLanguageUrl,
body: LanguageDTO(language: code)
) as Result<SimpleResponse, ResourceError>
do {
let response: SimpleResponse = try await AppNetworkHelper.put(
path: APIEndpoints.updateLanguageUrl,
body: LanguageDTO(language: code)
)
} catch {
print("Failed to update language on server")
}
}
}
func updateTheme(code: String) {
Task {
await AppNetworkHelper.put(
path: APIEndpoints.updateThemeUrl,
body: ThemeDTO(theme: code)
) as Result<SimpleResponse, ResourceError>
do {
let response: SimpleResponse = try await AppNetworkHelper.put(
path: APIEndpoints.updateThemeUrl,
body: ThemeDTO(theme: code)
)
} catch {
print("Failed to update theme on server")
}
}
}
}

View file

@ -1,9 +1,21 @@
//
// ReviewsService.swift
// OMaps
//
// Created by Macbook Pro on 04/09/24.
// Copyright © 2024 Organic Maps. All rights reserved.
//
import Combine
import Foundation
protocol ReviewsService {
func getReviewsByPlaceId(id: Int64) async throws -> ReviewsDTO
func postReview(review: ReviewToPost) async throws -> ReviewDTO
func deleteReview(feedbacks: ReviewIdsDTO) async throws -> SimpleResponse
}
class ReviewsServiceImpl : ReviewsService {
func getReviewsByPlaceId(id: Int64) async throws -> ReviewsDTO {
return try await AppNetworkHelper.get(path: APIEndpoints.getReviewsByPlaceIdUrl(id: id))
}
func postReview(review: ReviewToPost) async throws -> ReviewDTO {
return try await AppNetworkHelper.post(path: APIEndpoints.postReviewUrl, body: review)
}
func deleteReview(feedbacks: ReviewIdsDTO) async throws -> SimpleResponse {
return try await AppNetworkHelper.delete(path: APIEndpoints.deleteReviewsUrl, body: feedbacks)
}
}

View file

@ -22,6 +22,7 @@ class CombineNetworkHelper {
let jsonData = try AppNetworkHelper.encodeRequestBody(body)
return performRequest(url: url, method: "POST", body: jsonData, headers: headers, decoder: decoder)
} catch {
print(error)
return Fail(error: ResourceError.other(message: "Encoding error: \(error)")).eraseToAnyPublisher()
}
}
@ -62,12 +63,12 @@ class AppNetworkHelper {
path: String,
headers: [String: String] = [:],
decoder: JSONDecoder = JSONDecoder()
) async -> Result<T, ResourceError> {
) async throws -> T {
guard let url = URL(string: path) else {
return .failure(.other(message: "Invalid URL"))
throw ResourceError.other(message: "Invalid URL")
}
return await performRequest(
return try await performRequest(
url: url,
method: "GET",
headers: headers,
@ -81,14 +82,14 @@ class AppNetworkHelper {
body: U,
headers: [String: String] = [:],
decoder: JSONDecoder = JSONDecoder()
) async -> Result<T, ResourceError> {
) async throws -> T {
guard let url = URL(string: path) else {
return .failure(.other(message: "Invalid URL"))
throw ResourceError.other(message: "Invalid URL")
}
do {
let jsonData = try AppNetworkHelper.encodeRequestBody(body)
return await performRequest(
return try await performRequest(
url: url,
method: "POST",
body: jsonData,
@ -96,7 +97,8 @@ class AppNetworkHelper {
decoder: decoder
)
} catch {
return .failure(ResourceError.other(message: "Encoding error"))
print(error)
throw ResourceError.other(message: "Encoding error")
}
}
@ -104,12 +106,12 @@ class AppNetworkHelper {
path: String,
headers: [String: String] = [:],
decoder: JSONDecoder = JSONDecoder()
) async -> Result<T, ResourceError> {
) async throws -> T {
guard let url = URL(string: path) else {
return .failure(.other(message: "Invalid URL"))
throw ResourceError.other(message: "Invalid URL")
}
return await performRequest(
return try await performRequest(
url: url,
method: "POST",
headers: headers,
@ -122,14 +124,14 @@ class AppNetworkHelper {
body: U,
headers: [String: String] = [:],
decoder: JSONDecoder = JSONDecoder()
) async -> Result<T, ResourceError> {
) async throws -> T {
guard let url = URL(string: path) else {
return .failure(.other(message: "Invalid URL"))
throw ResourceError.other(message: "Invalid URL")
}
do {
let jsonData = try AppNetworkHelper.encodeRequestBody(body)
return await performRequest(
return try await performRequest(
url: url,
method: "PUT",
body: jsonData,
@ -137,7 +139,33 @@ class AppNetworkHelper {
decoder: decoder
)
} catch {
return .failure(ResourceError.other(message: "Encoding error"))
print(error)
throw ResourceError.other(message: "Encoding error")
}
}
static func delete<T: Decodable, U: Encodable>(
path: String,
body: U,
headers: [String: String] = [:],
decoder: JSONDecoder = JSONDecoder()
) async throws -> T {
guard let url = URL(string: path) else {
throw ResourceError.other(message: "Invalid URL")
}
do {
let jsonData = try AppNetworkHelper.encodeRequestBody(body)
return try await performRequest(
url: url,
method: "DELETE",
body: jsonData,
headers: headers,
decoder: decoder
)
} catch {
print(error)
throw ResourceError.other(message: "Encoding error")
}
}
@ -147,8 +175,10 @@ class AppNetworkHelper {
body: Data? = nil,
headers: [String: String] = [:],
decoder: JSONDecoder
) async -> Result<T, ResourceError> {
var request = createRequest(url: url, method: method, headers: headers, body: body)
) async throws -> T {
let loggingInfo = "url: \(url), \nwith method: \(method)"
print("Performing request on\n\(loggingInfo)")
let request = createRequest(url: url, method: method, headers: headers, body: body)
do {
let (data, response) = try await URLSession.shared.data(for: request)
@ -156,12 +186,15 @@ class AppNetworkHelper {
// Handle response and decode data
do {
let decodedData: T = try handleResponse(data: data, response: response, decoder: decoder)
return .success(decodedData)
print("handling response \n\(loggingInfo)")
return decodedData
} catch {
return .failure(.other(message: "Failed to handle response: \(error.localizedDescription)"))
print(error)
throw ResourceError.other(message: "Failed to handle response: \(error.localizedDescription) on \n\(loggingInfo)")
}
} catch {
return .failure(handleMappingError(error))
print("error: \(error)")
throw handleMappingError(error)
}
}

View file

@ -1,38 +1,206 @@
import Combine
class PlacesRepositoryImpl: PlacesRepository {
func downloadAllData() -> AnyPublisher<SimpleResponse, ResourceError> {
// TODO: cmon
return PassthroughSubject<SimpleResponse, ResourceError>().eraseToAnyPublisher()
let placesService: PlacesService
let placesPersistenceController: PlacesPersistenceController
let reviewsPersistenceController: ReviewsPersistenceController
let hashesPersistenceController: HashesPersistenceController
var downloadProgress: PassthroughSubject<DownloadProgress, ResourceError>
var searchResource: PassthroughSubject<[PlaceShort], ResourceError>
var placesByCategoryResource: PassthroughSubject<[PlaceShort], ResourceError>
var topSightsResource: PassthroughSubject<[PlaceShort], ResourceError>
var topRestaurantsResource: PassthroughSubject<[PlaceShort], ResourceError>
var placeResource: PassthroughSubject<PlaceFull, ResourceError>
var favoritesResource: PassthroughSubject<[PlaceShort], ResourceError>
init(
placesService: PlacesService,
placesPersistenceController: PlacesPersistenceController,
reviewsPersistenceController: ReviewsPersistenceController,
hashesPersistenceController: HashesPersistenceController
) {
self.placesService = placesService
self.placesPersistenceController = placesPersistenceController
self.hashesPersistenceController = hashesPersistenceController
self.reviewsPersistenceController = reviewsPersistenceController
downloadProgress = PassthroughSubject<DownloadProgress, ResourceError>()
downloadProgress.send(DownloadProgress.idle)
searchResource = placesPersistenceController.searchSubject
placesByCategoryResource = placesPersistenceController.placesByCatSubject
topSightsResource = placesPersistenceController.topSightsSubject
topRestaurantsResource = placesPersistenceController.topRestaurantsSubject
placeResource = placesPersistenceController.singlePlaceSubject
favoritesResource = placesPersistenceController.favoritePlacesSubject
}
func search(query: String) -> AnyPublisher<[PlaceShort], ResourceError> {
// TODO: cmon
return PassthroughSubject<[PlaceShort], ResourceError>().eraseToAnyPublisher()
func downloadAllData() async throws {
do {
let hashes = hashesPersistenceController.getHashes()
let favoritesDto = try await placesService.getFavorites()
if(hashes.isEmpty) {
downloadProgress.send(DownloadProgress.loading)
let allData = try await placesService.getAllPlaces()
// get data
let favorites = favoritesDto.data.map { placeDto in
placeDto.toPlaceFull(isFavorite: true)
}
var reviews: [Review] = []
func toPlaceFull(placeDto: PlaceDTO) -> PlaceFull {
var placeFull = placeDto.toPlaceFull(isFavorite: false)
placeFull.isFavorite = favorites.contains { $0.id == placeFull.id }
if let placeReviews = placeFull.reviews {
reviews.append(contentsOf: placeReviews)
}
return placeFull
}
let sights = allData.attractions.map { placeDto in
toPlaceFull(placeDto: placeDto)
}
let restaurants = allData.restaurants.map { placeDto in
toPlaceFull(placeDto: placeDto)
}
let hotels = allData.accommodations.map { placeDto in
toPlaceFull(placeDto: placeDto)
}
// update places
placesPersistenceController.deleteAllPlaces()
placesPersistenceController.putPlaces(sights, categoryId: PlaceCategory.sights.id)
placesPersistenceController.putPlaces(restaurants, categoryId: PlaceCategory.restaurants.id)
placesPersistenceController.putPlaces(hotels, categoryId: PlaceCategory.hotels.id)
// update reviews
reviewsPersistenceController.deleteAllReviews()
reviewsPersistenceController.putReviews(reviews)
// update favorites
favorites.forEach { favorite in
placesPersistenceController.setFavorite(placeId: favorite.id, isFavorite: favorite.isFavorite)
}
// update hashes
hashesPersistenceController.putHashes(hashes: [
Hash(categoryId: PlaceCategory.sights.id, value: allData.attractionsHash),
Hash(categoryId: PlaceCategory.restaurants.id, value: allData.restaurantsHash),
Hash(categoryId: PlaceCategory.hotels.id, value: allData.accommodationsHash)
])
// return response
downloadProgress.send(DownloadProgress.success)
} else {
downloadProgress.send(DownloadProgress.success)
}
} catch let error as ResourceError {
downloadProgress.send(completion: .failure(error))
}
}
var placesByCategoryResource = PassthroughSubject<[PlaceShort], ResourceError>()
func getPlacesByCategoryAndUpdate(id: Int64) {
// TODO: cmon
func observeSearch(query: String) {
placesPersistenceController.observeSearch(query: query)
}
var topPlacesResource = PassthroughSubject<[PlaceShort], ResourceError>()
func getTopPlaces(is: Int64) {
// TODO: cmon
func observePlacesByCategoryAndUpdate(categoryId: Int64) {
placesPersistenceController.observePlacesByCategoryId(categoryId: categoryId)
Task {
try await getPlacesByCategoryFromApiIfThereIsChange(categoryId)
}
}
var placeResource = PassthroughSubject<PlaceFull, ResourceError>()
func getPlaceById(is: Int64) {
// TODO: cmon
func observeTopSightsAndUpdate() {
placesPersistenceController.observeTopSights()
Task {
try await getPlacesByCategoryFromApiIfThereIsChange(PlaceCategory.sights.id)
}
}
var favoritesResource = PassthroughSubject<[PlaceShort], ResourceError>()
func getFavorites(query: String) {
// TODO: cmon
func observeTopRestaurantsAndUpdate() {
placesPersistenceController.observeTopRestaurants()
Task {
try await getPlacesByCategoryFromApiIfThereIsChange(PlaceCategory.restaurants.id)
}
}
private func getPlacesByCategoryFromApiIfThereIsChange(
_ categoryId: Int64
) async throws -> Void {
let hash = hashesPersistenceController.getHash(categoryId: categoryId)
let favorites = placesPersistenceController.getFavoritePlaces()
let resource =
try await placesService.getPlacesByCategory(id: categoryId, hash: hash?.value ?? "")
let places = resource.data
if (hash != nil && !places.isEmpty) {
// update places
placesPersistenceController.deleteAllPlacesByCategory(categoryId: categoryId)
let places = places.map { placeDto in
var placeFull = placeDto.toPlaceFull(isFavorite: false)
placeFull.isFavorite = favorites.contains { $0.id == placeFull.id }
return placeFull
}
placesPersistenceController.putPlaces(places, categoryId: categoryId)
// update reviews
var reviews = [Review]()
places.forEach { place in
reviews.append(contentsOf: place.reviews ?? [])
}
reviewsPersistenceController.deleteAllReviews()
reviewsPersistenceController.putReviews(reviews)
// update hash
hashesPersistenceController.putHash(
Hash(categoryId: hash!.categoryId, value: resource.hash),
shouldSave: true
)
}
}
func observePlaceById(_ id: Int64) {
placesPersistenceController.observePlaceById(placeId: id)
}
func observeFavorites(query: String) {
placesPersistenceController.observeFavoritePlaces(query: query)
}
func setFavorite(placeId: Int64, isFavorite: Bool) {
// TODO: cmon
placesPersistenceController.setFavorite(placeId: placeId, isFavorite: isFavorite)
placesPersistenceController.addFavoriteSync(
placeId: placeId,
isFavorite: isFavorite
)
Task {
let favoritesIdsDto = FavoritesIdsDTO(marks: [placeId])
do {
if(isFavorite) {
try await placesService.addFavorites(ids: favoritesIdsDto)
} else {
try await placesService.removeFromFavorites(ids: favoritesIdsDto)
}
placesPersistenceController.removeFavoriteSync(placeIds: [placeId])
} catch {
placesPersistenceController.addFavoriteSync(placeId: placeId, isFavorite: isFavorite)
}
}
}
func syncFavorites() {

View file

@ -1,16 +1,41 @@
import Combine
class ReviewsRepositoryImpl : ReviewsRepository {
var reviewsResource = PassthroughSubject<[Review], ResourceError>()
var reviewsPersistenceController: ReviewsPersistenceController
var reviewsService: ReviewsService
func getReviewsForPlace(id: Int64) {
// TODO: cmon
var reviewsResource: PassthroughSubject<[Review], ResourceError>
var isThereReviewPlannedToPublishResource = PassthroughSubject<Bool, Never>()
init(
reviewsPersistenceController: ReviewsPersistenceController,
reviewsService: ReviewsService,
reviewsResource: PassthroughSubject<[Review], ResourceError>
) {
self.reviewsPersistenceController = reviewsPersistenceController
self.reviewsService = reviewsService
self.reviewsResource = reviewsPersistenceController.reviewsForPlaceSubject
reviewsPersistenceController.reviewsPlannedToPostSubject.sink { completion in } receiveValue: { reviews in
self.isThereReviewPlannedToPublishResource.send(reviews.isEmpty)
}
}
var isThereReviewPlannedToPublishPassthroughSubject = PassthroughSubject<[Review], ResourceError>()
func observeReviewsForPlace(id: Int64) {
reviewsPersistenceController.observeReviewsForPlace(placeId: id)
Task {
let reviewsDTO = try await reviewsService.getReviewsByPlaceId(id: id)
let reviews = reviewsDTO.data.map { reviewDto in reviewDto.toReview() }
reviewsPersistenceController.deleteAllPlaceReviews(placeId: id)
reviewsPersistenceController.putReviews(reviews)
}
}
func isThereReviewPlannedToPublish(id: Int64) {
// TODO: cmon
func isThereReviewPlannedToPublish(for placeId: Int64) {
reviewsPersistenceController.getReviewsPlannedToPost()
}
func postReview(review: ReviewToPost) -> AnyPublisher<SimpleResponse, ResourceError> {

View file

@ -5,6 +5,10 @@ enum PlaceCategory: String, Codable {
case restaurants = "2"
case hotels = "3"
var id: Int64 {
Int64(self.rawValue)!
}
var serverName: String {
switch self {
case .sights:

View file

@ -5,4 +5,13 @@ struct User: Codable, Hashable {
let name: String
let pfpUrl: String?
let countryCodeName: String
func toUserEntity() -> UserEntity {
return UserEntity(
userId: self.id,
fullName: self.name,
avatar: self.pfpUrl ?? "",
country: self.countryCodeName
)
}
}

View file

@ -0,0 +1,6 @@
enum DownloadProgress: Equatable {
case idle
case loading
case success
case error
}

View file

@ -1,21 +1,26 @@
import Combine
protocol PlacesRepository {
func downloadAllData() -> AnyPublisher<SimpleResponse, ResourceError>
var downloadProgress: PassthroughSubject<DownloadProgress, ResourceError> { get }
func downloadAllData() async throws
func search(query: String) -> AnyPublisher<[PlaceShort], ResourceError>
var searchResource: PassthroughSubject<[PlaceShort], ResourceError> { get }
func observeSearch(query: String)
var placesByCategoryResource: PassthroughSubject<[PlaceShort], ResourceError> { get }
func getPlacesByCategoryAndUpdate(id: Int64)
func observePlacesByCategoryAndUpdate(categoryId: Int64)
var topPlacesResource: PassthroughSubject<[PlaceShort], ResourceError> { get }
func getTopPlaces(is: Int64)
var topSightsResource: PassthroughSubject<[PlaceShort], ResourceError> { get }
func observeTopSightsAndUpdate()
var topRestaurantsResource: PassthroughSubject<[PlaceShort], ResourceError> { get }
func observeTopRestaurantsAndUpdate()
var placeResource: PassthroughSubject<PlaceFull, ResourceError> { get }
func getPlaceById(is: Int64)
func observePlaceById(_ id: Int64)
var favoritesResource: PassthroughSubject<[PlaceShort], ResourceError> { get }
func getFavorites(query: String)
func observeFavorites(query: String)
func setFavorite(placeId: Int64, isFavorite: Bool)

View file

@ -2,10 +2,10 @@ import Combine
protocol ReviewsRepository {
var reviewsResource: PassthroughSubject<[Review], ResourceError> { get }
func getReviewsForPlace(id: Int64)
func observeReviewsForPlace(id: Int64)
var isThereReviewPlannedToPublishPassthroughSubject: PassthroughSubject<[Review], ResourceError> { get }
func isThereReviewPlannedToPublish(id: Int64)
var isThereReviewPlannedToPublishResource: PassthroughSubject<Bool, Never> { get }
func isThereReviewPlannedToPublish(for placeId: Int64)
func postReview(review: ReviewToPost) -> AnyPublisher<SimpleResponse, ResourceError>

View file

@ -4,22 +4,13 @@ import SDWebImageSwiftUI
struct LoadImageView: View {
let url: String?
@State var isError = false
var body: some View {
if let urlString = url {
let errorImage = Image(systemName: "error_centered")
WebImage(url: URL(string: urlString))
.resizable()
.onSuccess(perform: { image, data, cacheType in
isError = false
})
.onFailure(perform: { error in
isError = true
})
.indicator(.activity)
.scaledToFill()
.transition(.fade(duration: 0.2))
.resizable()
.indicator(.activity)
.scaledToFill()
.transition(.fade(duration: 0.2))
} else {
Text(L("no_image"))
.foregroundColor(Color.surface)
@ -27,16 +18,8 @@ struct LoadImageView: View {
}
}
struct ContentView: View {
let imageUrl = Constants.imageUrlExample
var body: some View {
LoadImageView(url: imageUrl)
}
}
struct ContentView_Previews: PreviewProvider {
struct LoadImageView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
LoadImageView(url: Constants.imageUrlExample)
}
}

View file

@ -1,4 +1,5 @@
import SwiftUI
import WebKit
struct PlacesItem: View {
let place: PlaceShort

View file

@ -24,8 +24,9 @@ class CategoriesViewController: UIViewController {
CategoriesScreen(
categoriesVM: categoriesVM,
goToSearchScreen: { query in
let destinationVC = SearchViewController(searchVM: self.searchVM)
self.navigationController?.pushViewController(destinationVC, animated: true)
self.searchVM.query = query
let destinationVC = SearchViewController(searchVM: self.searchVM)
self.navigationController?.pushViewController(destinationVC, animated: true)
},
goToPlaceScreen: { id in
self.goToPlaceScreen(id: id)

View file

@ -1,6 +1,10 @@
import Combine
class CategoriesViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
private let placesRepository: PlacesRepository
@Published var query = ""
func clearQuery() { query = "" }
@ -19,21 +23,35 @@ class CategoriesViewModel: ObservableObject {
@Published var places: [PlaceShort] = []
init() {
init(placesRepository: PlacesRepository) {
self.placesRepository = placesRepository
if let firstCategory = categories.first {
self.selectedCategory = firstCategory
}
// TODO: put real data
places = [
PlaceShort(id: 1, name: "sight 1", cover: Constants.imageUrlExample, rating: 4.5, excerpt: "yep, just a placeyep, just a placeyep, just a placeyep, just a placeyep, just a placejust a placeyep, just a placejust a placeyep, just a placejust a placeyep, just a placejust a placeyep, just a place", isFavorite: false),
PlaceShort(id: 2, name: "sight 2", cover: Constants.imageUrlExample, rating: 4.0, excerpt: "yep, just a place", isFavorite: true)
]
observeCategoryPlaces()
}
func observeCategoryPlaces() {
placesRepository.placesByCategoryResource.sink { completion in
if case let .failure(error) = completion {
// nothing
}
} receiveValue: { places in
self.places = places
}
.store(in: &cancellables)
$selectedCategory.sink { category in
if let id = category?.id.id {
self.placesRepository.observePlacesByCategoryAndUpdate(categoryId: id)
}
}
.store(in: &cancellables)
}
func toggleFavorite(for placeId: Int64, isFavorite: Bool) {
if let index = places.firstIndex(where: { $0.id == placeId }) {
places[index].isFavorite = isFavorite
}
placesRepository.setFavorite(placeId: placeId, isFavorite: isFavorite)
}
}

View file

@ -1,7 +1,19 @@
import SwiftUI
class FavoritesViewController: UIViewController {
private var favoritesVM: FavoritesViewModel = FavoritesViewModel()
private var favoritesVM: FavoritesViewModel
init(favoritesVM: FavoritesViewModel) {
self.favoritesVM = favoritesVM
super.init(
nibName: nil,
bundle: nil
)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()

View file

@ -1,23 +1,40 @@
import Combine
class FavoritesViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
private let placesRepository: PlacesRepository
@Published var query = ""
func clearQuery() { query = "" }
@Published var places: [PlaceShort] = []
init() {
// TODO: put real data
places = [
PlaceShort(id: 1, name: "sight 1", cover: Constants.imageUrlExample, rating: 4.5, excerpt: "yep, just a placeyep, just a placeyep, just a placeyep, just a placeyep, just a placejust a placeyep, just a placejust a placeyep, just a placejust a placeyep, just a placejust a placeyep, just a place", isFavorite: false),
PlaceShort(id: 2, name: "sight 2", cover: Constants.imageUrlExample, rating: 4.0, excerpt: "yep, just a place", isFavorite: true)
]
init(placesRepository: PlacesRepository) {
self.placesRepository = placesRepository
self.placesRepository.observeFavorites(query: query)
observeFavorites()
}
func observeFavorites() {
placesRepository.favoritesResource.sink { completion in
if case let .failure(error) = completion {
// nothing
}
} receiveValue: { places in
self.places = places
}
.store(in: &cancellables)
$query.sink { q in
self.placesRepository.observeFavorites(query: q)
}
.store(in: &cancellables)
}
func toggleFavorite(for placeId: Int64, isFavorite: Bool) {
if let index = places.firstIndex(where: { $0.id == placeId }) {
places[index].isFavorite = isFavorite
}
placesRepository.setFavorite(placeId: placeId, isFavorite: isFavorite)
}
}

View file

@ -1,6 +1,11 @@
import Combine
class HomeViewModel : ObservableObject {
private let placesRepository: PlacesRepository
@Published var downloadProgress = DownloadProgress.idle
@Published var errorMessage = ""
@Published var query = ""
func clearQuery() { query = "" }
@ -8,16 +13,58 @@ class HomeViewModel : ObservableObject {
@Published var sights: [PlaceShort]? = nil
@Published var restaurants: [PlaceShort]? = nil
init() {
// TODO: put real data
sights = [
PlaceShort(id: 1, name: "sight 1", cover: Constants.imageUrlExample, rating: 4.5, excerpt: "yep, just a placeyep, just a placeyep, just a placeyep, just a placeyep, just a place", isFavorite: false),
PlaceShort(id: 2, name: "sight 2", cover: Constants.imageUrlExample, rating: 4.0, excerpt: "yep, just a place", isFavorite: true)
]
private var cancellables = Set<AnyCancellable>()
init(placesRepository: PlacesRepository) {
self.placesRepository = placesRepository
restaurants = [
PlaceShort(id: 1, name: "restaurant 1", cover: Constants.imageUrlExample, rating: 4.5, excerpt: "yep, just a placeyep, just a placeyep, just a placeyep, just a placeyep, just a place", isFavorite: false),
PlaceShort(id: 2, name: "restaurant 2", cover: Constants.imageUrlExample, rating: 4.0, excerpt: "yep, just a place", isFavorite: true)
]
downloadAllDataIfDidnt()
observeDownloadProgress()
observeTopSightsAndUpdate()
observeTopRestaurantsAndUpdate()
}
func observeDownloadProgress() {
placesRepository.downloadProgress.sink { completion in
if case let .failure(error) = completion {
Task { await MainActor.run {
self.downloadProgress = .error
}}
self.errorMessage = error.errorDescription
}
} receiveValue: { progress in
Task { await MainActor.run {
self.downloadProgress = progress
}}
}
.store(in: &cancellables)
}
func downloadAllDataIfDidnt() {
Task {
try await placesRepository.downloadAllData()
}
}
func observeTopSightsAndUpdate() {
placesRepository.topSightsResource.sink { _ in } receiveValue: { places in
self.sights = places
}
.store(in: &cancellables)
placesRepository.observeTopSightsAndUpdate()
}
func observeTopRestaurantsAndUpdate() {
placesRepository.topRestaurantsResource.sink { _ in } receiveValue: { places in
self.restaurants = places
}
.store(in: &cancellables)
placesRepository.observeTopRestaurantsAndUpdate()
}
func toggleFavorite(for placeId: Int64, isFavorite: Bool) {
placesRepository.setFavorite(placeId: placeId, isFavorite: isFavorite)
}
}

View file

@ -44,33 +44,37 @@ struct Place: View {
let height = 250.0
var body: some View {
ZStack(alignment: .bottom) {
ZStack() {
LoadImageView(url: place.cover)
HStack() {
VStack(alignment: .leading) {
Text(place.name)
.font(.semiBold(size: 15))
.foregroundColor(.white)
.lineLimit(2)
VerticalSpace(height: 4)
HStack(alignment: .center) {
Text(String(format: "%.1f", place.rating ?? 0.0))
VStack {
Spacer()
HStack() {
VStack(alignment: .leading) {
Text(place.name)
.font(.semiBold(size: 15))
.foregroundColor(.white)
Image(systemName: "star.fill")
.resizable()
.foregroundColor(Color.starYellow)
.frame(width: 10, height: 10)
.lineLimit(2)
VerticalSpace(height: 4)
HStack(alignment: .center) {
Text(String(format: "%.1f", place.rating ?? 0.0))
.font(.semiBold(size: 15))
.foregroundColor(.white)
Image(systemName: "star.fill")
.resizable()
.foregroundColor(Color.starYellow)
.frame(width: 10, height: 10)
}
}
.padding(12)
Spacer()
}
.padding(12)
Spacer()
.frame(width: width)
.background(SwiftUI.Color.black.opacity(0.5))
}
.frame(width: width)
.background(SwiftUI.Color.black.opacity(0.5))
HStack {
Spacer()
VStack {

View file

@ -2,11 +2,13 @@ import SwiftUI
import Combine
class HomeViewController: UIViewController {
private var homeVM: HomeViewModel
private var categoriesVM: CategoriesViewModel
private var searchVM: SearchViewModel
private var goToCategoriesTab: () -> Void
init(categoriesVM: CategoriesViewModel, searchVM: SearchViewModel, goToCategoriesTab: @escaping () -> Void) {
init(homeVM: HomeViewModel, categoriesVM: CategoriesViewModel, searchVM: SearchViewModel, goToCategoriesTab: @escaping () -> Void) {
self.homeVM = homeVM
self.categoriesVM = categoriesVM
self.searchVM = searchVM
self.goToCategoriesTab = goToCategoriesTab
@ -26,10 +28,11 @@ class HomeViewController: UIViewController {
integrateSwiftUIScreen(
HomeScreen(
homeVM: HomeViewModel(),
homeVM: homeVM,
categoriesVM: categoriesVM,
goToCategoriesTab: goToCategoriesTab,
goToSearchScreen: { query in
self.searchVM.query = query
let destinationVC = SearchViewController(searchVM: self.searchVM)
self.navigationController?.pushViewController(destinationVC, animated: false)
},
@ -51,77 +54,89 @@ struct HomeScreen: View {
@State var top30: SingleChoiceItem<Int>? = SingleChoiceItem(id: 1, label: L("top30"))
var body: some View {
ScrollView {
VStack (alignment: .leading) {
VerticalSpace(height: 16)
VStack {
AppTopBar(title: L("tjk"))
if(homeVM.downloadProgress == .loading) {
VStack(spacing: 16) {
ProgressView()
Text(L("plz_wait_dowloading"))
}
} else if (homeVM.downloadProgress == .success) {
ScrollView {
VStack (alignment: .leading) {
VerticalSpace(height: 16)
VStack {
AppTopBar(title: L("tjk"))
AppSearchBar(
query: $homeVM.query,
onSearchClicked: { query in
goToSearchScreen(query)
},
onClearClicked: {
homeVM.clearQuery()
}
)
}
.padding(16)
AppSearchBar(
query: $homeVM.query,
onSearchClicked: { query in
goToSearchScreen(query)
},
onClearClicked: {
homeVM.clearQuery()
VStack(spacing: 20) {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
HorizontalSpace(width: 16)
SingleChoiceItemView(
item: top30!,
isSelected: true,
onClick: {
// nothing, just static
},
selectedColor: Color.selected,
unselectedColor: Color.background
)
HorizontalSingleChoice(
items: categoriesVM.categories,
selected: $categoriesVM.selectedCategory,
onSelectedChanged: { item in
categoriesVM.setSelectedCategory(item)
goToCategoriesTab()
}
)
}
}
)
}
.padding(16)
VStack(spacing: 20) {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
HorizontalSpace(width: 16)
SingleChoiceItemView(
item: top30!,
isSelected: true,
onClick: {
// nothing, just static
if let sights = homeVM.sights {
HorizontalPlaces(
title: L("sights"),
items: sights,
onPlaceClick: { place in
goToPlaceScreen(place.id)
},
selectedColor: Color.selected,
unselectedColor: Color.background
setFavoriteChanged: { place, isFavorite in
homeVM.toggleFavorite(for: place.id, isFavorite: isFavorite)
}
)
HorizontalSingleChoice(
items: categoriesVM.categories,
selected: $categoriesVM.selectedCategory,
onSelectedChanged: { item in
categoriesVM.setSelectedCategory(item)
goToCategoriesTab()
}
if let restaurants = homeVM.restaurants {
HorizontalPlaces(
title: L("restaurants"),
items: restaurants,
onPlaceClick: { place in
goToPlaceScreen(place.id)
},
setFavoriteChanged: { place, isFavorite in
homeVM.toggleFavorite(for: place.id, isFavorite: isFavorite)
}
)
}
}
if let sights = homeVM.sights {
HorizontalPlaces(
title: L("sights"),
items: sights,
onPlaceClick: { place in
goToPlaceScreen(place.id)
},
setFavoriteChanged: { place, isFavorite in
// TODO: cmon
}
)
}
if let restaurants = homeVM.restaurants {
HorizontalPlaces(
title: L("restaurants"),
items: restaurants,
onPlaceClick: { place in
goToPlaceScreen(place.id)
},
setFavoriteChanged: { place, isFavorite in
// TODO: cmon
}
)
}
}
VerticalSpace(height: 32)
}
} else if (homeVM.downloadProgress == .error) {
VStack(spacing: 16) {
Text(L("download_failed"))
Text(homeVM.errorMessage)
}
VerticalSpace(height: 32)
}
}
}

View file

@ -1,7 +1,7 @@
import SwiftUI
struct PlaceTopBar: View {
let title: String
let title: String?
let picUrl: String?
let onBackClick: (() -> Void)?
let isFavorite: Bool
@ -18,7 +18,7 @@ struct PlaceTopBar: View {
LoadImageView(url: picUrl)
.frame(height: height)
.clipShape(shape
)
)
// Black overlay with opacity
SwiftUI.Color.black.opacity(0.3)
@ -53,14 +53,16 @@ struct PlaceTopBar: View {
VerticalSpace(height: 32)
// Title
Text(title)
.textStyle(TextStyle.h2)
.foregroundColor(.white)
.padding(.horizontal, padding)
.padding(.bottom, padding)
.lineLimit(1)
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
if let title = title {
Text(title)
.textStyle(TextStyle.h2)
.foregroundColor(.white)
.padding(.horizontal, padding)
.padding(.bottom, padding)
.lineLimit(1)
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.frame(maxWidth: .infinity, maxHeight: height)

View file

@ -11,7 +11,7 @@ struct GalleryScreen: View {
if let urls = urls, !urls.isEmpty {
VStack {
LoadImageView(url: urls.first)
.frame(height: 200)
.frame(maxWidth: UIScreen.main.bounds.width - 32, minHeight: 200, maxHeight: 200)
.clipShape(shape)
VerticalSpace(height: 16)

View file

@ -19,7 +19,14 @@ class PlaceViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let placeVM = PlaceViewModel()
let placesRepository = PlacesRepositoryImpl(
placesService: PlacesServiceImpl(),
placesPersistenceController: PlacesPersistenceController.shared,
reviewsPersistenceController: ReviewsPersistenceController.shared,
hashesPersistenceController: HashesPersistenceController.shared
)
let placeVM = PlaceViewModel(placesRepository: placesRepository, id: self.placeId)
integrateSwiftUIScreen(PlaceScreen(
placeVM: placeVM,
id: placeId,
@ -43,15 +50,15 @@ struct PlaceScreen: View {
if let place = placeVM.place {
VStack {
PlaceTopBar(
title: "place",
picUrl: Constants.imageUrlExample,
title: place.name,
picUrl: place.cover,
onBackClick: {
showBottomBar()
presentationMode.wrappedValue.dismiss()
},
isFavorite: false,
isFavorite: place.isFavorite,
onFavoriteChanged: { isFavorite in
// TODO: Cmon
placeVM.toggleFavorite(for: place.id, isFavorite: isFavorite)
},
onMapClick: {
// TODO: Cmon

View file

@ -1,9 +1,29 @@
import Combine
class PlaceViewModel : ObservableObject {
private var cancellables = Set<AnyCancellable>()
private let placesRepository: PlacesRepository
@Published var place: PlaceFull?
init() {
place = Constants.placeExample
init(placesRepository: PlacesRepository, id: Int64) {
self.placesRepository = placesRepository
observePlace(id: id)
}
func observePlace(id: Int64) {
placesRepository.placeResource
.sink(receiveCompletion: { _ in }, receiveValue: { place in
self.place = place
})
.store(in: &cancellables)
placesRepository.observePlaceById(id)
}
func toggleFavorite(for placeId: Int64, isFavorite: Bool) {
placesRepository.setFavorite(placeId: placeId, isFavorite: isFavorite)
}
}

View file

@ -1,7 +1,7 @@
import SwiftUI
struct ReviewsScreen: View {
@ObservedObject var reviewsVM = ReviewsViewModel()
// @ObservedObject var reviewsVM = ReviewsViewModel()
let placeId: Int64
let rating: Double?
@ -35,10 +35,10 @@ struct ReviewsScreen: View {
HStack {
Spacer()
NavigationLink(destination: AllReviewsScreen(reviewsVM: reviewsVM)) {
Text(L("see_all_reviews"))
.foregroundColor(Color.primary)
}
// NavigationLink(destination: AllReviewsScreen(reviewsVM: reviewsVM)) {
// Text(L("see_all_reviews"))
// .foregroundColor(Color.primary)
// }
}
// user review

View file

@ -1,19 +1,32 @@
import Combine
class ReviewsViewModel: ObservableObject {
private let cancellables = Set<AnyCancellable>()
private let reviewsRepository: ReviewsRepository
init(reviewsRepository: ReviewsRepository, id: Int64) {
self.reviewsRepository = reviewsRepository
observeReviews(id: id)
// TODO: cmon get isThereReviewPlannedToPublish from DB
}
@Published var reviews: [Review] = [Constants.reviewExample]
@Published var userReview: Review? = nil
@Published var isThereReviewPlannedToPublish = false
func getReviews(id: Int64) {
// TODO: cmon user review and all other reviews
func observeReviews(id: Int64) {
reviewsRepository.reviewsResource.sink { _ in } receiveValue: { reviews in
self.reviews = reviews
}
reviewsRepository.observeReviewsForPlace(id: id)
}
func deleteReview() {
// TODO: cmon
}
init() {
// TODO: cmon get isThereReviewPlannedToPublish from DB
}
}

View file

@ -1,23 +1,39 @@
import Combine
class SearchViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
private let placesRepository: PlacesRepository
@Published var query = ""
func clearQuery() { query = "" }
@Published var places: [PlaceShort] = []
init() {
// TODO: put real data
places = [
PlaceShort(id: 1, name: "sight 1", cover: Constants.imageUrlExample, rating: 4.5, excerpt: "yep, just a placeyep, just a placeyep, just a placeyep, just a placeyep, just a place", isFavorite: false),
PlaceShort(id: 2, name: "sight 2", cover: Constants.imageUrlExample, rating: 4.0, excerpt: "yep, just a place", isFavorite: true)
]
init(placesRepository: PlacesRepository) {
self.placesRepository = placesRepository
observeSearch()
}
func observeSearch() {
placesRepository.searchResource.sink { completion in
if case let .failure(error) = completion {
// nothing
}
} receiveValue: { places in
self.places = places
}
.store(in: &cancellables)
$query.sink { q in
self.placesRepository.observeSearch(query: q)
}
.store(in: &cancellables)
}
func toggleFavorite(for placeId: Int64, isFavorite: Bool) {
if let index = places.firstIndex(where: { $0.id == placeId }) {
places[index].isFavorite = isFavorite
}
placesRepository.setFavorite(placeId: placeId, isFavorite: isFavorite)
}
}

View file

@ -27,19 +27,31 @@ class TabBarController: UITabBarController {
let favoritesNav = UINavigationController()
let profileNav = UINavigationController()
// creating shared ViewModels
let categoriesVM = CategoriesViewModel()
let searchVM = SearchViewModel()
// creating repositories and shared ViewModels
let placesRepository = PlacesRepositoryImpl(
placesService: PlacesServiceImpl(),
placesPersistenceController: PlacesPersistenceController.shared,
reviewsPersistenceController: ReviewsPersistenceController.shared,
hashesPersistenceController: HashesPersistenceController.shared
)
let currencyRepository = CurrencyRepositoryImpl(
currencyService: CurrencyServiceImpl(),
currencyPersistenceController: CurrencyPersistenceController.shared
)
let profileRepository = ProfileRepositoryImpl (
profileService: ProfileServiceImpl(userPreferences: UserPreferences.shared),
personalDataPersistenceController: PersonalDataPersistenceController.shared
)
let authRepository = AuthRepositoryImpl(authService: AuthServiceImpl())
let homeVM = HomeViewModel(placesRepository: placesRepository)
let categoriesVM = CategoriesViewModel(placesRepository: placesRepository)
let favoritesVM = FavoritesViewModel(placesRepository: placesRepository)
let searchVM = SearchViewModel(placesRepository: placesRepository)
let profileVM = ProfileViewModel(
currencyRepository: CurrencyRepositoryImpl(
currencyService: CurrencyServiceImpl(),
currencyPersistenceController: CurrencyPersistenceController.shared
),
profileRepository: ProfileRepositoryImpl (
profileService: ProfileServiceImpl(userPreferences: UserPreferences.shared),
personalDataPersistenceController: PersonalDataPersistenceController.shared
),
authRepository: AuthRepositoryImpl(authService: AuthServiceImpl()),
currencyRepository: currencyRepository,
profileRepository: profileRepository,
authRepository: authRepository,
userPreferences: UserPreferences.shared
)
profileVM.onSignOutCompleted = {
@ -51,6 +63,7 @@ class TabBarController: UITabBarController {
// creating ViewControllers
let homeVC = HomeViewController(
homeVM: homeVM,
categoriesVM: categoriesVM,
searchVM: searchVM,
goToCategoriesTab: goToCategoriesTab
@ -59,7 +72,7 @@ class TabBarController: UITabBarController {
categoriesVM: categoriesVM,
searchVM: searchVM
)
let favoritesVC = FavoritesViewController()
let favoritesVC = FavoritesViewController(favoritesVM: favoritesVM)
let profileVC = ProfileViewController(profileVM: profileVM)
// setting up navigation
@ -74,198 +87,5 @@ class TabBarController: UITabBarController {
profileNav.tabBarItem = profileTab
viewControllers = [homeNav, categoriesNav, favoritesNav, profileNav]
PlacePersistenceControllerTesterBro.testAll()
}
}
class PlacePersistenceControllerTesterBro {
private static var cancellables = Set<AnyCancellable>()
private static let persistenceController = PlacePersistenceController.shared
private static let searchQuery = "place"
static func testAll() {
testSearchOperation()
testPlacesByCatFetchOperation()
testTopPlacesFetchOperation()
testSinglePlaceFetchOperation()
testFavoritePlacesFetchOperation()
testCRUDOperations()
}
private static func testCRUDOperations() {
print("Testing CRUD Operations...")
// Example PlaceFull object
let place = PlaceFull(
id: 1,
name: "Test Place",
rating: 5,
excerpt: "A great place",
description: "Detailed description",
placeLocation: nil,
cover: Constants.imageUrlExample,
pics: [Constants.imageUrlExample, Constants.imageUrlExample, Constants.anotherImageExample],
reviews: nil,
isFavorite: true
)
let place2 = PlaceFull(
id: 2,
name: "Test Place 2222",
rating: 4.9,
excerpt: "A great place",
description: "Detailed description",
placeLocation: nil,
cover: Constants.imageUrlExample,
pics: [Constants.imageUrlExample, Constants.imageUrlExample, Constants.anotherImageExample],
reviews: nil,
isFavorite: true
)
let place3 = PlaceFull(
id: 3,
name: "Test Place 3",
rating: 5,
excerpt: "A great place",
description: "Detailed description",
placeLocation: nil,
cover: Constants.imageUrlExample,
pics: [Constants.imageUrlExample, Constants.imageUrlExample, Constants.anotherImageExample],
reviews: nil,
isFavorite: false
)
var place4 = PlaceFull(
id: 4,
name: "Test Place 4",
rating: 4,
excerpt: "A great place",
description: "Detailed description",
placeLocation: nil,
cover: Constants.imageUrlExample,
pics: [Constants.imageUrlExample, Constants.imageUrlExample, Constants.anotherImageExample],
reviews: nil,
isFavorite: false
)
// Insert or update place
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.persistenceController.putPlaces([place], categoryId: 1)
print("Inserted/Updated places with ID: \(place.id)")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.persistenceController.putPlaces([place2, place3], categoryId: 2)
print("Inserted/Updated places with ID: \(place2.id), \(place3.id)")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.persistenceController.putPlaces([place4], categoryId: 3)
print("Inserted/Updated places with ID: \(place4.id)")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 9) {
place4.isFavorite = !place4.isFavorite
self.persistenceController.putPlaces([place4], categoryId: 3)
print("Inserted/Updated places with ID: \(place4.id)")
}
// Delete all
DispatchQueue.main.asyncAfter(deadline: .now() + 0) {
self.persistenceController.deleteAllPlaces()
print("Deleted all places")
}
// Delete places by category (assuming `categoryId` is available)
DispatchQueue.main.asyncAfter(deadline: .now() + 7) {
self.persistenceController.deleteAllPlacesByCategory(categoryId: 3)
print("Deleted places with category ID: 2")
}
}
private static func testSearchOperation() {
print("Testing Search Operation...")
persistenceController.searchSubject
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Search failed with error: \(error)")
}
}, receiveValue: { places in
print("Search Results:")
places.forEach { place in
print("ID: \(place.id), Name: \(place.name ?? ""), Excerpt: \(place.excerpt ?? "No excerpt")")
}
})
.store(in: &cancellables)
persistenceController.observeSearch(query: searchQuery)
}
private static func testPlacesByCatFetchOperation() {
print("Testing PlacesByCat Operation...")
persistenceController.placesByCatSubject
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("PlacesByCat failed with error: \(error)")
}
}, receiveValue: { places in
print("PlacesByCat Results:")
places.forEach { place in
print("ID: \(place.id), Name: \(place.name ?? ""), Excerpt: \(place.excerpt ?? "No excerpt")")
}
})
.store(in: &cancellables)
persistenceController.observePlacesByCategoryId(categoryId: 3)
}
private static func testTopPlacesFetchOperation() {
print("Testing TopPlaces Operation...")
persistenceController.topPlacesSubject
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("TopPlaces failed with error: \(error)")
}
}, receiveValue: { places in
print("TopPlaces Results:")
places.forEach { place in
print("ID: \(place.id), Name: \(place.name ?? ""), Excerpt: \(place.excerpt ?? "No excerpt")")
}
})
.store(in: &cancellables)
persistenceController.observeTopPlacesByCategoryId(categoryId: 2)
}
private static func testSinglePlaceFetchOperation() {
print("Testing SinglePlace Operation...")
persistenceController.singlePlaceSubject
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("SinglePlace failed with error: \(error)")
}
}, receiveValue: { place in
print("SinglePlace Results:")
if let place = place {
print("ID: \(place.id), Name: \(place.name ?? ""), Excerpt: \(place.excerpt ?? "No excerpt")")
}
})
.store(in: &cancellables)
persistenceController.observePlaceById(placeId: 1)
}
private static func testFavoritePlacesFetchOperation() {
print("Testing FavoritePlaces Operation...")
persistenceController.favoritePlacesSubject
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("FavoritePlaces failed with error: \(error)")
}
}, receiveValue: { places in
print("FavoritePlaces Results:")
places.forEach { place in
print("ID: \(place.id), Name: \(place.name ?? ""), Excerpt: \(place.excerpt ?? "No excerpt")")
}
})
.store(in: &cancellables)
persistenceController.observeFavoritePlaces()
}
}