2022-09-24 14:49:06 +00:00
|
|
|
//
|
|
|
|
// TimelineLikeController.swift
|
|
|
|
// Tusker
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 9/19/22.
|
|
|
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import OSLog
|
|
|
|
|
|
|
|
protocol TimelineLikeControllerDelegate<TimelineItem>: AnyObject {
|
|
|
|
associatedtype TimelineItem
|
|
|
|
|
|
|
|
func loadInitial() async throws -> [TimelineItem]
|
|
|
|
|
|
|
|
func loadNewer() async throws -> [TimelineItem]
|
|
|
|
|
|
|
|
func loadOlder() async throws -> [TimelineItem]
|
|
|
|
|
|
|
|
func canLoadOlder() async -> Bool
|
|
|
|
|
|
|
|
func handleEvent(_ event: TimelineLikeController<TimelineItem>.Event) async
|
|
|
|
}
|
|
|
|
|
|
|
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "TimelineLikeController")
|
|
|
|
|
|
|
|
actor TimelineLikeController<Item> {
|
|
|
|
|
|
|
|
unowned var delegate: any TimelineLikeControllerDelegate<Item>
|
|
|
|
|
2022-09-24 15:31:52 +00:00
|
|
|
private(set) var state = State.notLoadedInitial {
|
2022-09-24 14:49:06 +00:00
|
|
|
willSet {
|
|
|
|
precondition(state.canTransition(to: newValue))
|
|
|
|
logger.debug("State: \(self.state.debugDescription, privacy: .public) -> \(newValue.debugDescription, privacy: .public)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
init(delegate: any TimelineLikeControllerDelegate<Item>) {
|
|
|
|
self.delegate = delegate
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadInitial() async {
|
2022-09-24 15:31:52 +00:00
|
|
|
guard state == .notLoadedInitial else {
|
2022-09-24 14:49:06 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
let token = LoadAttemptToken()
|
|
|
|
state = .loadingInitial(token, hasAddedLoadingIndicator: false)
|
|
|
|
let loadingIndicator = DeferredLoadingIndicator(owner: self, state: state, addedIndicatorState: .loadingInitial(token, hasAddedLoadingIndicator: true))
|
|
|
|
do {
|
|
|
|
let items = try await delegate.loadInitial()
|
|
|
|
guard case .loadingInitial(token, _) = state else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
await loadingIndicator.end()
|
|
|
|
await emit(event: .replaceAllItems(items, token))
|
|
|
|
state = .idle
|
|
|
|
} catch {
|
|
|
|
await loadingIndicator.end()
|
|
|
|
await emit(event: .loadAllError(error, token))
|
|
|
|
state = .idle
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadNewer() async {
|
|
|
|
guard state == .idle else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let token = LoadAttemptToken()
|
|
|
|
state = .loadingNewer(token)
|
|
|
|
do {
|
|
|
|
let items = try await delegate.loadNewer()
|
|
|
|
guard case .loadingNewer(token) = state else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
await emit(event: .prependItems(items, token))
|
|
|
|
state = .idle
|
|
|
|
} catch {
|
|
|
|
await emit(event: .loadNewerError(error, token))
|
|
|
|
state = .idle
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadOlder() async {
|
|
|
|
guard state == .idle else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let token = LoadAttemptToken()
|
|
|
|
guard await delegate.canLoadOlder() else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
state = .loadingOlder(token, hasAddedLoadingIndicator: false)
|
|
|
|
let loadingIndicator = DeferredLoadingIndicator(owner: self, state: state, addedIndicatorState: .loadingOlder(token, hasAddedLoadingIndicator: true))
|
|
|
|
do {
|
|
|
|
let items = try await delegate.loadOlder()
|
|
|
|
guard case .loadingOlder(token, _) = state else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
await loadingIndicator.end()
|
|
|
|
await emit(event: .appendItems(items, token))
|
|
|
|
state = .idle
|
|
|
|
} catch {
|
|
|
|
await loadingIndicator.end()
|
|
|
|
await emit(event: .loadOlderError(error, token))
|
|
|
|
state = .idle
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func transition(to newState: State) {
|
|
|
|
self.state = newState
|
|
|
|
}
|
|
|
|
|
|
|
|
private func emit(event: Event) async {
|
|
|
|
precondition(state.canEmit(event: event))
|
|
|
|
await delegate.handleEvent(event)
|
|
|
|
}
|
|
|
|
|
|
|
|
enum State: Equatable, CustomDebugStringConvertible {
|
2022-09-24 15:31:52 +00:00
|
|
|
case notLoadedInitial
|
2022-09-24 14:49:06 +00:00
|
|
|
case idle
|
|
|
|
case loadingInitial(LoadAttemptToken, hasAddedLoadingIndicator: Bool)
|
|
|
|
case loadingNewer(LoadAttemptToken)
|
|
|
|
case loadingOlder(LoadAttemptToken, hasAddedLoadingIndicator: Bool)
|
|
|
|
|
|
|
|
var debugDescription: String {
|
|
|
|
switch self {
|
2022-09-24 15:31:52 +00:00
|
|
|
case .notLoadedInitial:
|
|
|
|
return "notLoadedInitial"
|
2022-09-24 14:49:06 +00:00
|
|
|
case .idle:
|
|
|
|
return "idle"
|
|
|
|
case .loadingInitial(let token, let hasAddedLoadingIndicator):
|
|
|
|
return "loadingInitial(\(ObjectIdentifier(token)), hasAddedLoadingIndicator: \(hasAddedLoadingIndicator))"
|
|
|
|
case .loadingNewer(let token):
|
|
|
|
return "loadingNewer(\(ObjectIdentifier(token)))"
|
|
|
|
case .loadingOlder(let token, let hasAddedLoadingIndicator):
|
|
|
|
return "loadingOlder(\(ObjectIdentifier(token)), hasAddedLoadingIndicator: \(hasAddedLoadingIndicator))"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func canTransition(to: State) -> Bool {
|
|
|
|
switch self {
|
2022-09-24 15:31:52 +00:00
|
|
|
case .notLoadedInitial:
|
|
|
|
switch to {
|
|
|
|
case .loadingInitial(_, hasAddedLoadingIndicator: _):
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
2022-09-24 14:49:06 +00:00
|
|
|
case .idle:
|
|
|
|
switch to {
|
2022-10-01 19:08:51 +00:00
|
|
|
case .loadingNewer(_), .loadingOlder(_, _):
|
2022-09-24 14:49:06 +00:00
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
case .loadingInitial(let token, let hasAddedLoadingIndicator):
|
|
|
|
return to == .idle || (!hasAddedLoadingIndicator && to == .loadingInitial(token, hasAddedLoadingIndicator: true))
|
|
|
|
case .loadingNewer(_):
|
|
|
|
return to == .idle
|
|
|
|
case .loadingOlder(let token, let hasAddedLoadingIndicator):
|
|
|
|
return to == .idle || (!hasAddedLoadingIndicator && to == .loadingOlder(token, hasAddedLoadingIndicator: true))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func canEmit(event: Event) -> Bool {
|
|
|
|
switch event {
|
|
|
|
case .addLoadingIndicator:
|
|
|
|
switch self {
|
|
|
|
case .loadingInitial(_, _), .loadingOlder(_, _):
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
case .removeLoadingIndicator:
|
|
|
|
switch self {
|
|
|
|
case .loadingInitial(_, hasAddedLoadingIndicator: true), .loadingOlder(_, hasAddedLoadingIndicator: true):
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
case .loadAllError(_, let token), .replaceAllItems(_, let token):
|
|
|
|
switch self {
|
|
|
|
case .loadingInitial(token, _):
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
case .loadNewerError(_, let token), .prependItems(_, let token):
|
|
|
|
switch self {
|
|
|
|
case .loadingNewer(token):
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
case .loadOlderError(_, let token), .appendItems(_, let token):
|
|
|
|
switch self {
|
|
|
|
case .loadingOlder(token, _):
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum Event {
|
|
|
|
case addLoadingIndicator
|
|
|
|
case removeLoadingIndicator
|
|
|
|
case loadAllError(Error, LoadAttemptToken)
|
|
|
|
case replaceAllItems([Item], LoadAttemptToken)
|
|
|
|
case loadNewerError(Error, LoadAttemptToken)
|
|
|
|
case prependItems([Item], LoadAttemptToken)
|
|
|
|
case loadOlderError(Error, LoadAttemptToken)
|
|
|
|
case appendItems([Item], LoadAttemptToken)
|
|
|
|
}
|
|
|
|
|
|
|
|
class LoadAttemptToken: Equatable {
|
|
|
|
static func ==(lhs: LoadAttemptToken, rhs: LoadAttemptToken) -> Bool {
|
|
|
|
return lhs === rhs
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class DeferredLoadingIndicator {
|
|
|
|
private let owner: TimelineLikeController<Item>
|
|
|
|
private let addedIndicatorState: State
|
|
|
|
private let task: Task<Void, Error>
|
|
|
|
|
|
|
|
init(owner: TimelineLikeController<Item>, state: State, addedIndicatorState: State) {
|
|
|
|
self.owner = owner
|
|
|
|
self.addedIndicatorState = addedIndicatorState
|
|
|
|
self.task = Task {
|
|
|
|
try await Task.sleep(nanoseconds: 150 * NSEC_PER_MSEC)
|
|
|
|
guard await state == owner.state else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
await owner.emit(event: .addLoadingIndicator)
|
|
|
|
await owner.transition(to: addedIndicatorState)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func end() async {
|
|
|
|
let state = await owner.state
|
|
|
|
if state == addedIndicatorState {
|
|
|
|
await owner.emit(event: .removeLoadingIndicator)
|
|
|
|
} else {
|
|
|
|
task.cancel()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|