diff --git a/NOICommunity.xcodeproj/project.pbxproj b/NOICommunity.xcodeproj/project.pbxproj index f934b88..4160217 100644 --- a/NOICommunity.xcodeproj/project.pbxproj +++ b/NOICommunity.xcodeproj/project.pbxproj @@ -30,14 +30,13 @@ 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 */; }; 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 */; }; @@ -57,14 +56,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 +83,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 +134,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 +150,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 */; }; @@ -145,17 +158,10 @@ 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 */; }; - 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 +182,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 +199,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 +215,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 */; }; @@ -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 */ @@ -271,13 +272,12 @@ 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 = ""; }; + 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 = ""; }; @@ -308,7 +308,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 +329,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 +380,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 +387,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 +394,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 +419,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 +434,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 +451,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 = ""; }; @@ -456,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 */ @@ -474,8 +478,6 @@ 31E058F22812F18800D1F7FE /* KeychainAccess in Frameworks */, 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 +513,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 = ( @@ -570,6 +525,7 @@ 3102F45C282C069500687CF7 /* View Controllers */ = { isa = PBXGroup; children = ( + 31FFE94F2D01E065007C699B /* NewsPageViewController.swift */, 3102F45E282C06B700687CF7 /* NewsViewController.swift */, 3182A1BC282D5159005439B4 /* NewsDetailsViewController.swift */, 3182A1BD282D5159005439B4 /* NewsDetailsViewController.xib */, @@ -679,7 +635,7 @@ 31CF8123270B3CD90077CB0D /* IntroFeature */, 3101FC4F283390D400A3416F /* TodayFeature */, 319C4656282BB7F300946AC7 /* NewsFeature */, - 319C0E8026F332FE00C6D38B /* EventsFeature */, + 316A829D2CFF1FF0002D62D3 /* EventsFeature */, 31D9C78726FDFC58001F2DBB /* OrientateFeature */, 31C640C126FE1E43004B71A2 /* MeetFeature */, 3167681427035D9500A9DB87 /* EatFeature */, @@ -795,6 +751,67 @@ path = EatFeature; sourceTree = ""; }; + 316A82862CFF1FF0002D62D3 /* Coordinators */ = { + isa = PBXGroup; + children = ( + 316A82852CFF1FF0002D62D3 /* EventsCoordinator.swift */, + ); + path = Coordinators; + sourceTree = ""; + }; + 316A82902CFF1FF0002D62D3 /* View Controllers */ = { + isa = PBXGroup; + children = ( + 31260A622CFF610F00ADBDEF /* EventPageViewController.swift */, + 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 = ( + 31260A642CFF68CF00ADBDEF /* EventDetailsViewModel.swift */, + 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 +843,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 +1086,6 @@ 3182F4A127DB3841005ADDAF /* AppPreferencesClient */, 3182F4A327DB3841005ADDAF /* AppPreferencesClientLive */, 3182F4A527DB3841005ADDAF /* EventShortClient */, - 3182F4A727DB3841005ADDAF /* EventShortClientLive */, 3182F4A927DB3841005ADDAF /* EventShortTypesClient */, 3182F4AB27DB3841005ADDAF /* EventShortTypesClientLive */, 31EF82582810517600EBE5F0 /* AppAuth */, @@ -1091,7 +1094,6 @@ 31E058F12812F18800D1F7FE /* KeychainAccess */, 311E0EC52825157800404DCE /* FirebaseMessaging */, 319C4652282BB32400946AC7 /* ArticlesClient */, - 319C4654282BB32400946AC7 /* ArticlesClientLive */, 317EC888283BB83E00F30B95 /* PeopleClient */, 317EC88A283BB83E00F30B95 /* PeopleClientLive */, 3181B9202B1E12BD000D2A0F /* Core */, @@ -1201,7 +1203,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 +1211,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 +1219,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 +1265,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 +1280,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 */, @@ -1310,7 +1308,9 @@ 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 */, 31157D1526FC83FE00A43B33 /* UIViewController+ShowCalenderError.swift in Sources */, 31CF8132270B49100077CB0D /* VersionContentView.swift in Sources */, @@ -1321,35 +1321,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 +1370,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 */, @@ -1380,6 +1385,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 */, @@ -1403,7 +1409,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 +2351,6 @@ isa = XCSwiftPackageProductDependency; productName = EventShortClient; }; - 3182F4A727DB3841005ADDAF /* EventShortClientLive */ = { - isa = XCSwiftPackageProductDependency; - productName = EventShortClientLive; - }; 3182F4A927DB3841005ADDAF /* EventShortTypesClient */ = { isa = XCSwiftPackageProductDependency; productName = EventShortTypesClient; @@ -2362,10 +2363,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/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/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 2a190a4..e40b757 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 @@ -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) } } @@ -174,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 @@ -193,66 +202,138 @@ private extension AppCoordinator { } func showNewsDetails(newsId: String, sender: Any?) { - func configureBindings( - viewModel: NewsDetailsViewModel, - detailsViewController: NewsDetailsViewController - ) { - viewModel.showExternalLinkPublisher - .sink { [weak self] (news, sender) in - self?.showNewsExternalLink(of: news, sender: sender) - } - .store(in: &subscriptions) - viewModel.showAskAQuestionPublisher - .sink { [weak self] (news, sender) in - self?.showNewsAskAQuestion(for: news, sender: sender) - } - .store(in: &subscriptions) - viewModel.$result - .sink { [weak detailsViewController] news in - guard let news = news - else { return } - - detailsViewController?.navigationItem.title = localizedValue( - from: news.languageToDetails - )? - .title - } - .store(in: &subscriptions) - } + guard let topViewController + else { return } let viewModel = dependencyContainer.makeNewsDetailsViewModel( - availableNews: nil + newsId: newsId ) - - let detailsVC = dependencyContainer.makeNewsDetailsViewController( - newsId: newsId, - viewModel: viewModel - ) - - configureBindings( - viewModel: viewModel, - detailsViewController: detailsVC - ) - - 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), + 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.leftBarButtonItem = UIBarButtonItem( + image: UIImage(systemName: "xmark.circle.fill"), + primaryAction: UIAction { [weak pageVC] _ in + pageVC?.dismiss(animated: true) + }) + pageVC.modalPresentationStyle = .fullScreen + + return pageVC + }() + + topViewController.present( + NavigationController(rootViewController: pageVC), animated: true ) } - - @objc func closeModal(sender: Any?) { - navigationController.dismiss(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?) { + guard let topViewController + else { return } + + 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.leftBarButtonItem = UIBarButtonItem( + image: UIImage(systemName: "xmark.circle.fill"), + primaryAction: UIAction { [weak pageVC] _ in + pageVC?.dismiss(animated: true) + }) + pageVC.modalPresentationStyle = .fullScreen + + return pageVC + }() + + topViewController.present( + NavigationController(rootViewController: pageVC), + animated: true + ) + } func showAccessNotGrantedCoordinator(animated: Bool) { let accessNotGrantedCoordinator = AccessNotGrantedCoordinator( 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 aafff01..f406644 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 62% rename from NOICommunity/TodayFeature/EventsFeature/Coordinators/EventsCoordinator.swift rename to NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift index c566422..94d43f9 100644 --- a/NOICommunity/TodayFeature/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,27 @@ 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) + } + return pageVC + }() + navigationController.pushViewController(pageVC, animated: true) } func goToFilters() { diff --git a/NOICommunity/TodayFeature/EventsFeature/EventsFeatureConstants.swift b/NOICommunity/EventsFeature/EventsFeatureConstants.swift similarity index 61% rename from NOICommunity/TodayFeature/EventsFeature/EventsFeatureConstants.swift rename to NOICommunity/EventsFeature/EventsFeatureConstants.swift index 1688ee2..c09e8e2 100644 --- a/NOICommunity/TodayFeature/EventsFeature/EventsFeatureConstants.swift +++ b/NOICommunity/EventsFeature/EventsFeatureConstants.swift @@ -12,6 +12,14 @@ 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 71% rename from NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.swift rename to NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.swift index 2c51215..295eabe 100644 --- a/NOICommunity/TodayFeature/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 - } } } @@ -167,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 @@ -187,21 +149,8 @@ private extension EventDetailsViewController { } else { addToCalendarButton.removeFromSuperview() } - } - - 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 + + contentStackView.insertArrangedSubview(_cardView, at: 0) } @IBAction func findOnMapsAction(sender: Any?) { @@ -220,6 +169,6 @@ private extension EventDetailsViewController { extension EventDetailsViewController: CurrentScrollOffsetProvider { var currentScrollOffset: CGPoint { - relatedEventsVC?.currentScrollOffset ?? .zero + .zero } } diff --git a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.xib b/NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.xib similarity index 80% rename from NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.xib rename to NOICommunity/EventsFeature/View Controllers/EventDetailsViewController.xib index 3764113..266e6a0 100644 --- a/NOICommunity/TodayFeature/EventsFeature/View Controllers/EventDetailsViewController.xib +++ b/NOICommunity/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 @@ - - - - - - - - - - - - - - - - - - - - - - - - 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/EventsFeature/View Controllers/EventPageViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift new file mode 100644 index 0000000..21d45b6 --- /dev/null +++ b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift @@ -0,0 +1,116 @@ +// 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() + + private var eventDetailsViewController: EventDetailsViewController? { + children + .lazy + .compactMap { $0 as? EventDetailsViewController } + .first + } + + var locateActionHandler: ((Event) -> Void)? { + didSet { + eventDetailsViewController?.locateActionHandler = { [weak self] in + self?.locateActionHandler?($0) + } + } + } + + var addToCalendarActionHandler: ((Event) -> Void)? { + didSet { + eventDetailsViewController?.addToCalendarActionHandler = { [weak self] in + self?.addToCalendarActionHandler?($0) + } + } + } + + var signupActionHandler: ((Event) -> Void)? { + didSet { + eventDetailsViewController?.signupActionHandler = { [weak self] in + self?.signupActionHandler?($0) + } + } + } + + 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.navigationItem.title = event.title + 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() + + navigationItem.largeTitleDisplayMode = .never + embedChild(containerViewController) + } + +} + +// MARK: Private APIs + +private extension EventPageViewController { + + func show(content: UIViewController) { + containerViewController.content = content + } + + func makeResultContent(for event: Event) -> EventDetailsViewController { + let result = EventDetailsViewController(for: event) + 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/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/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/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 68% rename from NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift rename to NOICommunity/EventsFeature/View Models/EventsViewModel.swift index 0475600..0262fa4 100644 --- a/NOICommunity/TodayFeature/EventsFeature/View Models/EventsViewModel.swift +++ b/NOICommunity/EventsFeature/View Models/EventsViewModel.swift @@ -11,128 +11,137 @@ import Foundation import Combine +import CoreUI import EventShortClient import EventShortTypesClient // MARK: - EventsViewModel -class EventsViewModel { - +final class EventsViewModel: BasePageViewModel { + @Published var isLoading = false @Published var error: Error! @Published var eventResults: [Event]! @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() { - 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", - 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 - ) - } - }) - } - - 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) + Task(priority: .userInitiated) { [weak self] in + await self?.performRefreshEvents() + } } func showFilters() { showFiltersHandler() } + override func configureBindings() { + super.configureBindings() + refreshCancellable = NotificationCenter + .default + .publisher(for: refreshEventsListNotification) + .sink { [weak self] _ in + self?.refreshEvents() + } + } + +} + +// 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?) { @@ -156,6 +165,7 @@ private extension DateIntervalFilter { // MARK: - EventShort Additions private extension EventShort { + var localizedEventDescriptions: [String:String] { Dictionary(uniqueKeysWithValues: [ eventDescriptionIT.map { ("it", $0) }, @@ -175,7 +185,8 @@ private extension EventShort { // MARK: - Event Additions -private extension Event { +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/Factories/DependencyContainer.swift b/NOICommunity/Factories/DependencyContainer.swift index b4f830e..2fbb871 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 { @@ -172,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 { @@ -202,7 +215,13 @@ extension DependencyContainer: ViewControllerFactory { func makeEventListViewController() -> EventListViewController { .init(items: []) } - + + func makeEventPageViewController( + viewModel: EventDetailsViewModel + ) -> EventPageViewController { + .init(viewModel: viewModel) + } + func makeEventFiltersViewController( viewModel: EventFiltersViewModel ) -> EventFiltersViewController { @@ -233,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 53a4150..4449ecc 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 @@ -36,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 aa3d3a3..9475c0e 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 @@ -27,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/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) diff --git a/NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift b/NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift index 8860f42..14f40e1 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,33 @@ 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) + } + 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..b8e846a --- /dev/null +++ b/NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift @@ -0,0 +1,111 @@ +// 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 = { [weak self] in + self?.externalLinkActionHandler?($0) + } + } + } + + var askQuestionActionHandler: ((Article) -> Void)? { + didSet { + newsDetailsViewController?.askQuestionActionHandler = { [weak self] in + self?.askQuestionActionHandler?($0) + } + } + } + + 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.navigationItem.title = localizedValue( + from: event.languageToDetails + )? + .title + + 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() + + navigationItem.largeTitleDisplayMode = .never + 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 = { [weak self] in + self?.externalLinkActionHandler?($0) + } + result.askQuestionActionHandler = { [weak self] in + self?.askQuestionActionHandler?($0) + } + return result + } + +} diff --git a/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift b/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift index 527762a..2eb09b0 100644 --- a/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift +++ b/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift @@ -10,67 +10,78 @@ // 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() - - private var fetchRequestCancellable: AnyCancellable? - init( articlesClient: ArticlesClient, - availableNews: Article?, - language: Language? + news: Article ) { self.articlesClient = articlesClient - self.result = availableNews - self.language = language - } - - 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 - }) - } - - func showExternalLink(sender: Any?) { - showExternalLinkSubject.send((result, sender)) + self.newsId = news.id + self.result = news + + super.init() } - func showAskAQuestion(sender: Any?) { - showAskAQuestionSubject.send((result, sender)) + 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?.performFetchNews(with: newsId) + } } - + + override func onViewDidLoad() { + super.onViewDidLoad() + + if result == nil { + fetchNews(with: newsId) + } + } + +} + +// MARK: Private APIs + +private extension NewsDetailsViewModel { + + func performFetchNews(with 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 0ace6c4..aa3b8d5 100644 --- a/NOICommunity/SceneDelegate.swift +++ b/NOICommunity/SceneDelegate.swift @@ -11,14 +11,14 @@ import UIKit import AppAuth -import EventShortClientLive +import EventShortClient import AppPreferencesClientLive import EventShortTypesClient import EventShortTypesClientLive import Core import AuthClientLive import AuthStateStorageClient -import ArticlesClientLive +import ArticlesClient import PeopleClientLive #if DEBUG @@ -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", @@ -74,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 95465f6..990a744 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"] @@ -60,10 +56,6 @@ let package = Package( name: "ArticlesClient", targets: ["ArticlesClient"] ), - .library( - name: "ArticlesClientLive", - targets: ["ArticlesClientLive"] - ), .library( name: "PeopleClient", targets: ["PeopleClient"] @@ -109,13 +101,6 @@ let package = Package( "Core" ] ), - .testTarget( - name: "EventShortTypesClientTests", - dependencies: [ - "Core", - "EventShortTypesClient" - ] - ), .target( name: "EventShortTypesClientLive", dependencies: [ @@ -129,13 +114,6 @@ let package = Package( "Core" ] ), - .testTarget( - name: "AppPreferencesClientTests", - dependencies: [ - "Core", - "AppPreferencesClient" - ] - ), .target( name: "AppPreferencesClientLive", dependencies: [ @@ -149,20 +127,6 @@ let package = Package( "Core", ] ), - .testTarget( - name: "EventShortClientTests", - dependencies: [ - "Core", - "EventShortClient" - ] - ), - .target( - name: "EventShortClientLive", - dependencies: [ - "Core", - "EventShortClient" - ] - ), .target( name: "AuthStateStorageClient", dependencies: [ @@ -186,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/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/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..8c04322 --- /dev/null +++ b/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift @@ -0,0 +1,47 @@ +// 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 + +} + +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 + ) + } + +} + + + + 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/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/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/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 + } + } + +} diff --git a/NOICommunityLib/Sources/EventShortClient/Endpoint+EventShort.swift b/NOICommunityLib/Sources/EventShortClient/Endpoint+EventShort.swift new file mode 100644 index 0000000..3d65768 --- /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: "fields", + 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..fdfd49b --- /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) - } -}