metatext-app-ios-iphone-ipad/View Controllers/TableViewController.swift

229 lines
8.8 KiB
Swift
Raw Normal View History

2020-08-21 04:29:01 +02:00
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
2020-09-15 03:39:35 +02:00
import SafariServices
2020-09-05 04:31:43 +02:00
import SwiftUI
2020-09-01 09:33:49 +02:00
import ViewModels
2020-08-21 04:29:01 +02:00
2020-09-27 03:44:33 +02:00
class TableViewController: UITableViewController {
2020-09-23 03:00:56 +02:00
private let viewModel: CollectionViewModel
2020-08-29 00:39:17 +02:00
private let loadingTableFooterView = LoadingTableFooterView()
2020-09-26 08:37:30 +02:00
private let webfingerIndicatorView = WebfingerIndicatorView()
2020-08-21 04:29:01 +02:00
private var cancellables = Set<AnyCancellable>()
2020-09-23 03:00:56 +02:00
private var cellHeightCaches = [CGFloat: [CollectionItem: CGFloat]]()
2020-09-02 11:07:09 +02:00
private let dataSourceQueue =
2020-09-23 03:00:56 +02:00
DispatchQueue(label: "com.metabolist.metatext.collection.data-source-queue")
private lazy var dataSource: UITableViewDiffableDataSource<Int, CollectionItem> = {
UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, item in
guard let self = self, let cellViewModel = self.viewModel.viewModel(item: item) else { return nil }
let cell = tableView.dequeueReusableCell(
withIdentifier: String(describing: item.kind.cellClass),
for: indexPath)
switch (cell, cellViewModel) {
case (let statusListCell as StatusListCell, let statusViewModel as StatusViewModel):
statusListCell.viewModel = statusViewModel
case (let accountListCell as AccountListCell, let accountViewModel as AccountViewModel):
accountListCell.viewModel = accountViewModel
default:
return nil
}
2020-08-21 04:29:01 +02:00
return cell
}
}()
2020-09-23 03:00:56 +02:00
init(viewModel: CollectionViewModel) {
2020-08-21 04:29:01 +02:00
self.viewModel = viewModel
super.init(style: .plain)
}
2020-08-29 00:39:17 +02:00
@available(*, unavailable)
2020-08-21 04:29:01 +02:00
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
2020-09-23 03:00:56 +02:00
for kind in CollectionItem.Kind.allCases {
tableView.register(kind.cellClass, forCellReuseIdentifier: String(describing: kind.cellClass))
}
2020-08-21 04:29:01 +02:00
tableView.dataSource = dataSource
2020-08-29 00:39:17 +02:00
tableView.prefetchDataSource = self
2020-08-21 04:29:01 +02:00
tableView.cellLayoutMarginsFollowReadableWidth = true
2020-08-29 00:39:17 +02:00
tableView.tableFooterView = UIView()
2020-08-21 04:29:01 +02:00
2020-09-26 08:37:30 +02:00
view.addSubview(webfingerIndicatorView)
webfingerIndicatorView.translatesAutoresizingMaskIntoConstraints = false
2020-09-15 01:32:34 +02:00
2020-09-26 08:37:30 +02:00
NSLayoutConstraint.activate([
webfingerIndicatorView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
webfingerIndicatorView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor)
])
2020-08-29 00:39:17 +02:00
2020-09-26 08:37:30 +02:00
setupViewModelBindings()
2020-08-21 04:29:01 +02:00
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
2020-09-23 03:00:56 +02:00
viewModel.request(maxID: nil, minID: nil)
2020-08-21 04:29:01 +02:00
}
override func tableView(_ tableView: UITableView,
willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
2020-09-23 03:00:56 +02:00
var heightCache = cellHeightCaches[tableView.frame.width] ?? [CollectionItem: CGFloat]()
2020-08-21 04:29:01 +02:00
heightCache[item] = cell.frame.height
cellHeightCaches[tableView.frame.width] = heightCache
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension }
return cellHeightCaches[tableView.frame.width]?[item] ?? UITableView.automaticDimension
}
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
2020-09-23 03:00:56 +02:00
guard let item = dataSource.itemIdentifier(for: indexPath) else { return true }
2020-08-26 10:25:34 +02:00
2020-09-23 03:00:56 +02:00
return viewModel.canSelect(item: item)
2020-08-21 04:29:01 +02:00
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
2020-09-23 03:00:56 +02:00
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
2020-08-21 04:29:01 +02:00
2020-09-23 03:00:56 +02:00
viewModel.itemSelected(item)
2020-08-21 04:29:01 +02:00
}
2020-08-29 00:39:17 +02:00
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
sizeTableHeaderFooterViews()
}
}
2020-09-27 03:44:33 +02:00
extension TableViewController: UITableViewDataSourcePrefetching {
2020-08-29 00:39:17 +02:00
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
guard
2020-09-24 03:33:13 +02:00
let maxID = viewModel.nextPageMaxID,
2020-08-29 00:39:17 +02:00
let indexPath = indexPaths.last,
indexPath.section == dataSource.numberOfSections(in: tableView) - 1,
2020-09-24 03:33:13 +02:00
indexPath.row == dataSource.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1
2020-08-29 00:39:17 +02:00
else { return }
2020-09-23 03:00:56 +02:00
viewModel.request(maxID: maxID, minID: nil)
2020-08-29 00:39:17 +02:00
}
2020-08-21 04:29:01 +02:00
}
2020-09-27 07:54:06 +02:00
extension TableViewController {
func sizeTableHeaderFooterViews() {
// https://useyourloaf.com/blog/variable-height-table-view-header/
if let headerView = tableView.tableHeaderView {
let size = headerView.systemLayoutSizeFitting(
CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
view.insertSubview(webfingerIndicatorView, aboveSubview: headerView)
}
if let footerView = tableView.tableFooterView {
let size = footerView.systemLayoutSizeFitting(
CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel)
if footerView.frame.size.height != size.height {
footerView.frame.size.height = size.height
tableView.tableFooterView = footerView
tableView.layoutIfNeeded()
}
}
}
}
2020-09-27 03:44:33 +02:00
private extension TableViewController {
2020-09-26 08:37:30 +02:00
func setupViewModelBindings() {
viewModel.title.sink { [weak self] in self?.navigationItem.title = $0 }.store(in: &cancellables)
2020-09-27 07:54:06 +02:00
viewModel.collectionItems.sink { [weak self] in self?.update(items: $0) }.store(in: &cancellables)
2020-09-26 08:37:30 +02:00
viewModel.navigationEvents.receive(on: DispatchQueue.main).sink { [weak self] in
guard let self = self else { return }
switch $0 {
case let .share(url):
self.share(url: url)
2020-09-27 07:54:06 +02:00
case let .collectionNavigation(viewModel):
self.show(TableViewController(viewModel: viewModel), sender: self)
case let .profileNavigation(viewModel):
self.show(ProfileViewController(viewModel: viewModel), sender: self)
2020-09-26 08:37:30 +02:00
case let .urlNavigation(url):
self.present(SFSafariViewController(url: url), animated: true)
case .webfingerStart:
self.webfingerIndicatorView.startAnimating()
case .webfingerEnd:
self.webfingerIndicatorView.stopAnimating()
}
}
.store(in: &cancellables)
2020-09-27 07:54:06 +02:00
viewModel.loading.receive(on: RunLoop.main).sink { [weak self] in
guard let self = self else { return }
2020-09-26 08:37:30 +02:00
2020-09-27 07:54:06 +02:00
self.tableView.tableFooterView = $0 ? self.loadingTableFooterView : UIView()
self.sizeTableHeaderFooterViews()
2020-09-26 08:37:30 +02:00
}
2020-09-27 07:54:06 +02:00
.store(in: &cancellables)
2020-09-26 08:37:30 +02:00
}
2020-09-23 03:00:56 +02:00
func update(items: [[CollectionItem]]) {
2020-09-15 07:41:09 +02:00
var offsetFromNavigationBar: CGFloat?
if
2020-09-23 03:00:56 +02:00
let item = viewModel.maintainScrollPositionOfItem,
let indexPath = dataSource.indexPath(for: item),
2020-09-15 07:41:09 +02:00
let navigationBar = navigationController?.navigationBar {
let navigationBarMaxY = tableView.convert(navigationBar.bounds, from: navigationBar).maxY
offsetFromNavigationBar = tableView.rectForRow(at: indexPath).origin.y - navigationBarMaxY
}
dataSourceQueue.async { [weak self] in
guard let self = self else { return }
2020-09-23 03:00:56 +02:00
self.dataSource.apply(items.snapshot(), animatingDifferences: false) {
2020-09-15 07:41:09 +02:00
if
2020-09-23 03:00:56 +02:00
let item = self.viewModel.maintainScrollPositionOfItem,
let indexPath = self.dataSource.indexPath(for: item) {
2020-09-15 07:41:09 +02:00
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
if let offsetFromNavigationBar = offsetFromNavigationBar {
self.tableView.contentOffset.y -= offsetFromNavigationBar
}
}
}
}
}
2020-09-15 01:32:34 +02:00
func share(url: URL) {
let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
present(activityViewController, animated: true, completion: nil)
}
2020-08-21 04:29:01 +02:00
}