metatext/View Controllers/StatusListViewController.swift

215 lines
8 KiB
Swift
Raw Normal View History

2020-08-21 02:29:01 +00:00
// Copyright © 2020 Metabolist. All rights reserved.
import Combine
2020-09-05 02:31:43 +00:00
import SwiftUI
2020-09-01 07:33:49 +00:00
import ViewModels
2020-08-21 02:29:01 +00:00
class StatusListViewController: UITableViewController {
2020-08-26 21:20:44 +00:00
private let viewModel: StatusListViewModel
2020-08-28 22:39:17 +00:00
private let loadingTableFooterView = LoadingTableFooterView()
2020-08-21 02:29:01 +00:00
private var cancellables = Set<AnyCancellable>()
2020-08-26 08:25:34 +00:00
private var cellHeightCaches = [CGFloat: [String: CGFloat]]()
2020-09-02 09:07:09 +00:00
private let dataSourceQueue =
DispatchQueue(label: "com.metabolist.metatext.status-list.data-source-queue")
2020-08-21 02:29:01 +00:00
2020-08-26 08:25:34 +00:00
private lazy var dataSource: UITableViewDiffableDataSource<Int, String> = {
UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, statusID in
2020-08-21 02:29:01 +00:00
guard
let self = self,
let cell = tableView.dequeueReusableCell(
withIdentifier: String(describing: StatusTableViewCell.self),
for: indexPath) as? StatusTableViewCell
else { return nil }
2020-08-26 08:25:34 +00:00
cell.viewModel = self.viewModel.statusViewModel(id: statusID)
2020-08-21 02:29:01 +00:00
cell.delegate = self
return cell
}
}()
2020-08-26 21:20:44 +00:00
init(viewModel: StatusListViewModel) {
2020-08-21 02:29:01 +00:00
self.viewModel = viewModel
super.init(style: .plain)
}
2020-08-28 22:39:17 +00:00
@available(*, unavailable)
2020-08-21 02:29:01 +00:00
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
for cellClass in [StatusTableViewCell.self] {
let classString = String(describing: cellClass)
tableView.register(
UINib(nibName: classString, bundle: nil),
forCellReuseIdentifier: classString)
}
tableView.dataSource = dataSource
2020-08-28 22:39:17 +00:00
tableView.prefetchDataSource = self
2020-08-21 02:29:01 +00:00
tableView.cellLayoutMarginsFollowReadableWidth = true
tableView.separatorInset = .zero
2020-08-28 22:39:17 +00:00
tableView.tableFooterView = UIView()
2020-08-21 02:29:01 +00:00
2020-08-26 08:25:34 +00:00
viewModel.$statusIDs
2020-09-02 09:07:09 +00:00
.sink { [weak self] statusIDs in
2020-08-23 08:38:39 +00:00
guard let self = self else { return }
var offsetFromNavigationBar: CGFloat?
if
let id = self.viewModel.maintainScrollPositionOfStatusID,
2020-08-26 08:25:34 +00:00
let indexPath = self.dataSource.indexPath(for: id),
2020-08-23 08:38:39 +00:00
let navigationBar = self.navigationController?.navigationBar {
let navigationBarMaxY = self.tableView.convert(navigationBar.bounds, from: navigationBar).maxY
offsetFromNavigationBar = self.tableView.rectForRow(at: indexPath).origin.y - navigationBarMaxY
}
2020-09-02 09:07:09 +00:00
self.dataSourceQueue.async {
self.dataSource.apply(statusIDs.snapshot(), animatingDifferences: false) {
if
let id = self.viewModel.maintainScrollPositionOfStatusID,
let indexPath = self.dataSource.indexPath(for: id) {
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
2020-09-02 07:39:42 +00:00
2020-09-02 09:07:09 +00:00
if let offsetFromNavigationBar = offsetFromNavigationBar {
self.tableView.contentOffset.y -= offsetFromNavigationBar
}
2020-09-02 07:39:42 +00:00
}
2020-08-23 08:38:39 +00:00
}
}
2020-08-21 02:29:01 +00:00
}
.store(in: &cancellables)
2020-08-28 22:39:17 +00:00
viewModel.$loading
.receive(on: RunLoop.main)
.sink { [weak self] in
guard let self = self else { return }
self.tableView.tableFooterView = $0 ? self.loadingTableFooterView : UIView()
self.sizeTableHeaderFooterViews()
}
.store(in: &cancellables)
2020-08-21 02:29:01 +00:00
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
viewModel.request()
}
override func tableView(_ tableView: UITableView,
willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
2020-08-26 08:25:34 +00:00
var heightCache = cellHeightCaches[tableView.frame.width] ?? [String: CGFloat]()
2020-08-21 02:29:01 +00: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-08-26 08:25:34 +00:00
guard let id = dataSource.itemIdentifier(for: indexPath) else { return true }
return id != viewModel.contextParentID
2020-08-21 02:29:01 +00:00
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
2020-09-02 09:07:09 +00:00
guard let id = dataSource.itemIdentifier(for: indexPath) else { return }
2020-08-21 02:29:01 +00:00
2020-09-04 17:27:25 +00:00
show(StatusListViewController(viewModel: viewModel.contextViewModel(id: id)), sender: self)
2020-08-21 02:29:01 +00:00
}
2020-08-28 22:39:17 +00:00
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
sizeTableHeaderFooterViews()
}
}
extension StatusListViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
guard
viewModel.paginates,
let indexPath = indexPaths.last,
indexPath.section == dataSource.numberOfSections(in: tableView) - 1,
indexPath.row == dataSource.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1,
let maxID = dataSource.itemIdentifier(for: indexPath)
else { return }
viewModel.request(maxID: maxID)
}
2020-08-21 02:29:01 +00:00
}
extension StatusListViewController: StatusTableViewCellDelegate {
func statusTableViewCellDidHaveShareButtonTapped(_ cell: StatusTableViewCell) {
2020-08-26 08:25:34 +00:00
guard let url = cell.viewModel?.sharingURL else { return }
2020-08-21 02:29:01 +00:00
share(url: url)
}
}
private extension StatusListViewController {
func share(url: URL) {
let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
present(activityViewController, animated: true, completion: nil)
}
2020-08-28 22:39:17 +00:00
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()
}
}
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-08-21 02:29:01 +00:00
}
2020-08-26 08:25:34 +00:00
private extension Array where Element: Sequence, Element.Element: Hashable {
func snapshot() -> NSDiffableDataSourceSnapshot<Int, Element.Element> {
var snapshot = NSDiffableDataSourceSnapshot<Int, Element.Element>()
2020-08-21 02:29:01 +00:00
let sections = [Int](0..<count)
snapshot.appendSections(sections)
for section in sections {
2020-08-26 08:25:34 +00:00
snapshot.appendItems(self[section].map { $0 }, toSection: section)
2020-08-21 02:29:01 +00:00
}
return snapshot
}
}