From 2df66fdd96b35eea08ebbdb7f99ce72288e2722e Mon Sep 17 00:00:00 2001 From: Matteo Matassoni <4108197+matax87@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:51:04 +0100 Subject: [PATCH 01/14] Ask only the needed fields to /v1/EventShort --- .../TodayFeature/EventsFeature/View Models/EventsViewModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift b/NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift index 0475600..c39d2c3 100644 --- a/NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift +++ b/NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift @@ -71,6 +71,7 @@ class EventsViewModel { endDate: endDate, eventLocation: .noi, publishedon: "noi-communityapp", + fields: ["AnchorVenue", "AnchorVenueRoomMapping", "CompanyName", "Display5", "EndDate", "EventDescriptionDE", "EventDescriptionEN", "EventDescriptionIT", "EventLocation", "EventTextDE", "EventTextEN", "EventTextIT", "Id", "ImageGallery", "StartDate", "WebAddress"], rawFilter: activeFilters.toQuery(), removeNullValues: true, optimizeDates: true From d35e3d7051530fada69edc0512f69e7447e1f66b Mon Sep 17 00:00:00 2001 From: Camilla Copetti Date: Tue, 3 Dec 2024 10:13:25 +0100 Subject: [PATCH 02/14] Removed references to Related Events --- .../Coordinators/EventsCoordinator.swift | 22 +-------- .../EventsFeatureConstants.swift | 1 - .../EventDetailsViewController.swift | 46 ++----------------- .../View Models/EventsViewModel.swift | 21 --------- 4 files changed, 5 insertions(+), 85 deletions(-) diff --git a/NOICommunity/TodayFeature/EventsFeature/Coordinators/EventsCoordinator.swift b/NOICommunity/TodayFeature/EventsFeature/Coordinators/EventsCoordinator.swift index c566422..41e3f36 100644 --- a/NOICommunity/TodayFeature/EventsFeature/Coordinators/EventsCoordinator.swift +++ b/NOICommunity/TodayFeature/EventsFeature/Coordinators/EventsCoordinator.swift @@ -116,22 +116,9 @@ private extension EventsCoordinator { transitionCollectionView: UICollectionView? = nil, transitionIndexPath: IndexPath? = nil ) { -// let transitionId = "event_\(event.id)" -// if -// let transitionCollectionView = transitionCollectionView, -// let transitionIndexPath = transitionIndexPath { -// let transitionInfo = EventsNavigationControllerDelegate.TransitionInfo( -// id: transitionId, -// collectionView: transitionCollectionView, -// indexPath: transitionIndexPath, -// event: event -// ) -// navigationDelegate.transitionInfos.append(transitionInfo) -// } let detailVC = EventDetailsViewController( - for: event, - relatedEvents: eventsViewModel.relatedEvent(of: event) + for: event ) //detailVC.cardView.transitionId = transitionId detailVC.addToCalendarActionHandler = { [weak self] in @@ -143,13 +130,6 @@ private extension EventsCoordinator { detailVC.signupActionHandler = { [weak self] in self?.signupEvent($0) } - detailVC.didSelectRelatedEventHandler = { [weak self] collectionView, _, indexPath, selectedEvent in - self?.goToDetails( - of: selectedEvent, - transitionCollectionView: collectionView, - transitionIndexPath: indexPath - ) - } detailVC.navigationItem.title = event.title detailVC.navigationItem.largeTitleDisplayMode = .never navigationController.pushViewController(detailVC, animated: true) diff --git a/NOICommunity/TodayFeature/EventsFeature/EventsFeatureConstants.swift b/NOICommunity/TodayFeature/EventsFeature/EventsFeatureConstants.swift index 1688ee2..9dc1e3b 100644 --- a/NOICommunity/TodayFeature/EventsFeature/EventsFeatureConstants.swift +++ b/NOICommunity/TodayFeature/EventsFeature/EventsFeatureConstants.swift @@ -13,5 +13,4 @@ import Foundation enum EventsFeatureConstants { static let maximumNumberOfEvents = 20 - static let maximumNumberOfRelatedEvents = 3 } diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.swift b/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.swift index 2c51215..8cbaf1d 100644 --- a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.swift +++ b/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.swift @@ -15,20 +15,12 @@ import Kingfisher class EventDetailsViewController: UIViewController { let event: Event - let relatedEvents: [Event] var locateActionHandler: ((Event) -> Void)? var addToCalendarActionHandler: ((Event) -> Void)? var signupActionHandler: ((Event) -> Void)? - - var didSelectRelatedEventHandler: (( - UICollectionView, - UICollectionViewCell, - IndexPath, - Event - ) -> Void)? private var _cardView: (UIView & UIContentView)! @@ -37,8 +29,6 @@ class EventDetailsViewController: UIViewController { return _cardView } - private var relatedEventsVC: EventListViewController! - @IBOutlet private var scrollView: UIScrollView! { didSet { scrollView.contentInset = UIEdgeInsets( @@ -82,18 +72,11 @@ class EventDetailsViewController: UIViewController { @IBOutlet private var relatedSection: UIView! { didSet { - if relatedEvents.isEmpty { - relatedSection.removeFromSuperview() - } + relatedSection.removeFromSuperview() } } - @IBOutlet private var relatedEventsLabel: UILabel! { - didSet { - relatedEventsLabel.font = .NOI.dynamic.headlineSemibold - relatedEventsLabel.text = .localized("label_interesting_for_you") - } - } + @IBOutlet private var relatedEventsLabel: UILabel! @IBOutlet private var relatedEventsContainerView: UIView! @@ -125,9 +108,8 @@ class EventDetailsViewController: UIViewController { } } - init(for item: Event, relatedEvents: [Event]) { + init(for item: Event) { self.event = item - self.relatedEvents = relatedEvents super.init(nibName: "\(EventDetailsViewController.self)", bundle: nil) } @@ -147,7 +129,6 @@ class EventDetailsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() configureViewHierarchy() - configureChilds() } override var preferredStatusBarStyle: UIStatusBarStyle { @@ -156,10 +137,6 @@ class EventDetailsViewController: UIViewController { override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { super.preferredContentSizeDidChange(forChildContentContainer: container) - - if container === relatedEventsVC { - relatedEventsContainerViewHeight.constant = container.preferredContentSize.height - } } } @@ -189,21 +166,6 @@ private extension EventDetailsViewController { } } - func configureChilds() { - guard !relatedEvents.isEmpty - else { return } - - relatedEventsVC = EventListViewController( - items: relatedEvents, - embeddedHorizontally: true - ) - relatedEventsVC.didSelectHandler = { [weak self] in - self?.didSelectRelatedEventHandler?($0, $1, $2, $3) - } - embedChild(relatedEventsVC, in: relatedEventsContainerView) - relatedEventsContainerViewHeight.constant = relatedEventsVC.preferredContentSize.height - } - @IBAction func findOnMapsAction(sender: Any?) { locateActionHandler?(event) } @@ -220,6 +182,6 @@ private extension EventDetailsViewController { extension EventDetailsViewController: CurrentScrollOffsetProvider { var currentScrollOffset: CGPoint { - relatedEventsVC?.currentScrollOffset ?? .zero + .zero } } diff --git a/NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift b/NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift index 0475600..80302ce 100644 --- a/NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift +++ b/NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift @@ -29,7 +29,6 @@ class EventsViewModel { let eventShortClient: EventShortClient let language: Language? let maximumNumberOfEvents: Int - let maximumNumberOfRelatedEvents: Int let showFiltersHandler: () -> Void private var subscriptions: Set = [] @@ -40,13 +39,11 @@ class EventsViewModel { eventShortClient: EventShortClient, language: Language?, maximumNumberOfEvents: Int = EventsFeatureConstants.maximumNumberOfEvents, - maximumNumberOfRelatedEvents: Int = EventsFeatureConstants.maximumNumberOfRelatedEvents, showFiltersHandler: @escaping () -> Void ) { self.eventShortClient = eventShortClient self.language = language self.maximumNumberOfEvents = maximumNumberOfEvents - self.maximumNumberOfRelatedEvents = maximumNumberOfRelatedEvents self.showFiltersHandler = showFiltersHandler } @@ -105,24 +102,6 @@ class EventsViewModel { } }) } - - func relatedEvent(of event: Event) -> [Event] { - let slice = eventResults - .lazy - .filter { candidateEvent in - guard candidateEvent.id != event.id - else { return false } - - for techField in event.technologyFields { - if candidateEvent.technologyFields.contains(techField) { - return true - } - } - return false - } - .prefix(maximumNumberOfRelatedEvents) - return Array(slice) - } func showFilters() { showFiltersHandler() From 39bc7d1d782193f891b67d800673a21cd3d417cb Mon Sep 17 00:00:00 2001 From: Camilla Copetti Date: Tue, 3 Dec 2024 11:35:52 +0100 Subject: [PATCH 03/14] Removed relatedEvents section from xib and removed outlets --- .../EventDetailsViewController.swift | 12 ------ .../EventDetailsViewController.xib | 43 +++---------------- 2 files changed, 5 insertions(+), 50 deletions(-) diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.swift b/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.swift index 8cbaf1d..2668d4a 100644 --- a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.swift +++ b/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.swift @@ -70,18 +70,6 @@ class EventDetailsViewController: UIViewController { } } - @IBOutlet private var relatedSection: UIView! { - didSet { - relatedSection.removeFromSuperview() - } - } - - @IBOutlet private var relatedEventsLabel: UILabel! - - @IBOutlet private var relatedEventsContainerView: UIView! - - @IBOutlet private var relatedEventsContainerViewHeight: NSLayoutConstraint! - @IBOutlet private var actionsContainersView: FooterView! @IBOutlet private var locateEventButton: UIButton! { diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.xib b/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.xib index 3764113..266e6a0 100644 --- a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.xib +++ b/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -20,10 +20,6 @@ - - - - @@ -38,7 +34,7 @@ - + @@ -47,13 +43,13 @@ - + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. @@ -71,35 +67,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - From 0b118f3a7aeba621cc6b62ea76aaf5f064f291c1 Mon Sep 17 00:00:00 2001 From: Matteo Matassoni <4108197+matax87@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:00:25 +0100 Subject: [PATCH 04/14] WIP refactoring EventShortClient --- NOICommunity.xcodeproj/project.pbxproj | 248 +++++++++--------- NOICommunity/DeepLinking.swift | 5 + .../Coordinators/EventsCoordinator.swift | 0 .../EventsFeatureConstants.swift | 9 + .../EventDetailsViewController.swift | 0 .../EventDetailsViewController.xib | 18 +- .../EventFiltersListViewController.swift | 0 .../EventFiltersViewController.swift | 0 .../EventFiltersViewController.xib | 0 .../EventListViewController.swift | 0 ...dinator.NavigationControllerDelegate.swift | 0 .../EventsMainViewController.swift | 0 .../EventsMainViewController.xib | 0 .../View Models/DateIntervalFilter.swift | 0 .../EventsFeature/View Models/Event.swift | 0 .../View Models/EventFiltersViewModel.swift | 0 .../View Models/EventsViewModel.swift | 109 ++++---- ...ardContentConfiguration.SharedConfig.swift | 0 .../Views/EventCardContentView.swift | 0 .../Views/EventCardContentView.xib | 0 .../Views/EventsFiltersBarView.swift | 0 .../Views/EventsFiltersBarView.xib | 0 NOICommunity/SceneDelegate.swift | 7 +- NOICommunityLib/Package.swift | 32 --- .../Sources/AppPreferencesClient/Mocks.swift | 2 +- .../AppPreferencesClientLive/Live.swift | 2 +- .../Sources/AuthClientLive/Live.swift | 2 +- .../Endpoint/Endpoint+QueryBuilder.swift | 0 .../{ => Network}/Endpoint/Endpoint.swift | 1 + .../HeaderAddingTransport.swift | 68 +++++ .../Implementations/MockTransport.swift | 52 ++++ .../RequestInspectableTransport.swift | 29 ++ .../StatusCodeCheckingTransport.swift | 54 ++++ .../URLSession+Transport.swift | 26 ++ .../Transport/Interfaces/Transport.swift | 27 ++ .../Endpoint+EventShort.swift | 240 +++++++++++++++++ .../EventShortClientImplementation.swift | 170 ++++++++++++ .../Sources/EventShortClient/Interface.swift | 26 -- .../Interfaces/EventShortClient.swift | 123 +++++++++ .../Sources/EventShortClient/Mocks.swift | 70 ----- .../{Models.swift => Models/EventShort.swift} | 93 +------ .../Models/EventShortListResponse.swift | 47 ++++ .../EventShortClient/Models/Order.swift | 17 ++ .../Sources/EventShortClientLive/Live.swift | 238 ----------------- .../EventShortClientTests.swift | 15 -- .../EventShortClientTests.swift | 15 -- 46 files changed, 1060 insertions(+), 685 deletions(-) rename NOICommunity/{TodayFeature => }/EventsFeature/Coordinators/EventsCoordinator.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/EventsFeatureConstants.swift (64%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Controllers/EventDetailsViewController.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Controllers/EventDetailsViewController.xib (97%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Controllers/EventFiltersListViewController.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Controllers/EventFiltersViewController.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Controllers/EventFiltersViewController.xib (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Controllers/EventListViewController.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Controllers/EventsCoordinator.NavigationControllerDelegate.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Controllers/EventsMainViewController.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Controllers/EventsMainViewController.xib (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Models/DateIntervalFilter.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Models/Event.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Models/EventFiltersViewModel.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/View Models/EventsViewModel.swift (74%) rename NOICommunity/{TodayFeature => }/EventsFeature/Views/EventCardContentConfiguration.SharedConfig.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/Views/EventCardContentView.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/Views/EventCardContentView.xib (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/Views/EventsFiltersBarView.swift (100%) rename NOICommunity/{TodayFeature => }/EventsFeature/Views/EventsFiltersBarView.xib (100%) rename NOICommunityLib/Sources/Core/{ => Network}/Endpoint/Endpoint+QueryBuilder.swift (100%) rename NOICommunityLib/Sources/Core/{ => Network}/Endpoint/Endpoint.swift (99%) create mode 100644 NOICommunityLib/Sources/Core/Network/Transport/Implementations/HeaderAddingTransport.swift create mode 100644 NOICommunityLib/Sources/Core/Network/Transport/Implementations/MockTransport.swift create mode 100644 NOICommunityLib/Sources/Core/Network/Transport/Implementations/RequestInspectableTransport.swift create mode 100644 NOICommunityLib/Sources/Core/Network/Transport/Implementations/StatusCodeCheckingTransport.swift create mode 100644 NOICommunityLib/Sources/Core/Network/Transport/Implementations/URLSession+Transport.swift create mode 100644 NOICommunityLib/Sources/Core/Network/Transport/Interfaces/Transport.swift create mode 100644 NOICommunityLib/Sources/EventShortClient/Endpoint+EventShort.swift create mode 100644 NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift delete mode 100644 NOICommunityLib/Sources/EventShortClient/Interface.swift create mode 100644 NOICommunityLib/Sources/EventShortClient/Interfaces/EventShortClient.swift delete mode 100644 NOICommunityLib/Sources/EventShortClient/Mocks.swift rename NOICommunityLib/Sources/EventShortClient/{Models.swift => Models/EventShort.swift} (66%) create mode 100644 NOICommunityLib/Sources/EventShortClient/Models/EventShortListResponse.swift create mode 100644 NOICommunityLib/Sources/EventShortClient/Models/Order.swift delete mode 100644 NOICommunityLib/Sources/EventShortClientLive/Live.swift delete mode 100644 NOICommunityLib/Tests/EventShortClientTests/EventShortClientTests.swift delete mode 100644 NOICommunityLib/Tests/EventShortTypesClientTests/EventShortClientTests.swift diff --git a/NOICommunity.xcodeproj/project.pbxproj b/NOICommunity.xcodeproj/project.pbxproj index f934b88..2b95127 100644 --- a/NOICommunity.xcodeproj/project.pbxproj +++ b/NOICommunity.xcodeproj/project.pbxproj @@ -30,10 +30,7 @@ 31157D1926FCA1EB00A43B33 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31157D1826FCA1EB00A43B33 /* WebViewController.swift */; }; 31157D1B26FCB26200A43B33 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31157D1A26FCB26200A43B33 /* String+Localization.swift */; }; 31178FCE26F8D7CC00BDCDAA /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31178FCD26F8D7CC00BDCDAA /* Localization.swift */; }; - 31178FD626F9D6EB00BDCDAA /* DateIntervalFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31178FD526F9D6EB00BDCDAA /* DateIntervalFilter.swift */; }; - 31178FD826F9FD3800BDCDAA /* EventsFeatureConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31178FD726F9FD3800BDCDAA /* EventsFeatureConstants.swift */; }; 31178FDA26FA16A200BDCDAA /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31178FD926FA16A200BDCDAA /* NavigationController.swift */; }; - 31178FDC26FA26C500BDCDAA /* EventDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31178FDB26FA26C500BDCDAA /* EventDetailsViewController.swift */; }; 31178FDE26FA2F7100BDCDAA /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31178FDD26FA2F7100BDCDAA /* TabBarController.swift */; }; 31178FE026FA318800BDCDAA /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31178FDF26FA318800BDCDAA /* Collection.swift */; }; 311E0EC62825157800404DCE /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 311E0EC52825157800404DCE /* FirebaseMessaging */; }; @@ -57,14 +54,12 @@ 3136CC422706F8D200C27129 /* UIView+TransitionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3136CC412706F8D200C27129 /* UIView+TransitionId.swift */; }; 3136CC442707093B00C27129 /* SourceDismissAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3136CC432707093B00C27129 /* SourceDismissAnimator.swift */; }; 313FB0B72719C518000AD9DA /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 313FB0B62719C518000AD9DA /* GoogleService-Info.plist */; }; - 313FB0B92719CFA9000AD9DA /* EventsMainViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 319C0EA226F4A43C00C6D38B /* EventsMainViewController.xib */; }; 314263A5271F048800DA4429 /* AppFeatureSwitches.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314263A4271F048800DA4429 /* AppFeatureSwitches.swift */; }; 3145054D2CE39190000F3E9F /* CoreUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3145054C2CE39190000F3E9F /* CoreUI */; }; 3145D23126B3F73F00F16787 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3145D23026B3F73F00F16787 /* AppDelegate.swift */; }; 3145D23326B3F73F00F16787 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3145D23226B3F73F00F16787 /* SceneDelegate.swift */; }; 3145D23A26B3F74000F16787 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3145D23926B3F74000F16787 /* Images.xcassets */; }; 3145D25326B3F74000F16787 /* UILaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3145D25226B3F74000F16787 /* UILaunchTests.swift */; }; - 314EB4A1270DDDF60067FACA /* EventsCoordinator.NavigationControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314EB4A0270DDDF60067FACA /* EventsCoordinator.NavigationControllerDelegate.swift */; }; 31515A0D286C5A9300642907 /* UIFont+NOI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31515A0C286C5A9300642907 /* UIFont+NOI.swift */; }; 31515A1C286C868600642907 /* SourceSansPro-BlackItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 31515A0F286C868600642907 /* SourceSansPro-BlackItalic.ttf */; }; 31515A1D286C868600642907 /* SourceSansPro-SemiBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 31515A10286C868600642907 /* SourceSansPro-SemiBoldItalic.ttf */; }; @@ -86,11 +81,30 @@ 315275672847A01000D5C8A1 /* PersonId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315275662847A01000D5C8A1 /* PersonId.swift */; }; 3153010528071D1200471153 /* AuthWelcomePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3153010428071D1200471153 /* AuthWelcomePageViewController.swift */; }; 3153010728071D5500471153 /* AuthWelcomePageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3153010628071D5500471153 /* AuthWelcomePageViewController.xib */; }; - 315F4BE427035679001905AF /* EventCardContentConfiguration.SharedConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315F4BE327035679001905AF /* EventCardContentConfiguration.SharedConfig.swift */; }; 3167681727035D9500A9DB87 /* EatMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3167681527035D9500A9DB87 /* EatMainViewController.swift */; }; 3167681827035D9500A9DB87 /* EatCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3167681627035D9500A9DB87 /* EatCoordinator.swift */; }; 3167681B27036A2F00A9DB87 /* PlaceCardContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3167681927036A2F00A9DB87 /* PlaceCardContentView.swift */; }; 3167681C27036A2F00A9DB87 /* PlaceCardContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3167681A27036A2F00A9DB87 /* PlaceCardContentView.xib */; }; + 316A829E2CFF1FF0002D62D3 /* EventFiltersListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A82892CFF1FF0002D62D3 /* EventFiltersListViewController.swift */; }; + 316A829F2CFF1FF0002D62D3 /* EventFiltersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A82932CFF1FF0002D62D3 /* EventFiltersViewModel.swift */; }; + 316A82A02CFF1FF0002D62D3 /* DateIntervalFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A82912CFF1FF0002D62D3 /* DateIntervalFilter.swift */; }; + 316A82A12CFF1FF0002D62D3 /* EventListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A828C2CFF1FF0002D62D3 /* EventListViewController.swift */; }; + 316A82A22CFF1FF0002D62D3 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A82922CFF1FF0002D62D3 /* Event.swift */; }; + 316A82A32CFF1FF0002D62D3 /* EventsFeatureConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A829C2CFF1FF0002D62D3 /* EventsFeatureConstants.swift */; }; + 316A82A42CFF1FF0002D62D3 /* EventDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A82872CFF1FF0002D62D3 /* EventDetailsViewController.swift */; }; + 316A82A52CFF1FF0002D62D3 /* EventCardContentConfiguration.SharedConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A82962CFF1FF0002D62D3 /* EventCardContentConfiguration.SharedConfig.swift */; }; + 316A82A62CFF1FF0002D62D3 /* EventsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A82852CFF1FF0002D62D3 /* EventsCoordinator.swift */; }; + 316A82A72CFF1FF0002D62D3 /* EventsMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A828E2CFF1FF0002D62D3 /* EventsMainViewController.swift */; }; + 316A82A82CFF1FF0002D62D3 /* EventFiltersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A828A2CFF1FF0002D62D3 /* EventFiltersViewController.swift */; }; + 316A82A92CFF1FF0002D62D3 /* EventsCoordinator.NavigationControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A828D2CFF1FF0002D62D3 /* EventsCoordinator.NavigationControllerDelegate.swift */; }; + 316A82AA2CFF1FF0002D62D3 /* EventCardContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A82972CFF1FF0002D62D3 /* EventCardContentView.swift */; }; + 316A82AB2CFF1FF0002D62D3 /* EventsFiltersBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A82992CFF1FF0002D62D3 /* EventsFiltersBarView.swift */; }; + 316A82AC2CFF1FF0002D62D3 /* EventsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316A82942CFF1FF0002D62D3 /* EventsViewModel.swift */; }; + 316A82AD2CFF1FF0002D62D3 /* EventsMainViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 316A828F2CFF1FF0002D62D3 /* EventsMainViewController.xib */; }; + 316A82AE2CFF1FF0002D62D3 /* EventsFiltersBarView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 316A829A2CFF1FF0002D62D3 /* EventsFiltersBarView.xib */; }; + 316A82AF2CFF1FF0002D62D3 /* EventFiltersViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 316A828B2CFF1FF0002D62D3 /* EventFiltersViewController.xib */; }; + 316A82B02CFF1FF0002D62D3 /* EventCardContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 316A82982CFF1FF0002D62D3 /* EventCardContentView.xib */; }; + 316A82B12CFF1FF0002D62D3 /* EventDetailsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 316A82882CFF1FF0002D62D3 /* EventDetailsViewController.xib */; }; 316ED05B270AFE140070D272 /* CurrentScrollOffsetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316ED05A270AFE140070D272 /* CurrentScrollOffsetProvider.swift */; }; 316ED05D270B12CF0070D272 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 316ED05C270B12C60070D272 /* Colors.xcassets */; }; 316F5615281C166B0075B09F /* MyAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316F5614281C166B0075B09F /* MyAccountViewController.swift */; }; @@ -118,7 +132,6 @@ 3182F4A227DB3841005ADDAF /* AppPreferencesClient in Frameworks */ = {isa = PBXBuildFile; productRef = 3182F4A127DB3841005ADDAF /* AppPreferencesClient */; }; 3182F4A427DB3841005ADDAF /* AppPreferencesClientLive in Frameworks */ = {isa = PBXBuildFile; productRef = 3182F4A327DB3841005ADDAF /* AppPreferencesClientLive */; }; 3182F4A627DB3841005ADDAF /* EventShortClient in Frameworks */ = {isa = PBXBuildFile; productRef = 3182F4A527DB3841005ADDAF /* EventShortClient */; }; - 3182F4A827DB3841005ADDAF /* EventShortClientLive in Frameworks */ = {isa = PBXBuildFile; productRef = 3182F4A727DB3841005ADDAF /* EventShortClientLive */; }; 3182F4AA27DB3841005ADDAF /* EventShortTypesClient in Frameworks */ = {isa = PBXBuildFile; productRef = 3182F4A927DB3841005ADDAF /* EventShortTypesClient */; }; 3182F4AC27DB3841005ADDAF /* EventShortTypesClientLive in Frameworks */ = {isa = PBXBuildFile; productRef = 3182F4AB27DB3841005ADDAF /* EventShortTypesClientLive */; }; 3185AA10281FD77400767E31 /* AccessNotGrantedCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3185AA0F281FD77400767E31 /* AccessNotGrantedCoordinator.swift */; }; @@ -135,8 +148,6 @@ 3197B093281A70990002FA08 /* BaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3197B092281A70990002FA08 /* BaseCoordinator.swift */; }; 3197B095281A712B0002FA08 /* RootCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3197B094281A712B0002FA08 /* RootCoordinatorType.swift */; }; 3197B097281A71B80002FA08 /* BaseRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3197B096281A71B80002FA08 /* BaseRootCoordinator.swift */; }; - 319C0E8226F333A700C6D38B /* EventsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C0E8126F333A700C6D38B /* EventsViewModel.swift */; }; - 319C0E8426F33AD900C6D38B /* EventsMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C0E8326F33AD900C6D38B /* EventsMainViewController.swift */; }; 319C0E8A26F33BD000C6D38B /* UIAlertController+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C0E8826F33BD000C6D38B /* UIAlertController+Error.swift */; }; 319C0E8B26F33BD000C6D38B /* UIViewController+ShowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C0E8926F33BD000C6D38B /* UIViewController+ShowError.swift */; }; 319C0E8C26F33BD000C6D38B /* UIView+Subviews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C0E8626F33BD000C6D38B /* UIView+Subviews.swift */; }; @@ -149,13 +160,7 @@ 319C465A282BBD8000946AC7 /* NewsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C4657282BB80F00946AC7 /* NewsListViewModel.swift */; }; 319C465C282BBE5100946AC7 /* XCTestCase+awaitPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C465B282BBE5100946AC7 /* XCTestCase+awaitPublisher.swift */; }; 319C465E282BCC5D00946AC7 /* collectNext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C465D282BCC5D00946AC7 /* collectNext.swift */; }; - 31A5F04F27D22ECA00FA20BC /* EventFiltersListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A5F04E27D22ECA00FA20BC /* EventFiltersListViewController.swift */; }; - 31A5F05127D257B700FA20BC /* EventFiltersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A5F05027D257B700FA20BC /* EventFiltersViewModel.swift */; }; - 31A5F05527D25A9400FA20BC /* EventFiltersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A5F05427D25A9400FA20BC /* EventFiltersViewController.swift */; }; - 31AA31E826F09E3600744A00 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA31E726F09E3600744A00 /* Event.swift */; }; - 31AA31EA26F0A6D700744A00 /* EventListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA31E926F0A6D700744A00 /* EventListViewController.swift */; }; 31AA31EE26F0A94E00744A00 /* IdentifiableCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA31ED26F0A94E00744A00 /* IdentifiableCollectionViewCell.swift */; }; - 31AA31F126F0B40800744A00 /* EventCardContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA31F026F0B40800744A00 /* EventCardContentView.swift */; }; 31AAC053270F2FFD00C26850 /* FooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AAC052270F2FFD00C26850 /* FooterView.swift */; }; 31AD1DB22B27947B006F71A4 /* ComeOnBoardOnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AD1DB12B27947B006F71A4 /* ComeOnBoardOnboardingViewModel.swift */; }; 31AD1DB32B279558006F71A4 /* ComeOnBoardOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AD1DB02B2791D8006F71A4 /* ComeOnBoardOnboardingViewController.swift */; }; @@ -176,11 +181,7 @@ 31C028412B55924B00D851EE /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C028402B55924B00D851EE /* FeatureFlag.swift */; }; 31C14C2327DA3597009AF69D /* UIScrollView+ScrollToView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C14C2227DA3597009AF69D /* UIScrollView+ScrollToView.swift */; }; 31C1ECB82C74E248004C9104 /* FiltersBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C1ECB72C74E248004C9104 /* FiltersBarView.swift */; }; - 31C2261C270F596E0098A70E /* EventCardContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 315F4BE127034C00001905AF /* EventCardContentView.xib */; }; - 31C2261D270F59750098A70E /* EventDetailsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3187667826FB3A9300782FA6 /* EventDetailsViewController.xib */; }; 31C28BF62719709B00312A62 /* NOICells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C28BF52719709B00312A62 /* NOICells.swift */; }; - 31C28BF8271992F500312A62 /* EventsFiltersBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C28BF7271992F500312A62 /* EventsFiltersBarView.swift */; }; - 31C28BFA2719931400312A62 /* EventsFiltersBarView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 31C28BF92719931400312A62 /* EventsFiltersBarView.xib */; }; 31C28BFD2719BD7200312A62 /* LoadingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 31C28BFB2719BD7200312A62 /* LoadingViewController.xib */; }; 31C28BFE2719BD7200312A62 /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C28BFC2719BD7200312A62 /* LoadingViewController.swift */; }; 31C3629F270EFC5C00C92532 /* UIButton+Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C3629E270EFC5C00C92532 /* UIButton+Background.swift */; }; @@ -197,7 +198,6 @@ 31C640B126FE1449004B71A2 /* TabCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C640B026FE1449004B71A2 /* TabCoordinatorType.swift */; }; 31C640B326FE148C004B71A2 /* BaseNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C640B226FE148C004B71A2 /* BaseNavigationCoordinator.swift */; }; 31C640B526FE14ED004B71A2 /* TabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C640B426FE14ED004B71A2 /* TabCoordinator.swift */; }; - 31C640B726FE17F6004B71A2 /* EventsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C640B626FE17F6004B71A2 /* EventsCoordinator.swift */; }; 31C640C526FE1E43004B71A2 /* MeetCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C640C226FE1E43004B71A2 /* MeetCoordinator.swift */; }; 31C640C626FE1E43004B71A2 /* MeetMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C640C326FE1E43004B71A2 /* MeetMainViewController.swift */; }; 31C8F52B282522150009BCB7 /* NoiNewsTopic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C8F52A282522150009BCB7 /* NoiNewsTopic.swift */; }; @@ -214,7 +214,6 @@ 31DAC7BC2B56853A00F24D79 /* URL+Params.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DAC7BB2B56853A00F24D79 /* URL+Params.swift */; }; 31DAC7BE2B568A0100F24D79 /* IndexPathAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DAC7BD2B568A0100F24D79 /* IndexPathAdditions.swift */; }; 31E058F22812F18800D1F7FE /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 31E058F12812F18800D1F7FE /* KeychainAccess */; }; - 31E7872027D64F2300C67188 /* EventFiltersViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 31E7871F27D64E9600C67188 /* EventFiltersViewController.xib */; }; 31E7872427D7419200C67188 /* UIButton+Insets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E7872327D7419200C67188 /* UIButton+Insets.swift */; }; 31E7872627D8918800C67188 /* UIImageAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E7872527D8918800C67188 /* UIImageAdditions.swift */; }; 31EBA3742807FAD9001AAE8F /* AuthWelcomeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 31EBA3732807FAD9001AAE8F /* AuthWelcomeViewController.xib */; }; @@ -271,10 +270,7 @@ 31157D1826FCA1EB00A43B33 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; 31157D1A26FCB26200A43B33 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = ""; }; 31178FCD26F8D7CC00BDCDAA /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; - 31178FD526F9D6EB00BDCDAA /* DateIntervalFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateIntervalFilter.swift; sourceTree = ""; }; - 31178FD726F9FD3800BDCDAA /* EventsFeatureConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsFeatureConstants.swift; sourceTree = ""; }; 31178FD926FA16A200BDCDAA /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; - 31178FDB26FA26C500BDCDAA /* EventDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailsViewController.swift; sourceTree = ""; }; 31178FDD26FA2F7100BDCDAA /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; 31178FDF26FA318800BDCDAA /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; 3121AFDD2858B43A00248CDF /* MeetMainViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MeetMainViewController.xib; sourceTree = ""; }; @@ -308,7 +304,6 @@ 3145D24E26B3F74000F16787 /* NOICommunityUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NOICommunityUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3145D25226B3F74000F16787 /* UILaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILaunchTests.swift; sourceTree = ""; }; 3145D25426B3F74000F16787 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 314EB4A0270DDDF60067FACA /* EventsCoordinator.NavigationControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsCoordinator.NavigationControllerDelegate.swift; sourceTree = ""; }; 31515A0C286C5A9300642907 /* UIFont+NOI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+NOI.swift"; sourceTree = ""; }; 31515A0F286C868600642907 /* SourceSansPro-BlackItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceSansPro-BlackItalic.ttf"; sourceTree = ""; }; 31515A10286C868600642907 /* SourceSansPro-SemiBoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceSansPro-SemiBoldItalic.ttf"; sourceTree = ""; }; @@ -330,13 +325,31 @@ 315275662847A01000D5C8A1 /* PersonId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonId.swift; sourceTree = ""; }; 3153010428071D1200471153 /* AuthWelcomePageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthWelcomePageViewController.swift; sourceTree = ""; }; 3153010628071D5500471153 /* AuthWelcomePageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AuthWelcomePageViewController.xib; sourceTree = ""; }; - 315F4BE127034C00001905AF /* EventCardContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = EventCardContentView.xib; sourceTree = ""; }; - 315F4BE327035679001905AF /* EventCardContentConfiguration.SharedConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCardContentConfiguration.SharedConfig.swift; sourceTree = ""; }; 3164BAC326F8630D00443CF0 /* NOICommunity.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = NOICommunity.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 3167681527035D9500A9DB87 /* EatMainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EatMainViewController.swift; sourceTree = ""; }; 3167681627035D9500A9DB87 /* EatCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EatCoordinator.swift; sourceTree = ""; }; 3167681927036A2F00A9DB87 /* PlaceCardContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceCardContentView.swift; sourceTree = ""; }; 3167681A27036A2F00A9DB87 /* PlaceCardContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PlaceCardContentView.xib; sourceTree = ""; }; + 316A82852CFF1FF0002D62D3 /* EventsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsCoordinator.swift; sourceTree = ""; }; + 316A82872CFF1FF0002D62D3 /* EventDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailsViewController.swift; sourceTree = ""; }; + 316A82882CFF1FF0002D62D3 /* EventDetailsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EventDetailsViewController.xib; sourceTree = ""; }; + 316A82892CFF1FF0002D62D3 /* EventFiltersListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventFiltersListViewController.swift; sourceTree = ""; }; + 316A828A2CFF1FF0002D62D3 /* EventFiltersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventFiltersViewController.swift; sourceTree = ""; }; + 316A828B2CFF1FF0002D62D3 /* EventFiltersViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EventFiltersViewController.xib; sourceTree = ""; }; + 316A828C2CFF1FF0002D62D3 /* EventListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListViewController.swift; sourceTree = ""; }; + 316A828D2CFF1FF0002D62D3 /* EventsCoordinator.NavigationControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsCoordinator.NavigationControllerDelegate.swift; sourceTree = ""; }; + 316A828E2CFF1FF0002D62D3 /* EventsMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsMainViewController.swift; sourceTree = ""; }; + 316A828F2CFF1FF0002D62D3 /* EventsMainViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EventsMainViewController.xib; sourceTree = ""; }; + 316A82912CFF1FF0002D62D3 /* DateIntervalFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateIntervalFilter.swift; sourceTree = ""; }; + 316A82922CFF1FF0002D62D3 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; + 316A82932CFF1FF0002D62D3 /* EventFiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventFiltersViewModel.swift; sourceTree = ""; }; + 316A82942CFF1FF0002D62D3 /* EventsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsViewModel.swift; sourceTree = ""; }; + 316A82962CFF1FF0002D62D3 /* EventCardContentConfiguration.SharedConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCardContentConfiguration.SharedConfig.swift; sourceTree = ""; }; + 316A82972CFF1FF0002D62D3 /* EventCardContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCardContentView.swift; sourceTree = ""; }; + 316A82982CFF1FF0002D62D3 /* EventCardContentView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EventCardContentView.xib; sourceTree = ""; }; + 316A82992CFF1FF0002D62D3 /* EventsFiltersBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsFiltersBarView.swift; sourceTree = ""; }; + 316A829A2CFF1FF0002D62D3 /* EventsFiltersBarView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EventsFiltersBarView.xib; sourceTree = ""; }; + 316A829C2CFF1FF0002D62D3 /* EventsFeatureConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsFeatureConstants.swift; sourceTree = ""; }; 316ED05A270AFE140070D272 /* CurrentScrollOffsetProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentScrollOffsetProvider.swift; sourceTree = ""; }; 316ED05C270B12C60070D272 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; 316F5614281C166B0075B09F /* MyAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountViewController.swift; sourceTree = ""; }; @@ -363,7 +376,6 @@ 318664A12B22471E0088A752 /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = ""; }; 3187667426FB35C100782FA6 /* String+NotDefined.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+NotDefined.swift"; sourceTree = ""; }; 3187667626FB38D600782FA6 /* DateIntervalFormatter+Factory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateIntervalFormatter+Factory.swift"; sourceTree = ""; }; - 3187667826FB3A9300782FA6 /* EventDetailsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EventDetailsViewController.xib; sourceTree = ""; }; 31892F2B281BC09F00DD89F7 /* MoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreViewModel.swift; sourceTree = ""; }; 31892F2D281BC32800DD89F7 /* MyAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountViewModel.swift; sourceTree = ""; }; 318DA63328354F2D00E5819E /* DeepLinking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinking.swift; sourceTree = ""; }; @@ -371,8 +383,6 @@ 3197B092281A70990002FA08 /* BaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCoordinator.swift; sourceTree = ""; }; 3197B094281A712B0002FA08 /* RootCoordinatorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootCoordinatorType.swift; sourceTree = ""; }; 3197B096281A71B80002FA08 /* BaseRootCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseRootCoordinator.swift; sourceTree = ""; }; - 319C0E8126F333A700C6D38B /* EventsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsViewModel.swift; sourceTree = ""; }; - 319C0E8326F33AD900C6D38B /* EventsMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsMainViewController.swift; sourceTree = ""; }; 319C0E8526F33BD000C6D38B /* ContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerViewController.swift; sourceTree = ""; }; 319C0E8626F33BD000C6D38B /* UIView+Subviews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Subviews.swift"; sourceTree = ""; }; 319C0E8726F33BD000C6D38B /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = ""; }; @@ -380,18 +390,11 @@ 319C0E8926F33BD000C6D38B /* UIViewController+ShowError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+ShowError.swift"; sourceTree = ""; }; 319C0E9926F3483600C6D38B /* UIControl+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Combine.swift"; sourceTree = ""; }; 319C0E9F26F4790300C6D38B /* CalendarAdditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarAdditions.swift; sourceTree = ""; }; - 319C0EA226F4A43C00C6D38B /* EventsMainViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EventsMainViewController.xib; sourceTree = ""; }; 319C4657282BB80F00946AC7 /* NewsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsListViewModel.swift; sourceTree = ""; }; 319C4658282BBB6100946AC7 /* NewsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsListViewModelTests.swift; sourceTree = ""; }; 319C465B282BBE5100946AC7 /* XCTestCase+awaitPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+awaitPublisher.swift"; sourceTree = ""; }; 319C465D282BCC5D00946AC7 /* collectNext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = collectNext.swift; sourceTree = ""; }; - 31A5F04E27D22ECA00FA20BC /* EventFiltersListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventFiltersListViewController.swift; sourceTree = ""; }; - 31A5F05027D257B700FA20BC /* EventFiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventFiltersViewModel.swift; sourceTree = ""; }; - 31A5F05427D25A9400FA20BC /* EventFiltersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventFiltersViewController.swift; sourceTree = ""; }; - 31AA31E726F09E3600744A00 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; - 31AA31E926F0A6D700744A00 /* EventListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListViewController.swift; sourceTree = ""; }; 31AA31ED26F0A94E00744A00 /* IdentifiableCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableCollectionViewCell.swift; sourceTree = ""; }; - 31AA31F026F0B40800744A00 /* EventCardContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCardContentView.swift; sourceTree = ""; }; 31AAC052270F2FFD00C26850 /* FooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FooterView.swift; sourceTree = ""; }; 31AD1DAE2B2791A4006F71A4 /* ComeOnBoardOnboardingViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComeOnBoardOnboardingViewController.xib; sourceTree = ""; }; 31AD1DB02B2791D8006F71A4 /* ComeOnBoardOnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComeOnBoardOnboardingViewController.swift; sourceTree = ""; }; @@ -412,8 +415,6 @@ 31C14C2227DA3597009AF69D /* UIScrollView+ScrollToView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+ScrollToView.swift"; sourceTree = ""; }; 31C1ECB72C74E248004C9104 /* FiltersBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersBarView.swift; sourceTree = ""; }; 31C28BF52719709B00312A62 /* NOICells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NOICells.swift; sourceTree = ""; }; - 31C28BF7271992F500312A62 /* EventsFiltersBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsFiltersBarView.swift; sourceTree = ""; }; - 31C28BF92719931400312A62 /* EventsFiltersBarView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EventsFiltersBarView.xib; sourceTree = ""; }; 31C28BFB2719BD7200312A62 /* LoadingViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LoadingViewController.xib; sourceTree = ""; }; 31C28BFC2719BD7200312A62 /* LoadingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; 31C3629E270EFC5C00C92532 /* UIButton+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Background.swift"; sourceTree = ""; }; @@ -429,7 +430,6 @@ 31C640B026FE1449004B71A2 /* TabCoordinatorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCoordinatorType.swift; sourceTree = ""; }; 31C640B226FE148C004B71A2 /* BaseNavigationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationCoordinator.swift; sourceTree = ""; }; 31C640B426FE14ED004B71A2 /* TabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCoordinator.swift; sourceTree = ""; }; - 31C640B626FE17F6004B71A2 /* EventsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsCoordinator.swift; sourceTree = ""; }; 31C640C226FE1E43004B71A2 /* MeetCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetCoordinator.swift; sourceTree = ""; }; 31C640C326FE1E43004B71A2 /* MeetMainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetMainViewController.swift; sourceTree = ""; }; 31C8F52A282522150009BCB7 /* NoiNewsTopic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiNewsTopic.swift; sourceTree = ""; }; @@ -447,7 +447,6 @@ 31DAC7BD2B568A0100F24D79 /* IndexPathAdditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexPathAdditions.swift; sourceTree = ""; }; 31E058F32812F29500D1F7FE /* NOICommunityDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NOICommunityDebug.entitlements; sourceTree = ""; }; 31E058F42812F2A700D1F7FE /* NOICommunityRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NOICommunityRelease.entitlements; sourceTree = ""; }; - 31E7871F27D64E9600C67188 /* EventFiltersViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EventFiltersViewController.xib; sourceTree = ""; }; 31E7872327D7419200C67188 /* UIButton+Insets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Insets.swift"; sourceTree = ""; }; 31E7872527D8918800C67188 /* UIImageAdditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageAdditions.swift; sourceTree = ""; }; 31EBA3732807FAD9001AAE8F /* AuthWelcomeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AuthWelcomeViewController.xib; sourceTree = ""; }; @@ -475,7 +474,6 @@ 319C4653282BB32400946AC7 /* ArticlesClient in Frameworks */, 3182F4A427DB3841005ADDAF /* AppPreferencesClientLive in Frameworks */, 319C4655282BB32400946AC7 /* ArticlesClientLive in Frameworks */, - 3182F4A827DB3841005ADDAF /* EventShortClientLive in Frameworks */, 317B6F9C28118BD6008D07C0 /* AuthClient in Frameworks */, 3182F4AC27DB3841005ADDAF /* EventShortTypesClientLive in Frameworks */, 311E0EC62825157800404DCE /* FirebaseMessaging in Frameworks */, @@ -511,53 +509,6 @@ path = TodayFeature; sourceTree = ""; }; - 3101FC5C2833952200A3416F /* Views */ = { - isa = PBXGroup; - children = ( - 315F4BE327035679001905AF /* EventCardContentConfiguration.SharedConfig.swift */, - 31AA31F026F0B40800744A00 /* EventCardContentView.swift */, - 315F4BE127034C00001905AF /* EventCardContentView.xib */, - 31C28BF7271992F500312A62 /* EventsFiltersBarView.swift */, - 31C28BF92719931400312A62 /* EventsFiltersBarView.xib */, - ); - path = Views; - sourceTree = ""; - }; - 3101FC5D2833952700A3416F /* View Controllers */ = { - isa = PBXGroup; - children = ( - 31178FDB26FA26C500BDCDAA /* EventDetailsViewController.swift */, - 3187667826FB3A9300782FA6 /* EventDetailsViewController.xib */, - 31A5F04E27D22ECA00FA20BC /* EventFiltersListViewController.swift */, - 31A5F05427D25A9400FA20BC /* EventFiltersViewController.swift */, - 31E7871F27D64E9600C67188 /* EventFiltersViewController.xib */, - 31AA31E926F0A6D700744A00 /* EventListViewController.swift */, - 314EB4A0270DDDF60067FACA /* EventsCoordinator.NavigationControllerDelegate.swift */, - 319C0E8326F33AD900C6D38B /* EventsMainViewController.swift */, - 319C0EA226F4A43C00C6D38B /* EventsMainViewController.xib */, - ); - path = "View Controllers"; - sourceTree = ""; - }; - 3101FC5E2833952F00A3416F /* View Models */ = { - isa = PBXGroup; - children = ( - 31178FD526F9D6EB00BDCDAA /* DateIntervalFilter.swift */, - 31AA31E726F09E3600744A00 /* Event.swift */, - 31A5F05027D257B700FA20BC /* EventFiltersViewModel.swift */, - 319C0E8126F333A700C6D38B /* EventsViewModel.swift */, - ); - path = "View Models"; - sourceTree = ""; - }; - 3101FC5F2833956C00A3416F /* Coordinators */ = { - isa = PBXGroup; - children = ( - 31C640B626FE17F6004B71A2 /* EventsCoordinator.swift */, - ); - path = Coordinators; - sourceTree = ""; - }; 3102F45B282C068A00687CF7 /* View Models */ = { isa = PBXGroup; children = ( @@ -679,7 +630,7 @@ 31CF8123270B3CD90077CB0D /* IntroFeature */, 3101FC4F283390D400A3416F /* TodayFeature */, 319C4656282BB7F300946AC7 /* NewsFeature */, - 319C0E8026F332FE00C6D38B /* EventsFeature */, + 316A829D2CFF1FF0002D62D3 /* EventsFeature */, 31D9C78726FDFC58001F2DBB /* OrientateFeature */, 31C640C126FE1E43004B71A2 /* MeetFeature */, 3167681427035D9500A9DB87 /* EatFeature */, @@ -795,6 +746,65 @@ path = EatFeature; sourceTree = ""; }; + 316A82862CFF1FF0002D62D3 /* Coordinators */ = { + isa = PBXGroup; + children = ( + 316A82852CFF1FF0002D62D3 /* EventsCoordinator.swift */, + ); + path = Coordinators; + sourceTree = ""; + }; + 316A82902CFF1FF0002D62D3 /* View Controllers */ = { + isa = PBXGroup; + children = ( + 316A82872CFF1FF0002D62D3 /* EventDetailsViewController.swift */, + 316A82882CFF1FF0002D62D3 /* EventDetailsViewController.xib */, + 316A82892CFF1FF0002D62D3 /* EventFiltersListViewController.swift */, + 316A828A2CFF1FF0002D62D3 /* EventFiltersViewController.swift */, + 316A828B2CFF1FF0002D62D3 /* EventFiltersViewController.xib */, + 316A828C2CFF1FF0002D62D3 /* EventListViewController.swift */, + 316A828D2CFF1FF0002D62D3 /* EventsCoordinator.NavigationControllerDelegate.swift */, + 316A828E2CFF1FF0002D62D3 /* EventsMainViewController.swift */, + 316A828F2CFF1FF0002D62D3 /* EventsMainViewController.xib */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + 316A82952CFF1FF0002D62D3 /* View Models */ = { + isa = PBXGroup; + children = ( + 316A82912CFF1FF0002D62D3 /* DateIntervalFilter.swift */, + 316A82922CFF1FF0002D62D3 /* Event.swift */, + 316A82932CFF1FF0002D62D3 /* EventFiltersViewModel.swift */, + 316A82942CFF1FF0002D62D3 /* EventsViewModel.swift */, + ); + path = "View Models"; + sourceTree = ""; + }; + 316A829B2CFF1FF0002D62D3 /* Views */ = { + isa = PBXGroup; + children = ( + 316A82962CFF1FF0002D62D3 /* EventCardContentConfiguration.SharedConfig.swift */, + 316A82972CFF1FF0002D62D3 /* EventCardContentView.swift */, + 316A82982CFF1FF0002D62D3 /* EventCardContentView.xib */, + 316A82992CFF1FF0002D62D3 /* EventsFiltersBarView.swift */, + 316A829A2CFF1FF0002D62D3 /* EventsFiltersBarView.xib */, + ); + path = Views; + sourceTree = ""; + }; + 316A829D2CFF1FF0002D62D3 /* EventsFeature */ = { + isa = PBXGroup; + children = ( + 316A82862CFF1FF0002D62D3 /* Coordinators */, + 316A82902CFF1FF0002D62D3 /* View Controllers */, + 316A82952CFF1FF0002D62D3 /* View Models */, + 316A829B2CFF1FF0002D62D3 /* Views */, + 316A829C2CFF1FF0002D62D3 /* EventsFeatureConstants.swift */, + ); + path = EventsFeature; + sourceTree = ""; + }; 3185AA13281FEAAE00767E31 /* AccessNotGrantedCoordinator */ = { isa = PBXGroup; children = ( @@ -826,19 +836,6 @@ path = Custom; sourceTree = ""; }; - 319C0E8026F332FE00C6D38B /* EventsFeature */ = { - isa = PBXGroup; - children = ( - 3101FC5F2833956C00A3416F /* Coordinators */, - 3101FC5D2833952700A3416F /* View Controllers */, - 3101FC5E2833952F00A3416F /* View Models */, - 3101FC5C2833952200A3416F /* Views */, - 31178FD726F9FD3800BDCDAA /* EventsFeatureConstants.swift */, - ); - name = EventsFeature; - path = TodayFeature/EventsFeature; - sourceTree = ""; - }; 319C0E8F26F33BE200C6D38B /* ViewControllers */ = { isa = PBXGroup; children = ( @@ -1082,7 +1079,6 @@ 3182F4A127DB3841005ADDAF /* AppPreferencesClient */, 3182F4A327DB3841005ADDAF /* AppPreferencesClientLive */, 3182F4A527DB3841005ADDAF /* EventShortClient */, - 3182F4A727DB3841005ADDAF /* EventShortClientLive */, 3182F4A927DB3841005ADDAF /* EventShortTypesClient */, 3182F4AB27DB3841005ADDAF /* EventShortTypesClientLive */, 31EF82582810517600EBE5F0 /* AppAuth */, @@ -1201,7 +1197,6 @@ 317D5C772841208400531432 /* PersonDetailHeaderContentView.xib in Resources */, 313FB0B72719C518000AD9DA /* GoogleService-Info.plist in Resources */, 31515A22286C868600642907 /* SourceSansPro-Black.ttf in Resources */, - 31C2261D270F59750098A70E /* EventDetailsViewController.xib in Resources */, 316ED05D270B12CF0070D272 /* Colors.xcassets in Resources */, 31CF8133270B49100077CB0D /* VersionContentView.xib in Resources */, 313010D42846640400AF6520 /* MeetFilterBarView.xib in Resources */, @@ -1210,7 +1205,6 @@ 31515A20286C868600642907 /* SourceSansPro-LightItalic.ttf in Resources */, 3121AFDE2858B43A00248CDF /* MeetMainViewController.xib in Resources */, 31515A1F286C868600642907 /* SourceSansPro-Bold.ttf in Resources */, - 31C28BFA2719931400312A62 /* EventsFiltersBarView.xib in Resources */, 313010CD2846102000AF6520 /* MyAccountViewController.xib in Resources */, 31EBA3742807FAD9001AAE8F /* AuthWelcomeViewController.xib in Resources */, 3132EF3A283D29E00016DF7F /* PersonCardContentView.xib in Resources */, @@ -1219,25 +1213,27 @@ 31515A23286C868600642907 /* OFL.txt in Resources */, 31515A1C286C868600642907 /* SourceSansPro-BlackItalic.ttf in Resources */, 31515A21286C868600642907 /* SourceSansPro-Light.ttf in Resources */, - 313FB0B92719CFA9000AD9DA /* EventsMainViewController.xib in Resources */, 31515A27286C868600642907 /* SourceSansPro-ExtraLightItalic.ttf in Resources */, 31515A26286C868600642907 /* SourceSansPro-SemiBold.ttf in Resources */, 3182A1BF282D5159005439B4 /* NewsDetailsViewController.xib in Resources */, 31CF812D270B3E340077CB0D /* LaunchScreen.storyboard in Resources */, 31C28BFD2719BD7200312A62 /* LoadingViewController.xib in Resources */, - 31C2261C270F596E0098A70E /* EventCardContentView.xib in Resources */, 317615CA26FDD637007FE10F /* MessageViewController.xib in Resources */, 31515A28286C868600642907 /* SourceSansPro-Italic.ttf in Resources */, 3182A177282CEFB7005439B4 /* NewsCardContentView.xib in Resources */, 31157D0526FC66FB00A43B33 /* InfoPlist.strings in Resources */, 317A5D71284E0EA300750412 /* CompaniesFiltersViewController.xib in Resources */, 31AD1DB62B279E78006F71A4 /* ComeOnBoardOnboardingViewController.xib in Resources */, + 316A82AD2CFF1FF0002D62D3 /* EventsMainViewController.xib in Resources */, + 316A82AE2CFF1FF0002D62D3 /* EventsFiltersBarView.xib in Resources */, + 316A82AF2CFF1FF0002D62D3 /* EventFiltersViewController.xib in Resources */, + 316A82B02CFF1FF0002D62D3 /* EventCardContentView.xib in Resources */, + 316A82B12CFF1FF0002D62D3 /* EventDetailsViewController.xib in Resources */, 311350E9282E5BD300AC9BC9 /* Photos.xcassets in Resources */, 3101FC52283390F900A3416F /* TopTabBarController.xib in Resources */, 31155BEC2B224FDE0027AEC1 /* BlockAccessViewController.xib in Resources */, 31CF8128270B3CD90077CB0D /* NOI_begins-with-you_Animation_black_short.mp4 in Resources */, 31157D0C26FC70A500A43B33 /* Localizable.strings in Resources */, - 31E7872027D64F2300C67188 /* EventFiltersViewController.xib in Resources */, 3145D23A26B3F74000F16787 /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1263,7 +1259,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 31178FD626F9D6EB00BDCDAA /* DateIntervalFilter.swift in Sources */, 3136CC402706F75F00C27129 /* SourcePresentAnimator.swift in Sources */, 31178FCE26F8D7CC00BDCDAA /* Localization.swift in Sources */, 31EE1A9227143AA1006A8EEF /* MapWebViewController.swift in Sources */, @@ -1279,16 +1274,13 @@ 31AA31EE26F0A94E00744A00 /* IdentifiableCollectionViewCell.swift in Sources */, 31F0DBD3280F0D3D00E782D5 /* AuthCoordinator.swift in Sources */, 317615C726FDD0FE007FE10F /* MessageViewController.swift in Sources */, - 319C0E8226F333A700C6D38B /* EventsViewModel.swift in Sources */, 31C640AD26FE13CC004B71A2 /* ViewModelFactory.swift in Sources */, 31AD1DB32B279558006F71A4 /* ComeOnBoardOnboardingViewController.swift in Sources */, 31C640B326FE148C004B71A2 /* BaseNavigationCoordinator.swift in Sources */, 3152755D28473D1C00D5C8A1 /* MeetFilterBarView.swift in Sources */, 31178FE026FA318800BDCDAA /* Collection.swift in Sources */, 3197B097281A71B80002FA08 /* BaseRootCoordinator.swift in Sources */, - 31AA31F126F0B40800744A00 /* EventCardContentView.swift in Sources */, 31D77E742733FFDD0054397C /* UIRefreshControlAdditions.swift in Sources */, - 319C0E8426F33AD900C6D38B /* EventsMainViewController.swift in Sources */, 314263A5271F048800DA4429 /* AppFeatureSwitches.swift in Sources */, 3145D23126B3F73F00F16787 /* AppDelegate.swift in Sources */, 3167681827035D9500A9DB87 /* EatCoordinator.swift in Sources */, @@ -1321,35 +1313,43 @@ 3182A1BB282D476D005439B4 /* RoundedLabel.swift in Sources */, 31C640A026FE0B9D004B71A2 /* BaseTabCoordinator.swift in Sources */, 31BFAB3B283FC88A00EF274D /* CALayer+XibConfiguration.swift in Sources */, - 31A5F04F27D22ECA00FA20BC /* EventFiltersListViewController.swift in Sources */, 3153010528071D1200471153 /* AuthWelcomePageViewController.swift in Sources */, 316F5615281C166B0075B09F /* MyAccountViewController.swift in Sources */, 31C028412B55924B00D851EE /* FeatureFlag.swift in Sources */, 31C1ECB82C74E248004C9104 /* FiltersBarView.swift in Sources */, 31C640B126FE1449004B71A2 /* TabCoordinatorType.swift in Sources */, - 31A5F05527D25A9400FA20BC /* EventFiltersViewController.swift in Sources */, 317B6F9A28118950008D07C0 /* ClientFactory.swift in Sources */, 31DA4A762705EA110098E395 /* MoreMainViewController.swift in Sources */, 31DAC7BC2B56853A00F24D79 /* URL+Params.swift in Sources */, 311350E7282E4F0A00AC9BC9 /* GalleryCollectionViewController.swift in Sources */, 3126E7272B2882620013F456 /* CacheKey.swift in Sources */, - 31178FDC26FA26C500BDCDAA /* EventDetailsViewController.swift in Sources */, + 316A829E2CFF1FF0002D62D3 /* EventFiltersListViewController.swift in Sources */, + 316A829F2CFF1FF0002D62D3 /* EventFiltersViewModel.swift in Sources */, + 316A82A02CFF1FF0002D62D3 /* DateIntervalFilter.swift in Sources */, + 316A82A12CFF1FF0002D62D3 /* EventListViewController.swift in Sources */, + 316A82A22CFF1FF0002D62D3 /* Event.swift in Sources */, + 316A82A32CFF1FF0002D62D3 /* EventsFeatureConstants.swift in Sources */, + 316A82A42CFF1FF0002D62D3 /* EventDetailsViewController.swift in Sources */, + 316A82A52CFF1FF0002D62D3 /* EventCardContentConfiguration.SharedConfig.swift in Sources */, + 316A82A62CFF1FF0002D62D3 /* EventsCoordinator.swift in Sources */, + 316A82A72CFF1FF0002D62D3 /* EventsMainViewController.swift in Sources */, + 316A82A82CFF1FF0002D62D3 /* EventFiltersViewController.swift in Sources */, + 316A82A92CFF1FF0002D62D3 /* EventsCoordinator.NavigationControllerDelegate.swift in Sources */, + 316A82AA2CFF1FF0002D62D3 /* EventCardContentView.swift in Sources */, + 316A82AB2CFF1FF0002D62D3 /* EventsFiltersBarView.swift in Sources */, + 316A82AC2CFF1FF0002D62D3 /* EventsViewModel.swift in Sources */, 31C362A1270EFCC200C92532 /* UIButton+Noi.swift in Sources */, 31BFAB39283FB8EC00EF274D /* PersonDetailsViewController.swift in Sources */, - 31A5F05127D257B700FA20BC /* EventFiltersViewModel.swift in Sources */, 317F74FA27D8B5610084E619 /* CircleButton.swift in Sources */, 31DA4A772705EA110098E395 /* MoreCoordinator.swift in Sources */, 312F5D292808252900C84598 /* WelcomeViewModel.swift in Sources */, 31DAC7BE2B568A0100F24D79 /* IndexPathAdditions.swift in Sources */, 3101FC5B283394FA00A3416F /* TodayCoordinator.swift in Sources */, - 31C28BF8271992F500312A62 /* EventsFiltersBarView.swift in Sources */, 3145D23326B3F73F00F16787 /* SceneDelegate.swift in Sources */, 31B192722C6A367D009872E9 /* DeveloperToolsViewController.swift in Sources */, - 31AA31E826F09E3600744A00 /* Event.swift in Sources */, 3187667726FB38D600782FA6 /* DateIntervalFormatter+Factory.swift in Sources */, 3102F45F282C06B700687CF7 /* NewsViewController.swift in Sources */, 319C0E8D26F33BD000C6D38B /* ContainerViewController.swift in Sources */, - 314EB4A1270DDDF60067FACA /* EventsCoordinator.NavigationControllerDelegate.swift in Sources */, 31155BEB2B224FDE0027AEC1 /* BlockAccessViewController.swift in Sources */, 315275652847A00700D5C8A1 /* CompanyId.swift in Sources */, 3197B095281A712B0002FA08 /* RootCoordinatorType.swift in Sources */, @@ -1362,11 +1362,8 @@ 3167681B27036A2F00A9DB87 /* PlaceCardContentView.swift in Sources */, 319C0E8B26F33BD000C6D38B /* UIViewController+ShowError.swift in Sources */, 318DA63428354F2D00E5819E /* DeepLinking.swift in Sources */, - 31178FD826F9FD3800BDCDAA /* EventsFeatureConstants.swift in Sources */, - 31AA31EA26F0A6D700744A00 /* EventListViewController.swift in Sources */, 3182A178282CEFB7005439B4 /* NewsCardContentConfiguration.SharedConfig.swift in Sources */, 3106A2E5282132840029839A /* AuthConstants.swift in Sources */, - 315F4BE427035679001905AF /* EventCardContentConfiguration.SharedConfig.swift in Sources */, 3185AA12281FDBED00767E31 /* LoadUserInfoCoordinator.swift in Sources */, 31FAEA7D28197D9700CDBC1B /* AppCoordinator.swift in Sources */, 31157D0026FC648700A43B33 /* Event+CalendarEvent.swift in Sources */, @@ -1403,7 +1400,6 @@ 31C640B526FE14ED004B71A2 /* TabCoordinator.swift in Sources */, 316ED05B270AFE140070D272 /* CurrentScrollOffsetProvider.swift in Sources */, 3101FC57283391B200A3416F /* TopTabCoordinatorType.swift in Sources */, - 31C640B726FE17F6004B71A2 /* EventsCoordinator.swift in Sources */, 31EFE4AB28575B0F00B496DB /* UIViewController+EmptyState.swift in Sources */, 31796C7C2809B489000542F6 /* GradientView.swift in Sources */, 317EC887283BB75900F30B95 /* PeopleViewModel.swift in Sources */, @@ -2346,10 +2342,6 @@ isa = XCSwiftPackageProductDependency; productName = EventShortClient; }; - 3182F4A727DB3841005ADDAF /* EventShortClientLive */ = { - isa = XCSwiftPackageProductDependency; - productName = EventShortClientLive; - }; 3182F4A927DB3841005ADDAF /* EventShortTypesClient */ = { isa = XCSwiftPackageProductDependency; productName = EventShortTypesClient; diff --git a/NOICommunity/DeepLinking.swift b/NOICommunity/DeepLinking.swift index aafff01..22ef409 100644 --- a/NOICommunity/DeepLinking.swift +++ b/NOICommunity/DeepLinking.swift @@ -15,6 +15,7 @@ import UIKit enum DeepLinkIntent { case showNews(newsId: String) + //case showEvent(eventId: String) } // MARK: - DeepLinkManager @@ -42,6 +43,9 @@ struct DeepLinkManager { case (URLConstant.newsDetailsPath, let newsId): // Matches: /newsDetails/{newsId} return .showNews(newsId: newsId) +// case (URLConstant.eventDetailsPath, let eventId): +// // Matches: /eventDetails/{eventId} +// return .showEvent(eventId: eventId) default: return nil } @@ -71,6 +75,7 @@ private extension DeepLinkManager { static let customURLScheme = "noi-community" static let host = "it.bz.noi.community" static let newsDetailsPath = "newsDetails" + static let eventDetailsPath = "eventDetails" } enum NotificationConstant { diff --git a/NOICommunity/TodayFeature/EventsFeature/Coordinators/EventsCoordinator.swift b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/Coordinators/EventsCoordinator.swift rename to NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/EventsFeatureConstants.swift b/NOICommunity/EventsFeature/EventsFeatureConstants.swift similarity index 64% rename from NOICommunity/TodayFeature/EventsFeature/EventsFeatureConstants.swift rename to NOICommunity/EventsFeature/EventsFeatureConstants.swift index 1688ee2..5710ede 100644 --- a/NOICommunity/TodayFeature/EventsFeature/EventsFeatureConstants.swift +++ b/NOICommunity/EventsFeature/EventsFeatureConstants.swift @@ -12,6 +12,15 @@ import Foundation enum EventsFeatureConstants { + static let maximumNumberOfEvents = 20 static let maximumNumberOfRelatedEvents = 3 + + public static let clientBaseURL: URL = { +#if TESTINGMACHINE_OAUTH + URL(string: "https://api.tourism.testingmachine.eu")! +#else + URL(string: "https://tourism.opendatahub.com")! +#endif + }() } diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.swift rename to NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.xib b/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.xib similarity index 97% rename from NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.xib rename to NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.xib index 3764113..8780bb8 100644 --- a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.xib +++ b/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -38,7 +38,7 @@ - + @@ -47,13 +47,13 @@ - + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. @@ -72,19 +72,19 @@ - + - + - + diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventFiltersListViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventFiltersListViewController.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/View Controllers/EventFiltersListViewController.swift rename to NOICommunity/EventsFeature/View Controllers/EventFiltersListViewController.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventFiltersViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventFiltersViewController.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/View Controllers/EventFiltersViewController.swift rename to NOICommunity/EventsFeature/View Controllers/EventFiltersViewController.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventFiltersViewController.xib b/NOICommunity/EventsFeature/View Controllers/EventFiltersViewController.xib similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/View Controllers/EventFiltersViewController.xib rename to NOICommunity/EventsFeature/View Controllers/EventFiltersViewController.xib diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventListViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventListViewController.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/View Controllers/EventListViewController.swift rename to NOICommunity/EventsFeature/View Controllers/EventListViewController.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventsCoordinator.NavigationControllerDelegate.swift b/NOICommunity/EventsFeature/View Controllers/EventsCoordinator.NavigationControllerDelegate.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/View Controllers/EventsCoordinator.NavigationControllerDelegate.swift rename to NOICommunity/EventsFeature/View Controllers/EventsCoordinator.NavigationControllerDelegate.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventsMainViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventsMainViewController.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/View Controllers/EventsMainViewController.swift rename to NOICommunity/EventsFeature/View Controllers/EventsMainViewController.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventsMainViewController.xib b/NOICommunity/EventsFeature/View Controllers/EventsMainViewController.xib similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/View Controllers/EventsMainViewController.xib rename to NOICommunity/EventsFeature/View Controllers/EventsMainViewController.xib diff --git a/NOICommunity/TodayFeature/EventsFeature/View Models/DateIntervalFilter.swift b/NOICommunity/EventsFeature/View Models/DateIntervalFilter.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/View Models/DateIntervalFilter.swift rename to NOICommunity/EventsFeature/View Models/DateIntervalFilter.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/View Models/Event.swift b/NOICommunity/EventsFeature/View Models/Event.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/View Models/Event.swift rename to NOICommunity/EventsFeature/View Models/Event.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/View Models/EventFiltersViewModel.swift b/NOICommunity/EventsFeature/View Models/EventFiltersViewModel.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/View Models/EventFiltersViewModel.swift rename to NOICommunity/EventsFeature/View Models/EventFiltersViewModel.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift b/NOICommunity/EventsFeature/View Models/EventsViewModel.swift similarity index 74% rename from NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift rename to NOICommunity/EventsFeature/View Models/EventsViewModel.swift index c39d2c3..afd6111 100644 --- a/NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift +++ b/NOICommunity/EventsFeature/View Models/EventsViewModel.swift @@ -16,8 +16,8 @@ import EventShortTypesClient // MARK: - EventsViewModel -class EventsViewModel { - +final class EventsViewModel { + @Published var isLoading = false @Published var error: Error! @Published var eventResults: [Event]! @@ -51,60 +51,9 @@ class EventsViewModel { } func refreshEvents() { - let (startDate, endDate) = dateIntervalFilter.toStartEndDates() - - isLoading = true - eventResults = nil - - let roomMappingPublisher: AnyPublisher<[String : String], Error> - if let roomMapping = roomMapping { - roomMappingPublisher = Just(roomMapping) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } else { - roomMappingPublisher = eventShortClient.roomMapping(language) - } - let eventListPublisher = eventShortClient - .list(EventShortListRequest( - pageSize: maximumNumberOfEvents, - startDate: startDate, - endDate: endDate, - eventLocation: .noi, - publishedon: "noi-communityapp", - fields: ["AnchorVenue", "AnchorVenueRoomMapping", "CompanyName", "Display5", "EndDate", "EventDescriptionDE", "EventDescriptionEN", "EventDescriptionIT", "EventLocation", "EventTextDE", "EventTextEN", "EventTextIT", "Id", "ImageGallery", "StartDate", "WebAddress"], - rawFilter: activeFilters.toQuery(), - removeNullValues: true, - optimizeDates: true - )) - - refreshEventsRequestCancellable = roomMappingPublisher - .zip(eventListPublisher) - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - self?.isLoading = false - - switch completion { - case .finished: - break - case .failure(let error): - self?.error = error - } - }, - receiveValue: { [weak self] in - guard let self = self - else { return } - - let (roomMappingResponse, eventShortListResponse) = $0 - self.roomMapping = roomMappingResponse - let allEventsShort = eventShortListResponse.items ?? [] - self.eventResults = allEventsShort.map { eventShort in - Event( - from: eventShort, - roomMapping: roomMappingResponse - ) - } - }) + Task(priority: .userInitiated) { [weak self] in + await self?.performRefreshEvents() + } } func relatedEvent(of event: Event) -> [Event] { @@ -131,9 +80,55 @@ class EventsViewModel { } +// MARK: Private APIs + +private extension EventsViewModel { + + func performRefreshEvents() async { + eventResults = nil + + isLoading = true + defer { + isLoading = false + } + + do { + roomMapping = if let availableRoomMapping = roomMapping { + availableRoomMapping + } else { + try await eventShortClient.getRoomMapping(language: language?.rawValue) + } + + let (startDate, endDate) = dateIntervalFilter.toStartEndDates() + let response = try await eventShortClient.getEventShortList( + pageSize: maximumNumberOfEvents, + startDate: startDate, + endDate: endDate, + eventLocation: .noi, + publishedon: "noi-communityapp", + fields: ["AnchorVenue", "AnchorVenueRoomMapping", "CompanyName", "Display5", "EndDate", "EventDescriptionDE", "EventDescriptionEN", "EventDescriptionIT", "EventLocation", "EventTextDE", "EventTextEN", "EventTextIT", "Id", "ImageGallery", "StartDate", "WebAddress"], + rawFilter: activeFilters.toQuery(), + removeNullValues: true, + optimizeDates: true + ) + eventResults = response + .items + .map { eventShort in + Event( + from: eventShort, + roomMapping: roomMapping + ) + } + } catch { + self.error = error + } + } +} + // MARK: - DateIntervalFilter Additions private extension DateIntervalFilter { + func toStartEndDates( using calendar: Calendar = .current ) -> (start: Date, end: Date?) { @@ -157,6 +152,7 @@ private extension DateIntervalFilter { // MARK: - EventShort Additions private extension EventShort { + var localizedEventDescriptions: [String:String] { Dictionary(uniqueKeysWithValues: [ eventDescriptionIT.map { ("it", $0) }, @@ -177,6 +173,7 @@ private extension EventShort { // MARK: - Event Additions private extension Event { + init( from eventShort: EventShort, roomMapping: [String:String] diff --git a/NOICommunity/TodayFeature/EventsFeature/Views/EventCardContentConfiguration.SharedConfig.swift b/NOICommunity/EventsFeature/Views/EventCardContentConfiguration.SharedConfig.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/Views/EventCardContentConfiguration.SharedConfig.swift rename to NOICommunity/EventsFeature/Views/EventCardContentConfiguration.SharedConfig.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/Views/EventCardContentView.swift b/NOICommunity/EventsFeature/Views/EventCardContentView.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/Views/EventCardContentView.swift rename to NOICommunity/EventsFeature/Views/EventCardContentView.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/Views/EventCardContentView.xib b/NOICommunity/EventsFeature/Views/EventCardContentView.xib similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/Views/EventCardContentView.xib rename to NOICommunity/EventsFeature/Views/EventCardContentView.xib diff --git a/NOICommunity/TodayFeature/EventsFeature/Views/EventsFiltersBarView.swift b/NOICommunity/EventsFeature/Views/EventsFiltersBarView.swift similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/Views/EventsFiltersBarView.swift rename to NOICommunity/EventsFeature/Views/EventsFiltersBarView.swift diff --git a/NOICommunity/TodayFeature/EventsFeature/Views/EventsFiltersBarView.xib b/NOICommunity/EventsFeature/Views/EventsFiltersBarView.xib similarity index 100% rename from NOICommunity/TodayFeature/EventsFeature/Views/EventsFiltersBarView.xib rename to NOICommunity/EventsFeature/Views/EventsFiltersBarView.xib diff --git a/NOICommunity/SceneDelegate.swift b/NOICommunity/SceneDelegate.swift index 0ace6c4..2a29d9d 100644 --- a/NOICommunity/SceneDelegate.swift +++ b/NOICommunity/SceneDelegate.swift @@ -11,7 +11,7 @@ import UIKit import AppAuth -import EventShortClientLive +import EventShortClient import AppPreferencesClientLive import EventShortTypesClient import EventShortTypesClientLive @@ -60,7 +60,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { context: self, tokenStorage: tokenStorage ), - eventShortClient: .live(), + eventShortClient: EventShortClientImplementation( + baseURL: EventsFeatureConstants.clientBaseURL, + transport: URLSession.shared + ), eventShortTypesClient: { if let fileURL = Bundle.main.url( forResource: "EventShortTypes", diff --git a/NOICommunityLib/Package.swift b/NOICommunityLib/Package.swift index 95465f6..a11bc96 100644 --- a/NOICommunityLib/Package.swift +++ b/NOICommunityLib/Package.swift @@ -40,10 +40,6 @@ let package = Package( name: "EventShortClient", targets: ["EventShortClient"] ), - .library( - name: "EventShortClientLive", - targets: ["EventShortClientLive"] - ), .library( name: "AuthStateStorageClient", targets: ["AuthStateStorageClient"] @@ -109,13 +105,6 @@ let package = Package( "Core" ] ), - .testTarget( - name: "EventShortTypesClientTests", - dependencies: [ - "Core", - "EventShortTypesClient" - ] - ), .target( name: "EventShortTypesClientLive", dependencies: [ @@ -129,13 +118,6 @@ let package = Package( "Core" ] ), - .testTarget( - name: "AppPreferencesClientTests", - dependencies: [ - "Core", - "AppPreferencesClient" - ] - ), .target( name: "AppPreferencesClientLive", dependencies: [ @@ -149,20 +131,6 @@ let package = Package( "Core", ] ), - .testTarget( - name: "EventShortClientTests", - dependencies: [ - "Core", - "EventShortClient" - ] - ), - .target( - name: "EventShortClientLive", - dependencies: [ - "Core", - "EventShortClient" - ] - ), .target( name: "AuthStateStorageClient", dependencies: [ diff --git a/NOICommunityLib/Sources/AppPreferencesClient/Mocks.swift b/NOICommunityLib/Sources/AppPreferencesClient/Mocks.swift index cf8f9d2..067d48d 100644 --- a/NOICommunityLib/Sources/AppPreferencesClient/Mocks.swift +++ b/NOICommunityLib/Sources/AppPreferencesClient/Mocks.swift @@ -12,7 +12,7 @@ import Foundation import Combine -// MARK: - EventShortClient+Live +// MARK: - AppPreferencesClient+Live public extension AppPreferencesClient { diff --git a/NOICommunityLib/Sources/AppPreferencesClientLive/Live.swift b/NOICommunityLib/Sources/AppPreferencesClientLive/Live.swift index 517a144..9f403b5 100644 --- a/NOICommunityLib/Sources/AppPreferencesClientLive/Live.swift +++ b/NOICommunityLib/Sources/AppPreferencesClientLive/Live.swift @@ -18,7 +18,7 @@ import AppPreferencesClient private let skipIntroKey = "skipIntro" private let skipComeOnBoardOnboardingKey = "skipComeOnBoardOnboarding" -// MARK: - EventShortClient+Live +// MARK: - AppPreferencesClient+Live public extension AppPreferencesClient { diff --git a/NOICommunityLib/Sources/AuthClientLive/Live.swift b/NOICommunityLib/Sources/AuthClientLive/Live.swift index 9e1b754..fb8dc2e 100644 --- a/NOICommunityLib/Sources/AuthClientLive/Live.swift +++ b/NOICommunityLib/Sources/AuthClientLive/Live.swift @@ -53,7 +53,7 @@ public protocol AuthContext { var presentationContext: () -> UIViewController { get } } -// MARK: - EventShortClient+Live +// MARK: - AuthClient+Live public extension AuthClient { diff --git a/NOICommunityLib/Sources/Core/Endpoint/Endpoint+QueryBuilder.swift b/NOICommunityLib/Sources/Core/Network/Endpoint/Endpoint+QueryBuilder.swift similarity index 100% rename from NOICommunityLib/Sources/Core/Endpoint/Endpoint+QueryBuilder.swift rename to NOICommunityLib/Sources/Core/Network/Endpoint/Endpoint+QueryBuilder.swift diff --git a/NOICommunityLib/Sources/Core/Endpoint/Endpoint.swift b/NOICommunityLib/Sources/Core/Network/Endpoint/Endpoint.swift similarity index 99% rename from NOICommunityLib/Sources/Core/Endpoint/Endpoint.swift rename to NOICommunityLib/Sources/Core/Network/Endpoint/Endpoint.swift index 88f3e9b..097ac86 100644 --- a/NOICommunityLib/Sources/Core/Endpoint/Endpoint.swift +++ b/NOICommunityLib/Sources/Core/Network/Endpoint/Endpoint.swift @@ -9,6 +9,7 @@ // Created by Matteo Matassoni on 04/08/21. // + import Foundation public struct Endpoint { diff --git a/NOICommunityLib/Sources/Core/Network/Transport/Implementations/HeaderAddingTransport.swift b/NOICommunityLib/Sources/Core/Network/Transport/Implementations/HeaderAddingTransport.swift new file mode 100644 index 0000000..687979c --- /dev/null +++ b/NOICommunityLib/Sources/Core/Network/Transport/Implementations/HeaderAddingTransport.swift @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// StatusCodeCheckingTransport.swift +// Core +// +// Created by Matteo Matassoni on 27/08/24. +// + +import Foundation + +// MARK: - StatusCodeCheckingTransport + +public final class HeaderAddingTransport: Transport { + + let wrapped: Transport + let headers: [String: String] + + public init(wrapping: Transport, headers: [String: String]) { + self.wrapped = wrapping + self.headers = headers + } + + public func send(request: URLRequest) async throws -> (Data, HTTPURLResponse) { + var mutableCopy = request + for (key, value) in headers { + mutableCopy.addValue(value, forHTTPHeaderField: key) + } + + return try await wrapped.send(request: request) + } + +} + +// MARK: - Transport+addingJSONHeaders + +public extension Transport { + + func addingJSONHeaders() -> Transport { + HeaderAddingTransport( + wrapping: self, + headers: [ + "Content-Type": "application/json", + "Accept": "application/json", + ] + ) + } + +} + +// MARK: - Transport+addingJSONHeaders + +public extension Transport { + + func authenticated(withBearerToken accessToken: String) -> Transport { + HeaderAddingTransport( + wrapping: self, + headers: [ + "Authorization": "Bearer \(accessToken)" + ] + ) + } + +} + + diff --git a/NOICommunityLib/Sources/Core/Network/Transport/Implementations/MockTransport.swift b/NOICommunityLib/Sources/Core/Network/Transport/Implementations/MockTransport.swift new file mode 100644 index 0000000..f553906 --- /dev/null +++ b/NOICommunityLib/Sources/Core/Network/Transport/Implementations/MockTransport.swift @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// MockTransport.swift +// Core +// +// Created by Matteo Matassoni on 27/08/24. +// + +import Foundation + +public final class MockTransport: Transport { + + public let data: Data + public let response: HTTPURLResponse + + public init(data: Data, response: HTTPURLResponse) { + self.data = data + self.response = response + } + + public convenience init(statusCode: Int, data: Data = .init()) { + self.init( + data: data, + response: .init( + url: URL(string: "example.com")!, + statusCode: statusCode, + httpVersion: nil, + headerFields: nil + )! + ) + } + + public func send(request: URLRequest) async throws -> (Data, HTTPURLResponse) { + (data, response) + } + +} + +extension MockTransport { + + public static func ok(data: Data = .init()) -> MockTransport { + MockTransport(statusCode: 200, data: data) + } + + public static func serverError(data: Data = .init()) -> MockTransport { + MockTransport(statusCode: 500, data: data) + } + +} diff --git a/NOICommunityLib/Sources/Core/Network/Transport/Implementations/RequestInspectableTransport.swift b/NOICommunityLib/Sources/Core/Network/Transport/Implementations/RequestInspectableTransport.swift new file mode 100644 index 0000000..46c6fad --- /dev/null +++ b/NOICommunityLib/Sources/Core/Network/Transport/Implementations/RequestInspectableTransport.swift @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// RequestInspectableTransport.swift +// Core +// +// Created by Matteo Matassoni on 27/08/24. +// + +import Foundation + +public final class RequestInspectableTransport: Transport { + + public var lastSeenRequest: URLRequest? + + let wrapping: Transport + + public init(wrapping: Transport) { + self.wrapping = wrapping + } + + public func send(request: URLRequest) async throws -> (Data, HTTPURLResponse) { + lastSeenRequest = request + return try await wrapping.send(request: request) + } + +} diff --git a/NOICommunityLib/Sources/Core/Network/Transport/Implementations/StatusCodeCheckingTransport.swift b/NOICommunityLib/Sources/Core/Network/Transport/Implementations/StatusCodeCheckingTransport.swift new file mode 100644 index 0000000..48d5886 --- /dev/null +++ b/NOICommunityLib/Sources/Core/Network/Transport/Implementations/StatusCodeCheckingTransport.swift @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// StatusCodeCheckingTransport.swift +// Core +// +// Created by Matteo Matassoni on 27/08/24. +// + +import Foundation + +private let validStatus = 200..<300 + +// MARK: - StatusCodeError + +public struct StatusCodeError: Error { + + var statusCode: Int + +} + +// MARK: - StatusCodeCheckingTransport + +public final class StatusCodeCheckingTransport: Transport { + + let wrapped: Transport + + public init(wrapping wrapped: Transport) { + self.wrapped = wrapped + } + + public func send(request: URLRequest) async throws -> (Data, HTTPURLResponse) { + let (data, response) = try await wrapped.send(request: request) + guard validStatus.contains(response.statusCode) + else { throw StatusCodeError(statusCode: response.statusCode) } + + return (data, response) + } + +} + +// MARK: - Transport+checkingStatusCodes + +public extension Transport { + + func checkingStatusCodes() -> Transport { + StatusCodeCheckingTransport(wrapping: self) + } + +} + + diff --git a/NOICommunityLib/Sources/Core/Network/Transport/Implementations/URLSession+Transport.swift b/NOICommunityLib/Sources/Core/Network/Transport/Implementations/URLSession+Transport.swift new file mode 100644 index 0000000..c0477cf --- /dev/null +++ b/NOICommunityLib/Sources/Core/Network/Transport/Implementations/URLSession+Transport.swift @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// URLSession+Transport.swift +// Core +// +// Created by Matteo Matassoni on 27/08/24. +// + +import Foundation + +public struct InvalidResponseError: Error {} + +extension URLSession: Transport { + + public func send(request: URLRequest) async throws -> (Data, HTTPURLResponse) { + let (data, response) = try await data(for: request) + guard let httpResponse = response as? HTTPURLResponse + else { throw InvalidResponseError() } + + return (data, httpResponse) + } + +} diff --git a/NOICommunityLib/Sources/Core/Network/Transport/Interfaces/Transport.swift b/NOICommunityLib/Sources/Core/Network/Transport/Interfaces/Transport.swift new file mode 100644 index 0000000..bfaccb3 --- /dev/null +++ b/NOICommunityLib/Sources/Core/Network/Transport/Interfaces/Transport.swift @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// Transport.swift +// Core +// +// Created by Matteo Matassoni on 27/08/24. +// + +import Foundation + +public protocol Transport { + + func send(request: URLRequest) async throws -> (Data, HTTPURLResponse) + +} + +public extension Transport { + + func get(from url: URL) async throws -> (Data, HTTPURLResponse) { + let request = URLRequest(url: url) + return try await send(request: request) + } + +} diff --git a/NOICommunityLib/Sources/EventShortClient/Endpoint+EventShort.swift b/NOICommunityLib/Sources/EventShortClient/Endpoint+EventShort.swift new file mode 100644 index 0000000..2011f37 --- /dev/null +++ b/NOICommunityLib/Sources/EventShortClient/Endpoint+EventShort.swift @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// Endpoint+EventShort.swift +// NOICommunityLib +// +// Created by Matteo Matassoni on 03/12/24. +// + +import Foundation +import Core + +// MARK: - Endpoint+EventShort + +extension Endpoint { + + static func eventShortList( + pageNumber: Int? = nil, + pageSize: Int? = nil, + startDate: Date? = nil, + endDate: Date? = nil, + source: Source? = nil, + eventLocation: EventLocation? = nil, + publishedon: String? = nil, + eventIds: [String]? = nil, + webAddress: String? = nil, + sortOrder: Order? = nil, + seed: Int? = nil, + language: String? = nil, + langFilter: [String]? = nil, + fields: [String]? = nil, + lastChange: Date? = nil, + searchFilter: String? = nil, + rawFilter: String? = nil, + rawSort: String? = nil, + removeNullValues: Bool? = nil, + optimizeDates: Bool? = nil + ) -> Endpoint { + let dateFormatter = DateFormatter() + dateFormatter.calendar = Calendar(identifier: .iso8601) + dateFormatter.timeZone = TimeZone(identifier: "Europe/Rome")! + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm" + + return Self(path: "/v1/EventShort") { + if let pageNumber { + URLQueryItem( + name: "pagenumber", + value: "\(pageNumber)" + ) + } + + if let pageSize { + URLQueryItem( + name: "pagesize", + value: "\(pageSize)" + ) + } + + if let startDate { + URLQueryItem( + name: "startdate", + value: dateFormatter.string(from: startDate) + ) + } + + if let endDate { + URLQueryItem( + name: "enddate", + value: dateFormatter.string(from: endDate) + ) + } + + if let source { + URLQueryItem( + name: "source", + value: source.rawValue) + } + + if let eventLocation { + URLQueryItem( + name: "eventlocation", + value: eventLocation.rawValue) + } + + if let publishedon { + URLQueryItem( + name: "publishedon", + value: publishedon) + } + + if let eventIds { + URLQueryItem( + name: "eventids", + value: eventIds.joined(separator: ",") + ) + } + + if let webAddress { + URLQueryItem( + name: "webaddress", + value: webAddress + ) + } + + if let sortOrder { + URLQueryItem( + name: "sortorder", + value: sortOrder.rawValue + ) + } + + if let seed { + URLQueryItem( + name: "seed", + value: "\(seed)" + ) + } + + if let language { + URLQueryItem( + name: "language", + value: language) + } + + if let langFilter { + URLQueryItem( + name: "langfilter", + value: langFilter.joined(separator: ",") + ) + } + + if let fields { + URLQueryItem( + name: "fields", + value: fields.joined(separator: ",") + ) + } + + if let lastChange { + URLQueryItem( + name: "lastchange", + value: dateFormatter.string(from: lastChange) + ) + } + + if let searchFilter { + URLQueryItem( + name: "searchfilter", + value: searchFilter + ) + } + + if let rawFilter { + URLQueryItem( + name: "rawfilter", + value: rawFilter + ) + } + + if let rawSort { + URLQueryItem( + name: "rawsort", + value: rawSort + ) + } + + if let removeNullValues { + URLQueryItem( + name: "removenullvalues", + value: String(removeNullValues) + ) + } + + if let optimizeDates { + URLQueryItem( + name: "optimizedates", + value: String(optimizeDates) + ) + } + } + } + + static func roomMapping( + language: String? = nil + ) -> Endpoint { + Self(path: "/v1/EventShort/RoomMapping") { + if let language { + URLQueryItem( + name: "language", + value: language + ) + } + } + } + + static func eventShort( + id: String, + language: String? = nil, + optimizeDates: Bool? = nil, + fields: [String]? = nil, + removeNullValues: Bool? = nil + ) -> Endpoint { + Self(path: "/v1/EventShort/\(id)") { + if let language { + URLQueryItem( + name: "language", + value: language + ) + } + + + if let fields { + URLQueryItem( + name: "optimizedates", + value: fields.joined(separator: ",") + ) + } + + + if let optimizeDates { + URLQueryItem( + name: "optimizedates", + value: String(optimizeDates) + ) + } + + + if let removeNullValues { + URLQueryItem( + name: "removenullvalues", + value: String(removeNullValues) + ) + } + } + } + +} diff --git a/NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift b/NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift new file mode 100644 index 0000000..a8e3cd4 --- /dev/null +++ b/NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift @@ -0,0 +1,170 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// EventShortClientImplementation.swift +// NOICommunityLib +// +// Created by Matteo Matassoni on 03/12/24. +// + +import Foundation +import Core + +// MARK: - EventShortClientImplementation + +public final class EventShortClientImplementation: EventShortClient { + + private let baseURL: URL + + private let transport: Transport + + private let jsonDecoder: JSONDecoder = { + let jsonDecoder = JSONDecoder() + + jsonDecoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let dateStr = try container.decode(String.self) + + let dateFormatter = DateFormatter() + dateFormatter.calendar = Calendar(identifier: .iso8601) + dateFormatter.timeZone = TimeZone(identifier: "Europe/Rome") + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Cannot decode date string \(dateStr)" + ) + } + + jsonDecoder.keyDecodingStrategy = .convertFromPascalCase + return jsonDecoder + }() + + public init( + baseURL: URL, + transport: Transport + ) { + self.baseURL = baseURL + self.transport = transport + .checkingStatusCodes() + .addingJSONHeaders() + } + + public func getEventShortList( + pageNumber: Int?, + pageSize: Int?, + startDate: Date?, + endDate: Date?, + source: Source?, + eventLocation: EventLocation?, + publishedon: String?, + eventIds: [String]?, + webAddress: String?, + sortOrder: Order?, + seed: Int?, + language: String?, + langFilter: [String]?, + fields: [String]?, + lastChange: Date?, + searchFilter: String?, + rawFilter: String?, + rawSort: String?, + removeNullValues: Bool?, + optimizeDates: Bool? + ) async throws -> EventShortListResponse { + let request = Endpoint + .eventShortList( + pageNumber: pageNumber, + pageSize: pageSize, + startDate: startDate, + endDate: endDate, + source: source, + eventLocation: eventLocation, + publishedon: publishedon, + eventIds: eventIds, + webAddress: webAddress, + sortOrder: sortOrder, + seed: seed, + language: language, + langFilter: langFilter, + fields: fields, + lastChange: lastChange, + searchFilter: searchFilter, + rawFilter: rawFilter, + rawSort: rawSort, + removeNullValues: removeNullValues, + optimizeDates: optimizeDates + ) + .makeRequest(withBaseURL: baseURL) + + let (data, _) = try await transport.send(request: request) + + try Task.checkCancellation() + + return try jsonDecoder.decode( + EventShortListResponse.self, + from: data + ) + } + + public func getRoomMapping( + language: String? + ) async throws -> [String:String] { + let request = Endpoint + .roomMapping(language: language) + .makeRequest(withBaseURL: baseURL) + + let (data, _) = try await transport.send(request: request) + + try Task.checkCancellation() + + return try jsonDecoder.decode([String:String].self, from: data) + } + + public func getEventShort( + id: String, + language: String?, + optimizeDates: Bool?, + fields: [String]?, + removeNullValues: Bool? + ) async throws -> EventShort { + let request = Endpoint + .eventShort( + id: id, + language: language, + optimizeDates: optimizeDates, + fields: fields, + removeNullValues: removeNullValues + ) + .makeRequest(withBaseURL: baseURL) + + let (data, _) = try await transport.send(request: request) + + try Task.checkCancellation() + + return try jsonDecoder.decode(EventShort.self, from: data) + } + +} diff --git a/NOICommunityLib/Sources/EventShortClient/Interface.swift b/NOICommunityLib/Sources/EventShortClient/Interface.swift deleted file mode 100644 index 436fdc2..0000000 --- a/NOICommunityLib/Sources/EventShortClient/Interface.swift +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -// -// Interface.swift -// EventShortClient -// -// Created by Matteo Matassoni on 16/09/21. -// - -import Foundation -import Combine - -public struct EventShortClient { - public var list: (EventShortListRequest?) -> AnyPublisher - public var roomMapping: (Language?) -> AnyPublisher<[String:String], Error> - - public init( - list: @escaping (EventShortListRequest?) -> AnyPublisher, - roomMapping: @escaping (Language?) -> AnyPublisher<[String:String], Error> - ) { - self.list = list - self.roomMapping = roomMapping - } -} diff --git a/NOICommunityLib/Sources/EventShortClient/Interfaces/EventShortClient.swift b/NOICommunityLib/Sources/EventShortClient/Interfaces/EventShortClient.swift new file mode 100644 index 0000000..90b537e --- /dev/null +++ b/NOICommunityLib/Sources/EventShortClient/Interfaces/EventShortClient.swift @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// EventShortClient.swift +// EventShortClient +// +// Created by Matteo Matassoni on 16/09/21. +// + +import Foundation + +public protocol EventShortClient { + + func getEventShortList( + pageNumber: Int?, + pageSize: Int?, + startDate: Date?, + endDate: Date?, + source: Source?, + eventLocation: EventLocation?, + publishedon: String?, + eventIds: [String]?, + webAddress: String?, + sortOrder: Order?, + seed: Int?, + language: String?, + langFilter: [String]?, + fields: [String]?, + lastChange: Date?, + searchFilter: String?, + rawFilter: String?, + rawSort: String?, + removeNullValues: Bool?, + optimizeDates: Bool? + ) async throws -> EventShortListResponse + + func getRoomMapping( + language: String? + ) async throws -> [String:String] + + func getEventShort( + id: String, + language: String?, + optimizeDates: Bool?, + fields: [String]?, + removeNullValues: Bool? + ) async throws -> EventShort + +} + +public extension EventShortClient { + + func getEventShortList( + pageNumber: Int? = nil, + pageSize: Int? = nil, + startDate: Date? = nil, + endDate: Date? = nil, + source: Source? = nil, + eventLocation: EventLocation? = nil, + publishedon: String? = nil, + eventIds: [String]? = nil, + webAddress: String? = nil, + sortOrder: Order? = nil, + seed: Int? = nil, + language: String? = nil, + langFilter: [String]? = nil, + fields: [String]? = nil, + lastChange: Date? = nil, + searchFilter: String? = nil, + rawFilter: String? = nil, + rawSort: String? = nil, + removeNullValues: Bool? = nil, + optimizeDates: Bool? = nil + ) async throws -> EventShortListResponse { + try await getEventShortList( + pageNumber: pageNumber, + pageSize: pageSize, + startDate: startDate, + endDate: endDate, + source: source, + eventLocation: eventLocation, + publishedon: publishedon, + eventIds: eventIds, + webAddress: webAddress, + sortOrder: sortOrder, + seed: seed, + language: language, + langFilter: langFilter, + fields: fields, + lastChange: lastChange, + searchFilter: searchFilter, + rawFilter: rawFilter, + rawSort: rawSort, + removeNullValues: removeNullValues, + optimizeDates: optimizeDates + ) + } + + func getRoomMapping( + language: String? = nil + ) async throws -> [String:String] { + try await getRoomMapping(language: language) + } + + func getEventShort( + id: String, + language: String? = nil, + optimizeDates: Bool? = nil, + fields: [String]? = nil, + removeNullValues: Bool? = nil + ) async throws -> EventShort { + try await getEventShort( + id: id, + language: language, + optimizeDates: optimizeDates, + fields: fields, + removeNullValues: removeNullValues + ) + } + +} diff --git a/NOICommunityLib/Sources/EventShortClient/Mocks.swift b/NOICommunityLib/Sources/EventShortClient/Mocks.swift deleted file mode 100644 index 19d84ba..0000000 --- a/NOICommunityLib/Sources/EventShortClient/Mocks.swift +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -// -// Mocks.swift -// EventShortClient -// -// Created by Matteo Matassoni on 16/09/21. -// - -import Foundation -import Combine - -extension EventShortClient { - - public static let empty = Self( - list: { _ in - Just(EventShortListResponse( - totalResults: 0, - totalPages: 0, - currentPage: 1, - onlineResults: nil, - resultId: nil, - seed: nil, - items: [] - )) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - }, - roomMapping: { _ in - Just([:]) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - ) - - public static let happyPath = Self( - list: { _ in - Just(EventShortListResponse( - totalResults: 0, - totalPages: 0, - currentPage: 1, - onlineResults: nil, - resultId: nil, - seed: nil, - items: [] - )) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - }, - roomMapping: { _ in - Just([:]) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - ) - - public static let failed = Self( - list: { _ in - Fail(error: NSError(domain: "", code: 1)) - .eraseToAnyPublisher() - }, - roomMapping: { _ in - Fail(error: NSError(domain: "", code: 1)) - .eraseToAnyPublisher() - } - ) - -} diff --git a/NOICommunityLib/Sources/EventShortClient/Models.swift b/NOICommunityLib/Sources/EventShortClient/Models/EventShort.swift similarity index 66% rename from NOICommunityLib/Sources/EventShortClient/Models.swift rename to NOICommunityLib/Sources/EventShortClient/Models/EventShort.swift index 3679655..2219eb1 100644 --- a/NOICommunityLib/Sources/EventShortClient/Models.swift +++ b/NOICommunityLib/Sources/EventShortClient/Models/EventShort.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // -// Models.swift +// EventShort.swift // EventShortClient // // Created by Matteo Matassoni on 16/09/21. @@ -11,21 +11,10 @@ import Foundation -// MARK: - EventShortListResponse - -public struct EventShortListResponse: Decodable, Equatable { - public let totalResults: Int - public let totalPages: Int - public let currentPage: Int - public let onlineResults: Int? - public let resultId: String? - public let seed: String? - public let items: [EventShort]? -} - // MARK: - EventShort public struct EventShort: Decodable, Equatable { + public let licenseInfo: LicenseInfo? public let id: String? public let source: String? @@ -93,75 +82,6 @@ public struct EventShort: Decodable, Equatable { public let publishedOn: [String?]? } -// MARK: - EventShortListRequest - -public struct EventShortListRequest { - public let pageNumber: Int? - public let pageSize: Int? - public let startDate: Date? - public let endDate: Date? - public let source: Source? - public let eventLocation: EventLocation? - public let publishedon: String? - public let eventIds: [String]? - public let webAddress: String? - public let sortOrder: Order? - public let seed: Int? - public let language: String? - public let langFilter: [String]? - public let fields: [String]? - public let lastChange: Date? - public let searchFilter: String? - public let rawFilter: String? - public let rawSort: String? - public let removeNullValues: Bool? - public let optimizeDates: Bool? - - public init( - pageNumber: Int? = nil, - pageSize: Int? = nil, - startDate: Date? = nil, - endDate: Date? = nil, - source: Source? = nil, - eventLocation: EventLocation? = nil, - publishedon: String? = nil, - eventIds: [String]? = nil, - webAddress: String? = nil, - sortOrder: Order? = nil, - seed: Int? = nil, - language: String? = nil, - langFilter: [String]? = nil, - fields: [String]? = nil, - lastChange: Date? = nil, - searchFilter: String? = nil, - rawFilter: String? = nil, - rawSort: String? = nil, - removeNullValues: Bool? = nil, - optimizeDates: Bool? = nil - ) { - self.pageNumber = pageNumber - self.pageSize = pageSize - self.startDate = startDate - self.endDate = endDate - self.source = source - self.eventLocation = eventLocation - self.publishedon = publishedon - self.eventIds = eventIds - self.webAddress = webAddress - self.sortOrder = sortOrder - self.seed = seed - self.language = language - self.langFilter = langFilter - self.fields = fields - self.lastChange = lastChange - self.searchFilter = searchFilter - self.rawFilter = rawFilter - self.rawSort = rawSort - self.removeNullValues = removeNullValues - self.optimizeDates = optimizeDates - } -} - // MARK: - Source public struct Source: Hashable { @@ -188,15 +108,6 @@ extension EventLocation: Decodable { } } -// MARK: - Order - -public struct Order: Hashable { - public let rawValue: String - - public static let ascending = Self(rawValue: "ASC") - public static let descending = Self(rawValue: "DESC") -} - // MARK: - LicenseInfo public struct LicenseInfo: Decodable, Equatable { diff --git a/NOICommunityLib/Sources/EventShortClient/Models/EventShortListResponse.swift b/NOICommunityLib/Sources/EventShortClient/Models/EventShortListResponse.swift new file mode 100644 index 0000000..585de12 --- /dev/null +++ b/NOICommunityLib/Sources/EventShortClient/Models/EventShortListResponse.swift @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// EventShortListResponse.swift +// EventShortClient +// +// Created by Matteo Matassoni on 16/09/21. +// + +import Foundation + +// MARK: - EventShortListResponse + +public struct EventShortListResponse: Decodable, Equatable { + + public let totalResults: Int + public let totalPages: Int + public let currentPage: Int + public let onlineResults: Int? + public let resultId: String? + public let seed: String? + public let items: [EventShort] + + enum CodingKeys: CodingKey { + case totalResults + case totalPages + case currentPage + case onlineResults + case resultId + case seed + case items + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.totalResults = try container.decode(Int.self, forKey: .totalResults) + self.totalPages = try container.decode(Int.self, forKey: .totalPages) + self.currentPage = try container.decode(Int.self, forKey: .currentPage) + self.onlineResults = try container.decodeIfPresent(Int.self, forKey: .onlineResults) + self.resultId = try container.decodeIfPresent(String.self, forKey: .resultId) + self.seed = try container.decodeIfPresent(String.self, forKey: .seed) + self.items = try container.decodeIfPresent([EventShort].self, forKey: .items) ?? [] + } + +} diff --git a/NOICommunityLib/Sources/EventShortClient/Models/Order.swift b/NOICommunityLib/Sources/EventShortClient/Models/Order.swift new file mode 100644 index 0000000..9d12ef7 --- /dev/null +++ b/NOICommunityLib/Sources/EventShortClient/Models/Order.swift @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// Order.swift +// NOICommunityLib +// +// Created by Matteo Matassoni on 03/12/24. +// + +// MARK: - Order + +public enum Order: String { + case ascending = "ASC" + case descending = "DESC" +} diff --git a/NOICommunityLib/Sources/EventShortClientLive/Live.swift b/NOICommunityLib/Sources/EventShortClientLive/Live.swift deleted file mode 100644 index a38e54a..0000000 --- a/NOICommunityLib/Sources/EventShortClientLive/Live.swift +++ /dev/null @@ -1,238 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -// -// Live.swift -// EventShortClientLive -// -// Created by Matteo Matassoni on 16/09/21. -// - -import Foundation -import Combine -import Core -import EventShortClient - -// MARK: - Private Constants - -private let baseUrl = URL(string: "https://tourism.opendatahub.com")! -private let eventsJsonDecoder: JSONDecoder = { - let jsonDecoder = JSONDecoder() - - jsonDecoder.dateDecodingStrategy = .custom { decoder in - let container = try decoder.singleValueContainer() - let dateStr = try container.decode(String.self) - - let dateFormatter = DateFormatter() - dateFormatter.calendar = Calendar(identifier: .iso8601) - dateFormatter.timeZone = TimeZone(identifier: "Europe/Rome") - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Cannot decode date string \(dateStr)" - ) - } - - jsonDecoder.keyDecodingStrategy = .convertFromPascalCase - return jsonDecoder -}() - -// MARK: - EventShortClient+Live - -extension EventShortClient { - public static func live(urlSession: URLSession = .shared) -> Self { - Self( - list: { request in - var urlComponents = URLComponents( - url: baseUrl, - resolvingAgainstBaseURL: false - )! - urlComponents.path = "/v1/EventShort" - if let request = request { - urlComponents.queryItems = request.asURLQueryItems() - } - - return urlSession - .dataTaskPublisher(for: urlComponents.url!) - .debug() - .map(\.data) - .decode( - type: EventShortListResponse.self, - decoder: eventsJsonDecoder - ) - .eraseToAnyPublisher() - }, - roomMapping: { language in - var urlComponents = URLComponents( - url: baseUrl, - resolvingAgainstBaseURL: false - )! - urlComponents.path = "/v1/EventShort/RoomMapping" - if let language = language { - urlComponents.queryItems = [language.asURLQueryItem()] - } - - return URLSession.shared - .dataTaskPublisher(for: urlComponents.url!) - .debug() - .map(\.data) - .decode( - type: [String:String].self, - decoder: eventsJsonDecoder - ) - .eraseToAnyPublisher() - } - ) - } -} - -// MARK: - EventShortListRequest+URLQueryItem - -private extension EventShortListRequest { - func asURLQueryItems() -> [URLQueryItem]? { - let dateFormatter = DateFormatter() - dateFormatter.calendar = Calendar(identifier: .iso8601) - dateFormatter.timeZone = TimeZone(identifier: "Europe/Rome")! - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm" - - var result: [URLQueryItem] = [] - - if let pageNumber = pageNumber { - result.append(URLQueryItem( - name: "pagenumber", - value: "\(pageNumber)")) - } - if let pageSize = pageSize { - result.append(URLQueryItem( - name: "pagesize", - value: "\(pageSize)")) - } - if let startDate = startDate { - result.append(URLQueryItem( - name: "startdate", - value: dateFormatter.string(from: startDate))) - } - if let endDate = endDate { - result.append(URLQueryItem( - name: "enddate", - value: dateFormatter.string(from: endDate))) - } - if let source = source { - result.append(URLQueryItem( - name: "source", - value: source.rawValue)) - } - if let eventLocation = eventLocation { - result.append(URLQueryItem( - name: "eventlocation", - value: eventLocation.rawValue)) - } - if let publishedon = publishedon { - result.append(URLQueryItem( - name: "publishedon", - value: publishedon)) - } - if let eventIds = eventIds { - result.append(URLQueryItem( - name: "eventids", - value: eventIds.joined(separator: ","))) - } - if let webAddress = webAddress { - result.append(URLQueryItem( - name: "webaddress", - value: webAddress) - ) - } - if let sortOrder = sortOrder { - result.append(URLQueryItem( - name: "sortorder", - value: sortOrder.rawValue)) - } - if let seed = seed { - result.append(URLQueryItem( - name: "seed", - value: "\(seed)")) - } - if let language = language { - result.append(URLQueryItem( - name: "language", - value: language)) - } - if let langFilter = langFilter { - result.append(URLQueryItem( - name: "langfilter", - value: langFilter.joined(separator: ","))) - } - if let fields = fields { - result.append(URLQueryItem( - name: "fields", - value: fields.joined(separator: ","))) - } - if let lastChange = lastChange { - result.append(URLQueryItem( - name: "lastchange", - value: dateFormatter.string(from: lastChange))) - } - if let searchFilter = searchFilter { - result.append(URLQueryItem( - name: "searchfilter", - value: searchFilter) - ) - } - if let rawFilter = rawFilter { - result.append(URLQueryItem( - name: "rawfilter", - value: rawFilter) - ) - } - if let rawSort = rawSort { - result.append(URLQueryItem( - name: "rawsort", - value: rawSort) - ) - } - if let removeNullValues = removeNullValues { - result.append(URLQueryItem( - name: "removenullvalues", - value: removeNullValues ? "true" : "false")) - } - if let optimizeDates = optimizeDates { - result.append(URLQueryItem( - name: "optimizedates", - value: optimizeDates ? "true" : "false")) - } - - return result.isEmpty ? nil : result - } -} - -// MARK: - Language+URLQueryItem - -private extension Language { - func asURLQueryItem() -> URLQueryItem { - URLQueryItem(name: "language", value: rawValue) - } -} diff --git a/NOICommunityLib/Tests/EventShortClientTests/EventShortClientTests.swift b/NOICommunityLib/Tests/EventShortClientTests/EventShortClientTests.swift deleted file mode 100644 index bbc8e55..0000000 --- a/NOICommunityLib/Tests/EventShortClientTests/EventShortClientTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -import XCTest -@testable import EventShortClient - -final class EventShortClientTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertTrue(true) - } -} diff --git a/NOICommunityLib/Tests/EventShortTypesClientTests/EventShortClientTests.swift b/NOICommunityLib/Tests/EventShortTypesClientTests/EventShortClientTests.swift deleted file mode 100644 index 87c7efa..0000000 --- a/NOICommunityLib/Tests/EventShortTypesClientTests/EventShortClientTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -import XCTest -@testable import EventShortTypesClient - -final class EventShortTypesClientTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertTrue(true) - } -} From 5118f2363d758ce080ef3f8feefb41d9408e065f Mon Sep 17 00:00:00 2001 From: Matteo Matassoni <4108197+matax87@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:21:51 +0100 Subject: [PATCH 05/14] Add event deeplinking from push notifications --- NOICommunity.xcodeproj/project.pbxproj | 8 ++ .../Custom/AppCoordinator.swift | 117 ++++++++++++++++- .../Custom/RootCoordinator.swift | 11 +- NOICommunity/DeepLinking.swift | 8 +- .../Coordinators/EventsCoordinator.swift | 78 ++++------- .../EventsFeatureConstants.swift | 1 - .../EventDetailsViewController.swift | 56 +------- .../EventDetailsViewController.xib | 39 +----- .../EventPageViewController.swift | 83 ++++++++++++ .../View Models/EventDetailsViewModel.swift | 123 ++++++++++++++++++ .../View Models/EventsViewModel.swift | 65 +++++---- .../Factories/DependencyContainer.swift | 19 ++- .../Factories/ViewControllerFactory.swift | 8 +- NOICommunity/Factories/ViewModelFactory.swift | 8 ++ .../CoreUI/BasePageViewController.swift | 78 +++++++++++ .../Sources/CoreUI/BasePageViewModel.swift | 35 +++++ .../Endpoint+EventShort.swift | 2 +- 17 files changed, 556 insertions(+), 183 deletions(-) create mode 100644 NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift create mode 100644 NOICommunity/EventsFeature/View Models/EventDetailsViewModel.swift create mode 100644 NOICommunityLib/Sources/CoreUI/BasePageViewController.swift create mode 100644 NOICommunityLib/Sources/CoreUI/BasePageViewModel.swift diff --git a/NOICommunity.xcodeproj/project.pbxproj b/NOICommunity.xcodeproj/project.pbxproj index 2b95127..82cad72 100644 --- a/NOICommunity.xcodeproj/project.pbxproj +++ b/NOICommunity.xcodeproj/project.pbxproj @@ -35,6 +35,8 @@ 31178FE026FA318800BDCDAA /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31178FDF26FA318800BDCDAA /* Collection.swift */; }; 311E0EC62825157800404DCE /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 311E0EC52825157800404DCE /* FirebaseMessaging */; }; 3121AFDE2858B43A00248CDF /* MeetMainViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3121AFDD2858B43A00248CDF /* MeetMainViewController.xib */; }; + 31260A632CFF611B00ADBDEF /* EventPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31260A622CFF610F00ADBDEF /* EventPageViewController.swift */; }; + 31260A652CFF68CF00ADBDEF /* EventDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31260A642CFF68CF00ADBDEF /* EventDetailsViewModel.swift */; }; 3126E7252B287F670013F456 /* LoadUserInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3126E7242B287F670013F456 /* LoadUserInfoViewModel.swift */; }; 3126E7272B2882620013F456 /* CacheKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3126E7262B2882620013F456 /* CacheKey.swift */; }; 312BBB5B2832658600AF84F0 /* UIViewController+PresentMailCompose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 312BBB5A2832658600AF84F0 /* UIViewController+PresentMailCompose.swift */; }; @@ -274,6 +276,8 @@ 31178FDD26FA2F7100BDCDAA /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; 31178FDF26FA318800BDCDAA /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; 3121AFDD2858B43A00248CDF /* MeetMainViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MeetMainViewController.xib; sourceTree = ""; }; + 31260A622CFF610F00ADBDEF /* EventPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventPageViewController.swift; sourceTree = ""; }; + 31260A642CFF68CF00ADBDEF /* EventDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailsViewModel.swift; sourceTree = ""; }; 3126E7242B287F670013F456 /* LoadUserInfoViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadUserInfoViewModel.swift; sourceTree = ""; }; 3126E7262B2882620013F456 /* CacheKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheKey.swift; sourceTree = ""; }; 312BBB5A2832658600AF84F0 /* UIViewController+PresentMailCompose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+PresentMailCompose.swift"; sourceTree = ""; }; @@ -757,6 +761,7 @@ 316A82902CFF1FF0002D62D3 /* View Controllers */ = { isa = PBXGroup; children = ( + 31260A622CFF610F00ADBDEF /* EventPageViewController.swift */, 316A82872CFF1FF0002D62D3 /* EventDetailsViewController.swift */, 316A82882CFF1FF0002D62D3 /* EventDetailsViewController.xib */, 316A82892CFF1FF0002D62D3 /* EventFiltersListViewController.swift */, @@ -773,6 +778,7 @@ 316A82952CFF1FF0002D62D3 /* View Models */ = { isa = PBXGroup; children = ( + 31260A642CFF68CF00ADBDEF /* EventDetailsViewModel.swift */, 316A82912CFF1FF0002D62D3 /* DateIntervalFilter.swift */, 316A82922CFF1FF0002D62D3 /* Event.swift */, 316A82932CFF1FF0002D62D3 /* EventFiltersViewModel.swift */, @@ -1303,6 +1309,7 @@ 31157D1926FCA1EB00A43B33 /* WebViewController.swift in Sources */, 31AD1DB22B27947B006F71A4 /* ComeOnBoardOnboardingViewModel.swift in Sources */, 312F5D272808250000C84598 /* AuthWelcomeViewController.swift in Sources */, + 31260A652CFF68CF00ADBDEF /* EventDetailsViewModel.swift in Sources */, 3185AA172820005700767E31 /* KeychainAuthStateStorageClient.swift in Sources */, 31157D1526FC83FE00A43B33 /* UIViewController+ShowCalenderError.swift in Sources */, 31CF8132270B49100077CB0D /* VersionContentView.swift in Sources */, @@ -1377,6 +1384,7 @@ 313010CB2845F99900AF6520 /* UIViewController+DirectionActionSheet.swift in Sources */, 31E7872627D8918800C67188 /* UIImageAdditions.swift in Sources */, 313010CF28463F1000AF6520 /* ContentConfiguration.swift in Sources */, + 31260A632CFF611B00ADBDEF /* EventPageViewController.swift in Sources */, 3185AA15281FEAE900767E31 /* AccessNotGrantedViewController.swift in Sources */, 317B6FA428118C95008D07C0 /* DependencyRepresentable.swift in Sources */, 31B192782C6A434B009872E9 /* BaseWindow.swift in Sources */, diff --git a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift index 2a190a4..7be514a 100644 --- a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift +++ b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift @@ -46,6 +46,7 @@ final class AppCoordinator: BaseNavigationCoordinator { } else { showLoadUserInfo() } + } func handle(deepLinkIntent: DeepLinkIntent) { @@ -59,6 +60,8 @@ final class AppCoordinator: BaseNavigationCoordinator { switch deepLinkIntent { case .showNews(let newsId): showNewsDetails(newsId: newsId, sender: deepLinkIntent) + case .showEvent(let eventId): + showEventDetails(eventId: eventId, sender: deepLinkIntent) } } @@ -198,20 +201,21 @@ private extension AppCoordinator { detailsViewController: NewsDetailsViewController ) { viewModel.showExternalLinkPublisher + .receive(on: DispatchQueue.main) .sink { [weak self] (news, sender) in self?.showNewsExternalLink(of: news, sender: sender) } .store(in: &subscriptions) viewModel.showAskAQuestionPublisher + .receive(on: DispatchQueue.main) .sink { [weak self] (news, sender) in self?.showNewsAskAQuestion(for: news, sender: sender) } .store(in: &subscriptions) viewModel.$result + .compactMap { $0 } + .receive(on: DispatchQueue.main) .sink { [weak detailsViewController] news in - guard let news = news - else { return } - detailsViewController?.navigationItem.title = localizedValue( from: news.languageToDetails )? @@ -249,7 +253,112 @@ private extension AppCoordinator { animated: true ) } - + + func addEventToCalendar( + _ event: Event, + from viewController: UIViewController + ) { + EventsCalendarManager.shared.presentCalendarModalToAddEvent( + event: event.toCalendarEvent(), + from: viewController + ) { [weak viewController] result in + guard case let .failure(error) = result + else { return } + + if let calendarError = error as? CalendarError { + viewController?.showCalendarError(calendarError) + } else { + viewController?.showError(error) + } + } + } + + func locateEvent( + _ event: Event, + from viewController: UIViewController + ) { + let mapViewController = MapWebViewController() + mapViewController.url = event.mapURL ?? .map + mapViewController.navigationItem.title = event.mapURL != nil ? + event.venue: + .localized("title_generic_noi_techpark_map") + viewController.navigationController?.pushViewController( + mapViewController, + animated: true + ) + } + + func signupEvent( + _ event: Event, + from viewController: UIViewController + ) { + UIApplication.shared.open( + event.signupURL!, + options: [:], + completionHandler: nil + ) + } + + func showEventDetails(eventId: String, sender: Any?) { + func configureBindings( + viewModel: EventDetailsViewModel, + pageViewController: EventPageViewController + ) { + viewModel.$result + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [weak pageViewController] event in + pageViewController?.navigationItem.title = event.title + } + .store(in: &subscriptions) + } + + let viewModel = dependencyContainer.makeEventDetailsViewModel( + eventId: eventId + ) + let pageVC = { + let pageVC = dependencyContainer.makeEventPageViewController( + viewModel: viewModel + ) + pageVC.addToCalendarActionHandler = { [weak self, weak pageVC] in + guard let pageVC + else { return } + + self?.addEventToCalendar($0, from: pageVC) + } + pageVC.locateActionHandler = { [weak self, weak pageVC] in + guard let pageVC + else { return } + + self?.locateEvent($0, from: pageVC) + } + pageVC.signupActionHandler = { [weak self, weak pageVC] in + guard let pageVC + else { return } + + + self?.signupEvent($0, from: pageVC) + } + pageVC.navigationItem.title = nil + pageVC.navigationItem.largeTitleDisplayMode = .never + pageVC.navigationItem.leftBarButtonItem = UIBarButtonItem( + image: UIImage(systemName: "xmark.circle.fill"), + style: .plain, + target: self, + action: #selector(closeModal(sender:)) + ) + pageVC.modalPresentationStyle = .fullScreen + return pageVC + }() + + configureBindings(viewModel: viewModel, pageViewController: pageVC) + + navigationController.present( + NavigationController(rootViewController: pageVC), + animated: true + ) + } + @objc func closeModal(sender: Any?) { navigationController.dismiss(animated: true) } diff --git a/NOICommunity/Coordinator/Implementations/Custom/RootCoordinator.swift b/NOICommunity/Coordinator/Implementations/Custom/RootCoordinator.swift index 47fd335..e64c50c 100644 --- a/NOICommunity/Coordinator/Implementations/Custom/RootCoordinator.swift +++ b/NOICommunity/Coordinator/Implementations/Custom/RootCoordinator.swift @@ -15,6 +15,7 @@ import AppPreferencesClient // MARK: - Refresh News List Notification let refreshNewsListNotification = Notification.Name("refreshNewsList") +let refreshEventsListNotification = Notification.Name("refreshEventsList") // MARK: - RootCoordinator @@ -52,6 +53,8 @@ final class RootCoordinator: BaseRootCoordinator { return case .showNews(newsId: _): refreshNewsList() + case .showEvent(eventId: _): + refreshEventsList() } } @@ -108,5 +111,11 @@ private extension RootCoordinator { .default .post(name: refreshNewsListNotification, object: self) } - + + func refreshEventsList() { + NotificationCenter + .default + .post(name: refreshEventsListNotification, object: self) + } + } diff --git a/NOICommunity/DeepLinking.swift b/NOICommunity/DeepLinking.swift index 22ef409..f406644 100644 --- a/NOICommunity/DeepLinking.swift +++ b/NOICommunity/DeepLinking.swift @@ -15,7 +15,7 @@ import UIKit enum DeepLinkIntent { case showNews(newsId: String) - //case showEvent(eventId: String) + case showEvent(eventId: String) } // MARK: - DeepLinkManager @@ -43,9 +43,9 @@ struct DeepLinkManager { case (URLConstant.newsDetailsPath, let newsId): // Matches: /newsDetails/{newsId} return .showNews(newsId: newsId) -// case (URLConstant.eventDetailsPath, let eventId): -// // Matches: /eventDetails/{eventId} -// return .showEvent(eventId: eventId) + case (URLConstant.eventDetailsPath, let eventId): + // Matches: /eventDetails/{eventId} + return .showEvent(eventId: eventId) default: return nil } diff --git a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift index c566422..b444baa 100644 --- a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift +++ b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift @@ -23,7 +23,6 @@ final class EventsCoordinator: BaseNavigationCoordinator { private var mainVC: EventsMainViewController! private var eventsViewModel: EventsViewModel! - //private var navigationDelegate: EventsNavigationControllerDelegate! private lazy var eventFiltersViewModel = dependencyContainer .makeEventFiltersViewModel { [weak self] in self?.closeFilters() @@ -31,21 +30,13 @@ final class EventsCoordinator: BaseNavigationCoordinator { private var subscriptions: Set = [] override func start(animated: Bool) { -// navigationDelegate = EventsNavigationControllerDelegate( -// navigationController: navigationController -// ) -// navigationController.delegate = navigationDelegate let eventsViewModel = dependencyContainer.makeEventsViewModel { [weak self] in self?.goToFilters() } self.eventsViewModel = eventsViewModel mainVC = EventsMainViewController(viewModel: eventsViewModel) - mainVC.didSelectHandler = { [weak self] collectionView, _, indexPath, event in - self?.goToDetails( - of: event, - transitionCollectionView: collectionView, - transitionIndexPath: indexPath - ) + mainVC.didSelectHandler = { [weak self] _, _, _, event in + self?.goToDetails(of: event) } mainVC.tabBarItem.title = .localized("events_top_tab") @@ -111,48 +102,29 @@ private extension EventsCoordinator { ) } - func goToDetails( - of event: Event, - transitionCollectionView: UICollectionView? = nil, - transitionIndexPath: IndexPath? = nil - ) { -// let transitionId = "event_\(event.id)" -// if -// let transitionCollectionView = transitionCollectionView, -// let transitionIndexPath = transitionIndexPath { -// let transitionInfo = EventsNavigationControllerDelegate.TransitionInfo( -// id: transitionId, -// collectionView: transitionCollectionView, -// indexPath: transitionIndexPath, -// event: event -// ) -// navigationDelegate.transitionInfos.append(transitionInfo) -// } - - let detailVC = EventDetailsViewController( - for: event, - relatedEvents: eventsViewModel.relatedEvent(of: event) - ) - //detailVC.cardView.transitionId = transitionId - detailVC.addToCalendarActionHandler = { [weak self] in - self?.addEventToCalendar($0) - } - detailVC.locateActionHandler = { [weak self] in - self?.locateEvent($0) - } - detailVC.signupActionHandler = { [weak self] in - self?.signupEvent($0) - } - detailVC.didSelectRelatedEventHandler = { [weak self] collectionView, _, indexPath, selectedEvent in - self?.goToDetails( - of: selectedEvent, - transitionCollectionView: collectionView, - transitionIndexPath: indexPath - ) - } - detailVC.navigationItem.title = event.title - detailVC.navigationItem.largeTitleDisplayMode = .never - navigationController.pushViewController(detailVC, animated: true) + func goToDetails(of event: Event) { + let viewModel = dependencyContainer.makeEventDetailsViewModel( + event: event + ) + let pageVC = { + let pageVC = dependencyContainer.makeEventPageViewController( + viewModel: viewModel + ) + pageVC.addToCalendarActionHandler = { [weak self] in + self?.addEventToCalendar($0) + } + pageVC.locateActionHandler = { [weak self] in + self?.locateEvent($0) + } + pageVC.signupActionHandler = { [weak self] in + self?.signupEvent($0) + } + pageVC.navigationItem.title = event.title + pageVC.navigationItem.largeTitleDisplayMode = .never + + return pageVC + }() + navigationController.pushViewController(pageVC, animated: true) } func goToFilters() { diff --git a/NOICommunity/EventsFeature/EventsFeatureConstants.swift b/NOICommunity/EventsFeature/EventsFeatureConstants.swift index 5710ede..c09e8e2 100644 --- a/NOICommunity/EventsFeature/EventsFeatureConstants.swift +++ b/NOICommunity/EventsFeature/EventsFeatureConstants.swift @@ -14,7 +14,6 @@ import Foundation enum EventsFeatureConstants { static let maximumNumberOfEvents = 20 - static let maximumNumberOfRelatedEvents = 3 public static let clientBaseURL: URL = { #if TESTINGMACHINE_OAUTH diff --git a/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.swift index 2c51215..14bbd12 100644 --- a/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.swift +++ b/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.swift @@ -13,22 +13,14 @@ import UIKit import Kingfisher class EventDetailsViewController: UIViewController { - + let event: Event - let relatedEvents: [Event] var locateActionHandler: ((Event) -> Void)? var addToCalendarActionHandler: ((Event) -> Void)? var signupActionHandler: ((Event) -> Void)? - - var didSelectRelatedEventHandler: (( - UICollectionView, - UICollectionViewCell, - IndexPath, - Event - ) -> Void)? private var _cardView: (UIView & UIContentView)! @@ -37,8 +29,6 @@ class EventDetailsViewController: UIViewController { return _cardView } - private var relatedEventsVC: EventListViewController! - @IBOutlet private var scrollView: UIScrollView! { didSet { scrollView.contentInset = UIEdgeInsets( @@ -80,25 +70,6 @@ class EventDetailsViewController: UIViewController { } } - @IBOutlet private var relatedSection: UIView! { - didSet { - if relatedEvents.isEmpty { - relatedSection.removeFromSuperview() - } - } - } - - @IBOutlet private var relatedEventsLabel: UILabel! { - didSet { - relatedEventsLabel.font = .NOI.dynamic.headlineSemibold - relatedEventsLabel.text = .localized("label_interesting_for_you") - } - } - - @IBOutlet private var relatedEventsContainerView: UIView! - - @IBOutlet private var relatedEventsContainerViewHeight: NSLayoutConstraint! - @IBOutlet private var actionsContainersView: FooterView! @IBOutlet private var locateEventButton: UIButton! { @@ -125,9 +96,8 @@ class EventDetailsViewController: UIViewController { } } - init(for item: Event, relatedEvents: [Event]) { + init(for item: Event) { self.event = item - self.relatedEvents = relatedEvents super.init(nibName: "\(EventDetailsViewController.self)", bundle: nil) } @@ -147,7 +117,6 @@ class EventDetailsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() configureViewHierarchy() - configureChilds() } override var preferredStatusBarStyle: UIStatusBarStyle { @@ -156,10 +125,6 @@ class EventDetailsViewController: UIViewController { override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { super.preferredContentSizeDidChange(forChildContentContainer: container) - - if container === relatedEventsVC { - relatedEventsContainerViewHeight.constant = container.preferredContentSize.height - } } } @@ -189,21 +154,6 @@ private extension EventDetailsViewController { } } - func configureChilds() { - guard !relatedEvents.isEmpty - else { return } - - relatedEventsVC = EventListViewController( - items: relatedEvents, - embeddedHorizontally: true - ) - relatedEventsVC.didSelectHandler = { [weak self] in - self?.didSelectRelatedEventHandler?($0, $1, $2, $3) - } - embedChild(relatedEventsVC, in: relatedEventsContainerView) - relatedEventsContainerViewHeight.constant = relatedEventsVC.preferredContentSize.height - } - @IBAction func findOnMapsAction(sender: Any?) { locateActionHandler?(event) } @@ -220,6 +170,6 @@ private extension EventDetailsViewController { extension EventDetailsViewController: CurrentScrollOffsetProvider { var currentScrollOffset: CGPoint { - relatedEventsVC?.currentScrollOffset ?? .zero + .zero } } diff --git a/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.xib b/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.xib index 8780bb8..266e6a0 100644 --- a/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.xib +++ b/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -20,10 +20,6 @@ - - - - @@ -38,7 +34,7 @@ - + @@ -71,35 +67,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift new file mode 100644 index 0000000..3752d6b --- /dev/null +++ b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// EventPageViewController.swift +// NOICommunity +// +// Created by Matteo Matassoni on 03/12/24. +// + +import UIKit +import CoreUI + +// MARK: - EventPageViewController + +final class EventPageViewController: BasePageViewController { + + private lazy var containerViewController = ContainerViewController() + + var locateActionHandler: ((Event) -> Void)? + + var addToCalendarActionHandler: ((Event) -> Void)? + + var signupActionHandler: ((Event) -> Void)? + + override func configureBindings() { + super.configureBindings() + + viewModel.$isLoading + .receive(on: DispatchQueue.main) + .sink { [weak self] isLoading in + if isLoading { + self?.show(content: LoadingViewController()) + } + } + .store(in: &subscriptions) + + viewModel.$result + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [weak self] event in + guard let self + else { return } + + self.show(content: self.makeResultContent(for: event)) + } + .store(in: &subscriptions) + + viewModel.$error + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [weak self] error in + self?.showError(error) + } + .store(in: &subscriptions) + } + + override func configureLayout() { + super.configureLayout() + + embedChild(containerViewController) + } + +} + +// MARK: Private APIs + +private extension EventPageViewController { + + func show(content: UIViewController) { + containerViewController.content = content + } + + func makeResultContent(for event: Event) -> UIViewController { + let result = EventDetailsViewController(for: event) + result.locateActionHandler = locateActionHandler + result.addToCalendarActionHandler = addToCalendarActionHandler + result.signupActionHandler = signupActionHandler + return result + } + +} diff --git a/NOICommunity/EventsFeature/View Models/EventDetailsViewModel.swift b/NOICommunity/EventsFeature/View Models/EventDetailsViewModel.swift new file mode 100644 index 0000000..7fea22e --- /dev/null +++ b/NOICommunity/EventsFeature/View Models/EventDetailsViewModel.swift @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// EventDetailsViewModel.swift +// NOICommunity +// +// Created by Matteo Matassoni on 03/12/24. +// + +import Foundation +import Combine +import CoreUI +import EventShortClient + +// MARK: - EventDetailsViewModel + +final class EventDetailsViewModel: BasePageViewModel { + + let eventShortClient: EventShortClient + let eventId: String + + @Published private(set) var isLoading = false + @Published private(set) var error: Error! + @Published private(set) var result: Event! + + private var roomMapping: [String:String]! + + private var fetchRequestCancellable: AnyCancellable? + + @available(*, unavailable) + required public init() { + fatalError("\(#function) not available") + } + + init( + eventShortClient: EventShortClient, + eventId: String + ) { + self.eventShortClient = eventShortClient + self.eventId = eventId + + super.init() + } + + init( + eventShortClient: EventShortClient, + event: Event + ) { + self.eventShortClient = eventShortClient + self.eventId = event.id + self.result = event + + super.init() + } + + override func onViewDidLoad() { + super.onViewDidLoad() + + if result == nil { + fetchEvent(eventId: eventId) + } + } + + func fetchEvent(eventId: String) { + Task(priority: .userInitiated) { [weak self] in + await self?.performFetchEvent(withId: eventId) + } + } + +} + +// MARK: Private APIs + +private extension EventDetailsViewModel { + + func performFetchEvent(withId eventId: String) async { + isLoading = true + defer { + isLoading = false + } + + do { + roomMapping = if let availableRoomMapping = roomMapping { + availableRoomMapping + } else { + try await eventShortClient.getRoomMapping() + } + + let eventShort = try await eventShortClient.getEventShort( + id: eventId, + optimizeDates: true, + fields: [ + "AnchorVenue", + "AnchorVenueRoomMapping", + "CompanyName", + "Display5", + "EndDate", + "EventDescriptionDE", + "EventDescriptionEN", + "EventDescriptionIT", + "EventLocation", + "EventTextDE", + "EventTextEN", + "EventTextIT", + "Id", + "ImageGallery", + "StartDate", + "WebAddress" + ], + removeNullValues: true + ) + result = .init( + from: eventShort, + roomMapping: roomMapping + ) + } catch { + self.error = error + } + } + +} diff --git a/NOICommunity/EventsFeature/View Models/EventsViewModel.swift b/NOICommunity/EventsFeature/View Models/EventsViewModel.swift index afd6111..0262fa4 100644 --- a/NOICommunity/EventsFeature/View Models/EventsViewModel.swift +++ b/NOICommunity/EventsFeature/View Models/EventsViewModel.swift @@ -11,12 +11,13 @@ import Foundation import Combine +import CoreUI import EventShortClient import EventShortTypesClient // MARK: - EventsViewModel -final class EventsViewModel { +final class EventsViewModel: BasePageViewModel { @Published var isLoading = false @Published var error: Error! @@ -24,60 +25,55 @@ final class EventsViewModel { @Published var dateIntervalFilter: DateIntervalFilter = .all @Published var activeFilters: Set = [] + private var refreshCancellable: AnyCancellable? private var refreshEventsRequestCancellable: AnyCancellable? let eventShortClient: EventShortClient let language: Language? let maximumNumberOfEvents: Int - let maximumNumberOfRelatedEvents: Int let showFiltersHandler: () -> Void private var subscriptions: Set = [] private var roomMapping: [String:String]! - + + @available(*, unavailable) + required init() { + fatalError("\(#function) not available") + } + init( eventShortClient: EventShortClient, language: Language?, maximumNumberOfEvents: Int = EventsFeatureConstants.maximumNumberOfEvents, - maximumNumberOfRelatedEvents: Int = EventsFeatureConstants.maximumNumberOfRelatedEvents, showFiltersHandler: @escaping () -> Void ) { self.eventShortClient = eventShortClient self.language = language self.maximumNumberOfEvents = maximumNumberOfEvents - self.maximumNumberOfRelatedEvents = maximumNumberOfRelatedEvents self.showFiltersHandler = showFiltersHandler } - + func refreshEvents() { Task(priority: .userInitiated) { [weak self] in await self?.performRefreshEvents() } } - - func relatedEvent(of event: Event) -> [Event] { - let slice = eventResults - .lazy - .filter { candidateEvent in - guard candidateEvent.id != event.id - else { return false } - - for techField in event.technologyFields { - if candidateEvent.technologyFields.contains(techField) { - return true - } - } - return false - } - .prefix(maximumNumberOfRelatedEvents) - return Array(slice) - } func showFilters() { showFiltersHandler() } + override func configureBindings() { + super.configureBindings() + refreshCancellable = NotificationCenter + .default + .publisher(for: refreshEventsListNotification) + .sink { [weak self] _ in + self?.refreshEvents() + } + } + } // MARK: Private APIs @@ -106,7 +102,24 @@ private extension EventsViewModel { endDate: endDate, eventLocation: .noi, publishedon: "noi-communityapp", - fields: ["AnchorVenue", "AnchorVenueRoomMapping", "CompanyName", "Display5", "EndDate", "EventDescriptionDE", "EventDescriptionEN", "EventDescriptionIT", "EventLocation", "EventTextDE", "EventTextEN", "EventTextIT", "Id", "ImageGallery", "StartDate", "WebAddress"], + fields: [ + "AnchorVenue", + "AnchorVenueRoomMapping", + "CompanyName", + "Display5", + "EndDate", + "EventDescriptionDE", + "EventDescriptionEN", + "EventDescriptionIT", + "EventLocation", + "EventTextDE", + "EventTextEN", + "EventTextIT", + "Id", + "ImageGallery", + "StartDate", + "WebAddress" + ], rawFilter: activeFilters.toQuery(), removeNullValues: true, optimizeDates: true @@ -172,7 +185,7 @@ private extension EventShort { // MARK: - Event Additions -private extension Event { +extension Event { init( from eventShort: EventShort, diff --git a/NOICommunity/Factories/DependencyContainer.swift b/NOICommunity/Factories/DependencyContainer.swift index b4f830e..9f5be6e 100644 --- a/NOICommunity/Factories/DependencyContainer.swift +++ b/NOICommunity/Factories/DependencyContainer.swift @@ -122,7 +122,16 @@ extension DependencyContainer: ViewModelFactory { showFiltersHandler: showFiltersHandler ) } - + + + func makeEventDetailsViewModel(eventId: String) -> EventDetailsViewModel { + .init(eventShortClient: eventShortClient, eventId: eventId) + } + + func makeEventDetailsViewModel(event: Event) -> EventDetailsViewModel { + .init(eventShortClient: eventShortClient, event: event) + } + func makeEventFiltersViewModel( showFilteredResultsHandler: @escaping () -> Void ) -> EventFiltersViewModel { @@ -202,7 +211,13 @@ extension DependencyContainer: ViewControllerFactory { func makeEventListViewController() -> EventListViewController { .init(items: []) } - + + func makeEventPageViewController( + viewModel: EventDetailsViewModel + ) -> EventPageViewController { + .init(viewModel: viewModel) + } + func makeEventFiltersViewController( viewModel: EventFiltersViewModel ) -> EventFiltersViewController { diff --git a/NOICommunity/Factories/ViewControllerFactory.swift b/NOICommunity/Factories/ViewControllerFactory.swift index 53a4150..5825c72 100644 --- a/NOICommunity/Factories/ViewControllerFactory.swift +++ b/NOICommunity/Factories/ViewControllerFactory.swift @@ -15,11 +15,15 @@ import PeopleClient protocol ViewControllerFactory { func makeEventListViewController() -> EventListViewController - + + func makeEventPageViewController( + viewModel: EventDetailsViewModel + ) -> EventPageViewController + func makeEventFiltersViewController( viewModel: EventFiltersViewModel ) -> EventFiltersViewController - + func makeWelcomeViewController( viewModel: WelcomeViewModel ) -> AuthWelcomeViewController diff --git a/NOICommunity/Factories/ViewModelFactory.swift b/NOICommunity/Factories/ViewModelFactory.swift index aa3d3a3..32b534c 100644 --- a/NOICommunity/Factories/ViewModelFactory.swift +++ b/NOICommunity/Factories/ViewModelFactory.swift @@ -18,6 +18,14 @@ protocol ViewModelFactory { showFiltersHandler: @escaping () -> Void ) -> EventsViewModel + func makeEventDetailsViewModel( + eventId: String + ) -> EventDetailsViewModel + + func makeEventDetailsViewModel( + event: Event + ) -> EventDetailsViewModel + func makeEventFiltersViewModel( showFilteredResultsHandler: @escaping () -> Void ) -> EventFiltersViewModel diff --git a/NOICommunityLib/Sources/CoreUI/BasePageViewController.swift b/NOICommunityLib/Sources/CoreUI/BasePageViewController.swift new file mode 100644 index 0000000..a6ab520 --- /dev/null +++ b/NOICommunityLib/Sources/CoreUI/BasePageViewController.swift @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// BasePageViewController.swift +// NOICommunityLib +// +// Created by Matteo Matassoni on 03/12/24. +// + +import UIKit +import Combine + +// MARK: - BasePageViewController + +open class BasePageViewController: UIViewController { + + public let viewModel: VM + public var subscriptions: Set = [] + + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("\(#function) not available") + } + + @available(*, unavailable) + public override init( + nibName nibNameOrNil: String?, + bundle nibBundleOrNil: Bundle? + ) { + fatalError("\(#function) not available") + } + + public init(viewModel: VM) { + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + + configureBindings() + } + + open func configureBindings() {} + + open func configureLayout() {} + + open override func viewDidLoad() { + super.viewDidLoad() + + viewModel.onViewDidLoad() + configureLayout() + } + + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + viewModel.onViewWillAppear(animated) + } + + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + viewModel.onViewDidAppear(animated) + } + + open override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + viewModel.onViewWillDisappear(animated) + } + + open override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + viewModel.onViewDidDisappear(animated) + } + +} diff --git a/NOICommunityLib/Sources/CoreUI/BasePageViewModel.swift b/NOICommunityLib/Sources/CoreUI/BasePageViewModel.swift new file mode 100644 index 0000000..92cccfc --- /dev/null +++ b/NOICommunityLib/Sources/CoreUI/BasePageViewModel.swift @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// BaseViewModel.swift +// NOICommunityLib +// +// Created by Matteo Matassoni on 04/12/24. +// + +import Foundation +import Combine + +open class BasePageViewModel { + + public var cancellables: Set = [] + + public required init() { + configureBindings() + } + + open func configureBindings() {} + + open func onViewDidLoad() {} + + open func onViewWillAppear(_ animated: Bool) { } + + open func onViewDidAppear(_ animated: Bool) { } + + open func onViewWillDisappear(_ animated: Bool) { } + + open func onViewDidDisappear(_ animated: Bool) { } + +} diff --git a/NOICommunityLib/Sources/EventShortClient/Endpoint+EventShort.swift b/NOICommunityLib/Sources/EventShortClient/Endpoint+EventShort.swift index 2011f37..3d65768 100644 --- a/NOICommunityLib/Sources/EventShortClient/Endpoint+EventShort.swift +++ b/NOICommunityLib/Sources/EventShortClient/Endpoint+EventShort.swift @@ -214,7 +214,7 @@ extension Endpoint { if let fields { URLQueryItem( - name: "optimizedates", + name: "fields", value: fields.joined(separator: ",") ) } From 964558cbf7c896a575607c95f14fa5dbf88665d2 Mon Sep 17 00:00:00 2001 From: Matteo Matassoni <4108197+matax87@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:27:38 +0100 Subject: [PATCH 06/14] Localize some no value label --- .../AccessNotGrantedViewController.swift | 2 +- .../MeetFeature/View Controllers/MeetMainViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NOICommunity/AuthFeature/AccessNotGrantedCoordinator/AccessNotGrantedViewController.swift b/NOICommunity/AuthFeature/AccessNotGrantedCoordinator/AccessNotGrantedViewController.swift index 30b9dfa..6c8fe8c 100644 --- a/NOICommunity/AuthFeature/AccessNotGrantedCoordinator/AccessNotGrantedViewController.swift +++ b/NOICommunity/AuthFeature/AccessNotGrantedCoordinator/AccessNotGrantedViewController.swift @@ -102,7 +102,7 @@ private extension AccessNotGrantedViewController { let detailedAttributedText: NSAttributedString = { let text = String.localizedStringWithFormat( .localized("outsider_user_body_format"), - userInfo.email ?? "N/D" + userInfo.email ?? .localized("label_no_value") ) let mAttributedText = NSMutableAttributedString(string: text, attributes: [ diff --git a/NOICommunity/MeetFeature/View Controllers/MeetMainViewController.swift b/NOICommunity/MeetFeature/View Controllers/MeetMainViewController.swift index ec6621b..9b168a5 100644 --- a/NOICommunity/MeetFeature/View Controllers/MeetMainViewController.swift +++ b/NOICommunity/MeetFeature/View Controllers/MeetMainViewController.swift @@ -268,7 +268,7 @@ private extension MeetMainViewController.CollectionViewController { var contentConfiguration = PersonCardContentConfiguration() contentConfiguration.fullname = person.fullname - contentConfiguration.company = company?.name ?? "N/D" + contentConfiguration.company = company?.name ?? .localized("label_no_value") contentConfiguration.avatarText = [ person.firstname.prefix(1), person.lastname.prefix(1) From 6866db992e914602a5ab7e8a9a1280b35d841f2e Mon Sep 17 00:00:00 2001 From: Matteo Matassoni <4108197+matax87@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:04:21 +0100 Subject: [PATCH 07/14] Fix not load image event --- .../View Controllers/EventDetailsViewController.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.swift index 14bbd12..295eabe 100644 --- a/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.swift +++ b/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.swift @@ -132,10 +132,7 @@ class EventDetailsViewController: UIViewController { private extension EventDetailsViewController { func configureViewHierarchy() { var contentConfiguration = EventCardContentConfiguration.makeDetailedContentConfiguration(for: event) - defer { - _cardView = contentConfiguration.makeContentView() - contentStackView.insertArrangedSubview(_cardView, at: 0) - } + _cardView = contentConfiguration.makeContentView() if let imageURL = event.imageURL { KingfisherManager.shared.retrieveImage(with: imageURL) { [weak _cardView] result in @@ -152,6 +149,8 @@ private extension EventDetailsViewController { } else { addToCalendarButton.removeFromSuperview() } + + contentStackView.insertArrangedSubview(_cardView, at: 0) } @IBAction func findOnMapsAction(sender: Any?) { From 107347f68d81692e115d8c22ec374831b859d8ec Mon Sep 17 00:00:00 2001 From: Matteo Matassoni <4108197+matax87@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:00:45 +0100 Subject: [PATCH 08/14] Refactor ArticleClient --- NOICommunity.xcodeproj/project.pbxproj | 7 -- .../View Models/NewsDetailsViewModel.swift | 42 +++---- .../View Models/NewsListViewModel.swift | 111 ++++++++--------- NOICommunity/SceneDelegate.swift | 7 +- NOICommunityLib/Package.swift | 13 +- .../Endpoints+ArticleClient.swift} | 17 +-- .../ArticlesClientImplementation.swift | 112 ++++++++++++++++++ .../Sources/ArticlesClient/Interface.swift | 29 ----- .../Interfaces/ArticlesClient.swift | 29 +++++ .../Sources/ArticlesClient/Mocks.swift | 48 -------- .../Sources/ArticlesClient/Models.swift | 54 +++++++++ .../Sources/ArticlesClientLive/Live.swift | 102 ---------------- .../Sources/ArticlesClientLive/Models.swift | 67 ----------- .../EventShortClientImplementation.swift | 2 +- 14 files changed, 284 insertions(+), 356 deletions(-) rename NOICommunityLib/Sources/{ArticlesClientLive/Endpoints.swift => ArticlesClient/Endpoints+ArticleClient.swift} (90%) create mode 100644 NOICommunityLib/Sources/ArticlesClient/Implementations/ArticlesClientImplementation.swift delete mode 100644 NOICommunityLib/Sources/ArticlesClient/Interface.swift create mode 100644 NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift delete mode 100644 NOICommunityLib/Sources/ArticlesClient/Mocks.swift delete mode 100644 NOICommunityLib/Sources/ArticlesClientLive/Live.swift delete mode 100644 NOICommunityLib/Sources/ArticlesClientLive/Models.swift diff --git a/NOICommunity.xcodeproj/project.pbxproj b/NOICommunity.xcodeproj/project.pbxproj index 82cad72..a0a0cb9 100644 --- a/NOICommunity.xcodeproj/project.pbxproj +++ b/NOICommunity.xcodeproj/project.pbxproj @@ -158,7 +158,6 @@ 319C0E9A26F3483600C6D38B /* UIControl+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C0E9926F3483600C6D38B /* UIControl+Combine.swift */; }; 319C0EA026F4790300C6D38B /* CalendarAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C0E9F26F4790300C6D38B /* CalendarAdditions.swift */; }; 319C4653282BB32400946AC7 /* ArticlesClient in Frameworks */ = {isa = PBXBuildFile; productRef = 319C4652282BB32400946AC7 /* ArticlesClient */; }; - 319C4655282BB32400946AC7 /* ArticlesClientLive in Frameworks */ = {isa = PBXBuildFile; productRef = 319C4654282BB32400946AC7 /* ArticlesClientLive */; }; 319C465A282BBD8000946AC7 /* NewsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C4657282BB80F00946AC7 /* NewsListViewModel.swift */; }; 319C465C282BBE5100946AC7 /* XCTestCase+awaitPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C465B282BBE5100946AC7 /* XCTestCase+awaitPublisher.swift */; }; 319C465E282BCC5D00946AC7 /* collectNext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319C465D282BCC5D00946AC7 /* collectNext.swift */; }; @@ -477,7 +476,6 @@ 31E058F22812F18800D1F7FE /* KeychainAccess in Frameworks */, 319C4653282BB32400946AC7 /* ArticlesClient in Frameworks */, 3182F4A427DB3841005ADDAF /* AppPreferencesClientLive in Frameworks */, - 319C4655282BB32400946AC7 /* ArticlesClientLive in Frameworks */, 317B6F9C28118BD6008D07C0 /* AuthClient in Frameworks */, 3182F4AC27DB3841005ADDAF /* EventShortTypesClientLive in Frameworks */, 311E0EC62825157800404DCE /* FirebaseMessaging in Frameworks */, @@ -1093,7 +1091,6 @@ 31E058F12812F18800D1F7FE /* KeychainAccess */, 311E0EC52825157800404DCE /* FirebaseMessaging */, 319C4652282BB32400946AC7 /* ArticlesClient */, - 319C4654282BB32400946AC7 /* ArticlesClientLive */, 317EC888283BB83E00F30B95 /* PeopleClient */, 317EC88A283BB83E00F30B95 /* PeopleClientLive */, 3181B9202B1E12BD000D2A0F /* Core */, @@ -2362,10 +2359,6 @@ isa = XCSwiftPackageProductDependency; productName = ArticlesClient; }; - 319C4654282BB32400946AC7 /* ArticlesClientLive */ = { - isa = XCSwiftPackageProductDependency; - productName = ArticlesClientLive; - }; 31B1926E2C6A2136009872E9 /* FirebaseCrashlytics */ = { isa = XCSwiftPackageProductDependency; package = 311E0EC42825157800404DCE /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; diff --git a/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift b/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift index 527762a..724c820 100644 --- a/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift +++ b/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift @@ -32,8 +32,6 @@ final class NewsDetailsViewModel { lazy var showAskAQuestionPublisher = showAskAQuestionSubject .eraseToAnyPublisher() - private var fetchRequestCancellable: AnyCancellable? - init( articlesClient: ArticlesClient, availableNews: Article?, @@ -45,24 +43,9 @@ final class NewsDetailsViewModel { } func refreshNewsDetails(newsId: String) { - isLoading = true - - fetchRequestCancellable = articlesClient.detail(newsId) - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - self?.isLoading = false - - switch completion { - case .finished: - break - case .failure(let error): - self?.error = error - } - }, - receiveValue: { [weak self] in - self?.result = $0 - }) + Task(priority: .userInitiated) { [weak self] in + await self?.performRefreshNewsDetails(newsId: newsId) + } } func showExternalLink(sender: Any?) { @@ -74,3 +57,22 @@ final class NewsDetailsViewModel { } } + +// MARK: Private APIs + +private extension NewsDetailsViewModel { + + func performRefreshNewsDetails(newsId: String) async { + isLoading = true + defer { + isLoading = false + } + + do { + result = try await articlesClient.getArticle(newsId: newsId) + } catch { + self.error = error + } + } + +} diff --git a/NOICommunity/NewsFeature/View Models/NewsListViewModel.swift b/NOICommunity/NewsFeature/View Models/NewsListViewModel.swift index f63b395..ac7c4a1 100644 --- a/NOICommunity/NewsFeature/View Models/NewsListViewModel.swift +++ b/NOICommunity/NewsFeature/View Models/NewsListViewModel.swift @@ -53,66 +53,9 @@ final class NewsListViewModel { } func fetchNews(refresh: Bool = false) { - guard nextPage != nil || refresh - else { return } - - let pageNumber: Int - - if refresh { - pageNumber = firstPage - } else { - pageNumber = nextPage! - } - - let currentNewsIds: [String] - if refresh { - currentNewsIds = [] - } else { - currentNewsIds = newsIds - } - - isLoadingFirstPage = pageNumber == firstPage - isLoading = true - - var articlesListPublisher = articlesClient.list( - Date(), - "noi-communityapp", - pageSize, - pageNumber - ) - - if refresh { - articlesListPublisher = articlesListPublisher - .delay(for: 0.3, scheduler: RunLoop.main) - .eraseToAnyPublisher() - } - - fetchRequestCancellable = articlesListPublisher - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - self?.isLoadingFirstPage = false - self?.isLoading = false - - switch completion { - case .finished: - break - case .failure(let error): - self?.error = error - } - }, - receiveValue: { [weak self] pagination in - guard let self = self - else { return } - - self.nextPage = pagination.nextPage - - guard let newItems = pagination.items - else { return } - - newItems.forEach { self.idToNews[$0.id] = $0 } - self.newsIds = currentNewsIds + newItems.map(\.id) - }) + Task(priority: .userInitiated) { [weak self] in + await self?.performFetchNews(refresh: refresh) + } } func news(withId newsId: String) -> Article { @@ -131,7 +74,53 @@ final class NewsListViewModel { // MARK: Private APIs private extension NewsListViewModel { - + + func performFetchNews(refresh: Bool = false) async { + guard nextPage != nil || refresh + else { return } + + let pageNumber: Int + + if refresh { + pageNumber = firstPage + } else { + pageNumber = nextPage! + } + + let currentNewsIds: [String] + if refresh { + currentNewsIds = [] + } else { + currentNewsIds = newsIds + } + + isLoadingFirstPage = pageNumber == firstPage + isLoading = true + defer { + isLoadingFirstPage = false + isLoading = false + } + + do { + let pagination = try await articlesClient.getArticleList( + startDate: Date(), + publishedOn: "noi-communityapp", + pageSize: pageSize, + pageNumber: pageNumber + ) + + nextPage = pagination.nextPage + + if let newItems = pagination.items { + newItems.forEach { idToNews[$0.id] = $0 } + newsIds = currentNewsIds + newItems.map(\.id) + } + } catch { + self.error = error + } + + } + func configureBindings() { refreshCancellable = NotificationCenter .default diff --git a/NOICommunity/SceneDelegate.swift b/NOICommunity/SceneDelegate.swift index 2a29d9d..aa3b8d5 100644 --- a/NOICommunity/SceneDelegate.swift +++ b/NOICommunity/SceneDelegate.swift @@ -18,7 +18,7 @@ import EventShortTypesClientLive import Core import AuthClientLive import AuthStateStorageClient -import ArticlesClientLive +import ArticlesClient import PeopleClientLive #if DEBUG @@ -77,7 +77,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return .live() } }(), - articleClient: .live(), + articleClient: ArticlesClientImplementation( + baseURL: EventsFeatureConstants.clientBaseURL, + transport: URLSession.shared + ), peopleClient: .live(baseURL: MeetConstant.clientBaseURL) ) }() diff --git a/NOICommunityLib/Package.swift b/NOICommunityLib/Package.swift index a11bc96..990a744 100644 --- a/NOICommunityLib/Package.swift +++ b/NOICommunityLib/Package.swift @@ -56,10 +56,6 @@ let package = Package( name: "ArticlesClient", targets: ["ArticlesClient"] ), - .library( - name: "ArticlesClientLive", - targets: ["ArticlesClientLive"] - ), .library( name: "PeopleClient", targets: ["PeopleClient"] @@ -154,14 +150,9 @@ let package = Package( ), .target( name: "ArticlesClient", - dependencies: [] - ), - .target( - name: "ArticlesClientLive", dependencies: [ - "Core", - "ArticlesClient", - ] + "Core" + ] ), .target( name: "PeopleClient", diff --git a/NOICommunityLib/Sources/ArticlesClientLive/Endpoints.swift b/NOICommunityLib/Sources/ArticlesClient/Endpoints+ArticleClient.swift similarity index 90% rename from NOICommunityLib/Sources/ArticlesClientLive/Endpoints.swift rename to NOICommunityLib/Sources/ArticlesClient/Endpoints+ArticleClient.swift index 018848a..0e4bed5 100644 --- a/NOICommunityLib/Sources/ArticlesClientLive/Endpoints.swift +++ b/NOICommunityLib/Sources/ArticlesClient/Endpoints+ArticleClient.swift @@ -3,15 +3,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // -// Endpoints.swift -// ArticlesClientLive +// Endpoints+ArticleClient.swift +// ArticlesClient // // Created by Matteo Matassoni on 11/05/22. // import Foundation import Core -import ArticlesClient private let dateFormatter: DateFormatter = { dateFormatter in dateFormatter.calendar = Calendar(identifier: .iso8601) @@ -24,16 +23,18 @@ private let dateFormatter: DateFormatter = { dateFormatter in extension Endpoint { static func articleList( - startDate: Date, + startDate: Date?, publishedon: String?, pageSize: Int?, pageNumber: Int? ) -> Endpoint { Self(path: "/v1/Article") { - URLQueryItem( - name: "startDate", - value: dateFormatter.string(from: startDate) - ) + if let startDate { + URLQueryItem( + name: "startDate", + value: dateFormatter.string(from: startDate) + ) + } if let publishedon = publishedon { URLQueryItem( diff --git a/NOICommunityLib/Sources/ArticlesClient/Implementations/ArticlesClientImplementation.swift b/NOICommunityLib/Sources/ArticlesClient/Implementations/ArticlesClientImplementation.swift new file mode 100644 index 0000000..87bf29c --- /dev/null +++ b/NOICommunityLib/Sources/ArticlesClient/Implementations/ArticlesClientImplementation.swift @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// ArticlesClientImplementation.swift +// NOICommunityLib +// +// Created by Matteo Matassoni on 05/12/24. +// + +import Foundation +import Core + +public final class ArticlesClientImplementation: ArticlesClient { + + private let baseURL: URL + + private let transport: Transport + + private let jsonDecoder: JSONDecoder = { + let jsonDecoder = JSONDecoder() + + jsonDecoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let dateStr = try container.decode(String.self) + + let dateFormatter = DateFormatter() + dateFormatter.calendar = Calendar(identifier: .iso8601) + dateFormatter.timeZone = TimeZone(identifier: "Europe/Rome") + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + if let date = dateFormatter.date(from: dateStr) { + return date + } + + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Cannot decode date string \(dateStr)" + ) + } + + jsonDecoder.keyDecodingStrategy = .convertFromPascalCase + return jsonDecoder + }() + + public init( + baseURL: URL, + transport: Transport + ) { + self.baseURL = baseURL + self.transport = transport + .checkingStatusCodes() + .addingJSONHeaders() + } + + public func getArticleList( + startDate: Date?, + publishedOn: String?, + pageSize: Int?, + pageNumber: Int? + ) async throws -> ArticleListResponse { + let request = Endpoint + .articleList( + startDate: startDate, + publishedon: publishedOn, + pageSize: pageSize, + pageNumber: pageNumber + ) + .makeRequest(withBaseURL: baseURL) + + let (data, _) = try await transport.send(request: request) + + try Task.checkCancellation() + + let myArticleListResponse = try jsonDecoder.decode( + MyArticleListResponse.self, + from: data + ) + return .init(from: myArticleListResponse) + } + + public func getArticle(newsId: String) async throws -> Article { + let request = Endpoint + .article(id: newsId) + .makeRequest(withBaseURL: baseURL) + + let (data, _) = try await transport.send(request: request) + + try Task.checkCancellation() + + return try jsonDecoder.decode(Article.self, from: data) + } + +} + diff --git a/NOICommunityLib/Sources/ArticlesClient/Interface.swift b/NOICommunityLib/Sources/ArticlesClient/Interface.swift deleted file mode 100644 index a6ee8fc..0000000 --- a/NOICommunityLib/Sources/ArticlesClient/Interface.swift +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -// -// Interface.swift -// ArticlesClient -// -// Created by Matteo Matassoni on 10/05/22. -// - -import Foundation -import Combine - -public struct ArticlesClient { - - public var list: (Date, String?, Int?, Int?) -> AnyPublisher - - public typealias ArticleId = String - public var detail: (String) -> AnyPublisher - - public init( - list: @escaping (Date, String?, Int?, Int?) -> AnyPublisher, - detail: @escaping (String) -> AnyPublisher - ) { - self.list = list - self.detail = detail - } -} diff --git a/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift b/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift new file mode 100644 index 0000000..97487f4 --- /dev/null +++ b/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// ArticlesClient.swift +// ArticlesClient +// +// Created by Matteo Matassoni on 10/05/22. +// + +import Foundation + +public protocol ArticlesClient { + + func getArticleList( + startDate: Date?, + publishedOn: String?, + pageSize: Int?, + pageNumber: Int? + ) async throws -> ArticleListResponse + + func getArticle(newsId: String) async throws -> Article + +} + + + + diff --git a/NOICommunityLib/Sources/ArticlesClient/Mocks.swift b/NOICommunityLib/Sources/ArticlesClient/Mocks.swift deleted file mode 100644 index 6ad35f8..0000000 --- a/NOICommunityLib/Sources/ArticlesClient/Mocks.swift +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -//// -//// Mocks.swift -//// ArticlesClient -//// -//// Created by Matteo Matassoni on 10/05/22. -//// -// -//import Foundation -//import Combine -// -//extension ArticlesClient { -// -// public static let empty = Self( -// list: { _, _, _ in -// Just(ArticleListResponse( -// totalResults: 0, -// totalPages: 0, -// currentPage: 1, -// previousPage: nil, -// nextPage: nil, -// items: [] -// )) -// .setFailureType(to: Error.self) -// .eraseToAnyPublisher() -// }, -// detail: { _, _ in -// Fail(error: NSError(domain: "", code: 1)) -// .eraseToAnyPublisher() -// } -// ) -// -// public static let happyPath = empty -// -// public static let failed = Self( -// list: { _, _, _ in -// Fail(error: NSError(domain: "", code: 1)) -// .eraseToAnyPublisher() -// }, -// detail: { _, _ in -// Fail(error: NSError(domain: "", code: 1)) -// .eraseToAnyPublisher() -// } -// ) -//} diff --git a/NOICommunityLib/Sources/ArticlesClient/Models.swift b/NOICommunityLib/Sources/ArticlesClient/Models.swift index 06a5d66..5822dda 100644 --- a/NOICommunityLib/Sources/ArticlesClient/Models.swift +++ b/NOICommunityLib/Sources/ArticlesClient/Models.swift @@ -166,3 +166,57 @@ extension Article { } } + +// MARK: - ArticleListResponse + +struct MyArticleListResponse: Codable, Equatable { + + let totalResults: Int + + let totalPages: Int + + let currentPage: Int + + let previousPageURL: URL? + + let nextPageURL: URL? + + let items: [Article]? + + private enum CodingKeys: String, CodingKey { + case totalResults + case totalPages + case currentPage + case previousPageURL = "previousPage" + case nextPageURL = "nextPage" + case items + } + +} + +private func extractPageNumber(url: URL) -> Int? { + let urlComponents = URLComponents( + url: url, + resolvingAgainstBaseURL: true + ) + return urlComponents? + .queryItems? + .first { $0.name == "pagenumber"}? + .value + .flatMap(Int.init) +} + +extension ArticleListResponse { + + init(from response: MyArticleListResponse) { + self.init( + totalResults: response.totalResults, + totalPages: response.totalPages, + currentPage: response.currentPage, + previousPage: response.previousPageURL.flatMap(extractPageNumber(url:)), + nextPage: response.nextPageURL.flatMap(extractPageNumber(url:)), + items: response.items + ) + } + +} diff --git a/NOICommunityLib/Sources/ArticlesClientLive/Live.swift b/NOICommunityLib/Sources/ArticlesClientLive/Live.swift deleted file mode 100644 index 0f3bf39..0000000 --- a/NOICommunityLib/Sources/ArticlesClientLive/Live.swift +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -// -// Live.swift -// ArticlesClientLive -// -// Created by Matteo Matassoni on 10/05/22. -// - -import Foundation -import Combine -import Core -import ArticlesClient - -// MARK: - Private Constants - -private let baseURL = URL(string: "https://tourism.opendatahub.com")! -private let articlesJsonDecoder: JSONDecoder = { - let jsonDecoder = JSONDecoder() - - jsonDecoder.dateDecodingStrategy = .custom { decoder in - let container = try decoder.singleValueContainer() - let dateStr = try container.decode(String.self) - - let dateFormatter = DateFormatter() - dateFormatter.calendar = Calendar(identifier: .iso8601) - dateFormatter.timeZone = TimeZone(identifier: "Europe/Rome") - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZ" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - if let date = dateFormatter.date(from: dateStr) { - return date - } - - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Cannot decode date string \(dateStr)" - ) - } - - jsonDecoder.keyDecodingStrategy = .convertFromPascalCase - return jsonDecoder -}() - -// MARK: - ArticlesClient+Live - -extension ArticlesClient { - - public static func live(urlSession: URLSession = .shared) -> Self { - Self( - list: { startDate, publishedon, pageSize, pageNumber in - let urlRequest = Endpoint.articleList( - startDate: startDate, - publishedon: publishedon, - pageSize: pageSize, - pageNumber: pageNumber - ).makeRequest(withBaseURL: baseURL) - - return urlSession - .dataTaskPublisher(for: urlRequest) - .map { data, response in data } - .decode( - type: MyArticleListResponse.self, - decoder: articlesJsonDecoder - ) - .map(ArticleListResponse.init(from:)) - .eraseToAnyPublisher() - }, - detail: { id in - let urlRequest = Endpoint.article(id: id) - .makeRequest(withBaseURL: baseURL) - - return urlSession - .dataTaskPublisher(for: urlRequest) - .map { data, response in data } - .decode( - type: Article.self, - decoder: articlesJsonDecoder - ) - .eraseToAnyPublisher() - } - ) - } - -} diff --git a/NOICommunityLib/Sources/ArticlesClientLive/Models.swift b/NOICommunityLib/Sources/ArticlesClientLive/Models.swift deleted file mode 100644 index b0347b1..0000000 --- a/NOICommunityLib/Sources/ArticlesClientLive/Models.swift +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-FileCopyrightText: NOI Techpark -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -// -// Models.swift -// ArticlesClient -// -// Created by Matteo Matassoni on 10/05/22. -// - -import Foundation -import ArticlesClient - -// MARK: - ArticleListResponse - -struct MyArticleListResponse: Codable, Equatable { - - let totalResults: Int - - let totalPages: Int - - let currentPage: Int - - let previousPageURL: URL? - - let nextPageURL: URL? - - let items: [Article]? - - private enum CodingKeys: String, CodingKey { - case totalResults - case totalPages - case currentPage - case previousPageURL = "previousPage" - case nextPageURL = "nextPage" - case items - } - -} - -private func extractPageNumber(url: URL) -> Int? { - let urlComponents = URLComponents( - url: url, - resolvingAgainstBaseURL: true - ) - return urlComponents? - .queryItems? - .first { $0.name == "pagenumber"}? - .value - .flatMap(Int.init) -} - -extension ArticleListResponse { - - init(from response: MyArticleListResponse) { - self.init( - totalResults: response.totalResults, - totalPages: response.totalPages, - currentPage: response.currentPage, - previousPage: response.previousPageURL.flatMap(extractPageNumber(url:)), - nextPage: response.nextPageURL.flatMap(extractPageNumber(url:)), - items: response.items - ) - } - -} diff --git a/NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift b/NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift index a8e3cd4..fdfd49b 100644 --- a/NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift +++ b/NOICommunityLib/Sources/EventShortClient/Implementations/EventShortClientImplementation.swift @@ -149,7 +149,7 @@ public final class EventShortClientImplementation: EventShortClient { optimizeDates: Bool?, fields: [String]?, removeNullValues: Bool? - ) async throws -> EventShort { + ) async throws -> EventShort { let request = Endpoint .eventShort( id: id, From 691fd8483c434aad3b4afcbe9b8105c603ac0bd0 Mon Sep 17 00:00:00 2001 From: Matteo Matassoni <4108197+matax87@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:10:57 +0100 Subject: [PATCH 09/14] Refactor and improve news and event details flow --- NOICommunity.xcodeproj/project.pbxproj | 4 + .../Custom/AppCoordinator.swift | 88 ++++---- .../Coordinators/EventsCoordinator.swift | 2 + .../EventPageViewController.swift | 27 ++- .../Factories/DependencyContainer.swift | 27 ++- .../Factories/ViewControllerFactory.swift | 5 +- NOICommunity/Factories/ViewModelFactory.swift | 12 +- .../Coordinators/NewsCoordinator.swift | 54 ++--- .../NewsDetailsViewController.swift | 210 +++++++----------- .../NewsPageViewController.swift | 97 ++++++++ .../View Models/NewsDetailsViewModel.swift | 65 +++--- .../Interfaces/ArticlesClient.swift | 22 +- 12 files changed, 365 insertions(+), 248 deletions(-) create mode 100644 NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift diff --git a/NOICommunity.xcodeproj/project.pbxproj b/NOICommunity.xcodeproj/project.pbxproj index a0a0cb9..4160217 100644 --- a/NOICommunity.xcodeproj/project.pbxproj +++ b/NOICommunity.xcodeproj/project.pbxproj @@ -224,6 +224,7 @@ 31F0DBD3280F0D3D00E782D5 /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F0DBD2280F02DC00E782D5 /* AuthCoordinator.swift */; }; 31FAEA7D28197D9700CDBC1B /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31FAEA7C28197D9700CDBC1B /* AppCoordinator.swift */; }; 31FAEA7F28197EC200CDBC1B /* NavigationCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31FAEA7E28197EC200CDBC1B /* NavigationCoordinatorType.swift */; }; + 31FFE9502D01E068007C699B /* NewsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31FFE94F2D01E065007C699B /* NewsPageViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -458,6 +459,7 @@ 31F0DBD2280F02DC00E782D5 /* AuthCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; 31FAEA7C28197D9700CDBC1B /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 31FAEA7E28197EC200CDBC1B /* NavigationCoordinatorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinatorType.swift; sourceTree = ""; }; + 31FFE94F2D01E065007C699B /* NewsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsPageViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -523,6 +525,7 @@ 3102F45C282C069500687CF7 /* View Controllers */ = { isa = PBXGroup; children = ( + 31FFE94F2D01E065007C699B /* NewsPageViewController.swift */, 3102F45E282C06B700687CF7 /* NewsViewController.swift */, 3182A1BC282D5159005439B4 /* NewsDetailsViewController.swift */, 3182A1BD282D5159005439B4 /* NewsDetailsViewController.xib */, @@ -1305,6 +1308,7 @@ 31FAEA7F28197EC200CDBC1B /* NavigationCoordinatorType.swift in Sources */, 31157D1926FCA1EB00A43B33 /* WebViewController.swift in Sources */, 31AD1DB22B27947B006F71A4 /* ComeOnBoardOnboardingViewModel.swift in Sources */, + 31FFE9502D01E068007C699B /* NewsPageViewController.swift in Sources */, 312F5D272808250000C84598 /* AuthWelcomeViewController.swift in Sources */, 31260A652CFF68CF00ADBDEF /* EventDetailsViewModel.swift in Sources */, 3185AA172820005700767E31 /* KeychainAuthStateStorageClient.swift in Sources */, diff --git a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift index 7be514a..7a0ba4a 100644 --- a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift +++ b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift @@ -177,18 +177,24 @@ private extension AppCoordinator { showAuthCoordinator(animated: animated) } - func showNewsExternalLink(of news: Article, sender: Any?) { + func showNewsExternalLink( + of news: Article, + from viewController: UIViewController + ) { let author = localizedValue(from: news.languageToAuthor) let safariVC = SFSafariViewController(url: author!.externalURL!) - navigationController.presentedViewController?.present( + navigationController.presentedViewController?.present( safariVC, animated: true ) } - func showNewsAskAQuestion(for news: Article, sender: Any?) { + func showNewsAskAQuestion( + for news: Article, + from viewController: UIViewController + ) { let author = localizedValue(from: news.languageToAuthor) - navigationController.presentedViewController?.mailTo( + navigationController.presentedViewController?.mailTo( author!.email!, delegate: self, completion: nil @@ -198,25 +204,13 @@ private extension AppCoordinator { func showNewsDetails(newsId: String, sender: Any?) { func configureBindings( viewModel: NewsDetailsViewModel, - detailsViewController: NewsDetailsViewController + pageViewController: NewsPageViewController ) { - viewModel.showExternalLinkPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] (news, sender) in - self?.showNewsExternalLink(of: news, sender: sender) - } - .store(in: &subscriptions) - viewModel.showAskAQuestionPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] (news, sender) in - self?.showNewsAskAQuestion(for: news, sender: sender) - } - .store(in: &subscriptions) viewModel.$result .compactMap { $0 } .receive(on: DispatchQueue.main) - .sink { [weak detailsViewController] news in - detailsViewController?.navigationItem.title = localizedValue( + .sink { [weak pageViewController] news in + pageViewController?.navigationItem.title = localizedValue( from: news.languageToDetails )? .title @@ -225,31 +219,46 @@ private extension AppCoordinator { } let viewModel = dependencyContainer.makeNewsDetailsViewModel( - availableNews: nil - ) - - let detailsVC = dependencyContainer.makeNewsDetailsViewController( - newsId: newsId, - viewModel: viewModel + newsId: newsId ) - + let pageVC = { + let pageVC = dependencyContainer.makeNewsPageViewController( + viewModel: viewModel + ) + + pageVC.externalLinkActionHandler = { [weak self, weak pageVC] in + guard let pageVC + else { return } + + self?.showNewsExternalLink(of: $0, from: pageVC) + } + pageVC.askQuestionActionHandler = { [weak self, weak pageVC] in + guard let pageVC + else { return } + + self?.showNewsAskAQuestion(for: $0, from: pageVC) + } + + pageVC.navigationItem.title = nil + pageVC.navigationItem.largeTitleDisplayMode = .never + pageVC.navigationItem.leftBarButtonItem = UIBarButtonItem( + image: UIImage(systemName: "xmark.circle.fill"), + style: .plain, + target: self, + action: #selector(closeModal(sender:)) + ) + pageVC.modalPresentationStyle = .fullScreen + + return pageVC + }() + configureBindings( viewModel: viewModel, - detailsViewController: detailsVC + pageViewController: pageVC ) - detailsVC.navigationItem.title = nil - detailsVC.navigationItem.largeTitleDisplayMode = .never - detailsVC.navigationItem.leftBarButtonItem = UIBarButtonItem( - image: UIImage(systemName: "xmark.circle.fill"), - style: .plain, - target: self, - action: #selector(closeModal(sender:)) - ) - detailsVC.modalPresentationStyle = .fullScreen - navigationController.present( - NavigationController(rootViewController: detailsVC), + NavigationController(rootViewController: pageVC), animated: true ) } @@ -320,6 +329,7 @@ private extension AppCoordinator { let pageVC = dependencyContainer.makeEventPageViewController( viewModel: viewModel ) + pageVC.addToCalendarActionHandler = { [weak self, weak pageVC] in guard let pageVC else { return } @@ -339,6 +349,7 @@ private extension AppCoordinator { self?.signupEvent($0, from: pageVC) } + pageVC.navigationItem.title = nil pageVC.navigationItem.largeTitleDisplayMode = .never pageVC.navigationItem.leftBarButtonItem = UIBarButtonItem( @@ -348,6 +359,7 @@ private extension AppCoordinator { action: #selector(closeModal(sender:)) ) pageVC.modalPresentationStyle = .fullScreen + return pageVC }() diff --git a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift index b444baa..edcd7f5 100644 --- a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift +++ b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift @@ -110,6 +110,7 @@ private extension EventsCoordinator { let pageVC = dependencyContainer.makeEventPageViewController( viewModel: viewModel ) + pageVC.addToCalendarActionHandler = { [weak self] in self?.addEventToCalendar($0) } @@ -119,6 +120,7 @@ private extension EventsCoordinator { pageVC.signupActionHandler = { [weak self] in self?.signupEvent($0) } + pageVC.navigationItem.title = event.title pageVC.navigationItem.largeTitleDisplayMode = .never diff --git a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift index 3752d6b..4a49a22 100644 --- a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift +++ b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift @@ -18,11 +18,30 @@ final class EventPageViewController: BasePageViewController Void)? + private var eventDetailsViewController: EventDetailsViewController? { + children + .lazy + .compactMap { $0 as? EventDetailsViewController } + .first + } - var addToCalendarActionHandler: ((Event) -> Void)? + var locateActionHandler: ((Event) -> Void)? { + didSet { + eventDetailsViewController?.locateActionHandler = locateActionHandler + } + } - var signupActionHandler: ((Event) -> Void)? + var addToCalendarActionHandler: ((Event) -> Void)? { + didSet { + eventDetailsViewController?.addToCalendarActionHandler = addToCalendarActionHandler + } + } + + var signupActionHandler: ((Event) -> Void)? { + didSet { + eventDetailsViewController?.signupActionHandler = signupActionHandler + } + } override func configureBindings() { super.configureBindings() @@ -72,7 +91,7 @@ private extension EventPageViewController { containerViewController.content = content } - func makeResultContent(for event: Event) -> UIViewController { + func makeResultContent(for event: Event) -> EventDetailsViewController { let result = EventDetailsViewController(for: event) result.locateActionHandler = locateActionHandler result.addToCalendarActionHandler = addToCalendarActionHandler diff --git a/NOICommunity/Factories/DependencyContainer.swift b/NOICommunity/Factories/DependencyContainer.swift index 9f5be6e..2fbb871 100644 --- a/NOICommunity/Factories/DependencyContainer.swift +++ b/NOICommunity/Factories/DependencyContainer.swift @@ -181,13 +181,17 @@ extension DependencyContainer: ViewModelFactory { func makeNewsListViewModel() -> NewsListViewModel { .init(articlesClient: makeArticlesClient()) } - - func makeNewsDetailsViewModel(availableNews: Article?) -> NewsDetailsViewModel { - .init( - articlesClient: makeArticlesClient(), - availableNews: availableNews, - language: nil - ) + + func makeNewsDetailsViewModel( + newsId: String + ) -> NewsDetailsViewModel { + .init(articlesClient: makeArticlesClient(), newsId: newsId) + } + + func makeNewsDetailsViewModel( + news: Article + ) -> NewsDetailsViewModel { + .init(articlesClient: makeArticlesClient(), news: news) } func makePeopleViewModel() -> PeopleViewModel { @@ -248,11 +252,10 @@ extension DependencyContainer: ViewControllerFactory { .init(viewModel: viewModel) } - func makeNewsDetailsViewController( - newsId: String, - viewModel: NewsDetailsViewModel - ) -> NewsDetailsViewController { - .init(newsId: newsId, viewModel: viewModel) + func makeNewsPageViewController( + viewModel: NewsDetailsViewModel + ) -> NewsPageViewController { + .init(viewModel: viewModel) } func makeMeetMainViewController( diff --git a/NOICommunity/Factories/ViewControllerFactory.swift b/NOICommunity/Factories/ViewControllerFactory.swift index 5825c72..4449ecc 100644 --- a/NOICommunity/Factories/ViewControllerFactory.swift +++ b/NOICommunity/Factories/ViewControllerFactory.swift @@ -40,10 +40,9 @@ protocol ViewControllerFactory { viewModel: NewsListViewModel ) -> NewsViewController - func makeNewsDetailsViewController( - newsId: String, + func makeNewsPageViewController( viewModel: NewsDetailsViewModel - ) -> NewsDetailsViewController + ) -> NewsPageViewController func makeMeetMainViewController( viewModel: PeopleViewModel diff --git a/NOICommunity/Factories/ViewModelFactory.swift b/NOICommunity/Factories/ViewModelFactory.swift index 32b534c..9475c0e 100644 --- a/NOICommunity/Factories/ViewModelFactory.swift +++ b/NOICommunity/Factories/ViewModelFactory.swift @@ -35,10 +35,14 @@ protocol ViewModelFactory { func makeMyAccountViewModel() -> MyAccountViewModel func makeNewsListViewModel() -> NewsListViewModel - - func makeNewsDetailsViewModel( - availableNews: Article? - ) -> NewsDetailsViewModel + + func makeNewsDetailsViewModel( + newsId: String + ) -> NewsDetailsViewModel + + func makeNewsDetailsViewModel( + news: Article + ) -> NewsDetailsViewModel func makeLoadUserInfoViewModel() -> LoadUserInfoViewModel diff --git a/NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift b/NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift index 8860f42..ac3f599 100644 --- a/NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift +++ b/NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift @@ -31,8 +31,8 @@ final class NewsCoordinator: BaseNavigationCoordinator { override func start(animated: Bool) { newsListViewModel = dependencyContainer.makeNewsListViewModel() - newsListViewModel.showDetailsHandler = { [weak self] in - self?.goToDetails(of: $0, sender: $1) + newsListViewModel.showDetailsHandler = { [weak self] news, _ in + self?.goToDetails(of: news) } mainVC = dependencyContainer.makeNewsViewController( viewModel: newsListViewModel @@ -45,40 +45,40 @@ final class NewsCoordinator: BaseNavigationCoordinator { private extension NewsCoordinator { - func goToDetails(of news: Article, sender: Any?) { + func goToDetails(of news: Article) { let viewModel = dependencyContainer.makeNewsDetailsViewModel( - availableNews: news + news: news ) - viewModel.showExternalLinkPublisher - .sink { [weak self] (news, sender) in - self?.showExternalLink(of: news, sender: sender) - } - .store(in: &subscriptions) - viewModel.showAskAQuestionPublisher - .sink { [weak self] (news, sender) in - self?.showAskAQuestion(for: news, sender: sender) - } - .store(in: &subscriptions) - - let detailVC = dependencyContainer.makeNewsDetailsViewController( - newsId: news.id, - viewModel: viewModel - ) - detailVC.navigationItem.title = localizedValue( - from: news.languageToDetails - )? - .title - detailVC.navigationItem.largeTitleDisplayMode = .never - navigationController.pushViewController(detailVC, animated: true) + let pageVC = { + let pageVC = dependencyContainer.makeNewsPageViewController( + viewModel: viewModel + ) + + pageVC.externalLinkActionHandler = { [weak self] in + self?.showExternalLink(of: $0) + } + pageVC.askQuestionActionHandler = { [weak self] in + self?.showAskAQuestion(for: $0) + } + + pageVC.navigationItem.title = localizedValue( + from: news.languageToDetails + )? + .title + pageVC.navigationItem.largeTitleDisplayMode = .never + + return pageVC + }() + navigationController.pushViewController(pageVC, animated: true) } - func showExternalLink(of news: Article, sender: Any?) { + func showExternalLink(of news: Article) { let author = localizedValue(from: news.languageToAuthor) let safariVC = SFSafariViewController(url: author!.externalURL!) navigationController.present(safariVC, animated: true) } - func showAskAQuestion(for news: Article, sender: Any?) { + func showAskAQuestion(for news: Article) { let author = localizedValue(from: news.languageToAuthor) navigationController.mailTo( author!.email!, diff --git a/NOICommunity/NewsFeature/View Controllers/NewsDetailsViewController.swift b/NOICommunity/NewsFeature/View Controllers/NewsDetailsViewController.swift index 635046c..fa93094 100644 --- a/NOICommunity/NewsFeature/View Controllers/NewsDetailsViewController.swift +++ b/NOICommunity/NewsFeature/View Controllers/NewsDetailsViewController.swift @@ -14,22 +14,15 @@ import Combine import ArticlesClient class NewsDetailsViewController: UIViewController { + + let news: Article + + var externalLinkActionHandler: ((Article) -> Void)? - let newsId: String - - let viewModel: NewsDetailsViewModel - - var externalLinkActionHandler: ((Article, Any?) -> Void)? - - var askQuestionActionHandler: ((Article, Any?) -> Void)? - - private var subscriptions: Set = [] - - private lazy var refreshControl: UIRefreshControl = { refreshControl in - scrollView.refreshControl = refreshControl - return refreshControl - }(UIRefreshControl()) - + var askQuestionActionHandler: ((Article) -> Void)? + + private var subscriptions: Set = [] + @IBOutlet private var scrollView: UIScrollView! @IBOutlet private var containerView: UIView! @@ -100,12 +93,11 @@ class NewsDetailsViewController: UIViewController { spacing: 17, placeholderImage: .image(withColor: .noiPlaceholderImageColor) ) - - init(newsId: String, viewModel: NewsDetailsViewModel) { - self.newsId = newsId - self.viewModel = viewModel - super.init(nibName: "\(NewsDetailsViewController.self)", bundle: nil) - } + + init(for item: Article) { + self.news = item + super.init(nibName: "\(NewsDetailsViewController.self)", bundle: nil) + } @available(*, unavailable) required init?(coder: NSCoder) { @@ -117,20 +109,6 @@ class NewsDetailsViewController: UIViewController { fatalError("\(#function) not available") } - override func viewDidLoad() { - super.viewDidLoad() - - configureBindings() - - refreshControl = .init() - - if let news = viewModel.result { - updateUI(news: news) - } else { - viewModel.refreshNewsDetails(newsId: newsId) - } - } - override func traitCollectionDidChange( _ previousTraitCollection: UITraitCollection? ) { @@ -163,7 +141,59 @@ class NewsDetailsViewController: UIViewController { override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } - + + override func viewDidLoad() { + super.viewDidLoad() + + configureBindings() + let author = localizedValue(from: news.languageToAuthor) + + imageView.kf.setImage(with: author?.logoURL) + + authorLabel.text = author?.name ?? .notDefined + + if author?.externalURL == nil { + externalLinkButton.removeFromSuperview() + } + if author?.email == nil { + askQuestionButton.removeFromSuperview() + } + if actionsStackView.subviews.isEmpty { + footerView.removeFromSuperview() + } + + publishedDateLabel.text = news.date + .flatMap { publishedDateFormatter.string(from: $0) } + + let details = localizedValue(from: news.languageToDetails) + titleLabel.text = details?.title + abstractLabel.text = details?.abstract + + textView.attributedText = details?.attributedText()? + .updatedFonts(usingTextStyle: .body) + + + if details?.text == nil { + textView.removeFromSuperview() + } + + if news.imageGallery.isNilOrEmpty { + galleryContainerView.removeFromSuperview() + } else { + galleryVC.imageURLs = news.imageGallery?.compactMap(\.url) ?? [] + if galleryVC.parent != self { + embedChild(galleryVC, in: galleryContainerView) + } + } + + if galleryTextStackView.subviews.isEmpty { + NSLayoutConstraint.deactivate(fullDetailConstraints) + NSLayoutConstraint.activate(shortDetailConstraints) + + galleryTextStackView.removeFromSuperview() + } + } + } // UITextViewDelegate @@ -188,107 +218,27 @@ private extension NewsDetailsViewController { func configureBindings() { externalLinkButton.publisher(for: .primaryActionTriggered) - .sink { [weak viewModel, externalLinkButton] in - viewModel?.showExternalLink(sender: externalLinkButton) - } - .store(in: &subscriptions) - - askQuestionButton.publisher(for: .primaryActionTriggered) - .sink { [weak viewModel, askQuestionButton] in - viewModel?.showAskAQuestion(sender: askQuestionButton) - } - .store(in: &subscriptions) - - refreshControl.publisher(for: .valueChanged) - .sink { [weak viewModel, newsId] in - viewModel?.refreshNewsDetails(newsId: newsId) - } - .store(in: &subscriptions) - - viewModel.$isLoading - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [unowned refreshControl] isLoading in - refreshControl.isLoading = isLoading - }) - .store(in: &subscriptions) - - viewModel.$result - .receive(on: DispatchQueue.main) .sink { [weak self] in - self?.updateUI(news: $0) + guard let self + else { return } + + self.externalLinkActionHandler?(self.news) } .store(in: &subscriptions) - viewModel.$error - .receive(on: DispatchQueue.main) - .sink { [weak self] error in - guard let error = error - else { return } - - self?.showError(error) - } + askQuestionButton.publisher(for: .primaryActionTriggered) + .sink { [weak self] in + guard let self + else { return } + + self.askQuestionActionHandler?(self.news) + } .store(in: &subscriptions) } - func updateUI(news: Article?) { - guard let news = news else { - containerView.isHidden = true - footerView.isHidden = true - return - } - - let author = localizedValue(from: news.languageToAuthor) - - imageView.kf.setImage(with: author?.logoURL) - - authorLabel.text = author?.name ?? .notDefined - - if author?.externalURL == nil { - externalLinkButton.removeFromSuperview() - } - if author?.email == nil { - askQuestionButton.removeFromSuperview() - } - if actionsStackView.subviews.isEmpty { - footerView.removeFromSuperview() - } - - publishedDateLabel.text = news.date - .flatMap { publishedDateFormatter.string(from: $0) } - - let details = localizedValue(from: news.languageToDetails) - titleLabel.text = details?.title - abstractLabel.text = details?.abstract - - textView.attributedText = details?.attributedText()? - .updatedFonts(usingTextStyle: .body) - - - if details?.text == nil { - textView.removeFromSuperview() - } - - if news.imageGallery.isNilOrEmpty { - galleryContainerView.removeFromSuperview() - } else { - galleryVC.imageURLs = news.imageGallery?.compactMap(\.url) ?? [] - if galleryVC.parent != self { - embedChild(galleryVC, in: galleryContainerView) - } - } - - if galleryTextStackView.subviews.isEmpty { - NSLayoutConstraint.deactivate(fullDetailConstraints) - NSLayoutConstraint.activate(shortDetailConstraints) - - galleryTextStackView.removeFromSuperview() - } - - containerView.isHidden = false - footerView.isHidden = false - } - - func preferredContentSizeCategoryDidChange(previousPreferredContentSizeCategory: UIContentSizeCategory?) { + func preferredContentSizeCategoryDidChange( + previousPreferredContentSizeCategory: UIContentSizeCategory? + ) { textView.attributedText = textView.attributedText?.updatedFonts(usingTextStyle: .body) } diff --git a/NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift b/NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift new file mode 100644 index 0000000..3ed03f6 --- /dev/null +++ b/NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// NewsPageViewController.swift +// NOICommunity +// +// Created by Matteo Matassoni on 05/12/24. +// + +import UIKit +import CoreUI +import ArticlesClient + +// MARK: - NewsPageViewController + +final class NewsPageViewController: BasePageViewController { + + private lazy var containerViewController = ContainerViewController() + + private var newsDetailsViewController: NewsDetailsViewController? { + children + .lazy + .compactMap { $0 as? NewsDetailsViewController } + .first + } + + var externalLinkActionHandler: ((Article) -> Void)? { + didSet { + newsDetailsViewController?.externalLinkActionHandler = externalLinkActionHandler + } + } + + var askQuestionActionHandler: ((Article) -> Void)? { + didSet { + newsDetailsViewController?.askQuestionActionHandler = askQuestionActionHandler + } + } + + override func configureBindings() { + super.configureBindings() + + viewModel.$isLoading + .receive(on: DispatchQueue.main) + .sink { [weak self] isLoading in + if isLoading { + self?.show(content: LoadingViewController()) + } + } + .store(in: &subscriptions) + + viewModel.$result + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [weak self] event in + guard let self + else { return } + + self.show(content: self.makeResultContent(for: event)) + } + .store(in: &subscriptions) + + viewModel.$error + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [weak self] error in + self?.showError(error) + } + .store(in: &subscriptions) + } + + override func configureLayout() { + super.configureLayout() + + embedChild(containerViewController) + } + + +} + +// MARK: Private APIs + +private extension NewsPageViewController { + + func show(content: UIViewController) { + containerViewController.content = content + } + + func makeResultContent(for news: Article) -> NewsDetailsViewController { + let result = NewsDetailsViewController(for: news) + result.externalLinkActionHandler = externalLinkActionHandler + result.askQuestionActionHandler = askQuestionActionHandler + return result + } + +} diff --git a/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift b/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift index 724c820..2eb09b0 100644 --- a/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift +++ b/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift @@ -10,59 +10,68 @@ // import Foundation +import CoreUI import Combine import ArticlesClient // MARK: - NewsDetailsViewModel -final class NewsDetailsViewModel { - +final class NewsDetailsViewModel: BasePageViewModel { + let articlesClient: ArticlesClient - let language: Language? - + let newsId: String + @Published private(set) var isLoading = false @Published private(set) var error: Error! @Published private(set) var result: Article! - private var showExternalLinkSubject: PassthroughSubject<(Article, Any?), Never> = .init() - lazy var showExternalLinkPublisher = showExternalLinkSubject - .eraseToAnyPublisher() - - private var showAskAQuestionSubject: PassthroughSubject<(Article, Any?), Never> = .init() - lazy var showAskAQuestionPublisher = showAskAQuestionSubject - .eraseToAnyPublisher() - init( articlesClient: ArticlesClient, - availableNews: Article?, - language: Language? + news: Article ) { self.articlesClient = articlesClient - self.result = availableNews - self.language = language + self.newsId = news.id + self.result = news + + super.init() } - - func refreshNewsDetails(newsId: String) { + + init( + articlesClient: ArticlesClient, + newsId: String + ) { + self.articlesClient = articlesClient + self.newsId = newsId + + super.init() + } + + @available(*, unavailable) + required init() { + fatalError("\(#function) not available") + } + + func fetchNews(with newsId: String) { Task(priority: .userInitiated) { [weak self] in - await self?.performRefreshNewsDetails(newsId: newsId) + await self?.performFetchNews(with: newsId) } } - - func showExternalLink(sender: Any?) { - showExternalLinkSubject.send((result, sender)) - } - func showAskAQuestion(sender: Any?) { - showAskAQuestionSubject.send((result, sender)) - } - + override func onViewDidLoad() { + super.onViewDidLoad() + + if result == nil { + fetchNews(with: newsId) + } + } + } // MARK: Private APIs private extension NewsDetailsViewModel { - func performRefreshNewsDetails(newsId: String) async { + func performFetchNews(with newsId: String) async { isLoading = true defer { isLoading = false diff --git a/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift b/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift index 97487f4..8c04322 100644 --- a/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift +++ b/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift @@ -12,16 +12,34 @@ import Foundation public protocol ArticlesClient { - + func getArticleList( startDate: Date?, publishedOn: String?, pageSize: Int?, pageNumber: Int? ) async throws -> ArticleListResponse - + func getArticle(newsId: String) async throws -> Article + +} +public extension ArticlesClient { + + func getArticleList( + startDate: Date? = nil, + publishedOn: String? = nil, + pageSize: Int? = nil, + pageNumber: Int? = nil + ) async throws -> ArticleListResponse { + try await getArticleList( + startDate: startDate, + publishedOn: publishedOn, + pageSize: pageSize, + pageNumber: pageNumber + ) + } + } From 24be6e8ed8fbf6c1c6d8039da401cf87ce971610 Mon Sep 17 00:00:00 2001 From: Matteo Matassoni <4108197+matax87@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:11:11 +0100 Subject: [PATCH 10/14] Fix deep linking preventing subsequent event/news screen from being displayed --- .../Base/BaseCoordinator.swift | 9 ++++- .../Custom/AppCoordinator.swift | 32 +++++++-------- .../EventPageViewController.swift | 24 +++++++++--- .../NewsPageViewController.swift | 16 ++++++-- .../CoreUI/UIWindow+topViewController.swift | 39 +++++++++++++++++++ 5 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 NOICommunityLib/Sources/CoreUI/UIWindow+topViewController.swift diff --git a/NOICommunity/Coordinator/Implementations/Base/BaseCoordinator.swift b/NOICommunity/Coordinator/Implementations/Base/BaseCoordinator.swift index 21a8177..cebc384 100644 --- a/NOICommunity/Coordinator/Implementations/Base/BaseCoordinator.swift +++ b/NOICommunity/Coordinator/Implementations/Base/BaseCoordinator.swift @@ -16,7 +16,14 @@ class BaseCoordinator: NSObject, CoordinatorType { var childCoordinators: [CoordinatorType] = [] var dependencyContainer: DependencyRepresentable - + + var topViewController: UIViewController? { + guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? UIWindowSceneDelegate + else { return nil } + + return sceneDelegate.window??.topViewController + } + @available(*, unavailable) override init() { fatalError("\(#function) not available") diff --git a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift index 7a0ba4a..70a75b8 100644 --- a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift +++ b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift @@ -31,7 +31,7 @@ final class AppCoordinator: BaseNavigationCoordinator { private var pendingDeepLinkIntent: DeepLinkIntent? private weak var tabCoordinator: TabCoordinator! - + override func start(animated: Bool) { NotificationCenter .default @@ -202,6 +202,9 @@ private extension AppCoordinator { } func showNewsDetails(newsId: String, sender: Any?) { + guard let topViewController + else { return } + func configureBindings( viewModel: NewsDetailsViewModel, pageViewController: NewsPageViewController @@ -243,10 +246,9 @@ private extension AppCoordinator { pageVC.navigationItem.largeTitleDisplayMode = .never pageVC.navigationItem.leftBarButtonItem = UIBarButtonItem( image: UIImage(systemName: "xmark.circle.fill"), - style: .plain, - target: self, - action: #selector(closeModal(sender:)) - ) + primaryAction: UIAction { [weak pageVC] _ in + pageVC?.dismiss(animated: true) + }) pageVC.modalPresentationStyle = .fullScreen return pageVC @@ -256,8 +258,8 @@ private extension AppCoordinator { viewModel: viewModel, pageViewController: pageVC ) - - navigationController.present( + + topViewController.present( NavigationController(rootViewController: pageVC), animated: true ) @@ -309,6 +311,9 @@ private extension AppCoordinator { } func showEventDetails(eventId: String, sender: Any?) { + guard let topViewController + else { return } + func configureBindings( viewModel: EventDetailsViewModel, pageViewController: EventPageViewController @@ -354,10 +359,9 @@ private extension AppCoordinator { pageVC.navigationItem.largeTitleDisplayMode = .never pageVC.navigationItem.leftBarButtonItem = UIBarButtonItem( image: UIImage(systemName: "xmark.circle.fill"), - style: .plain, - target: self, - action: #selector(closeModal(sender:)) - ) + primaryAction: UIAction { [weak pageVC] _ in + pageVC?.dismiss(animated: true) + }) pageVC.modalPresentationStyle = .fullScreen return pageVC @@ -365,16 +369,12 @@ private extension AppCoordinator { configureBindings(viewModel: viewModel, pageViewController: pageVC) - navigationController.present( + topViewController.present( NavigationController(rootViewController: pageVC), animated: true ) } - @objc func closeModal(sender: Any?) { - navigationController.dismiss(animated: true) - } - func showAccessNotGrantedCoordinator(animated: Bool) { let accessNotGrantedCoordinator = AccessNotGrantedCoordinator( navigationController: navigationController, diff --git a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift index 4a49a22..77adff0 100644 --- a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift +++ b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift @@ -27,19 +27,25 @@ final class EventPageViewController: BasePageViewController Void)? { didSet { - eventDetailsViewController?.locateActionHandler = locateActionHandler + eventDetailsViewController?.locateActionHandler = { [weak self] in + self?.locateActionHandler?($0) + } } } var addToCalendarActionHandler: ((Event) -> Void)? { didSet { - eventDetailsViewController?.addToCalendarActionHandler = addToCalendarActionHandler + eventDetailsViewController?.addToCalendarActionHandler = { [weak self] in + self?.addToCalendarActionHandler?($0) + } } } var signupActionHandler: ((Event) -> Void)? { didSet { - eventDetailsViewController?.signupActionHandler = signupActionHandler + eventDetailsViewController?.signupActionHandler = { [weak self] in + self?.signupActionHandler?($0) + } } } @@ -93,9 +99,15 @@ private extension EventPageViewController { func makeResultContent(for event: Event) -> EventDetailsViewController { let result = EventDetailsViewController(for: event) - result.locateActionHandler = locateActionHandler - result.addToCalendarActionHandler = addToCalendarActionHandler - result.signupActionHandler = signupActionHandler + result.locateActionHandler = { [weak self] in + self?.locateActionHandler?($0) + } + result.addToCalendarActionHandler = { [weak self] in + self?.addToCalendarActionHandler?($0) + } + result.signupActionHandler = { [weak self] in + self?.signupActionHandler?($0) + } return result } diff --git a/NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift b/NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift index 3ed03f6..2471b11 100644 --- a/NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift +++ b/NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift @@ -28,13 +28,17 @@ final class NewsPageViewController: BasePageViewController var externalLinkActionHandler: ((Article) -> Void)? { didSet { - newsDetailsViewController?.externalLinkActionHandler = externalLinkActionHandler + newsDetailsViewController?.externalLinkActionHandler = { [weak self] in + self?.externalLinkActionHandler?($0) + } } } var askQuestionActionHandler: ((Article) -> Void)? { didSet { - newsDetailsViewController?.askQuestionActionHandler = askQuestionActionHandler + newsDetailsViewController?.askQuestionActionHandler = { [weak self] in + self?.askQuestionActionHandler?($0) + } } } @@ -89,8 +93,12 @@ private extension NewsPageViewController { func makeResultContent(for news: Article) -> NewsDetailsViewController { let result = NewsDetailsViewController(for: news) - result.externalLinkActionHandler = externalLinkActionHandler - result.askQuestionActionHandler = askQuestionActionHandler + result.externalLinkActionHandler = { [weak self] in + self?.externalLinkActionHandler?($0) + } + result.askQuestionActionHandler = { [weak self] in + self?.askQuestionActionHandler?($0) + } return result } diff --git a/NOICommunityLib/Sources/CoreUI/UIWindow+topViewController.swift b/NOICommunityLib/Sources/CoreUI/UIWindow+topViewController.swift new file mode 100644 index 0000000..6e1fca4 --- /dev/null +++ b/NOICommunityLib/Sources/CoreUI/UIWindow+topViewController.swift @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// UIWindow+topViewController.swift +// NOICommunityLib +// +// Created by Matteo Matassoni on 06/12/24. +// + +import UIKit + +public extension UIWindow { + + var topViewController: UIViewController? { + topMostViewController(from: rootViewController) + } + +} + +private extension UIWindow { + + func topMostViewController( + from viewController: UIViewController? + ) -> UIViewController? { + if let presentedViewController = viewController?.presentedViewController { + topMostViewController(from: presentedViewController) + } else if let tabBarController = viewController as? UITabBarController { + topMostViewController(from: tabBarController.selectedViewController) + } else if let navigationController = viewController as? UINavigationController, + let topViewController = navigationController.topViewController { + topMostViewController(from: topViewController) + } else { + viewController + } + } + +} From bf312fa7f465e5f18f7d1b5093a79607a354de1a Mon Sep 17 00:00:00 2001 From: Matteo Matassoni <4108197+matax87@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:32:30 +0100 Subject: [PATCH 11/14] WIP refactor title of news/event details screen --- .../Coordinator/Implementations/Custom/AppCoordinator.swift | 5 +++++ .../EventsFeature/Coordinators/EventsCoordinator.swift | 2 ++ .../View Controllers/EventPageViewController.swift | 2 ++ NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift | 3 ++- 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift index 70a75b8..2c7d3f9 100644 --- a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift +++ b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift @@ -205,6 +205,7 @@ private extension AppCoordinator { guard let topViewController else { return } + // TODO: remove after title refactor func configureBindings( viewModel: NewsDetailsViewModel, pageViewController: NewsPageViewController @@ -254,6 +255,7 @@ private extension AppCoordinator { return pageVC }() + // TODO: remove after title refactor configureBindings( viewModel: viewModel, pageViewController: pageVC @@ -314,6 +316,7 @@ private extension AppCoordinator { guard let topViewController else { return } + // TODO: move set navigationItem title here func configureBindings( viewModel: EventDetailsViewModel, pageViewController: EventPageViewController @@ -355,7 +358,9 @@ private extension AppCoordinator { self?.signupEvent($0, from: pageVC) } + // TODO: move set navigationItem title here pageVC.navigationItem.title = nil + pageVC.navigationItem.largeTitleDisplayMode = .never pageVC.navigationItem.leftBarButtonItem = UIBarButtonItem( image: UIImage(systemName: "xmark.circle.fill"), diff --git a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift index edcd7f5..3924969 100644 --- a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift +++ b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift @@ -121,7 +121,9 @@ private extension EventsCoordinator { self?.signupEvent($0) } + // TODO: remove after title refactor pageVC.navigationItem.title = event.title + pageVC.navigationItem.largeTitleDisplayMode = .never return pageVC diff --git a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift index 77adff0..c36a0eb 100644 --- a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift +++ b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift @@ -68,6 +68,8 @@ final class EventPageViewController: BasePageViewController Date: Fri, 6 Dec 2024 11:58:01 +0100 Subject: [PATCH 12/14] Refactoring navigationItem.title --- .../Custom/AppCoordinator.swift | 46 +------------------ .../Coordinators/EventsCoordinator.swift | 3 -- .../EventPageViewController.swift | 2 +- .../Coordinators/NewsCoordinator.swift | 7 +-- .../NewsPageViewController.swift | 6 +++ 5 files changed, 10 insertions(+), 54 deletions(-) diff --git a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift index 2c7d3f9..2958c37 100644 --- a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift +++ b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift @@ -204,23 +204,6 @@ private extension AppCoordinator { func showNewsDetails(newsId: String, sender: Any?) { guard let topViewController else { return } - - // TODO: remove after title refactor - func configureBindings( - viewModel: NewsDetailsViewModel, - pageViewController: NewsPageViewController - ) { - viewModel.$result - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { [weak pageViewController] news in - pageViewController?.navigationItem.title = localizedValue( - from: news.languageToDetails - )? - .title - } - .store(in: &subscriptions) - } let viewModel = dependencyContainer.makeNewsDetailsViewModel( newsId: newsId @@ -255,12 +238,6 @@ private extension AppCoordinator { return pageVC }() - // TODO: remove after title refactor - configureBindings( - viewModel: viewModel, - pageViewController: pageVC - ) - topViewController.present( NavigationController(rootViewController: pageVC), animated: true @@ -315,21 +292,7 @@ private extension AppCoordinator { func showEventDetails(eventId: String, sender: Any?) { guard let topViewController else { return } - - // TODO: move set navigationItem title here - func configureBindings( - viewModel: EventDetailsViewModel, - pageViewController: EventPageViewController - ) { - viewModel.$result - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { [weak pageViewController] event in - pageViewController?.navigationItem.title = event.title - } - .store(in: &subscriptions) - } - + let viewModel = dependencyContainer.makeEventDetailsViewModel( eventId: eventId ) @@ -357,10 +320,7 @@ private extension AppCoordinator { self?.signupEvent($0, from: pageVC) } - - // TODO: move set navigationItem title here - pageVC.navigationItem.title = nil - + pageVC.navigationItem.title = nil pageVC.navigationItem.largeTitleDisplayMode = .never pageVC.navigationItem.leftBarButtonItem = UIBarButtonItem( image: UIImage(systemName: "xmark.circle.fill"), @@ -372,8 +332,6 @@ private extension AppCoordinator { return pageVC }() - configureBindings(viewModel: viewModel, pageViewController: pageVC) - topViewController.present( NavigationController(rootViewController: pageVC), animated: true diff --git a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift index 3924969..5a17835 100644 --- a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift +++ b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift @@ -120,9 +120,6 @@ private extension EventsCoordinator { pageVC.signupActionHandler = { [weak self] in self?.signupEvent($0) } - - // TODO: remove after title refactor - pageVC.navigationItem.title = event.title pageVC.navigationItem.largeTitleDisplayMode = .never diff --git a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift index c36a0eb..a6a93ba 100644 --- a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift +++ b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift @@ -69,7 +69,7 @@ final class EventPageViewController: BasePageViewController .sink { [weak self] event in guard let self else { return } + + // TODO: move set navigationItem title here + self.navigationItem.title = localizedValue( + from: event.languageToDetails + )? + .title self.show(content: self.makeResultContent(for: event)) } From 707c4edb6e377c4d1c8029671cd98b59eb7861f2 Mon Sep 17 00:00:00 2001 From: Camilla Copetti Date: Fri, 6 Dec 2024 12:26:52 +0100 Subject: [PATCH 13/14] Refactor title of news/event details screen --- .../View Controllers/EventPageViewController.swift | 1 - .../NewsFeature/View Controllers/NewsPageViewController.swift | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift index a6a93ba..5a60d14 100644 --- a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift +++ b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift @@ -68,7 +68,6 @@ final class EventPageViewController: BasePageViewController .sink { [weak self] event in guard let self else { return } - - // TODO: move set navigationItem title here + self.navigationItem.title = localizedValue( from: event.languageToDetails )? From 6c86e4bb4c353cf58c52913373d272deae4462ef Mon Sep 17 00:00:00 2001 From: Matteo Matassoni <4108197+matax87@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:33:24 +0100 Subject: [PATCH 14/14] Refactor event/news details configuration --- .../Coordinator/Implementations/Custom/AppCoordinator.swift | 5 +---- .../EventsFeature/Coordinators/EventsCoordinator.swift | 3 --- .../View Controllers/EventPageViewController.swift | 1 + NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift | 3 --- .../View Controllers/NewsPageViewController.swift | 1 + 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift index 2958c37..e40b757 100644 --- a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift +++ b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift @@ -226,8 +226,6 @@ private extension AppCoordinator { self?.showNewsAskAQuestion(for: $0, from: pageVC) } - pageVC.navigationItem.title = nil - pageVC.navigationItem.largeTitleDisplayMode = .never pageVC.navigationItem.leftBarButtonItem = UIBarButtonItem( image: UIImage(systemName: "xmark.circle.fill"), primaryAction: UIAction { [weak pageVC] _ in @@ -320,8 +318,7 @@ private extension AppCoordinator { self?.signupEvent($0, from: pageVC) } - pageVC.navigationItem.title = nil - pageVC.navigationItem.largeTitleDisplayMode = .never + pageVC.navigationItem.leftBarButtonItem = UIBarButtonItem( image: UIImage(systemName: "xmark.circle.fill"), primaryAction: UIAction { [weak pageVC] _ in diff --git a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift index 5a17835..94d43f9 100644 --- a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift +++ b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift @@ -120,9 +120,6 @@ private extension EventsCoordinator { pageVC.signupActionHandler = { [weak self] in self?.signupEvent($0) } - - pageVC.navigationItem.largeTitleDisplayMode = .never - return pageVC }() navigationController.pushViewController(pageVC, animated: true) diff --git a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift index 5a60d14..21d45b6 100644 --- a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift +++ b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift @@ -85,6 +85,7 @@ final class EventPageViewController: BasePageViewController override func configureLayout() { super.configureLayout() + navigationItem.largeTitleDisplayMode = .never embedChild(containerViewController) }