Pagination
This commit is contained in:
parent
6e8db9586f
commit
c8b2defbb8
|
@ -27,6 +27,8 @@
|
|||
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; };
|
||||
D0A652AD24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A652AC24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift */; };
|
||||
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */; };
|
||||
D0BEB1F524F9A216001B0F04 /* Paged.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F424F9A216001B0F04 /* Paged.swift */; };
|
||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */; };
|
||||
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42224F76169001EBDBB /* IdentitiesView.swift */; };
|
||||
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */; };
|
||||
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42424F76169001EBDBB /* AddIdentityView.swift */; };
|
||||
|
@ -194,6 +196,8 @@
|
|||
D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0A652AC24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreferencesEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||
D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
|
||||
D0BEB1F424F9A216001B0F04 /* Paged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paged.swift; sourceTree = "<group>"; };
|
||||
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableFooterView.swift; sourceTree = "<group>"; };
|
||||
D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; };
|
||||
D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentitiesView.swift; sourceTree = "<group>"; };
|
||||
|
@ -416,10 +420,11 @@
|
|||
D0C7D42024F76169001EBDBB /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D01F41E024F8885900D55A2D /* Attachments */,
|
||||
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
||||
D01F41E024F8885900D55A2D /* Attachments */,
|
||||
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
||||
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
||||
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
||||
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */,
|
||||
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
||||
|
@ -577,6 +582,7 @@
|
|||
D0C7D48324F76169001EBDBB /* ContextEndpoint.swift */,
|
||||
D0C7D48224F76169001EBDBB /* DeletionEndpoint.swift */,
|
||||
D0C7D47D24F76169001EBDBB /* InstanceEndpoint.swift */,
|
||||
D0BEB1F424F9A216001B0F04 /* Paged.swift */,
|
||||
D0C7D47C24F76169001EBDBB /* PreferencesEndpoint.swift */,
|
||||
D0C7D47B24F76169001EBDBB /* PushSubscriptionEndpoint.swift */,
|
||||
D0C7D48424F76169001EBDBB /* StatusEndpoint.swift */,
|
||||
|
@ -860,6 +866,7 @@
|
|||
D0C7D4CD24F7616A001EBDBB /* AddIdentityViewModel.swift in Sources */,
|
||||
D03658D124EDD80900AC17EC /* ContextEndpoint+Stubbing.swift in Sources */,
|
||||
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */,
|
||||
D0BEB1F524F9A216001B0F04 /* Paged.swift in Sources */,
|
||||
D0DC174A24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */,
|
||||
D0C7D49A24F7616A001EBDBB /* StatusListView.swift in Sources */,
|
||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
||||
|
@ -908,6 +915,7 @@
|
|||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||
D0C7D4CF24F7616A001EBDBB /* StatusViewModel.swift in Sources */,
|
||||
D0C7D4C724F7616A001EBDBB /* PostingReadingPreferencesViewModel.swift in Sources */,
|
||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||
D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||
D0C7D4F124F7616A001EBDBB /* IdentityService.swift in Sources */,
|
||||
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Paged<T: MastodonEndpoint> {
|
||||
let endpoint: T
|
||||
let maxID: String?
|
||||
let minID: String?
|
||||
let sinceID: String?
|
||||
let limit: Int?
|
||||
|
||||
init(_ endpoint: T, maxID: String? = nil, minID: String? = nil, sinceID: String? = nil, limit: Int? = nil) {
|
||||
self.endpoint = endpoint
|
||||
self.maxID = maxID
|
||||
self.minID = minID
|
||||
self.sinceID = sinceID
|
||||
self.limit = limit
|
||||
}
|
||||
}
|
||||
|
||||
extension Paged: MastodonEndpoint {
|
||||
typealias ResultType = T.ResultType
|
||||
|
||||
var APIVersion: String { endpoint.APIVersion }
|
||||
|
||||
var context: [String] { endpoint.context }
|
||||
|
||||
var pathComponentsInContext: [String] { endpoint.pathComponentsInContext }
|
||||
|
||||
var method: HTTPMethod { endpoint.method }
|
||||
|
||||
var encoding: ParameterEncoding { endpoint.encoding }
|
||||
|
||||
var parameters: [String: Any]? {
|
||||
var parameters = endpoint.parameters ?? [String: Any]()
|
||||
|
||||
parameters["max_id"] = maxID
|
||||
parameters["min_id"] = minID
|
||||
parameters["since_id"] = sinceID
|
||||
parameters["limit"] = limit
|
||||
|
||||
return parameters
|
||||
}
|
||||
|
||||
var headers: HTTPHeaders? { endpoint.headers }
|
||||
}
|
|
@ -5,6 +5,7 @@ import Combine
|
|||
|
||||
struct ContextService {
|
||||
let statusSections: AnyPublisher<[[Status]], Error>
|
||||
let paginates = false
|
||||
|
||||
private let status: Status
|
||||
private let context = CurrentValueSubject<MastodonContext, Never>(MastodonContext(ancestors: [], descendants: []))
|
||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
|||
|
||||
protocol StatusListService {
|
||||
var statusSections: AnyPublisher<[[Status]], Error> { get }
|
||||
var paginates: Bool { get }
|
||||
var contextParentID: String? { get }
|
||||
func isPinned(status: Status) -> Bool
|
||||
func isReplyInContext(status: Status) -> Bool
|
||||
|
@ -15,6 +16,8 @@ protocol StatusListService {
|
|||
}
|
||||
|
||||
extension StatusListService {
|
||||
var paginates: Bool { true }
|
||||
|
||||
var contextParentID: String? { nil }
|
||||
|
||||
func isPinned(status: Status) -> Bool { false }
|
||||
|
|
|
@ -22,7 +22,7 @@ struct TimelineService {
|
|||
|
||||
extension TimelineService: StatusListService {
|
||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error> {
|
||||
return networkClient.request(timeline.endpoint)
|
||||
networkClient.request(Paged(timeline.endpoint, maxID: maxID, minID: minID))
|
||||
.map { ($0, timeline) }
|
||||
.flatMap(contentDatabase.insert(statuses:collection:))
|
||||
.eraseToAnyPublisher()
|
||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
|||
|
||||
class StatusListViewController: UITableViewController {
|
||||
private let viewModel: StatusListViewModel
|
||||
private let loadingTableFooterView = LoadingTableFooterView()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var cellHeightCaches = [CGFloat: [String: CGFloat]]()
|
||||
|
||||
|
@ -30,6 +31,7 @@ class StatusListViewController: UITableViewController {
|
|||
super.init(style: .plain)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
@ -45,8 +47,10 @@ class StatusListViewController: UITableViewController {
|
|||
}
|
||||
|
||||
tableView.dataSource = dataSource
|
||||
tableView.prefetchDataSource = self
|
||||
tableView.cellLayoutMarginsFollowReadableWidth = true
|
||||
tableView.separatorInset = .zero
|
||||
tableView.tableFooterView = UIView()
|
||||
|
||||
viewModel.$statusIDs
|
||||
.sink { [weak self] in
|
||||
|
@ -73,6 +77,16 @@ class StatusListViewController: UITableViewController {
|
|||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -114,6 +128,26 @@ class StatusListViewController: UITableViewController {
|
|||
StatusListViewController(viewModel: contextViewModel),
|
||||
animated: true)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusListViewController: StatusTableViewCellDelegate {
|
||||
|
@ -130,6 +164,35 @@ private extension StatusListViewController {
|
|||
|
||||
present(activityViewController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element: Sequence, Element.Element: Hashable {
|
||||
|
|
|
@ -29,6 +29,8 @@ class StatusListViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
extension StatusListViewModel {
|
||||
var paginates: Bool { statusListService.paginates }
|
||||
|
||||
var contextParentID: String? { statusListService.contextParentID }
|
||||
|
||||
func request(maxID: String? = nil, minID: String? = nil) {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
class LoadingTableFooterView: UIView {
|
||||
let activityIndicatorView = UIActivityIndicatorView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(activityIndicatorView)
|
||||
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
|
||||
activityIndicatorView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true
|
||||
activityIndicatorView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true
|
||||
activityIndicatorView.startAnimating()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue