DiscoveryNewsViewController.swift 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. //
  2. // DiscoveryNewsViewController.swift
  3. // Mastodon
  4. //
  5. // Created by MainasuK on 2022-4-13.
  6. //
  7. import os.log
  8. import UIKit
  9. import Combine
  10. import MastodonCore
  11. import MastodonUI
  12. final class DiscoveryNewsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
  13. let logger = Logger(subsystem: "TrendPostsViewController", category: "ViewController")
  14. weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
  15. weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
  16. var disposeBag = Set<AnyCancellable>()
  17. var viewModel: DiscoveryNewsViewModel!
  18. let mediaPreviewTransitionController = MediaPreviewTransitionController()
  19. lazy var tableView: UITableView = {
  20. let tableView = UITableView()
  21. tableView.rowHeight = UITableView.automaticDimension
  22. tableView.estimatedRowHeight = 100
  23. tableView.separatorStyle = .none
  24. tableView.backgroundColor = .clear
  25. return tableView
  26. }()
  27. let refreshControl = RefreshControl()
  28. deinit {
  29. os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
  30. }
  31. }
  32. extension DiscoveryNewsViewController {
  33. override func viewDidLoad() {
  34. super.viewDidLoad()
  35. view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
  36. ThemeService.shared.currentTheme
  37. .receive(on: DispatchQueue.main)
  38. .sink { [weak self] theme in
  39. guard let self = self else { return }
  40. self.view.backgroundColor = theme.secondarySystemBackgroundColor
  41. }
  42. .store(in: &disposeBag)
  43. tableView.translatesAutoresizingMaskIntoConstraints = false
  44. view.addSubview(tableView)
  45. NSLayoutConstraint.activate([
  46. tableView.topAnchor.constraint(equalTo: view.topAnchor),
  47. tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  48. tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  49. tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  50. ])
  51. tableView.delegate = self
  52. viewModel.setupDiffableDataSource(
  53. tableView: tableView
  54. )
  55. tableView.refreshControl = refreshControl
  56. refreshControl.addTarget(self, action: #selector(DiscoveryNewsViewController.refreshControlValueChanged(_:)), for: .valueChanged)
  57. viewModel.didLoadLatest
  58. .receive(on: DispatchQueue.main)
  59. .sink { [weak self] _ in
  60. guard let self = self else { return }
  61. self.refreshControl.endRefreshing()
  62. }
  63. .store(in: &disposeBag)
  64. // setup batch fetch
  65. viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
  66. viewModel.listBatchFetchViewModel.shouldFetch
  67. .receive(on: DispatchQueue.main)
  68. .sink { [weak self] _ in
  69. guard let self = self else { return }
  70. guard self.view.window != nil else { return }
  71. self.viewModel.stateMachine.enter(DiscoveryNewsViewModel.State.Loading.self)
  72. }
  73. .store(in: &disposeBag)
  74. }
  75. override func viewWillAppear(_ animated: Bool) {
  76. super.viewWillAppear(animated)
  77. refreshControl.endRefreshing()
  78. tableView.deselectRow(with: transitionCoordinator, animated: animated)
  79. }
  80. }
  81. extension DiscoveryNewsViewController {
  82. @objc private func refreshControlValueChanged(_ sender: RefreshControl) {
  83. guard viewModel.stateMachine.enter(DiscoveryNewsViewModel.State.Reloading.self) else {
  84. sender.endRefreshing()
  85. return
  86. }
  87. }
  88. }
  89. // MARK: - UITableViewDelegate
  90. extension DiscoveryNewsViewController: UITableViewDelegate {
  91. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  92. logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(indexPath)")
  93. guard case let .link(link) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
  94. guard let url = URL(string: link.url) else { return }
  95. coordinator.present(
  96. scene: .safari(url: url),
  97. from: self,
  98. transition: .safariPresent(animated: true, completion: nil)
  99. )
  100. }
  101. }
  102. // MARK: ScrollViewContainer
  103. extension DiscoveryNewsViewController: ScrollViewContainer {
  104. var scrollView: UIScrollView { tableView }
  105. }
  106. extension DiscoveryNewsViewController {
  107. override var keyCommands: [UIKeyCommand]? {
  108. return navigationKeyCommands
  109. }
  110. }
  111. extension DiscoveryNewsViewController: TableViewControllerNavigateable {
  112. func navigate(direction: TableViewNavigationDirection) {
  113. if let indexPathForSelectedRow = tableView.indexPathForSelectedRow {
  114. // navigate up/down on the current selected item
  115. navigateToLink(direction: direction, indexPath: indexPathForSelectedRow)
  116. } else {
  117. // set first visible item selected
  118. navigateToFirstVisibleLink()
  119. }
  120. }
  121. private func navigateToLink(direction: TableViewNavigationDirection, indexPath: IndexPath) {
  122. guard let diffableDataSource = viewModel.diffableDataSource else { return }
  123. let items = diffableDataSource.snapshot().itemIdentifiers
  124. guard let selectedItem = diffableDataSource.itemIdentifier(for: indexPath),
  125. let selectedItemIndex = items.firstIndex(of: selectedItem) else {
  126. return
  127. }
  128. let _navigateToItem: DiscoveryItem? = {
  129. var index = selectedItemIndex
  130. while 0..<items.count ~= index {
  131. index = {
  132. switch direction {
  133. case .up: return index - 1
  134. case .down: return index + 1
  135. }
  136. }()
  137. guard 0..<items.count ~= index else { return nil }
  138. let item = items[index]
  139. guard Self.validNavigateableItem(item) else { continue }
  140. return item
  141. }
  142. return nil
  143. }()
  144. guard let item = _navigateToItem, let indexPath = diffableDataSource.indexPath(for: item) else { return }
  145. let scrollPosition: UITableView.ScrollPosition = overrideNavigationScrollPosition ?? Self.navigateScrollPosition(tableView: tableView, indexPath: indexPath)
  146. tableView.selectRow(at: indexPath, animated: true, scrollPosition: scrollPosition)
  147. }
  148. private func navigateToFirstVisibleLink() {
  149. guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows else { return }
  150. guard let diffableDataSource = viewModel.diffableDataSource else { return }
  151. var visibleItems: [DiscoveryItem] = indexPathsForVisibleRows.sorted().compactMap { indexPath in
  152. guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil }
  153. guard Self.validNavigateableItem(item) else { return nil }
  154. return item
  155. }
  156. if indexPathsForVisibleRows.first?.row != 0, visibleItems.count > 1 {
  157. // drop first when visible not the first cell of table
  158. visibleItems.removeFirst()
  159. }
  160. guard let item = visibleItems.first, let indexPath = diffableDataSource.indexPath(for: item) else { return }
  161. let scrollPosition: UITableView.ScrollPosition = overrideNavigationScrollPosition ?? Self.navigateScrollPosition(tableView: tableView, indexPath: indexPath)
  162. tableView.selectRow(at: indexPath, animated: true, scrollPosition: scrollPosition)
  163. }
  164. static func validNavigateableItem(_ item: DiscoveryItem) -> Bool {
  165. switch item {
  166. case .link:
  167. return true
  168. default:
  169. return false
  170. }
  171. }
  172. func open() {
  173. guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
  174. guard let diffableDataSource = viewModel.diffableDataSource else { return }
  175. guard let item = diffableDataSource.itemIdentifier(for: indexPathForSelectedRow) else { return }
  176. guard case let .link(link) = item else { return }
  177. guard let url = URL(string: link.url) else { return }
  178. coordinator.present(
  179. scene: .safari(url: url),
  180. from: self,
  181. transition: .safariPresent(animated: true, completion: nil)
  182. )
  183. }
  184. func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
  185. navigateKeyCommandHandler(sender)
  186. }
  187. }