diff --git a/NOICommunity.xcodeproj/project.pbxproj b/NOICommunity.xcodeproj/project.pbxproj index a0a0cb9..4160217 100644 --- a/NOICommunity.xcodeproj/project.pbxproj +++ b/NOICommunity.xcodeproj/project.pbxproj @@ -224,6 +224,7 @@ 31F0DBD3280F0D3D00E782D5 /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F0DBD2280F02DC00E782D5 /* AuthCoordinator.swift */; }; 31FAEA7D28197D9700CDBC1B /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31FAEA7C28197D9700CDBC1B /* AppCoordinator.swift */; }; 31FAEA7F28197EC200CDBC1B /* NavigationCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31FAEA7E28197EC200CDBC1B /* NavigationCoordinatorType.swift */; }; + 31FFE9502D01E068007C699B /* NewsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31FFE94F2D01E065007C699B /* NewsPageViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -458,6 +459,7 @@ 31F0DBD2280F02DC00E782D5 /* AuthCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; 31FAEA7C28197D9700CDBC1B /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 31FAEA7E28197EC200CDBC1B /* NavigationCoordinatorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinatorType.swift; sourceTree = ""; }; + 31FFE94F2D01E065007C699B /* NewsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsPageViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -523,6 +525,7 @@ 3102F45C282C069500687CF7 /* View Controllers */ = { isa = PBXGroup; children = ( + 31FFE94F2D01E065007C699B /* NewsPageViewController.swift */, 3102F45E282C06B700687CF7 /* NewsViewController.swift */, 3182A1BC282D5159005439B4 /* NewsDetailsViewController.swift */, 3182A1BD282D5159005439B4 /* NewsDetailsViewController.xib */, @@ -1305,6 +1308,7 @@ 31FAEA7F28197EC200CDBC1B /* NavigationCoordinatorType.swift in Sources */, 31157D1926FCA1EB00A43B33 /* WebViewController.swift in Sources */, 31AD1DB22B27947B006F71A4 /* ComeOnBoardOnboardingViewModel.swift in Sources */, + 31FFE9502D01E068007C699B /* NewsPageViewController.swift in Sources */, 312F5D272808250000C84598 /* AuthWelcomeViewController.swift in Sources */, 31260A652CFF68CF00ADBDEF /* EventDetailsViewModel.swift in Sources */, 3185AA172820005700767E31 /* KeychainAuthStateStorageClient.swift in Sources */, diff --git a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift index 7be514a..7a0ba4a 100644 --- a/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift +++ b/NOICommunity/Coordinator/Implementations/Custom/AppCoordinator.swift @@ -177,18 +177,24 @@ private extension AppCoordinator { showAuthCoordinator(animated: animated) } - func showNewsExternalLink(of news: Article, sender: Any?) { + func showNewsExternalLink( + of news: Article, + from viewController: UIViewController + ) { let author = localizedValue(from: news.languageToAuthor) let safariVC = SFSafariViewController(url: author!.externalURL!) - navigationController.presentedViewController?.present( + navigationController.presentedViewController?.present( safariVC, animated: true ) } - func showNewsAskAQuestion(for news: Article, sender: Any?) { + func showNewsAskAQuestion( + for news: Article, + from viewController: UIViewController + ) { let author = localizedValue(from: news.languageToAuthor) - navigationController.presentedViewController?.mailTo( + navigationController.presentedViewController?.mailTo( author!.email!, delegate: self, completion: nil @@ -198,25 +204,13 @@ private extension AppCoordinator { func showNewsDetails(newsId: String, sender: Any?) { func configureBindings( viewModel: NewsDetailsViewModel, - detailsViewController: NewsDetailsViewController + pageViewController: NewsPageViewController ) { - viewModel.showExternalLinkPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] (news, sender) in - self?.showNewsExternalLink(of: news, sender: sender) - } - .store(in: &subscriptions) - viewModel.showAskAQuestionPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] (news, sender) in - self?.showNewsAskAQuestion(for: news, sender: sender) - } - .store(in: &subscriptions) viewModel.$result .compactMap { $0 } .receive(on: DispatchQueue.main) - .sink { [weak detailsViewController] news in - detailsViewController?.navigationItem.title = localizedValue( + .sink { [weak pageViewController] news in + pageViewController?.navigationItem.title = localizedValue( from: news.languageToDetails )? .title @@ -225,31 +219,46 @@ private extension AppCoordinator { } let viewModel = dependencyContainer.makeNewsDetailsViewModel( - availableNews: nil - ) - - let detailsVC = dependencyContainer.makeNewsDetailsViewController( - newsId: newsId, - viewModel: viewModel + newsId: newsId ) - + let pageVC = { + let pageVC = dependencyContainer.makeNewsPageViewController( + viewModel: viewModel + ) + + pageVC.externalLinkActionHandler = { [weak self, weak pageVC] in + guard let pageVC + else { return } + + self?.showNewsExternalLink(of: $0, from: pageVC) + } + pageVC.askQuestionActionHandler = { [weak self, weak pageVC] in + guard let pageVC + else { return } + + self?.showNewsAskAQuestion(for: $0, from: pageVC) + } + + pageVC.navigationItem.title = nil + pageVC.navigationItem.largeTitleDisplayMode = .never + pageVC.navigationItem.leftBarButtonItem = UIBarButtonItem( + image: UIImage(systemName: "xmark.circle.fill"), + style: .plain, + target: self, + action: #selector(closeModal(sender:)) + ) + pageVC.modalPresentationStyle = .fullScreen + + return pageVC + }() + configureBindings( viewModel: viewModel, - detailsViewController: detailsVC + pageViewController: pageVC ) - detailsVC.navigationItem.title = nil - detailsVC.navigationItem.largeTitleDisplayMode = .never - detailsVC.navigationItem.leftBarButtonItem = UIBarButtonItem( - image: UIImage(systemName: "xmark.circle.fill"), - style: .plain, - target: self, - action: #selector(closeModal(sender:)) - ) - detailsVC.modalPresentationStyle = .fullScreen - navigationController.present( - NavigationController(rootViewController: detailsVC), + NavigationController(rootViewController: pageVC), animated: true ) } @@ -320,6 +329,7 @@ private extension AppCoordinator { let pageVC = dependencyContainer.makeEventPageViewController( viewModel: viewModel ) + pageVC.addToCalendarActionHandler = { [weak self, weak pageVC] in guard let pageVC else { return } @@ -339,6 +349,7 @@ private extension AppCoordinator { self?.signupEvent($0, from: pageVC) } + pageVC.navigationItem.title = nil pageVC.navigationItem.largeTitleDisplayMode = .never pageVC.navigationItem.leftBarButtonItem = UIBarButtonItem( @@ -348,6 +359,7 @@ private extension AppCoordinator { action: #selector(closeModal(sender:)) ) pageVC.modalPresentationStyle = .fullScreen + return pageVC }() diff --git a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift index b444baa..edcd7f5 100644 --- a/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift +++ b/NOICommunity/EventsFeature/Coordinators/EventsCoordinator.swift @@ -110,6 +110,7 @@ private extension EventsCoordinator { let pageVC = dependencyContainer.makeEventPageViewController( viewModel: viewModel ) + pageVC.addToCalendarActionHandler = { [weak self] in self?.addEventToCalendar($0) } @@ -119,6 +120,7 @@ private extension EventsCoordinator { pageVC.signupActionHandler = { [weak self] in self?.signupEvent($0) } + pageVC.navigationItem.title = event.title pageVC.navigationItem.largeTitleDisplayMode = .never diff --git a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift index 3752d6b..4a49a22 100644 --- a/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift +++ b/NOICommunity/EventsFeature/View Controllers/EventPageViewController.swift @@ -18,11 +18,30 @@ final class EventPageViewController: BasePageViewController Void)? + private var eventDetailsViewController: EventDetailsViewController? { + children + .lazy + .compactMap { $0 as? EventDetailsViewController } + .first + } - var addToCalendarActionHandler: ((Event) -> Void)? + var locateActionHandler: ((Event) -> Void)? { + didSet { + eventDetailsViewController?.locateActionHandler = locateActionHandler + } + } - var signupActionHandler: ((Event) -> Void)? + var addToCalendarActionHandler: ((Event) -> Void)? { + didSet { + eventDetailsViewController?.addToCalendarActionHandler = addToCalendarActionHandler + } + } + + var signupActionHandler: ((Event) -> Void)? { + didSet { + eventDetailsViewController?.signupActionHandler = signupActionHandler + } + } override func configureBindings() { super.configureBindings() @@ -72,7 +91,7 @@ private extension EventPageViewController { containerViewController.content = content } - func makeResultContent(for event: Event) -> UIViewController { + func makeResultContent(for event: Event) -> EventDetailsViewController { let result = EventDetailsViewController(for: event) result.locateActionHandler = locateActionHandler result.addToCalendarActionHandler = addToCalendarActionHandler diff --git a/NOICommunity/Factories/DependencyContainer.swift b/NOICommunity/Factories/DependencyContainer.swift index 9f5be6e..2fbb871 100644 --- a/NOICommunity/Factories/DependencyContainer.swift +++ b/NOICommunity/Factories/DependencyContainer.swift @@ -181,13 +181,17 @@ extension DependencyContainer: ViewModelFactory { func makeNewsListViewModel() -> NewsListViewModel { .init(articlesClient: makeArticlesClient()) } - - func makeNewsDetailsViewModel(availableNews: Article?) -> NewsDetailsViewModel { - .init( - articlesClient: makeArticlesClient(), - availableNews: availableNews, - language: nil - ) + + func makeNewsDetailsViewModel( + newsId: String + ) -> NewsDetailsViewModel { + .init(articlesClient: makeArticlesClient(), newsId: newsId) + } + + func makeNewsDetailsViewModel( + news: Article + ) -> NewsDetailsViewModel { + .init(articlesClient: makeArticlesClient(), news: news) } func makePeopleViewModel() -> PeopleViewModel { @@ -248,11 +252,10 @@ extension DependencyContainer: ViewControllerFactory { .init(viewModel: viewModel) } - func makeNewsDetailsViewController( - newsId: String, - viewModel: NewsDetailsViewModel - ) -> NewsDetailsViewController { - .init(newsId: newsId, viewModel: viewModel) + func makeNewsPageViewController( + viewModel: NewsDetailsViewModel + ) -> NewsPageViewController { + .init(viewModel: viewModel) } func makeMeetMainViewController( diff --git a/NOICommunity/Factories/ViewControllerFactory.swift b/NOICommunity/Factories/ViewControllerFactory.swift index 5825c72..4449ecc 100644 --- a/NOICommunity/Factories/ViewControllerFactory.swift +++ b/NOICommunity/Factories/ViewControllerFactory.swift @@ -40,10 +40,9 @@ protocol ViewControllerFactory { viewModel: NewsListViewModel ) -> NewsViewController - func makeNewsDetailsViewController( - newsId: String, + func makeNewsPageViewController( viewModel: NewsDetailsViewModel - ) -> NewsDetailsViewController + ) -> NewsPageViewController func makeMeetMainViewController( viewModel: PeopleViewModel diff --git a/NOICommunity/Factories/ViewModelFactory.swift b/NOICommunity/Factories/ViewModelFactory.swift index 32b534c..9475c0e 100644 --- a/NOICommunity/Factories/ViewModelFactory.swift +++ b/NOICommunity/Factories/ViewModelFactory.swift @@ -35,10 +35,14 @@ protocol ViewModelFactory { func makeMyAccountViewModel() -> MyAccountViewModel func makeNewsListViewModel() -> NewsListViewModel - - func makeNewsDetailsViewModel( - availableNews: Article? - ) -> NewsDetailsViewModel + + func makeNewsDetailsViewModel( + newsId: String + ) -> NewsDetailsViewModel + + func makeNewsDetailsViewModel( + news: Article + ) -> NewsDetailsViewModel func makeLoadUserInfoViewModel() -> LoadUserInfoViewModel diff --git a/NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift b/NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift index 8860f42..ac3f599 100644 --- a/NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift +++ b/NOICommunity/NewsFeature/Coordinators/NewsCoordinator.swift @@ -31,8 +31,8 @@ final class NewsCoordinator: BaseNavigationCoordinator { override func start(animated: Bool) { newsListViewModel = dependencyContainer.makeNewsListViewModel() - newsListViewModel.showDetailsHandler = { [weak self] in - self?.goToDetails(of: $0, sender: $1) + newsListViewModel.showDetailsHandler = { [weak self] news, _ in + self?.goToDetails(of: news) } mainVC = dependencyContainer.makeNewsViewController( viewModel: newsListViewModel @@ -45,40 +45,40 @@ final class NewsCoordinator: BaseNavigationCoordinator { private extension NewsCoordinator { - func goToDetails(of news: Article, sender: Any?) { + func goToDetails(of news: Article) { let viewModel = dependencyContainer.makeNewsDetailsViewModel( - availableNews: news + news: news ) - viewModel.showExternalLinkPublisher - .sink { [weak self] (news, sender) in - self?.showExternalLink(of: news, sender: sender) - } - .store(in: &subscriptions) - viewModel.showAskAQuestionPublisher - .sink { [weak self] (news, sender) in - self?.showAskAQuestion(for: news, sender: sender) - } - .store(in: &subscriptions) - - let detailVC = dependencyContainer.makeNewsDetailsViewController( - newsId: news.id, - viewModel: viewModel - ) - detailVC.navigationItem.title = localizedValue( - from: news.languageToDetails - )? - .title - detailVC.navigationItem.largeTitleDisplayMode = .never - navigationController.pushViewController(detailVC, animated: true) + let pageVC = { + let pageVC = dependencyContainer.makeNewsPageViewController( + viewModel: viewModel + ) + + pageVC.externalLinkActionHandler = { [weak self] in + self?.showExternalLink(of: $0) + } + pageVC.askQuestionActionHandler = { [weak self] in + self?.showAskAQuestion(for: $0) + } + + pageVC.navigationItem.title = localizedValue( + from: news.languageToDetails + )? + .title + pageVC.navigationItem.largeTitleDisplayMode = .never + + return pageVC + }() + navigationController.pushViewController(pageVC, animated: true) } - func showExternalLink(of news: Article, sender: Any?) { + func showExternalLink(of news: Article) { let author = localizedValue(from: news.languageToAuthor) let safariVC = SFSafariViewController(url: author!.externalURL!) navigationController.present(safariVC, animated: true) } - func showAskAQuestion(for news: Article, sender: Any?) { + func showAskAQuestion(for news: Article) { let author = localizedValue(from: news.languageToAuthor) navigationController.mailTo( author!.email!, diff --git a/NOICommunity/NewsFeature/View Controllers/NewsDetailsViewController.swift b/NOICommunity/NewsFeature/View Controllers/NewsDetailsViewController.swift index 635046c..fa93094 100644 --- a/NOICommunity/NewsFeature/View Controllers/NewsDetailsViewController.swift +++ b/NOICommunity/NewsFeature/View Controllers/NewsDetailsViewController.swift @@ -14,22 +14,15 @@ import Combine import ArticlesClient class NewsDetailsViewController: UIViewController { + + let news: Article + + var externalLinkActionHandler: ((Article) -> Void)? - let newsId: String - - let viewModel: NewsDetailsViewModel - - var externalLinkActionHandler: ((Article, Any?) -> Void)? - - var askQuestionActionHandler: ((Article, Any?) -> Void)? - - private var subscriptions: Set = [] - - private lazy var refreshControl: UIRefreshControl = { refreshControl in - scrollView.refreshControl = refreshControl - return refreshControl - }(UIRefreshControl()) - + var askQuestionActionHandler: ((Article) -> Void)? + + private var subscriptions: Set = [] + @IBOutlet private var scrollView: UIScrollView! @IBOutlet private var containerView: UIView! @@ -100,12 +93,11 @@ class NewsDetailsViewController: UIViewController { spacing: 17, placeholderImage: .image(withColor: .noiPlaceholderImageColor) ) - - init(newsId: String, viewModel: NewsDetailsViewModel) { - self.newsId = newsId - self.viewModel = viewModel - super.init(nibName: "\(NewsDetailsViewController.self)", bundle: nil) - } + + init(for item: Article) { + self.news = item + super.init(nibName: "\(NewsDetailsViewController.self)", bundle: nil) + } @available(*, unavailable) required init?(coder: NSCoder) { @@ -117,20 +109,6 @@ class NewsDetailsViewController: UIViewController { fatalError("\(#function) not available") } - override func viewDidLoad() { - super.viewDidLoad() - - configureBindings() - - refreshControl = .init() - - if let news = viewModel.result { - updateUI(news: news) - } else { - viewModel.refreshNewsDetails(newsId: newsId) - } - } - override func traitCollectionDidChange( _ previousTraitCollection: UITraitCollection? ) { @@ -163,7 +141,59 @@ class NewsDetailsViewController: UIViewController { override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } - + + override func viewDidLoad() { + super.viewDidLoad() + + configureBindings() + let author = localizedValue(from: news.languageToAuthor) + + imageView.kf.setImage(with: author?.logoURL) + + authorLabel.text = author?.name ?? .notDefined + + if author?.externalURL == nil { + externalLinkButton.removeFromSuperview() + } + if author?.email == nil { + askQuestionButton.removeFromSuperview() + } + if actionsStackView.subviews.isEmpty { + footerView.removeFromSuperview() + } + + publishedDateLabel.text = news.date + .flatMap { publishedDateFormatter.string(from: $0) } + + let details = localizedValue(from: news.languageToDetails) + titleLabel.text = details?.title + abstractLabel.text = details?.abstract + + textView.attributedText = details?.attributedText()? + .updatedFonts(usingTextStyle: .body) + + + if details?.text == nil { + textView.removeFromSuperview() + } + + if news.imageGallery.isNilOrEmpty { + galleryContainerView.removeFromSuperview() + } else { + galleryVC.imageURLs = news.imageGallery?.compactMap(\.url) ?? [] + if galleryVC.parent != self { + embedChild(galleryVC, in: galleryContainerView) + } + } + + if galleryTextStackView.subviews.isEmpty { + NSLayoutConstraint.deactivate(fullDetailConstraints) + NSLayoutConstraint.activate(shortDetailConstraints) + + galleryTextStackView.removeFromSuperview() + } + } + } // UITextViewDelegate @@ -188,107 +218,27 @@ private extension NewsDetailsViewController { func configureBindings() { externalLinkButton.publisher(for: .primaryActionTriggered) - .sink { [weak viewModel, externalLinkButton] in - viewModel?.showExternalLink(sender: externalLinkButton) - } - .store(in: &subscriptions) - - askQuestionButton.publisher(for: .primaryActionTriggered) - .sink { [weak viewModel, askQuestionButton] in - viewModel?.showAskAQuestion(sender: askQuestionButton) - } - .store(in: &subscriptions) - - refreshControl.publisher(for: .valueChanged) - .sink { [weak viewModel, newsId] in - viewModel?.refreshNewsDetails(newsId: newsId) - } - .store(in: &subscriptions) - - viewModel.$isLoading - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [unowned refreshControl] isLoading in - refreshControl.isLoading = isLoading - }) - .store(in: &subscriptions) - - viewModel.$result - .receive(on: DispatchQueue.main) .sink { [weak self] in - self?.updateUI(news: $0) + guard let self + else { return } + + self.externalLinkActionHandler?(self.news) } .store(in: &subscriptions) - viewModel.$error - .receive(on: DispatchQueue.main) - .sink { [weak self] error in - guard let error = error - else { return } - - self?.showError(error) - } + askQuestionButton.publisher(for: .primaryActionTriggered) + .sink { [weak self] in + guard let self + else { return } + + self.askQuestionActionHandler?(self.news) + } .store(in: &subscriptions) } - func updateUI(news: Article?) { - guard let news = news else { - containerView.isHidden = true - footerView.isHidden = true - return - } - - let author = localizedValue(from: news.languageToAuthor) - - imageView.kf.setImage(with: author?.logoURL) - - authorLabel.text = author?.name ?? .notDefined - - if author?.externalURL == nil { - externalLinkButton.removeFromSuperview() - } - if author?.email == nil { - askQuestionButton.removeFromSuperview() - } - if actionsStackView.subviews.isEmpty { - footerView.removeFromSuperview() - } - - publishedDateLabel.text = news.date - .flatMap { publishedDateFormatter.string(from: $0) } - - let details = localizedValue(from: news.languageToDetails) - titleLabel.text = details?.title - abstractLabel.text = details?.abstract - - textView.attributedText = details?.attributedText()? - .updatedFonts(usingTextStyle: .body) - - - if details?.text == nil { - textView.removeFromSuperview() - } - - if news.imageGallery.isNilOrEmpty { - galleryContainerView.removeFromSuperview() - } else { - galleryVC.imageURLs = news.imageGallery?.compactMap(\.url) ?? [] - if galleryVC.parent != self { - embedChild(galleryVC, in: galleryContainerView) - } - } - - if galleryTextStackView.subviews.isEmpty { - NSLayoutConstraint.deactivate(fullDetailConstraints) - NSLayoutConstraint.activate(shortDetailConstraints) - - galleryTextStackView.removeFromSuperview() - } - - containerView.isHidden = false - footerView.isHidden = false - } - - func preferredContentSizeCategoryDidChange(previousPreferredContentSizeCategory: UIContentSizeCategory?) { + func preferredContentSizeCategoryDidChange( + previousPreferredContentSizeCategory: UIContentSizeCategory? + ) { textView.attributedText = textView.attributedText?.updatedFonts(usingTextStyle: .body) } diff --git a/NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift b/NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift new file mode 100644 index 0000000..3ed03f6 --- /dev/null +++ b/NOICommunity/NewsFeature/View Controllers/NewsPageViewController.swift @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: NOI Techpark +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +// +// NewsPageViewController.swift +// NOICommunity +// +// Created by Matteo Matassoni on 05/12/24. +// + +import UIKit +import CoreUI +import ArticlesClient + +// MARK: - NewsPageViewController + +final class NewsPageViewController: BasePageViewController { + + private lazy var containerViewController = ContainerViewController() + + private var newsDetailsViewController: NewsDetailsViewController? { + children + .lazy + .compactMap { $0 as? NewsDetailsViewController } + .first + } + + var externalLinkActionHandler: ((Article) -> Void)? { + didSet { + newsDetailsViewController?.externalLinkActionHandler = externalLinkActionHandler + } + } + + var askQuestionActionHandler: ((Article) -> Void)? { + didSet { + newsDetailsViewController?.askQuestionActionHandler = askQuestionActionHandler + } + } + + override func configureBindings() { + super.configureBindings() + + viewModel.$isLoading + .receive(on: DispatchQueue.main) + .sink { [weak self] isLoading in + if isLoading { + self?.show(content: LoadingViewController()) + } + } + .store(in: &subscriptions) + + viewModel.$result + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [weak self] event in + guard let self + else { return } + + self.show(content: self.makeResultContent(for: event)) + } + .store(in: &subscriptions) + + viewModel.$error + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [weak self] error in + self?.showError(error) + } + .store(in: &subscriptions) + } + + override func configureLayout() { + super.configureLayout() + + embedChild(containerViewController) + } + + +} + +// MARK: Private APIs + +private extension NewsPageViewController { + + func show(content: UIViewController) { + containerViewController.content = content + } + + func makeResultContent(for news: Article) -> NewsDetailsViewController { + let result = NewsDetailsViewController(for: news) + result.externalLinkActionHandler = externalLinkActionHandler + result.askQuestionActionHandler = askQuestionActionHandler + return result + } + +} diff --git a/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift b/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift index 724c820..2eb09b0 100644 --- a/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift +++ b/NOICommunity/NewsFeature/View Models/NewsDetailsViewModel.swift @@ -10,59 +10,68 @@ // import Foundation +import CoreUI import Combine import ArticlesClient // MARK: - NewsDetailsViewModel -final class NewsDetailsViewModel { - +final class NewsDetailsViewModel: BasePageViewModel { + let articlesClient: ArticlesClient - let language: Language? - + let newsId: String + @Published private(set) var isLoading = false @Published private(set) var error: Error! @Published private(set) var result: Article! - private var showExternalLinkSubject: PassthroughSubject<(Article, Any?), Never> = .init() - lazy var showExternalLinkPublisher = showExternalLinkSubject - .eraseToAnyPublisher() - - private var showAskAQuestionSubject: PassthroughSubject<(Article, Any?), Never> = .init() - lazy var showAskAQuestionPublisher = showAskAQuestionSubject - .eraseToAnyPublisher() - init( articlesClient: ArticlesClient, - availableNews: Article?, - language: Language? + news: Article ) { self.articlesClient = articlesClient - self.result = availableNews - self.language = language + self.newsId = news.id + self.result = news + + super.init() } - - func refreshNewsDetails(newsId: String) { + + init( + articlesClient: ArticlesClient, + newsId: String + ) { + self.articlesClient = articlesClient + self.newsId = newsId + + super.init() + } + + @available(*, unavailable) + required init() { + fatalError("\(#function) not available") + } + + func fetchNews(with newsId: String) { Task(priority: .userInitiated) { [weak self] in - await self?.performRefreshNewsDetails(newsId: newsId) + await self?.performFetchNews(with: newsId) } } - - func showExternalLink(sender: Any?) { - showExternalLinkSubject.send((result, sender)) - } - func showAskAQuestion(sender: Any?) { - showAskAQuestionSubject.send((result, sender)) - } - + override func onViewDidLoad() { + super.onViewDidLoad() + + if result == nil { + fetchNews(with: newsId) + } + } + } // MARK: Private APIs private extension NewsDetailsViewModel { - func performRefreshNewsDetails(newsId: String) async { + func performFetchNews(with newsId: String) async { isLoading = true defer { isLoading = false diff --git a/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift b/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift index 97487f4..8c04322 100644 --- a/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift +++ b/NOICommunityLib/Sources/ArticlesClient/Interfaces/ArticlesClient.swift @@ -12,16 +12,34 @@ import Foundation public protocol ArticlesClient { - + func getArticleList( startDate: Date?, publishedOn: String?, pageSize: Int?, pageNumber: Int? ) async throws -> ArticleListResponse - + func getArticle(newsId: String) async throws -> Article + +} +public extension ArticlesClient { + + func getArticleList( + startDate: Date? = nil, + publishedOn: String? = nil, + pageSize: Int? = nil, + pageNumber: Int? = nil + ) async throws -> ArticleListResponse { + try await getArticleList( + startDate: startDate, + publishedOn: publishedOn, + pageSize: pageSize, + pageNumber: pageNumber + ) + } + }