2022-09-24 14:49:06 +00:00
//
// T i m e l i n e L i k e C o n t r o l l e r . s w i f t
// T u s k e r
//
// C r e a t e d b y S h a d o w f a c t s o n 9 / 1 9 / 2 2 .
// C o p y r i g h t © 2 0 2 2 S h a d o w f a c t s . A l l r i g h t s r e s e r v e d .
//
import Foundation
import OSLog
2023-10-20 01:21:50 +00:00
import Combine
2022-09-24 14:49:06 +00:00
2024-03-10 18:49:57 +00:00
protocol TimelineLikeControllerDataSource < TimelineItem > : AnyObject {
2023-02-19 20:23:25 +00:00
associatedtype TimelineItem : Sendable
2024-03-10 18:49:57 +00:00
2022-09-24 14:49:06 +00:00
func loadInitial ( ) async throws -> [ TimelineItem ]
func loadNewer ( ) async throws -> [ TimelineItem ]
func canLoadOlder ( ) async -> Bool
2022-11-19 01:49:15 +00:00
func loadOlder ( ) async throws -> [ TimelineItem ]
func loadGap ( in direction : TimelineGapDirection ) async throws -> [ TimelineItem ]
2024-03-10 18:49:57 +00:00
}
@ MainActor
protocol TimelineLikeControllerDelegate < TimelineItem > : AnyObject {
associatedtype TimelineItem : Sendable
2022-11-18 22:29:55 +00:00
2022-10-08 15:45:02 +00:00
func handleAddLoadingIndicator ( ) async
func handleRemoveLoadingIndicator ( ) async
func handleLoadAllError ( _ error : Swift . Error ) async
func handleReplaceAllItems ( _ timelineItems : [ TimelineItem ] ) async
func handleLoadNewerError ( _ error : Swift . Error ) async
func handlePrependItems ( _ timelineItems : [ TimelineItem ] ) async
func handleLoadOlderError ( _ error : Swift . Error ) async
func handleAppendItems ( _ timelineItems : [ TimelineItem ] ) async
2022-11-19 01:49:15 +00:00
func handleLoadGapError ( _ error : Swift . Error , direction : TimelineGapDirection ) async
func handleFillGap ( _ timelineItems : [ TimelineItem ] , direction : TimelineGapDirection ) async
2022-09-24 14:49:06 +00:00
}
private let logger = Logger ( subsystem : Bundle . main . bundleIdentifier ! , category : " TimelineLikeController " )
2022-11-19 01:49:15 +00:00
@ MainActor
2023-02-19 20:23:25 +00:00
class TimelineLikeController < Item : Sendable > {
2022-09-24 14:49:06 +00:00
2022-11-19 01:49:15 +00:00
private unowned var delegate : any TimelineLikeControllerDelegate < Item >
2024-03-10 18:49:57 +00:00
private unowned var dataSource : any TimelineLikeControllerDataSource < Item >
2023-05-11 19:11:43 +00:00
private let ownerType : String
2022-09-24 14:49:06 +00:00
2023-10-20 01:21:50 +00:00
@ AsyncObservable private ( set ) var state = State . notLoadedInitial {
2022-09-24 14:49:06 +00:00
willSet {
2022-10-08 19:12:10 +00:00
guard state . canTransition ( to : newValue ) else {
2023-05-11 19:11:43 +00:00
logger . error ( " \( self . ownerType , privacy : . public ) State \( self . state . debugDescription , privacy : . public ) cannot transition to \( newValue . debugDescription , privacy : . public ) " )
2022-10-30 21:10:58 +00:00
fatalError ( " State \( state ) cannot transition to \( newValue ) " )
2022-10-08 19:12:10 +00:00
}
2023-05-11 19:11:43 +00:00
logger . debug ( " \( self . ownerType , privacy : . public ) State: \( self . state . debugDescription , privacy : . public ) -> \( newValue . debugDescription , privacy : . public ) " )
2022-09-24 14:49:06 +00:00
}
}
2024-03-10 18:49:57 +00:00
init ( delegate : any TimelineLikeControllerDelegate < Item > , dataSource : any TimelineLikeControllerDataSource < Item > , ownerType : String ) {
2022-09-24 14:49:06 +00:00
self . delegate = delegate
2024-03-10 18:49:57 +00:00
self . dataSource = dataSource
2023-05-11 19:11:43 +00:00
self . ownerType = ownerType
2022-09-24 14:49:06 +00:00
}
2023-10-20 01:21:50 +00:00
// / W a i t s f o r t h e c o n t r o l l e r t o f i n i s h t h e c u r r e n t o p e r a t i o n a n d a r r i v e a t t h e i d l e s t a t e .
// /
// / I f t h e c u r r e n t s t a t e i s ` n o t L o a d e d I n i t i a l ` , t h i s w i l l w a i t u n t i l t h e c o n t r o l l e r
// / s e t t l e s a f t e r t h e i n i t i a l l o a d .
func finishPendingOperation ( ) async {
guard state != . idle else {
return
}
for await state in $ state where state = = . idle {
break
}
}
2022-09-24 14:49:06 +00:00
func loadInitial ( ) async {
2022-11-02 01:06:06 +00:00
guard state = = . notLoadedInitial || state = = . idle else {
2022-09-24 14:49:06 +00:00
return
}
let token = LoadAttemptToken ( )
state = . loadingInitial ( token , hasAddedLoadingIndicator : false )
2023-05-28 18:11:15 +00:00
await emit ( event : . addLoadingIndicator )
state = . loadingInitial ( token , hasAddedLoadingIndicator : true )
2022-09-24 14:49:06 +00:00
do {
2024-03-10 18:49:57 +00:00
let items = try await dataSource . loadInitial ( )
2022-09-24 14:49:06 +00:00
guard case . loadingInitial ( token , _ ) = state else {
return
}
await emit ( event : . replaceAllItems ( items , token ) )
2023-05-28 18:11:15 +00:00
await emit ( event : . removeLoadingIndicator )
2022-09-24 14:49:06 +00:00
state = . idle
2022-11-03 03:00:29 +00:00
} catch is CancellationError {
return
2022-09-24 14:49:06 +00:00
} catch {
2023-05-28 18:11:15 +00:00
await emit ( event : . removeLoadingIndicator )
2022-09-24 14:49:06 +00:00
await emit ( event : . loadAllError ( error , token ) )
2022-11-10 00:15:08 +00:00
state = . notLoadedInitial
2022-09-24 14:49:06 +00:00
}
}
2022-11-23 16:35:25 +00:00
// / U s e d t o i n d i c a t e t o t h e c o n t r o l l e r t h a t t h e i n i t i a l s e t o f p o s t s h a v e b e e n r e s t o r e d e x t e r n a l l y .
2022-12-23 21:35:58 +00:00
func restoreInitial ( doRestore : ( ) async -> Void ) async {
guard state = = . notLoadedInitial || state = = . idle else {
2022-11-23 16:35:25 +00:00
return
}
2023-01-20 18:38:33 +00:00
let token = LoadAttemptToken ( )
state = . restoringInitial ( token , hasAddedLoadingIndicator : false )
2023-05-28 18:11:15 +00:00
await emit ( event : . addLoadingIndicator )
state = . restoringInitial ( token , hasAddedLoadingIndicator : true )
2022-12-23 21:35:58 +00:00
await doRestore ( )
2023-05-28 18:11:15 +00:00
await emit ( event : . removeLoadingIndicator )
2022-11-23 16:35:25 +00:00
state = . idle
}
2022-09-24 14:49:06 +00:00
func loadNewer ( ) async {
guard state = = . idle else {
return
}
let token = LoadAttemptToken ( )
state = . loadingNewer ( token )
do {
2024-03-10 18:49:57 +00:00
let items = try await dataSource . loadNewer ( )
2022-09-24 14:49:06 +00:00
guard case . loadingNewer ( token ) = state else {
return
}
await emit ( event : . prependItems ( items , token ) )
state = . idle
2022-11-03 03:00:29 +00:00
} catch is CancellationError {
return
2022-09-24 14:49:06 +00:00
} catch {
await emit ( event : . loadNewerError ( error , token ) )
state = . idle
}
}
func loadOlder ( ) async {
guard state = = . idle else {
return
}
let token = LoadAttemptToken ( )
2024-03-10 18:49:57 +00:00
guard await dataSource . canLoadOlder ( ) ,
2022-10-31 21:45:36 +00:00
// M a k e s u r e w e ' r e s t i l l i n t h e i d l e s t a t e b e f o r e c o n t i n u i n g o n , s i n c e t h a t m a y h a v e c h n a g e d w h i l e w a i t i n g f o r u s e r i n p u t .
// I f t h e l o a d m o r e c e l l a p p e a r s , t h e n t h e u s e r s s c r o l l s u p a n d b a c k d o w n , t h e V C m a y k i c k o f f a s e c o n d l o a d O l d e r t a s k
// b u t w e o n l y w a n t o n e t o p r o c e e d . T h e a c t o r p r e v e n t s a d a t a r a c e , a n d t h i s p r e v e n t s m u l t i p l e s i m u l t a n e o u s l l o a d O l d e r t a s k s f r o m r u n n i n g .
state = = . idle else {
2022-09-24 14:49:06 +00:00
return
}
state = . loadingOlder ( token , hasAddedLoadingIndicator : false )
2023-05-28 18:11:15 +00:00
await emit ( event : . addLoadingIndicator )
state = . loadingOlder ( token , hasAddedLoadingIndicator : true )
2022-09-24 14:49:06 +00:00
do {
2024-03-10 18:49:57 +00:00
let items = try await dataSource . loadOlder ( )
2022-09-24 14:49:06 +00:00
guard case . loadingOlder ( token , _ ) = state else {
return
}
await emit ( event : . appendItems ( items , token ) )
2023-05-28 18:11:15 +00:00
await emit ( event : . removeLoadingIndicator )
2022-09-24 14:49:06 +00:00
state = . idle
2022-11-03 03:00:29 +00:00
} catch is CancellationError {
return
2022-09-24 14:49:06 +00:00
} catch {
2023-05-28 18:11:15 +00:00
await emit ( event : . removeLoadingIndicator )
2022-09-24 14:49:06 +00:00
await emit ( event : . loadOlderError ( error , token ) )
state = . idle
}
}
2022-11-19 01:49:15 +00:00
func fillGap ( in direction : TimelineGapDirection ) async {
2022-11-18 22:29:55 +00:00
guard state = = . idle else {
return
}
let token = LoadAttemptToken ( )
state = . loadingGap ( token , direction )
do {
2024-03-10 18:49:57 +00:00
let items = try await dataSource . loadGap ( in : direction )
2022-11-18 22:29:55 +00:00
guard case . loadingGap ( token , direction ) = state else {
return
}
await emit ( event : . fillGap ( items , direction , token ) )
state = . idle
} catch is CancellationError {
return
} catch {
await emit ( event : . loadGapError ( error , direction , token ) )
state = . idle
}
}
2022-09-24 14:49:06 +00:00
private func transition ( to newState : State ) {
self . state = newState
}
private func emit ( event : Event ) async {
2022-10-08 19:12:10 +00:00
guard state . canEmit ( event : event ) else {
2023-05-11 19:11:43 +00:00
logger . error ( " \( self . ownerType , privacy : . public ) State \( self . state . debugDescription , privacy : . public ) cannot emit event: \( event . debugDescription , privacy : . public ) " )
2022-10-30 21:10:58 +00:00
fatalError ( " State \( state ) cannot emit event: \( event ) " )
2022-10-08 19:12:10 +00:00
}
2022-10-08 15:45:02 +00:00
switch event {
case . addLoadingIndicator :
await delegate . handleAddLoadingIndicator ( )
case . removeLoadingIndicator :
await delegate . handleRemoveLoadingIndicator ( )
case . loadAllError ( let error , _ ) :
await delegate . handleLoadAllError ( error )
case . replaceAllItems ( let items , _ ) :
await delegate . handleReplaceAllItems ( items )
case . loadNewerError ( let error , _ ) :
await delegate . handleLoadNewerError ( error )
case . prependItems ( let items , _ ) :
await delegate . handlePrependItems ( items )
case . loadOlderError ( let error , _ ) :
await delegate . handleLoadOlderError ( error )
case . appendItems ( let items , _ ) :
await delegate . handleAppendItems ( items )
2022-11-18 22:29:55 +00:00
case . loadGapError ( let error , let direction , _ ) :
await delegate . handleLoadGapError ( error , direction : direction )
case . fillGap ( let items , let direction , _ ) :
await delegate . handleFillGap ( items , direction : direction )
2022-10-08 15:45:02 +00:00
}
2022-09-24 14:49:06 +00:00
}
2024-01-27 20:48:58 +00:00
enum State : Equatable , CustomDebugStringConvertible , Sendable {
2022-09-24 15:31:52 +00:00
case notLoadedInitial
2022-09-24 14:49:06 +00:00
case idle
2023-01-20 18:38:33 +00:00
case restoringInitial ( LoadAttemptToken , hasAddedLoadingIndicator : Bool )
2022-09-24 14:49:06 +00:00
case loadingInitial ( LoadAttemptToken , hasAddedLoadingIndicator : Bool )
case loadingNewer ( LoadAttemptToken )
case loadingOlder ( LoadAttemptToken , hasAddedLoadingIndicator : Bool )
2022-11-19 01:49:15 +00:00
case loadingGap ( LoadAttemptToken , TimelineGapDirection )
2022-09-24 14:49:06 +00:00
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 "
2023-01-20 18:38:33 +00:00
case . restoringInitial ( let token , let hasAddedLoadingIndicator ) :
return " restoringInitial( \( ObjectIdentifier ( token ) ) , hasAddedLoadingIndicator: \( hasAddedLoadingIndicator ) ) "
2022-09-24 14:49:06 +00:00
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 ) ) "
2022-11-18 22:29:55 +00:00
case . loadingGap ( let token , let direction ) :
2022-11-19 16:15:14 +00:00
return " loadingGap( \( ObjectIdentifier ( token ) ) , \( direction ) ) "
2022-09-24 14:49:06 +00:00
}
}
func canTransition ( to : State ) -> Bool {
switch self {
2022-09-24 15:31:52 +00:00
case . notLoadedInitial :
switch to {
2022-11-23 16:35:25 +00:00
case . restoringInitial , . loadingInitial ( _ , _ ) :
2022-09-24 15:31:52 +00:00
return true
default :
return false
}
2022-09-24 14:49:06 +00:00
case . idle :
switch to {
2023-01-20 18:38:33 +00:00
case . restoringInitial ( _ , _ ) , . loadingInitial ( _ , _ ) , . loadingNewer ( _ ) , . loadingOlder ( _ , _ ) , . loadingGap ( _ , _ ) :
2022-09-24 14:49:06 +00:00
return true
default :
return false
}
2023-01-20 18:38:33 +00:00
case . restoringInitial ( let token , let hasAddedLoadingIndicator ) :
return to = = . idle || ( ! hasAddedLoadingIndicator && to = = . restoringInitial ( token , hasAddedLoadingIndicator : true ) )
2022-09-24 14:49:06 +00:00
case . loadingInitial ( let token , let hasAddedLoadingIndicator ) :
2022-11-10 00:15:08 +00:00
return to = = . notLoadedInitial || to = = . idle || ( ! hasAddedLoadingIndicator && to = = . loadingInitial ( token , hasAddedLoadingIndicator : true ) )
2022-09-24 14:49:06 +00:00
case . loadingNewer ( _ ) :
return to = = . idle
case . loadingOlder ( let token , let hasAddedLoadingIndicator ) :
return to = = . idle || ( ! hasAddedLoadingIndicator && to = = . loadingOlder ( token , hasAddedLoadingIndicator : true ) )
2022-11-18 22:29:55 +00:00
case . loadingGap ( _ , _ ) :
return to = = . idle
2022-09-24 14:49:06 +00:00
}
}
func canEmit ( event : Event ) -> Bool {
switch event {
case . addLoadingIndicator :
switch self {
2023-01-20 18:38:33 +00:00
case . restoringInitial ( _ , _ ) , . loadingInitial ( _ , _ ) , . loadingOlder ( _ , _ ) :
2022-09-24 14:49:06 +00:00
return true
default :
return false
}
case . removeLoadingIndicator :
switch self {
2023-01-20 18:38:33 +00:00
case . restoringInitial ( _ , hasAddedLoadingIndicator : true ) , . loadingInitial ( _ , hasAddedLoadingIndicator : true ) , . loadingOlder ( _ , hasAddedLoadingIndicator : true ) :
2022-09-24 14:49:06 +00:00
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
}
2022-11-18 22:29:55 +00:00
case . loadGapError ( _ , let direction , let token ) , . fillGap ( _ , let direction , let token ) :
switch self {
case . loadingGap ( token , direction ) :
return true
default :
return false
}
2022-09-24 14:49:06 +00:00
}
}
}
2022-10-08 19:12:10 +00:00
enum Event : CustomDebugStringConvertible {
2022-09-24 14:49:06 +00:00
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 )
2022-11-19 01:49:15 +00:00
case loadGapError ( Error , TimelineGapDirection , LoadAttemptToken )
case fillGap ( [ Item ] , TimelineGapDirection , LoadAttemptToken )
2022-10-08 19:12:10 +00:00
var debugDescription : String {
switch self {
case . addLoadingIndicator :
return " addLoadingIndicator "
case . removeLoadingIndicator :
return " removeLoadingIndicator "
case . loadAllError ( let error , let token ) :
return " loadAllError( \( error ) , \( token ) ) "
case . replaceAllItems ( _ , let token ) :
2023-05-11 19:11:43 +00:00
return " replaceAllItems(<omitted>, \( token ) ) "
2022-10-08 19:12:10 +00:00
case . loadNewerError ( let error , let token ) :
return " loadNewerError( \( error ) , \( token ) ) "
case . prependItems ( _ , let token ) :
return " prependItems(<omitted>, \( token ) ) "
case . loadOlderError ( let error , let token ) :
return " loadOlderError( \( error ) , \( token ) ) "
case . appendItems ( _ , let token ) :
return " appendItems(<omitted>, \( token ) ) "
2022-11-18 22:29:55 +00:00
case . loadGapError ( let error , let direction , let token ) :
return " loadGapError( \( error ) , \( direction ) , \( token ) ) "
case . fillGap ( _ , let direction , let token ) :
return " loadGapError(<omitted>, \( direction ) , \( token ) ) "
2022-10-08 19:12:10 +00:00
}
}
2022-09-24 14:49:06 +00:00
}
2024-01-27 20:48:58 +00:00
final class LoadAttemptToken : Equatable , Sendable {
2022-09-24 14:49:06 +00:00
static func = = ( lhs : LoadAttemptToken , rhs : LoadAttemptToken ) -> Bool {
return lhs = = = rhs
}
}
2022-11-19 01:49:15 +00:00
}
enum TimelineGapDirection {
// / F i l l i n b e l o w t h e g a p . I . e . , s t a t u s e s t h a t a r e i m m e d i a t e l y n e w e r t h a n t h e s t a t u s b e l o w t h e g a p .
case below
// / F i l l i n a b o v e t h e g a p . I . e . , s t a t u s e s t h a t a r e i m m e d i a t e l y o l d e r t h a n t h e s t a t u s a b o v e t h e g a p .
case above
2022-12-05 23:43:32 +00:00
var accessibilityLabel : String {
switch self {
case . below :
return " Newer "
case . above :
return " Older "
}
}
2022-09-24 14:49:06 +00:00
}
2023-10-20 01:21:50 +00:00
// I w o u l d l o v e t o b e a b l e t o d o t h i s w i t h @ O b s e r v a b l e , b u t i t ' s n o t c l e a r h o w t o d o s o .
@ propertyWrapper
private class AsyncObservable < Value > : ObservableObject {
@ Published var wrappedValue : Value
var projectedValue : AsyncPublisher < Published < Value > . Publisher > {
$ wrappedValue . values
}
init ( wrappedValue : Value ) {
self . wrappedValue = wrappedValue
}
}