Refactoring
This commit is contained in:
parent
9e40c45b0f
commit
e74daa4df0
|
@ -4,10 +4,11 @@ import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class StatusesViewModel: ObservableObject {
|
class StatusesViewModel: ObservableObject {
|
||||||
@Published private(set) var statusSections = [[Status]]()
|
@Published private(set) var statusIDs = [[String]]()
|
||||||
@Published var alertItem: AlertItem?
|
@Published var alertItem: AlertItem?
|
||||||
@Published private(set) var loading = false
|
@Published private(set) var loading = false
|
||||||
private(set) var maintainScrollPositionOfStatusID: String?
|
private(set) var maintainScrollPositionOfStatusID: String?
|
||||||
|
private var statuses = [String: Status]()
|
||||||
private let statusListService: StatusListService
|
private let statusListService: StatusListService
|
||||||
private var statusViewModelCache = [Status: (StatusViewModel, AnyCancellable)]()
|
private var statusViewModelCache = [Status: (StatusViewModel, AnyCancellable)]()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
@ -19,9 +20,11 @@ class StatusesViewModel: ObservableObject {
|
||||||
.handleEvents(receiveOutput: { [weak self] in
|
.handleEvents(receiveOutput: { [weak self] in
|
||||||
self?.determineIfScrollPositionShouldBeMaintained(newStatusSections: $0)
|
self?.determineIfScrollPositionShouldBeMaintained(newStatusSections: $0)
|
||||||
self?.cleanViewModelCache(newStatusSections: $0)
|
self?.cleanViewModelCache(newStatusSections: $0)
|
||||||
|
self?.statuses = Dictionary(uniqueKeysWithValues: $0.reduce([], +).map { ($0.id, $0) })
|
||||||
})
|
})
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$statusSections)
|
.map { $0.map { section in section.map(\.id) } }
|
||||||
|
.assign(to: &$statusIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +41,9 @@ extension StatusesViewModel {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusViewModel(status: Status) -> StatusViewModel {
|
func statusViewModel(id: String) -> StatusViewModel? {
|
||||||
|
guard let status = statuses[id] else { return nil }
|
||||||
|
|
||||||
var statusViewModel: StatusViewModel
|
var statusViewModel: StatusViewModel
|
||||||
|
|
||||||
if let cachedViewModel = statusViewModelCache[status]?.0 {
|
if let cachedViewModel = statusViewModelCache[status]?.0 {
|
||||||
|
@ -59,8 +64,10 @@ extension StatusesViewModel {
|
||||||
return statusViewModel
|
return statusViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextViewModel(status: Status) -> StatusesViewModel {
|
func contextViewModel(id: String) -> StatusesViewModel? {
|
||||||
StatusesViewModel(statusListService: statusListService.contextService(status: status))
|
guard let status = statuses[id] else { return nil }
|
||||||
|
|
||||||
|
return StatusesViewModel(statusListService: statusListService.contextService(status: status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +76,7 @@ private extension StatusesViewModel {
|
||||||
maintainScrollPositionOfStatusID = nil // clear old value
|
maintainScrollPositionOfStatusID = nil // clear old value
|
||||||
|
|
||||||
// Maintain scroll position of parent after initial load of context
|
// Maintain scroll position of parent after initial load of context
|
||||||
if let contextParentID = contextParentID, statusSections.reduce([], +).map(\.id) == [contextParentID] {
|
if let contextParentID = contextParentID, statusIDs.reduce([], +) == [contextParentID] {
|
||||||
maintainScrollPositionOfStatusID = contextParentID
|
maintainScrollPositionOfStatusID = contextParentID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,10 @@ import Combine
|
||||||
class StatusListViewController: UITableViewController {
|
class StatusListViewController: UITableViewController {
|
||||||
private let viewModel: StatusesViewModel
|
private let viewModel: StatusesViewModel
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var cellHeightCaches = [CGFloat: [Status: CGFloat]]()
|
private var cellHeightCaches = [CGFloat: [String: CGFloat]]()
|
||||||
|
|
||||||
private lazy var dataSource: UITableViewDiffableDataSource<Int, Status> = {
|
private lazy var dataSource: UITableViewDiffableDataSource<Int, String> = {
|
||||||
UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, status in
|
UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, statusID in
|
||||||
guard
|
guard
|
||||||
let self = self,
|
let self = self,
|
||||||
let cell = tableView.dequeueReusableCell(
|
let cell = tableView.dequeueReusableCell(
|
||||||
|
@ -17,9 +17,7 @@ class StatusListViewController: UITableViewController {
|
||||||
for: indexPath) as? StatusTableViewCell
|
for: indexPath) as? StatusTableViewCell
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
let statusViewModel = self.viewModel.statusViewModel(status: status)
|
cell.viewModel = self.viewModel.statusViewModel(id: statusID)
|
||||||
|
|
||||||
cell.viewModel = statusViewModel
|
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
|
@ -50,7 +48,7 @@ class StatusListViewController: UITableViewController {
|
||||||
tableView.cellLayoutMarginsFollowReadableWidth = true
|
tableView.cellLayoutMarginsFollowReadableWidth = true
|
||||||
tableView.separatorInset = .zero
|
tableView.separatorInset = .zero
|
||||||
|
|
||||||
viewModel.$statusSections.map { $0.snapshot() }
|
viewModel.$statusIDs
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
@ -58,16 +56,16 @@ class StatusListViewController: UITableViewController {
|
||||||
|
|
||||||
if
|
if
|
||||||
let id = self.viewModel.maintainScrollPositionOfStatusID,
|
let id = self.viewModel.maintainScrollPositionOfStatusID,
|
||||||
let indexPath = self.indexPath(statusID: id),
|
let indexPath = self.dataSource.indexPath(for: id),
|
||||||
let navigationBar = self.navigationController?.navigationBar {
|
let navigationBar = self.navigationController?.navigationBar {
|
||||||
let navigationBarMaxY = self.tableView.convert(navigationBar.bounds, from: navigationBar).maxY
|
let navigationBarMaxY = self.tableView.convert(navigationBar.bounds, from: navigationBar).maxY
|
||||||
offsetFromNavigationBar = self.tableView.rectForRow(at: indexPath).origin.y - navigationBarMaxY
|
offsetFromNavigationBar = self.tableView.rectForRow(at: indexPath).origin.y - navigationBarMaxY
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dataSource.apply($0, animatingDifferences: false) {
|
self.dataSource.apply($0.snapshot(), animatingDifferences: false) {
|
||||||
if
|
if
|
||||||
let id = self.viewModel.maintainScrollPositionOfStatusID,
|
let id = self.viewModel.maintainScrollPositionOfStatusID,
|
||||||
let indexPath = self.indexPath(statusID: id),
|
let indexPath = self.dataSource.indexPath(for: id),
|
||||||
let offsetFromNavigationBar = offsetFromNavigationBar {
|
let offsetFromNavigationBar = offsetFromNavigationBar {
|
||||||
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
||||||
self.tableView.contentOffset.y -= offsetFromNavigationBar
|
self.tableView.contentOffset.y -= offsetFromNavigationBar
|
||||||
|
@ -88,7 +86,7 @@ class StatusListViewController: UITableViewController {
|
||||||
forRowAt indexPath: IndexPath) {
|
forRowAt indexPath: IndexPath) {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
var heightCache = cellHeightCaches[tableView.frame.width] ?? [Status: CGFloat]()
|
var heightCache = cellHeightCaches[tableView.frame.width] ?? [String: CGFloat]()
|
||||||
|
|
||||||
heightCache[item] = cell.frame.height
|
heightCache[item] = cell.frame.height
|
||||||
cellHeightCaches[tableView.frame.width] = heightCache
|
cellHeightCaches[tableView.frame.width] = heightCache
|
||||||
|
@ -101,41 +99,32 @@ class StatusListViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||||
viewModel.statusSections[indexPath.section][indexPath.row].id != viewModel.contextParentID
|
guard let id = dataSource.itemIdentifier(for: indexPath) else { return true }
|
||||||
|
|
||||||
|
return id != viewModel.contextParentID
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
let status = viewModel.statusSections[indexPath.section][indexPath.row]
|
guard
|
||||||
|
let id = dataSource.itemIdentifier(for: indexPath),
|
||||||
|
let contextViewModel = viewModel.contextViewModel(id: id)
|
||||||
|
else { return }
|
||||||
|
|
||||||
navigationController?.pushViewController(
|
navigationController?.pushViewController(
|
||||||
StatusListViewController(viewModel: viewModel.contextViewModel(status: status)),
|
StatusListViewController(viewModel: contextViewModel),
|
||||||
animated: true)
|
animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusListViewController: StatusTableViewCellDelegate {
|
extension StatusListViewController: StatusTableViewCellDelegate {
|
||||||
func statusTableViewCellDidHaveShareButtonTapped(_ cell: StatusTableViewCell) {
|
func statusTableViewCellDidHaveShareButtonTapped(_ cell: StatusTableViewCell) {
|
||||||
guard let url = cell.viewModel.sharingURL else { return }
|
guard let url = cell.viewModel?.sharingURL else { return }
|
||||||
|
|
||||||
share(url: url)
|
share(url: url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension StatusListViewController {
|
private extension StatusListViewController {
|
||||||
func indexPath(statusID: String) -> IndexPath? {
|
|
||||||
for section in 0..<dataSource.numberOfSections(in: tableView) {
|
|
||||||
for row in 0..<dataSource.tableView(tableView, numberOfRowsInSection: section) {
|
|
||||||
let indexPath = IndexPath(row: row, section: section)
|
|
||||||
|
|
||||||
if dataSource.itemIdentifier(for: indexPath)?.id == statusID {
|
|
||||||
return indexPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func share(url: URL) {
|
func share(url: URL) {
|
||||||
let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
||||||
|
|
||||||
|
@ -143,16 +132,16 @@ private extension StatusListViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Array where Element == [Status] {
|
private extension Array where Element: Sequence, Element.Element: Hashable {
|
||||||
func snapshot() -> NSDiffableDataSourceSnapshot<Int, Status> {
|
func snapshot() -> NSDiffableDataSourceSnapshot<Int, Element.Element> {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Int, Status>()
|
var snapshot = NSDiffableDataSourceSnapshot<Int, Element.Element>()
|
||||||
|
|
||||||
let sections = [Int](0..<count)
|
let sections = [Int](0..<count)
|
||||||
|
|
||||||
snapshot.appendSections(sections)
|
snapshot.appendSections(sections)
|
||||||
|
|
||||||
for section in sections {
|
for section in sections {
|
||||||
snapshot.appendItems(self[section], toSection: section)
|
snapshot.appendItems(self[section].map { $0 }, toSection: section)
|
||||||
}
|
}
|
||||||
|
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
|
@ -58,8 +58,10 @@ class StatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
@IBOutlet private var separatorConstraints: [NSLayoutConstraint]!
|
@IBOutlet private var separatorConstraints: [NSLayoutConstraint]!
|
||||||
|
|
||||||
var viewModel: StatusViewModel! {
|
var viewModel: StatusViewModel? {
|
||||||
didSet {
|
didSet {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
let mutableContent = NSMutableAttributedString(attributedString: viewModel.content)
|
let mutableContent = NSMutableAttributedString(attributedString: viewModel.content)
|
||||||
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
|
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
|
||||||
let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText)
|
let mutableSpoilerText = NSMutableAttributedString(string: viewModel.spoilerText)
|
||||||
|
@ -274,7 +276,7 @@ extension StatusTableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func favoriteButtonTapped(_ sender: UIButton) {
|
@IBAction func favoriteButtonTapped(_ sender: UIButton) {
|
||||||
viewModel.toggleFavorited()
|
viewModel?.toggleFavorited()
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func actionsButtonTapped(_ sender: Any) {
|
@IBAction func actionsButtonTapped(_ sender: Any) {
|
||||||
|
@ -325,7 +327,7 @@ private extension StatusTableViewCell {
|
||||||
let reblogColor: UIColor = reblogged ? .systemGreen : .secondaryLabel
|
let reblogColor: UIColor = reblogged ? .systemGreen : .secondaryLabel
|
||||||
let reblogButton: UIButton
|
let reblogButton: UIButton
|
||||||
|
|
||||||
if viewModel.isContextParent {
|
if viewModel?.isContextParent ?? false {
|
||||||
reblogButton = contextParentReblogButton
|
reblogButton = contextParentReblogButton
|
||||||
} else {
|
} else {
|
||||||
reblogButton = self.reblogButton
|
reblogButton = self.reblogButton
|
||||||
|
@ -340,7 +342,7 @@ private extension StatusTableViewCell {
|
||||||
let favoriteButton: UIButton
|
let favoriteButton: UIButton
|
||||||
let scale: UIImage.SymbolScale
|
let scale: UIImage.SymbolScale
|
||||||
|
|
||||||
if viewModel.isContextParent {
|
if viewModel?.isContextParent ?? false {
|
||||||
favoriteButton = contextParentFavoriteButton
|
favoriteButton = contextParentFavoriteButton
|
||||||
scale = .medium
|
scale = .medium
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue