123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- //
- // DiscoveryNewsViewController.swift
- // Mastodon
- //
- // Created by MainasuK on 2022-4-13.
- //
- import os.log
- import UIKit
- import Combine
- import MastodonCore
- import MastodonUI
- final class DiscoveryNewsViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
-
- let logger = Logger(subsystem: "TrendPostsViewController", category: "ViewController")
-
- weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
- weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
-
- var disposeBag = Set<AnyCancellable>()
- var viewModel: DiscoveryNewsViewModel!
-
- let mediaPreviewTransitionController = MediaPreviewTransitionController()
- lazy var tableView: UITableView = {
- let tableView = UITableView()
- tableView.rowHeight = UITableView.automaticDimension
- tableView.estimatedRowHeight = 100
- tableView.separatorStyle = .none
- tableView.backgroundColor = .clear
- return tableView
- }()
-
- let refreshControl = RefreshControl()
-
- deinit {
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- }
-
- }
- extension DiscoveryNewsViewController {
- override func viewDidLoad() {
- super.viewDidLoad()
-
- view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
- ThemeService.shared.currentTheme
- .receive(on: DispatchQueue.main)
- .sink { [weak self] theme in
- guard let self = self else { return }
- self.view.backgroundColor = theme.secondarySystemBackgroundColor
- }
- .store(in: &disposeBag)
-
- tableView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(tableView)
- NSLayoutConstraint.activate([
- tableView.topAnchor.constraint(equalTo: view.topAnchor),
- tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- ])
- tableView.delegate = self
- viewModel.setupDiffableDataSource(
- tableView: tableView
- )
-
- tableView.refreshControl = refreshControl
- refreshControl.addTarget(self, action: #selector(DiscoveryNewsViewController.refreshControlValueChanged(_:)), for: .valueChanged)
- viewModel.didLoadLatest
- .receive(on: DispatchQueue.main)
- .sink { [weak self] _ in
- guard let self = self else { return }
- self.refreshControl.endRefreshing()
- }
- .store(in: &disposeBag)
-
- // setup batch fetch
- viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
- viewModel.listBatchFetchViewModel.shouldFetch
- .receive(on: DispatchQueue.main)
- .sink { [weak self] _ in
- guard let self = self else { return }
- guard self.view.window != nil else { return }
- self.viewModel.stateMachine.enter(DiscoveryNewsViewModel.State.Loading.self)
- }
- .store(in: &disposeBag)
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
-
- refreshControl.endRefreshing()
- tableView.deselectRow(with: transitionCoordinator, animated: animated)
- }
- }
- extension DiscoveryNewsViewController {
-
- @objc private func refreshControlValueChanged(_ sender: RefreshControl) {
- guard viewModel.stateMachine.enter(DiscoveryNewsViewModel.State.Reloading.self) else {
- sender.endRefreshing()
- return
- }
- }
-
- }
- // MARK: - UITableViewDelegate
- extension DiscoveryNewsViewController: UITableViewDelegate {
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(indexPath)")
- guard case let .link(link) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
- guard let url = URL(string: link.url) else { return }
- coordinator.present(
- scene: .safari(url: url),
- from: self,
- transition: .safariPresent(animated: true, completion: nil)
- )
- }
- }
- // MARK: ScrollViewContainer
- extension DiscoveryNewsViewController: ScrollViewContainer {
- var scrollView: UIScrollView { tableView }
- }
- extension DiscoveryNewsViewController {
- override var keyCommands: [UIKeyCommand]? {
- return navigationKeyCommands
- }
- }
- extension DiscoveryNewsViewController: TableViewControllerNavigateable {
-
- func navigate(direction: TableViewNavigationDirection) {
- if let indexPathForSelectedRow = tableView.indexPathForSelectedRow {
- // navigate up/down on the current selected item
- navigateToLink(direction: direction, indexPath: indexPathForSelectedRow)
- } else {
- // set first visible item selected
- navigateToFirstVisibleLink()
- }
- }
-
- private func navigateToLink(direction: TableViewNavigationDirection, indexPath: IndexPath) {
- guard let diffableDataSource = viewModel.diffableDataSource else { return }
- let items = diffableDataSource.snapshot().itemIdentifiers
- guard let selectedItem = diffableDataSource.itemIdentifier(for: indexPath),
- let selectedItemIndex = items.firstIndex(of: selectedItem) else {
- return
- }
- let _navigateToItem: DiscoveryItem? = {
- var index = selectedItemIndex
- while 0..<items.count ~= index {
- index = {
- switch direction {
- case .up: return index - 1
- case .down: return index + 1
- }
- }()
- guard 0..<items.count ~= index else { return nil }
- let item = items[index]
-
- guard Self.validNavigateableItem(item) else { continue }
- return item
- }
- return nil
- }()
-
- guard let item = _navigateToItem, let indexPath = diffableDataSource.indexPath(for: item) else { return }
- let scrollPosition: UITableView.ScrollPosition = overrideNavigationScrollPosition ?? Self.navigateScrollPosition(tableView: tableView, indexPath: indexPath)
- tableView.selectRow(at: indexPath, animated: true, scrollPosition: scrollPosition)
- }
-
- private func navigateToFirstVisibleLink() {
- guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows else { return }
- guard let diffableDataSource = viewModel.diffableDataSource else { return }
-
- var visibleItems: [DiscoveryItem] = indexPathsForVisibleRows.sorted().compactMap { indexPath in
- guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil }
- guard Self.validNavigateableItem(item) else { return nil }
- return item
- }
- if indexPathsForVisibleRows.first?.row != 0, visibleItems.count > 1 {
- // drop first when visible not the first cell of table
- visibleItems.removeFirst()
- }
- guard let item = visibleItems.first, let indexPath = diffableDataSource.indexPath(for: item) else { return }
- let scrollPosition: UITableView.ScrollPosition = overrideNavigationScrollPosition ?? Self.navigateScrollPosition(tableView: tableView, indexPath: indexPath)
- tableView.selectRow(at: indexPath, animated: true, scrollPosition: scrollPosition)
- }
-
- static func validNavigateableItem(_ item: DiscoveryItem) -> Bool {
- switch item {
- case .link:
- return true
- default:
- return false
- }
- }
-
- func open() {
- guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
- guard let diffableDataSource = viewModel.diffableDataSource else { return }
- guard let item = diffableDataSource.itemIdentifier(for: indexPathForSelectedRow) else { return }
-
- guard case let .link(link) = item else { return }
- guard let url = URL(string: link.url) else { return }
- coordinator.present(
- scene: .safari(url: url),
- from: self,
- transition: .safariPresent(animated: true, completion: nil)
- )
- }
-
- func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
- navigateKeyCommandHandler(sender)
- }
- }
|