mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 17:50:59 +00:00
Pagination
This commit is contained in:
parent
6e8db9586f
commit
c8b2defbb8
8 changed files with 148 additions and 2 deletions
|
@ -27,6 +27,8 @@
|
||||||
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074577924D29366004758DB /* URLSessionConfiguration+Extensions.swift */; };
|
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 */; };
|
D0A652AD24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A652AC24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift */; };
|
||||||
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.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 */; };
|
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42224F76169001EBDBB /* IdentitiesView.swift */; };
|
||||||
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */; };
|
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */; };
|
||||||
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42424F76169001EBDBB /* AddIdentityView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentitiesView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -416,10 +420,11 @@
|
||||||
D0C7D42024F76169001EBDBB /* Views */ = {
|
D0C7D42024F76169001EBDBB /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D01F41E024F8885900D55A2D /* Attachments */,
|
|
||||||
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
||||||
|
D01F41E024F8885900D55A2D /* Attachments */,
|
||||||
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
||||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
||||||
|
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
||||||
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */,
|
||||||
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */,
|
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */,
|
||||||
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
D0C7D42624F76169001EBDBB /* PreferencesView.swift */,
|
||||||
|
@ -577,6 +582,7 @@
|
||||||
D0C7D48324F76169001EBDBB /* ContextEndpoint.swift */,
|
D0C7D48324F76169001EBDBB /* ContextEndpoint.swift */,
|
||||||
D0C7D48224F76169001EBDBB /* DeletionEndpoint.swift */,
|
D0C7D48224F76169001EBDBB /* DeletionEndpoint.swift */,
|
||||||
D0C7D47D24F76169001EBDBB /* InstanceEndpoint.swift */,
|
D0C7D47D24F76169001EBDBB /* InstanceEndpoint.swift */,
|
||||||
|
D0BEB1F424F9A216001B0F04 /* Paged.swift */,
|
||||||
D0C7D47C24F76169001EBDBB /* PreferencesEndpoint.swift */,
|
D0C7D47C24F76169001EBDBB /* PreferencesEndpoint.swift */,
|
||||||
D0C7D47B24F76169001EBDBB /* PushSubscriptionEndpoint.swift */,
|
D0C7D47B24F76169001EBDBB /* PushSubscriptionEndpoint.swift */,
|
||||||
D0C7D48424F76169001EBDBB /* StatusEndpoint.swift */,
|
D0C7D48424F76169001EBDBB /* StatusEndpoint.swift */,
|
||||||
|
@ -860,6 +866,7 @@
|
||||||
D0C7D4CD24F7616A001EBDBB /* AddIdentityViewModel.swift in Sources */,
|
D0C7D4CD24F7616A001EBDBB /* AddIdentityViewModel.swift in Sources */,
|
||||||
D03658D124EDD80900AC17EC /* ContextEndpoint+Stubbing.swift in Sources */,
|
D03658D124EDD80900AC17EC /* ContextEndpoint+Stubbing.swift in Sources */,
|
||||||
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */,
|
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */,
|
||||||
|
D0BEB1F524F9A216001B0F04 /* Paged.swift in Sources */,
|
||||||
D0DC174A24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */,
|
D0DC174A24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */,
|
||||||
D0C7D49A24F7616A001EBDBB /* StatusListView.swift in Sources */,
|
D0C7D49A24F7616A001EBDBB /* StatusListView.swift in Sources */,
|
||||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
||||||
|
@ -908,6 +915,7 @@
|
||||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||||
D0C7D4CF24F7616A001EBDBB /* StatusViewModel.swift in Sources */,
|
D0C7D4CF24F7616A001EBDBB /* StatusViewModel.swift in Sources */,
|
||||||
D0C7D4C724F7616A001EBDBB /* PostingReadingPreferencesViewModel.swift in Sources */,
|
D0C7D4C724F7616A001EBDBB /* PostingReadingPreferencesViewModel.swift in Sources */,
|
||||||
|
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||||
D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||||
D0C7D4F124F7616A001EBDBB /* IdentityService.swift in Sources */,
|
D0C7D4F124F7616A001EBDBB /* IdentityService.swift in Sources */,
|
||||||
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
||||||
|
|
46
Networking/Mastodon API/Endpoints/Paged.swift
Normal file
46
Networking/Mastodon API/Endpoints/Paged.swift
Normal file
|
@ -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 {
|
struct ContextService {
|
||||||
let statusSections: AnyPublisher<[[Status]], Error>
|
let statusSections: AnyPublisher<[[Status]], Error>
|
||||||
|
let paginates = false
|
||||||
|
|
||||||
private let status: Status
|
private let status: Status
|
||||||
private let context = CurrentValueSubject<MastodonContext, Never>(MastodonContext(ancestors: [], descendants: []))
|
private let context = CurrentValueSubject<MastodonContext, Never>(MastodonContext(ancestors: [], descendants: []))
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
||||||
|
|
||||||
protocol StatusListService {
|
protocol StatusListService {
|
||||||
var statusSections: AnyPublisher<[[Status]], Error> { get }
|
var statusSections: AnyPublisher<[[Status]], Error> { get }
|
||||||
|
var paginates: Bool { get }
|
||||||
var contextParentID: String? { get }
|
var contextParentID: String? { get }
|
||||||
func isPinned(status: Status) -> Bool
|
func isPinned(status: Status) -> Bool
|
||||||
func isReplyInContext(status: Status) -> Bool
|
func isReplyInContext(status: Status) -> Bool
|
||||||
|
@ -15,6 +16,8 @@ protocol StatusListService {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusListService {
|
extension StatusListService {
|
||||||
|
var paginates: Bool { true }
|
||||||
|
|
||||||
var contextParentID: String? { nil }
|
var contextParentID: String? { nil }
|
||||||
|
|
||||||
func isPinned(status: Status) -> Bool { false }
|
func isPinned(status: Status) -> Bool { false }
|
||||||
|
|
|
@ -22,7 +22,7 @@ struct TimelineService {
|
||||||
|
|
||||||
extension TimelineService: StatusListService {
|
extension TimelineService: StatusListService {
|
||||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error> {
|
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) }
|
.map { ($0, timeline) }
|
||||||
.flatMap(contentDatabase.insert(statuses:collection:))
|
.flatMap(contentDatabase.insert(statuses:collection:))
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
||||||
|
|
||||||
class StatusListViewController: UITableViewController {
|
class StatusListViewController: UITableViewController {
|
||||||
private let viewModel: StatusListViewModel
|
private let viewModel: StatusListViewModel
|
||||||
|
private let loadingTableFooterView = LoadingTableFooterView()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var cellHeightCaches = [CGFloat: [String: CGFloat]]()
|
private var cellHeightCaches = [CGFloat: [String: CGFloat]]()
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@ class StatusListViewController: UITableViewController {
|
||||||
super.init(style: .plain)
|
super.init(style: .plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
@ -45,8 +47,10 @@ class StatusListViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
tableView.dataSource = dataSource
|
tableView.dataSource = dataSource
|
||||||
|
tableView.prefetchDataSource = self
|
||||||
tableView.cellLayoutMarginsFollowReadableWidth = true
|
tableView.cellLayoutMarginsFollowReadableWidth = true
|
||||||
tableView.separatorInset = .zero
|
tableView.separatorInset = .zero
|
||||||
|
tableView.tableFooterView = UIView()
|
||||||
|
|
||||||
viewModel.$statusIDs
|
viewModel.$statusIDs
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
|
@ -73,6 +77,16 @@ class StatusListViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.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) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -114,6 +128,26 @@ class StatusListViewController: UITableViewController {
|
||||||
StatusListViewController(viewModel: contextViewModel),
|
StatusListViewController(viewModel: contextViewModel),
|
||||||
animated: true)
|
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 {
|
extension StatusListViewController: StatusTableViewCellDelegate {
|
||||||
|
@ -130,6 +164,35 @@ private extension StatusListViewController {
|
||||||
|
|
||||||
present(activityViewController, animated: true, completion: nil)
|
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 {
|
private extension Array where Element: Sequence, Element.Element: Hashable {
|
||||||
|
|
|
@ -29,6 +29,8 @@ class StatusListViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusListViewModel {
|
extension StatusListViewModel {
|
||||||
|
var paginates: Bool { statusListService.paginates }
|
||||||
|
|
||||||
var contextParentID: String? { statusListService.contextParentID }
|
var contextParentID: String? { statusListService.contextParentID }
|
||||||
|
|
||||||
func request(maxID: String? = nil, minID: String? = nil) {
|
func request(maxID: String? = nil, minID: String? = nil) {
|
||||||
|
|
23
Views/LoadingTableFooterView.swift
Normal file
23
Views/LoadingTableFooterView.swift
Normal file
|
@ -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 a new issue