From 04d4e7f33ad30c13195ff7bc0be19034b57236e7 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Wed, 3 Feb 2021 13:01:50 +0800 Subject: [PATCH] feat: add bottomLoader --- Mastodon.xcodeproj/project.pbxproj | 22 ++++++ Mastodon/Diffiable/Item/Item.swift | 12 ++-- .../Diffiable/Section/TimelineSection.swift | 4 ++ Mastodon/Generated/Strings.swift | 9 +++ ...adMoreConfigurableTableViewContainer.swift | 42 +++++++++++ .../Resources/en.lproj/Localizable.strings | 1 + ...imelineViewController+StatusProvider.swift | 2 + .../PublicTimelineViewController.swift | 32 +++++---- .../PublicTimelineViewModel+State.swift | 12 +++- .../PublicTimelineViewModel.swift | 9 ++- .../TimelineBottomLoaderTableViewCell.swift | 18 +++++ .../TimelineLoaderTableViewCell.swift | 70 +++++++++++++++++++ 12 files changed, 211 insertions(+), 22 deletions(-) create mode 100644 Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift create mode 100644 Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift create mode 100644 Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 58680f7f1..046fbf1e4 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; + 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */; }; 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */; }; @@ -33,6 +34,8 @@ 2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; }; 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; }; 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; }; + 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; }; + 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; }; 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; }; 3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */; }; 45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; @@ -159,6 +162,7 @@ 2D46976325C2A71500CF4AA9 /* UIIamge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIIamge.swift; sourceTree = ""; }; 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = ""; }; 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; + 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = ""; }; 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewModel.swift; sourceTree = ""; }; 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewController+StatusProvider.swift"; sourceTree = ""; }; @@ -170,6 +174,9 @@ 2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; 2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; 2D927F1325C7EDD9004F19B8 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; + 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = ""; }; + 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = ""; }; + 2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = ""; }; 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = ""; }; 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -352,6 +359,14 @@ path = Persist; sourceTree = ""; }; + 2D69CFF225CA9E2200C3A1B2 /* Protocol */ = { + isa = PBXGroup; + children = ( + 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */, + ); + path = Protocol; + sourceTree = ""; + }; 2D76316325C14BAC00929FB9 /* PublicTimeline */ = { isa = PBXGroup; children = ( @@ -406,6 +421,8 @@ 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */, A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */, 2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */, + 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */, + 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */, ); path = TableviewCell; sourceTree = ""; @@ -503,6 +520,7 @@ children = ( DB427DE325BAA00100D1B89D /* Info.plist */, DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, + 2D69CFF225CA9E2200C3A1B2 /* Protocol */, 2D76319C25C151DE00929FB9 /* Diffiable */, DB8AF52A25C13561002E6C99 /* State */, 2D61335525C1886800CAE157 /* Service */, @@ -676,6 +694,7 @@ isa = PBXGroup; children = ( CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */, + 2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */, ); name = "Recovered References"; sourceTree = ""; @@ -1045,6 +1064,7 @@ 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, DB98338825C945ED00AD9700 /* Assets.swift in Sources */, + 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */, DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */, DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */, @@ -1073,10 +1093,12 @@ DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */, 2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */, DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, + 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */, DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */, 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */, 2D7631A825C1535600929FB9 /* TimelinePostTableViewCell.swift in Sources */, 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */, + 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */, DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */, DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */, DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift index d04261a3f..85aaabdf7 100644 --- a/Mastodon/Diffiable/Item/Item.swift +++ b/Mastodon/Diffiable/Item/Item.swift @@ -5,16 +5,17 @@ // Created by sxiaojian on 2021/1/27. // -import Foundation import CoreData -import MastodonSDK import CoreDataStack +import Foundation +import MastodonSDK /// Note: update Equatable when change case enum Item { - // normal list case toot(objectID: NSManagedObjectID) + + case bottomLoader } extension Item: Equatable { @@ -22,6 +23,8 @@ extension Item: Equatable { switch (lhs, rhs) { case (.toot(let objectIDLeft), .toot(let objectIDRight)): return objectIDLeft == objectIDRight + default: + return false } } } @@ -31,7 +34,8 @@ extension Item: Hashable { switch self { case .toot(let objectID): hasher.combine(objectID) + case .bottomLoader: + hasher.combine(String(describing: Item.bottomLoader.self)) } } } - diff --git a/Mastodon/Diffiable/Section/TimelineSection.swift b/Mastodon/Diffiable/Section/TimelineSection.swift index 881b51544..50fbdd520 100644 --- a/Mastodon/Diffiable/Section/TimelineSection.swift +++ b/Mastodon/Diffiable/Section/TimelineSection.swift @@ -37,6 +37,10 @@ extension TimelineSection { } cell.delegate = timelinePostTableViewCellDelegate return cell + case .bottomLoader: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell + cell.activityIndicatorView.startAnimating() + return cell } } } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index e06859b3f..3399d7352 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -10,6 +10,15 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces internal enum L10n { + + internal enum Common { + internal enum Controls { + internal enum Timeline { + /// Load More + internal static let loadMore = L10n.tr("Localizable", "Common.Controls.Timeline.LoadMore") + } + } + } } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces diff --git a/Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift b/Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift new file mode 100644 index 000000000..bc90ab1ed --- /dev/null +++ b/Mastodon/Protocol/LoadMoreConfigurableTableViewContainer.swift @@ -0,0 +1,42 @@ +// +// LoadMoreConfigurableTableViewContainer.swift +// Mastodon +// +// Created by sxiaojian on 2021/2/3. +// + +import UIKit +import GameplayKit + +/// The tableView container driven by state machines with "LoadMore" logic +protocol LoadMoreConfigurableTableViewContainer: UIViewController { + + associatedtype BottomLoaderTableViewCell: UITableViewCell + associatedtype LoadingState: GKState + + var loadMoreConfigurableTableView: UITableView { get } + var loadMoreConfigurableStateMachine: GKStateMachine { get } + func handleScrollViewDidScroll(_ scrollView: UIScrollView) +} + +extension LoadMoreConfigurableTableViewContainer { + func handleScrollViewDidScroll(_ scrollView: UIScrollView) { + guard scrollView === loadMoreConfigurableTableView else { return } + + let cells = loadMoreConfigurableTableView.visibleCells.compactMap { $0 as? BottomLoaderTableViewCell } + guard let loaderTableViewCell = cells.first else { return } + + if let tabBar = tabBarController?.tabBar, let window = view.window { + let loaderTableViewCellFrameInWindow = loadMoreConfigurableTableView.convert(loaderTableViewCell.frame, to: nil) + let windowHeight = window.frame.height + let loaderAppear = (loaderTableViewCellFrameInWindow.origin.y + 0.8 * loaderTableViewCell.frame.height) < (windowHeight - tabBar.frame.height) + if loaderAppear { + loadMoreConfigurableStateMachine.enter(LoadingState.self) + } else { + // do nothing + } + } else { + loadMoreConfigurableStateMachine.enter(LoadingState.self) + } + } +} diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index ac78f76b8..caa87e952 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -5,3 +5,4 @@ Created by MainasuK Cirno on 2021/1/22. */ +"Common.Controls.Timeline.LoadMore" = "Load More"; diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift index d27531cc7..cb0390f85 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift @@ -38,6 +38,8 @@ extension PublicTimelineViewController { let toot = managedObjectContext.object(with: objectID) as? Toot promise(.success(toot)) } + default: + promise(.success(nil)) } } } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift index 871b72b79..042d15823 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift @@ -24,6 +24,7 @@ final class PublicTimelineViewController: UIViewController, NeedsDependency, Tim lazy var tableView: UITableView = { let tableView = UITableView() tableView.register(TimelinePostTableViewCell.self, forCellReuseIdentifier: String(describing: TimelinePostTableViewCell.self)) + tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none return tableView @@ -77,23 +78,18 @@ extension PublicTimelineViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - viewModel.fetchLatest() - .receive(on: DispatchQueue.main) - .sink { completion in - switch completion { - case .failure(let error): - os_log("%{public}s[%{public}ld], %{public}s: fetch user timeline latest response error: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) - case .finished: - break - } - } receiveValue: { response in - let tootsIDs = response.value.map { $0.id } - self.viewModel.tootIDs.value = tootsIDs - } - .store(in: &viewModel.disposeBag) + viewModel.stateMachine.enter(PublicTimelineViewModel.State.Loading.self) } } +// MARK: - UIScrollViewDelegate +extension PublicTimelineViewController { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + handleScrollViewDidScroll(scrollView) + } + +} + // MARK: - Selector extension PublicTimelineViewController { @@ -130,3 +126,11 @@ extension PublicTimelineViewController: UITableViewDelegate { viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key)) } } +// MARK: - LoadMoreConfigurableTableViewContainer +extension PublicTimelineViewController: LoadMoreConfigurableTableViewContainer { + typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell + typealias LoadingState = PublicTimelineViewModel.State.LoadingMore + + var loadMoreConfigurableTableView: UITableView { return tableView } + var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine } +} diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift index ce55ae886..3174015b4 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift @@ -121,10 +121,16 @@ extension PublicTimelineViewModel.State { break } } receiveValue: { response in - viewModel.isFetchingLatestTimeline.value = false - let tootsIDs = response.value.map { $0.id } - viewModel.tootIDs.value = tootsIDs stateMachine.enter(Idle.self) + var oldTootsIDs = viewModel.tootIDs.value + for toot in response.value { + if !oldTootsIDs.contains(toot.id) { + oldTootsIDs.append(toot.id) + } + } + + viewModel.tootIDs.value = oldTootsIDs + } .store(in: &viewModel.disposeBag) } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift index 4b1a05a77..115a35f2b 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift @@ -73,7 +73,14 @@ class PublicTimelineViewModel: NSObject { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems(items) - + if let currentState = self.stateMachine.currentState { + switch currentState { + case is State.Idle, is State.LoadingMore, is State.Fail: + snapshot.appendItems([.bottomLoader], toSection: .main) + default: + break + } + } diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty) } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift new file mode 100644 index 000000000..f2892a841 --- /dev/null +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift @@ -0,0 +1,18 @@ +// +// TimelineBottomLoaderTableViewCell.swift +// Mastodon +// +// Created by sxiaojian on 2021/2/3. +// + +import UIKit +import Combine + +final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell { + override func _init() { + super._init() + + activityIndicatorView.isHidden = false + activityIndicatorView.startAnimating() + } +} diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift new file mode 100644 index 000000000..5fbaa5d73 --- /dev/null +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift @@ -0,0 +1,70 @@ +// +// TimelineLoaderTableViewCell.swift +// Mastodon +// +// Created by sxiaojian on 2021/2/3. +// + +import UIKit +import Combine + +class TimelineLoaderTableViewCell: UITableViewCell { + + static let cellHeight: CGFloat = 48 + + var disposeBag = Set() + + let loadMoreButton: UIButton = { + let button = UIButton(type: .system) + button.titleLabel?.font = .preferredFont(forTextStyle: .headline) + button.setTitle(L10n.Common.Controls.Timeline.loadMore, for: .normal) + return button + }() + + let activityIndicatorView: UIActivityIndicatorView = { + let activityIndicatorView = UIActivityIndicatorView(style: .medium) + activityIndicatorView.tintColor = .white + activityIndicatorView.hidesWhenStopped = true + return activityIndicatorView + }() + + override func prepareForReuse() { + super.prepareForReuse() + disposeBag.removeAll() + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + + func _init() { + selectionStyle = .none + backgroundColor = Asset.Colors.tootDark.color + loadMoreButton.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(loadMoreButton) + NSLayoutConstraint.activate([ + loadMoreButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + loadMoreButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + contentView.readableContentGuide.trailingAnchor.constraint(equalTo: loadMoreButton.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: loadMoreButton.bottomAnchor, constant: 8), + loadMoreButton.heightAnchor.constraint(equalToConstant: TimelineLoaderTableViewCell.cellHeight - 2 * 8).priority(.defaultHigh), + ]) + + activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false + addSubview(activityIndicatorView) + NSLayoutConstraint.activate([ + activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor), + activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor), + ]) + + loadMoreButton.isHidden = true + activityIndicatorView.isHidden = true + } + +}