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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import Foundation
import Nuke
struct PrioritizedRequests {
let high: [ImageRequest]
let normal: [ImageRequest]
let low: [ImageRequest]
var all: [ImageRequest] {
high + normal + low
}
}
enum ImagePrefetchPlanning {
// MARK: - Carousel
/// Tiered prefetch URLs for a gallery photo carousel.
///
/// - Page 0 (on appear): fullsize #0, #1 at high
/// - Page 1: fullsize #2 at high, all thumbs at normal
/// - Page 2+: next fullsize at high, remaining fullsize at normal
static func carouselPrefetchRequests(
photos: [(thumb: String, fullsize: String)],
currentPage: Int
) -> PrioritizedRequests {
guard !photos.isEmpty, currentPage >= 0, currentPage < photos.count else {
return PrioritizedRequests(high: [], normal: [], low: [])
}
var high: [ImageRequest] = []
var normal: [ImageRequest] = []
let low: [ImageRequest] = []
if currentPage == 0 {
// Prefetch first 2 fullsize
for i in 0 ..< min(2, photos.count) {
if let url = URL(string: photos[i].fullsize) {
high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
}
}
} else if currentPage == 1 {
// Fullsize #2 at high
if photos.count > 2, let url = URL(string: photos[2].fullsize) {
high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
}
// All thumbs at normal
for photo in photos {
if let url = URL(string: photo.thumb) {
normal.append(ImageRequest(url: url, priority: .normal))
}
}
} else {
// Page 2+: next fullsize at high, rest at normal
for i in (currentPage + 1) ..< photos.count {
if let url = URL(string: photos[i].fullsize) {
let priority: ImageRequest.Priority = i == currentPage + 1 ? .high : .normal
if priority == .high {
high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
} else {
normal.append(ImageRequest(url: url, priority: .normal, options: .disableDiskCacheWrites))
}
}
}
}
return PrioritizedRequests(high: high, normal: normal, low: low)
}
// MARK: - Feed
/// Prefetch first image (thumb + fullsize) of upcoming galleries in the feed.
///
/// - i+1: thumb at high, fullsize at high
/// - i+2: thumb at high, fullsize at normal
/// - i+3: thumb at high, fullsize at low
static func feedPrefetchRequests(
galleries: [(firstThumb: String?, firstFullsize: String?)],
currentIndex: Int
) -> PrioritizedRequests {
guard currentIndex >= 0, currentIndex < galleries.count else {
return PrioritizedRequests(high: [], normal: [], low: [])
}
var high: [ImageRequest] = []
var normal: [ImageRequest] = []
var low: [ImageRequest] = []
for offset in 1 ... 3 {
let idx = currentIndex + offset
guard idx < galleries.count else { break }
let gallery = galleries[idx]
// Thumbs are tiny — always high priority
if let thumb = gallery.firstThumb, let url = URL(string: thumb) {
high.append(ImageRequest(url: url, priority: .high))
}
if let fullsize = gallery.firstFullsize, let url = URL(string: fullsize) {
switch offset {
case 1:
high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
case 2:
normal.append(ImageRequest(url: url, priority: .normal, options: .disableDiskCacheWrites))
default:
low.append(ImageRequest(url: url, priority: .low, options: .disableDiskCacheWrites))
}
}
}
return PrioritizedRequests(high: high, normal: normal, low: low)
}
// MARK: - Stories
/// Prefetch story images with priority queue:
/// - Thumbs (all reachable authors) — high (tiny payload, used by the
/// StoryViewer's fullsize-loading placeholder; needs to be cache-hot to
/// avoid a 1-frame flash on transition)
/// 1. Next 2 stories of current author — fullsize at high
/// 2. First story of next author — fullsize at high
/// 3. Rest of current author's stack — fullsize at normal
/// 4. Next 2 stories of author-after-next — fullsize at normal
/// 5. First story of next 2 authors beyond — fullsize at low
static func storyPrefetchRequests(
currentStories: [(thumb: String, fullsize: String)],
currentStoryIndex: Int,
nextAuthorStories: [(thumb: String, fullsize: String)]?,
secondNextAuthorStories: [(thumb: String, fullsize: String)]?,
thirdNextFirstStory: (thumb: String, fullsize: String)?,
fourthNextFirstStory: (thumb: String, fullsize: String)?
) -> PrioritizedRequests {
var high: [ImageRequest] = []
var normal: [ImageRequest] = []
var low: [ImageRequest] = []
// Thumbs are tiny — prefetch every reachable thumb at high priority so
// the StoryViewer's fullsize-loading placeholder always hits the
// synchronous Nuke cache (mirrors `feedPrefetchRequests`' policy).
let allThumbStrings: [String] = currentStories.map(\.thumb)
+ (nextAuthorStories?.map(\.thumb) ?? [])
+ (secondNextAuthorStories?.map(\.thumb) ?? [])
+ [thirdNextFirstStory?.thumb, fourthNextFirstStory?.thumb].compactMap(\.self)
for thumbStr in allThumbStrings {
if let url = URL(string: thumbStr) {
high.append(ImageRequest(url: url, priority: .high))
}
}
// 1. Next 2 stories of current author — high
let storyEnd = min(currentStoryIndex + 3, currentStories.count)
for i in stride(from: currentStoryIndex + 1, to: storyEnd, by: 1) where i < currentStories.count {
if let url = URL(string: currentStories[i].fullsize) {
high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
}
}
// 2. First story of next author — high
if let first = nextAuthorStories?.first, let url = URL(string: first.fullsize) {
high.append(ImageRequest(url: url, priority: .high, options: .disableDiskCacheWrites))
}
// 3. Rest of current author's stack — normal
let restStart = currentStoryIndex + 3
if restStart < currentStories.count {
for i in restStart ..< currentStories.count {
if let url = URL(string: currentStories[i].fullsize) {
normal.append(ImageRequest(url: url, priority: .normal, options: .disableDiskCacheWrites))
}
}
}
// 4. Next 2 stories of author-after-next — normal
if let stories = secondNextAuthorStories {
for i in 0 ..< min(2, stories.count) {
if let url = URL(string: stories[i].fullsize) {
normal.append(ImageRequest(url: url, priority: .normal, options: .disableDiskCacheWrites))
}
}
}
// 5. First story of next 2 authors beyond — low
if let story = thirdNextFirstStory, let url = URL(string: story.fullsize) {
low.append(ImageRequest(url: url, priority: .low, options: .disableDiskCacheWrites))
}
if let story = fourthNextFirstStory, let url = URL(string: story.fullsize) {
low.append(ImageRequest(url: url, priority: .low, options: .disableDiskCacheWrites))
}
return PrioritizedRequests(high: high, normal: normal, low: low)
}
}