forked from davidskeck/FetchImage
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathScrollViewPrefetcher.swift
More file actions
97 lines (76 loc) · 3.34 KB
/
Copy pathScrollViewPrefetcher.swift
File metadata and controls
97 lines (76 loc) · 3.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
//
// The MIT License (MIT)
//
// Copyright (c) 2021 Alexander Grebenyuk (github.com/kean).
//
import SwiftUI
public protocol ScrollViewPrefetcherDelegate: AnyObject {
/// Returns all valid indices for the collection.
func getAllIndicesForPrefetcher(_ prefetcher: ScrollViewPrefetcher) -> Range<Int>
func prefetcher(_ prefetcher: ScrollViewPrefetcher, prefetchItemsAt indices: [Int])
func prefetcher(_ prefetcher: ScrollViewPrefetcher, cancelPrefechingForItemAt indices: [Int])
}
public final class ScrollViewPrefetcher {
private let prefetchWindowSize: Int
public weak var delegate: ScrollViewPrefetcherDelegate?
private var visibleIndices: Set<Int> = []
private var flushedVisibleIndices: Set<Int> = []
private var isRefreshScheduled = false
private var prefetchWindow = 0..<0
public init(prefetchWindowSize: Int = 12) {
self.prefetchWindowSize = prefetchWindowSize
}
public func onAppear(_ index: Int) {
visibleIndices.insert(index)
scheduleRefreshIfNeeded()
}
public func onDisappear(_ index: Int) {
visibleIndices.remove(index)
scheduleRefreshIfNeeded()
}
/// SwiftUI sometimes calls onAppear in unexpected order, buffer takes care of it.
///
internal func scheduleRefreshIfNeeded() {
guard !isRefreshScheduled else { return }
isRefreshScheduled = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in
self?.updatePrefetchWindow()
}
}
private func updatePrefetchWindow() {
isRefreshScheduled = false
guard !visibleIndices.isEmpty else { return } // Should never happen
if flushedVisibleIndices.isEmpty {
// Showing screen for the first time
let lowerBound = visibleIndices.max()! + 1
updatePrefetchWindow(lowerBound..<(lowerBound + prefetchWindowSize))
} else {
// Need to figure out in which direction we the user is scrolling
let isScrollingDown = visibleIndices.max()! > flushedVisibleIndices.max()!
if isScrollingDown || visibleIndices.contains(0) {
let lowerBound = visibleIndices.max()! + 1
updatePrefetchWindow(lowerBound..<(lowerBound + prefetchWindowSize))
} else {
let upperBound = visibleIndices.min()! - 1
updatePrefetchWindow((upperBound - prefetchWindowSize)..<upperBound)
}
}
flushedVisibleIndices = visibleIndices
}
private func updatePrefetchWindow(_ newPrefetchWindow: Range<Int>) {
let oldPrefetchIndices = Set(prefetchWindow)
let newPrefetchIndices = Set(newPrefetchWindow)
prefetchWindow = newPrefetchWindow
let allIndices = Set(delegate?.getAllIndicesForPrefetcher(self) ?? 0..<0)
let removedIndicides = oldPrefetchIndices
.subtracting(newPrefetchIndices)
.intersection(allIndices) // Only call for visible items
.sorted()
let addedIndices = newPrefetchIndices
.subtracting(oldPrefetchIndices)
.intersection(allIndices) // Only call for visible items
.sorted()
delegate?.prefetcher(self, prefetchItemsAt: addedIndices)
delegate?.prefetcher(self, cancelPrefechingForItemAt: removedIndicides)
}
}