From 52bf2acb91be549cd8f8e2f7082a30b4e1713c61 Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 16 Sep 2024 11:21:24 +0500 Subject: [PATCH] backup --- .../en-GB.lproj/Localizable.strings | 2 + .../en.lproj/Localizable.strings | 2 + .../ru.lproj/Localizable.strings | 2 + iphone/Maps/Maps.xcodeproj/project.pbxproj | 40 ++- iphone/Maps/Tourism/Data/Db/DBUtils.swift | 2 + .../Place.xcdatamodel/contents | 4 +- .../Data/Db/DataModels/UserEntity.swift | 2 +- .../Tourism/Data/Db/EntitiesMapping.swift | 64 ++++ ...wift => HashesPersistenceController.swift} | 20 +- ...wift => PlacesPersistenceController.swift} | 135 ++++---- .../ReviewsPersistenceController.swift | 298 ++++++++++++++++++ .../PlacePersistenceControllerTesterBro.swift | 192 +++++++++++ ...eviewsPersistenceControllerTesterBro.swift | 144 +++++++++ .../Tourism/Data/Network/APIEndpoints.swift | 4 +- .../Data/Network/DTO/Details/PlaceDTO.swift | 81 +++-- .../Data/Network/Services/PlacesService.swift | 28 +- .../Network/Services/ProfileService.swift | 24 +- .../Network/Services/ReviewsService.swift | 28 +- .../Data/Network/Utils/NetworkHelper.swift | 71 +++-- .../Repositories/PlacesRepositoryImpl.swift | 206 ++++++++++-- .../Repositories/ReviewsRepositoryImpl.swift | 37 ++- .../Models/Category/PlaceCategory.swift | 4 + .../Tourism/Domain/Models/Details/User.swift | 9 + .../Domain/Models/DownloadProgress.swift | 6 + .../Repositories/PlacesRepository.swift | 19 +- .../Repositories/ReviewsRepository.swift | 6 +- .../Presentation/Components/LoadImg.swift | 29 +- .../Components/Special/PlacesItem.swift | 1 + .../Categories/CategoriesViewController.swift | 5 +- .../Categories/CategoriesViewModel.swift | 36 ++- .../Favorites/FavoritesViewController.swift | 14 +- .../Favorites/FavoritesViewModel.swift | 35 +- .../Home/Screens/Home/HomeViewModel.swift | 67 +++- .../Home/Screens/Home/HorizontalPlaces.swift | 44 +-- .../New Group/HomeViewController.swift | 141 +++++---- .../PlaceDetails/Components/PlaceTopBar.swift | 22 +- .../PlaceDetails/Gallery/GalleryScreen.swift | 2 +- .../PlaceDetails/PlaceViewController.swift | 17 +- .../Profile/PlaceDetails/PlaceViewModel.swift | 24 +- .../PlaceDetails/Reviews/ReviewsScreen.swift | 10 +- .../Reviews/ReviewsViewModel.swift | 25 +- .../Home/Screens/Search/SearchViewModel.swift | 34 +- .../Presentation/Home/TabBarController.swift | 232 ++------------ 43 files changed, 1615 insertions(+), 553 deletions(-) rename iphone/Maps/Tourism/Data/Db/PersistenceControllers/{HashPersistenceController.swift => HashesPersistenceController.swift} (85%) rename iphone/Maps/Tourism/Data/Db/PersistenceControllers/{PlacePersistenceController.swift => PlacesPersistenceController.swift} (71%) create mode 100644 iphone/Maps/Tourism/Data/Db/PersistenceControllers/ReviewsPersistenceController.swift create mode 100644 iphone/Maps/Tourism/Data/Db/PersistenceControllers/Testers/PlacePersistenceControllerTesterBro.swift create mode 100644 iphone/Maps/Tourism/Data/Db/PersistenceControllers/Testers/ReviewsPersistenceControllerTesterBro.swift create mode 100644 iphone/Maps/Tourism/Domain/Models/DownloadProgress.swift diff --git a/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings index a5a292b9f1..a98bbcd455 100644 --- a/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/en-GB.lproj/Localizable.strings @@ -4093,6 +4093,8 @@ "plz_wait_dowloading" = "Please, wait, data being downloaded"; +"download_failed" = "Download failed"; + "no_content" = "Empty"; "empty_list" = "Empty"; diff --git a/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings index 04086527b3..3efefc2d7b 100644 --- a/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings @@ -4093,6 +4093,8 @@ "plz_wait_dowloading" = "Please, wait, data being downloaded"; +"download_failed" = "Download failed"; + "no_content" = "Empty"; "empty_list" = "Empty"; diff --git a/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings index 9cab3b6ae3..dc377fc954 100644 --- a/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/ru.lproj/Localizable.strings @@ -4093,6 +4093,8 @@ "plz_wait_dowloading" = "Пожалуйста подождите данные скачиваются"; +"download_failed" = "Загрузка данных провалена"; + "no_content" = "Пусто"; "empty_list" = "Пусто"; diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index 4dc476d784..3d7cd321fb 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -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 = ""; }; CDCA278F2248F3B800167D87 /* MWMLocationModeListener.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMLocationModeListener.h; sourceTree = ""; }; CDE0F3AD225B8D45008BA5C3 /* MWMSpeedCameraManagerMode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMSpeedCameraManagerMode.h; sourceTree = ""; }; + CE64501A2C93F5840075A59B /* PlacePersistenceControllerTesterBro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacePersistenceControllerTesterBro.swift; sourceTree = ""; }; + CE64501C2C93F8350075A59B /* ReviewsPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsPersistenceController.swift; sourceTree = ""; }; + CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsPersistenceControllerTesterBro.swift; sourceTree = ""; }; + CE6450232C9772310075A59B /* DownloadProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgress.swift; sourceTree = ""; }; CED0E0162C8ACF0D008C61CA /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = ""; }; CED0E0182C8AD57C008C61CA /* EmptyUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUI.swift; sourceTree = ""; }; CED0E01A2C8B048C008C61CA /* AllPicsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPicsScreen.swift; sourceTree = ""; }; @@ -1651,9 +1659,9 @@ CED0E0382C904868008C61CA /* NavigationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationUtils.swift; sourceTree = ""; }; CED0E03A2C904A06008C61CA /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = ""; }; CED0E03D2C905140008C61CA /* Place.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Place.xcdatamodel; sourceTree = ""; }; - CED0E0412C9077D3008C61CA /* HashPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashPersistenceController.swift; sourceTree = ""; }; + CED0E0412C9077D3008C61CA /* HashesPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashesPersistenceController.swift; sourceTree = ""; }; CED0E0442C918ED4008C61CA /* Hash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hash.swift; sourceTree = ""; }; - CED0E0462C919F44008C61CA /* PlacePersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacePersistenceController.swift; sourceTree = ""; }; + CED0E0462C919F44008C61CA /* PlacesPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlacesPersistenceController.swift; sourceTree = ""; }; CED0E0492C91A2A9008C61CA /* DBUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBUtils.swift; sourceTree = ""; }; CED0E04B2C91A6A3008C61CA /* CoordinatesEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesEntity.swift; sourceTree = ""; }; CED0E04D2C91A702008C61CA /* UserEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntity.swift; sourceTree = ""; }; @@ -3080,6 +3088,7 @@ 5260D3E12C66287D00C673B4 /* Auth */, 52ED919C2C71F639000EE25B /* SimpleResponse.swift */, 529A5F342C86DF99004FE4A1 /* PlaceLocation.swift */, + CE6450232C9772310075A59B /* DownloadProgress.swift */, ); path = Models; sourceTree = ""; @@ -3859,6 +3868,15 @@ path = Location; sourceTree = ""; }; + CE6450192C93F56E0075A59B /* Testers */ = { + isa = PBXGroup; + children = ( + CE64501A2C93F5840075A59B /* PlacePersistenceControllerTesterBro.swift */, + CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */, + ); + path = Testers; + sourceTree = ""; + }; 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 = ""; @@ -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 */, diff --git a/iphone/Maps/Tourism/Data/Db/DBUtils.swift b/iphone/Maps/Tourism/Data/Db/DBUtils.swift index d28a176647..71f93492d0 100644 --- a/iphone/Maps/Tourism/Data/Db/DBUtils.swift +++ b/iphone/Maps/Tourism/Data/Db/DBUtils.swift @@ -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 } } diff --git a/iphone/Maps/Tourism/Data/Db/DataModels/Place.xcdatamodeld/Place.xcdatamodel/contents b/iphone/Maps/Tourism/Data/Db/DataModels/Place.xcdatamodeld/Place.xcdatamodel/contents index 1d7cb7e4cd..c75601dc8f 100644 --- a/iphone/Maps/Tourism/Data/Db/DataModels/Place.xcdatamodeld/Place.xcdatamodel/contents +++ b/iphone/Maps/Tourism/Data/Db/DataModels/Place.xcdatamodeld/Place.xcdatamodel/contents @@ -25,12 +25,12 @@ - + - + diff --git a/iphone/Maps/Tourism/Data/Db/DataModels/UserEntity.swift b/iphone/Maps/Tourism/Data/Db/DataModels/UserEntity.swift index 74a33af1ed..4e479d5393 100644 --- a/iphone/Maps/Tourism/Data/Db/DataModels/UserEntity.swift +++ b/iphone/Maps/Tourism/Data/Db/DataModels/UserEntity.swift @@ -1,4 +1,4 @@ -struct UserEntity { +struct UserEntity: Encodable { let userId: Int64 let fullName: String let avatar: String diff --git a/iphone/Maps/Tourism/Data/Db/EntitiesMapping.swift b/iphone/Maps/Tourism/Data/Db/EntitiesMapping.swift index e91bbb50c4..38bfe102fb 100644 --- a/iphone/Maps/Tourism/Data/Db/EntitiesMapping.swift +++ b/iphone/Maps/Tourism/Data/Db/EntitiesMapping.swift @@ -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 + ) + } +} diff --git a/iphone/Maps/Tourism/Data/Db/PersistenceControllers/HashPersistenceController.swift b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/HashesPersistenceController.swift similarity index 85% rename from iphone/Maps/Tourism/Data/Db/PersistenceControllers/HashPersistenceController.swift rename to iphone/Maps/Tourism/Data/Db/PersistenceControllers/HashesPersistenceController.swift index 4606bef789..f69b095c78 100644 --- a/iphone/Maps/Tourism/Data/Db/PersistenceControllers/HashPersistenceController.swift +++ b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/HashesPersistenceController.swift @@ -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 { + func getHash(categoryId: Int64) -> Hash? { let context = container.viewContext let fetchRequest: NSFetchRequest = 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.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 [] } } } diff --git a/iphone/Maps/Tourism/Data/Db/PersistenceControllers/PlacePersistenceController.swift b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/PlacesPersistenceController.swift similarity index 71% rename from iphone/Maps/Tourism/Data/Db/PersistenceControllers/PlacePersistenceController.swift rename to iphone/Maps/Tourism/Data/Db/PersistenceControllers/PlacesPersistenceController.swift index 150c8494d3..f3324da2ee 100644 --- a/iphone/Maps/Tourism/Data/Db/PersistenceControllers/PlacePersistenceController.swift +++ b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/PlacesPersistenceController.swift @@ -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? private var placesByCatFetchedResultsController: NSFetchedResultsController? - private var topPlacesFetchedResultsController: NSFetchedResultsController? + private var topSightsFetchedResultsController: NSFetchedResultsController? + private var topRestaurantsFetchedResultsController: NSFetchedResultsController? private var singlePlaceFetchedResultsController: NSFetchedResultsController? private var favoritePlacesFetchedResultsController: NSFetchedResultsController? - let searchSubject = PassthroughSubject<[PlaceEntity], ResourceError>() - let placesByCatSubject = PassthroughSubject<[PlaceEntity], ResourceError>() - let topPlacesSubject = PassthroughSubject<[PlaceEntity], ResourceError>() - let singlePlaceSubject = PassthroughSubject() - 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() + 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 = PlaceEntity.fetchRequest() @@ -137,7 +130,9 @@ class PlacePersistenceController: NSObject, NSFetchedResultsControllerDelegate { func observeSearch(query: String) { let fetchRequest: NSFetchRequest = 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.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.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.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.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 } diff --git a/iphone/Maps/Tourism/Data/Db/PersistenceControllers/ReviewsPersistenceController.swift b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/ReviewsPersistenceController.swift new file mode 100644 index 0000000000..962b8c7c1a --- /dev/null +++ b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/ReviewsPersistenceController.swift @@ -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? + private var reviewsPlannedToPostFetchedResultsController: NSFetchedResultsController? + + 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.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.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.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.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 = 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.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.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.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.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.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.fetchRequest() + + do { + return try context.fetch(fetchRequest) + } catch { + print(error) + print("Failed to fetch planned reviews: \(error)") + return [] + } + } + + func observeReviewsPlannedToPost() { + let fetchRequest: NSFetchRequest = 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) { + 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 + } + } +} diff --git a/iphone/Maps/Tourism/Data/Db/PersistenceControllers/Testers/PlacePersistenceControllerTesterBro.swift b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/Testers/PlacePersistenceControllerTesterBro.swift new file mode 100644 index 0000000000..7b5e44b98d --- /dev/null +++ b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/Testers/PlacePersistenceControllerTesterBro.swift @@ -0,0 +1,192 @@ +//import Combine +// +//class PlacePersistenceControllerTesterBro { +// private static var cancellables = Set() +// 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() +// } +//} diff --git a/iphone/Maps/Tourism/Data/Db/PersistenceControllers/Testers/ReviewsPersistenceControllerTesterBro.swift b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/Testers/ReviewsPersistenceControllerTesterBro.swift new file mode 100644 index 0000000000..39e3c67afb --- /dev/null +++ b/iphone/Maps/Tourism/Data/Db/PersistenceControllers/Testers/ReviewsPersistenceControllerTesterBro.swift @@ -0,0 +1,144 @@ +import Combine +import Foundation + +class ReviewsPersistenceControllerTesterBro { + private static var cancellables = Set() + 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)") + } + } + } +} diff --git a/iphone/Maps/Tourism/Data/Network/APIEndpoints.swift b/iphone/Maps/Tourism/Data/Network/APIEndpoints.swift index b81b08262d..66718b2dac 100644 --- a/iphone/Maps/Tourism/Data/Network/APIEndpoints.swift +++ b/iphone/Maps/Tourism/Data/Network/APIEndpoints.swift @@ -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" diff --git a/iphone/Maps/Tourism/Data/Network/DTO/Details/PlaceDTO.swift b/iphone/Maps/Tourism/Data/Network/DTO/Details/PlaceDTO.swift index a741ec5c24..782ae3fbce 100644 --- a/iphone/Maps/Tourism/Data/Network/DTO/Details/PlaceDTO.swift +++ b/iphone/Maps/Tourism/Data/Network/DTO/Details/PlaceDTO.swift @@ -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") + } + } } diff --git a/iphone/Maps/Tourism/Data/Network/Services/PlacesService.swift b/iphone/Maps/Tourism/Data/Network/Services/PlacesService.swift index 92b8867997..75ab3d8c29 100644 --- a/iphone/Maps/Tourism/Data/Network/Services/PlacesService.swift +++ b/iphone/Maps/Tourism/Data/Network/Services/PlacesService.swift @@ -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) + } +} diff --git a/iphone/Maps/Tourism/Data/Network/Services/ProfileService.swift b/iphone/Maps/Tourism/Data/Network/Services/ProfileService.swift index 090834a224..8586cae5fb 100644 --- a/iphone/Maps/Tourism/Data/Network/Services/ProfileService.swift +++ b/iphone/Maps/Tourism/Data/Network/Services/ProfileService.swift @@ -114,19 +114,27 @@ class ProfileServiceImpl: ProfileService { func updateLanguage(code: String) { Task { - await AppNetworkHelper.put( - path: APIEndpoints.updateLanguageUrl, - body: LanguageDTO(language: code) - ) as Result + 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 + do { + let response: SimpleResponse = try await AppNetworkHelper.put( + path: APIEndpoints.updateThemeUrl, + body: ThemeDTO(theme: code) + ) + } catch { + print("Failed to update theme on server") + } } } } diff --git a/iphone/Maps/Tourism/Data/Network/Services/ReviewsService.swift b/iphone/Maps/Tourism/Data/Network/Services/ReviewsService.swift index 0ed2494895..985df9a93a 100644 --- a/iphone/Maps/Tourism/Data/Network/Services/ReviewsService.swift +++ b/iphone/Maps/Tourism/Data/Network/Services/ReviewsService.swift @@ -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) + } +} diff --git a/iphone/Maps/Tourism/Data/Network/Utils/NetworkHelper.swift b/iphone/Maps/Tourism/Data/Network/Utils/NetworkHelper.swift index 5a07387a06..fe29f4c7e8 100644 --- a/iphone/Maps/Tourism/Data/Network/Utils/NetworkHelper.swift +++ b/iphone/Maps/Tourism/Data/Network/Utils/NetworkHelper.swift @@ -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 { + ) 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 { + ) 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 { + ) 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 { + ) 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( + 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 { - 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) } } diff --git a/iphone/Maps/Tourism/Data/Repositories/PlacesRepositoryImpl.swift b/iphone/Maps/Tourism/Data/Repositories/PlacesRepositoryImpl.swift index 1228b15106..279d6f4ad5 100644 --- a/iphone/Maps/Tourism/Data/Repositories/PlacesRepositoryImpl.swift +++ b/iphone/Maps/Tourism/Data/Repositories/PlacesRepositoryImpl.swift @@ -1,38 +1,206 @@ import Combine class PlacesRepositoryImpl: PlacesRepository { - func downloadAllData() -> AnyPublisher { - // TODO: cmon - return PassthroughSubject().eraseToAnyPublisher() + let placesService: PlacesService + let placesPersistenceController: PlacesPersistenceController + let reviewsPersistenceController: ReviewsPersistenceController + let hashesPersistenceController: HashesPersistenceController + + var downloadProgress: PassthroughSubject + var searchResource: PassthroughSubject<[PlaceShort], ResourceError> + var placesByCategoryResource: PassthroughSubject<[PlaceShort], ResourceError> + var topSightsResource: PassthroughSubject<[PlaceShort], ResourceError> + var topRestaurantsResource: PassthroughSubject<[PlaceShort], ResourceError> + var placeResource: PassthroughSubject + 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.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() - 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() { diff --git a/iphone/Maps/Tourism/Data/Repositories/ReviewsRepositoryImpl.swift b/iphone/Maps/Tourism/Data/Repositories/ReviewsRepositoryImpl.swift index 6bb925a442..ffb516c217 100644 --- a/iphone/Maps/Tourism/Data/Repositories/ReviewsRepositoryImpl.swift +++ b/iphone/Maps/Tourism/Data/Repositories/ReviewsRepositoryImpl.swift @@ -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() + + 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 { diff --git a/iphone/Maps/Tourism/Domain/Models/Category/PlaceCategory.swift b/iphone/Maps/Tourism/Domain/Models/Category/PlaceCategory.swift index 5cff2cb14d..5530176c3d 100644 --- a/iphone/Maps/Tourism/Domain/Models/Category/PlaceCategory.swift +++ b/iphone/Maps/Tourism/Domain/Models/Category/PlaceCategory.swift @@ -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: diff --git a/iphone/Maps/Tourism/Domain/Models/Details/User.swift b/iphone/Maps/Tourism/Domain/Models/Details/User.swift index f8e8aeb395..dcd6557745 100644 --- a/iphone/Maps/Tourism/Domain/Models/Details/User.swift +++ b/iphone/Maps/Tourism/Domain/Models/Details/User.swift @@ -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 + ) + } } diff --git a/iphone/Maps/Tourism/Domain/Models/DownloadProgress.swift b/iphone/Maps/Tourism/Domain/Models/DownloadProgress.swift new file mode 100644 index 0000000000..3676c2f098 --- /dev/null +++ b/iphone/Maps/Tourism/Domain/Models/DownloadProgress.swift @@ -0,0 +1,6 @@ +enum DownloadProgress: Equatable { + case idle + case loading + case success + case error +} diff --git a/iphone/Maps/Tourism/Domain/Repositories/PlacesRepository.swift b/iphone/Maps/Tourism/Domain/Repositories/PlacesRepository.swift index 3e27dba1a0..915af87670 100644 --- a/iphone/Maps/Tourism/Domain/Repositories/PlacesRepository.swift +++ b/iphone/Maps/Tourism/Domain/Repositories/PlacesRepository.swift @@ -1,21 +1,26 @@ import Combine protocol PlacesRepository { - func downloadAllData() -> AnyPublisher + var downloadProgress: PassthroughSubject { 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 { 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) diff --git a/iphone/Maps/Tourism/Domain/Repositories/ReviewsRepository.swift b/iphone/Maps/Tourism/Domain/Repositories/ReviewsRepository.swift index aeb3f82b2e..f5d0e0ba3b 100644 --- a/iphone/Maps/Tourism/Domain/Repositories/ReviewsRepository.swift +++ b/iphone/Maps/Tourism/Domain/Repositories/ReviewsRepository.swift @@ -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 { get } + func isThereReviewPlannedToPublish(for placeId: Int64) func postReview(review: ReviewToPost) -> AnyPublisher diff --git a/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift b/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift index 3a8eeae0d2..8faa640a25 100644 --- a/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift +++ b/iphone/Maps/Tourism/Presentation/Components/LoadImg.swift @@ -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) } } diff --git a/iphone/Maps/Tourism/Presentation/Components/Special/PlacesItem.swift b/iphone/Maps/Tourism/Presentation/Components/Special/PlacesItem.swift index 408f41c42b..7b81b83746 100644 --- a/iphone/Maps/Tourism/Presentation/Components/Special/PlacesItem.swift +++ b/iphone/Maps/Tourism/Presentation/Components/Special/PlacesItem.swift @@ -1,4 +1,5 @@ import SwiftUI +import WebKit struct PlacesItem: View { let place: PlaceShort diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Categories/CategoriesViewController.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Categories/CategoriesViewController.swift index cf94172666..2492a4ba3a 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Categories/CategoriesViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Categories/CategoriesViewController.swift @@ -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) diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Categories/CategoriesViewModel.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Categories/CategoriesViewModel.swift index edb635adc6..9f65e754b8 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Categories/CategoriesViewModel.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Categories/CategoriesViewModel.swift @@ -1,6 +1,10 @@ import Combine class CategoriesViewModel: ObservableObject { + private var cancellables = Set() + + 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) } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Favorites/FavoritesViewController.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Favorites/FavoritesViewController.swift index d09b3c0f0f..f69c35064a 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Favorites/FavoritesViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Favorites/FavoritesViewController.swift @@ -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() diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Favorites/FavoritesViewModel.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Favorites/FavoritesViewModel.swift index 290b998b43..70f867950c 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Favorites/FavoritesViewModel.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Favorites/FavoritesViewModel.swift @@ -1,23 +1,40 @@ import Combine class FavoritesViewModel: ObservableObject { + private var cancellables = Set() + + 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) } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HomeViewModel.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HomeViewModel.swift index 5a5cb41ac7..f65c845030 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HomeViewModel.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HomeViewModel.swift @@ -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() + + 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) } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HorizontalPlaces.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HorizontalPlaces.swift index a3d2b5ed68..b0803a4200 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HorizontalPlaces.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Home/HorizontalPlaces.swift @@ -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 { diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/New Group/HomeViewController.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/New Group/HomeViewController.swift index f0915fc96e..63320382fc 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/New Group/HomeViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/New Group/HomeViewController.swift @@ -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? = 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) } } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Components/PlaceTopBar.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Components/PlaceTopBar.swift index 7454fc314b..aa8196e37f 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Components/PlaceTopBar.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Components/PlaceTopBar.swift @@ -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) diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Gallery/GalleryScreen.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Gallery/GalleryScreen.swift index c16efaaca8..efbf59a1cd 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Gallery/GalleryScreen.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Gallery/GalleryScreen.swift @@ -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) diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewController.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewController.swift index df01a145dd..801b2d2df6 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewController.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewController.swift @@ -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 diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewModel.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewModel.swift index 0b299a474b..d80bbc0026 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewModel.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/PlaceViewModel.swift @@ -1,9 +1,29 @@ import Combine class PlaceViewModel : ObservableObject { + private var cancellables = Set() + + 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) } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift index a9dc238eac..52677b97d1 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsScreen.swift @@ -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 diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsViewModel.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsViewModel.swift index a531eef268..1af97c8f9f 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsViewModel.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Profile/PlaceDetails/Reviews/ReviewsViewModel.swift @@ -1,19 +1,32 @@ import Combine class ReviewsViewModel: ObservableObject { + private let cancellables = Set() + + 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 - } } diff --git a/iphone/Maps/Tourism/Presentation/Home/Screens/Search/SearchViewModel.swift b/iphone/Maps/Tourism/Presentation/Home/Screens/Search/SearchViewModel.swift index 67e6853cde..9777c3f61b 100644 --- a/iphone/Maps/Tourism/Presentation/Home/Screens/Search/SearchViewModel.swift +++ b/iphone/Maps/Tourism/Presentation/Home/Screens/Search/SearchViewModel.swift @@ -1,23 +1,39 @@ import Combine class SearchViewModel: ObservableObject { + private var cancellables = Set() + + 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) } } diff --git a/iphone/Maps/Tourism/Presentation/Home/TabBarController.swift b/iphone/Maps/Tourism/Presentation/Home/TabBarController.swift index 9c7ee483a7..756b84d603 100644 --- a/iphone/Maps/Tourism/Presentation/Home/TabBarController.swift +++ b/iphone/Maps/Tourism/Presentation/Home/TabBarController.swift @@ -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() - 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() } }