Skip to content

Commit deae57e

Browse files
LYJW131claude
andcommitted
fix(artwork): let the overview row borrow the detail page's cover
A job added from the share extension opens on `amdl://download/<id>`, which pushes the detail page straight onto the navigation stack — the overview list never appears, so nothing ever requested its 256px cover. Backing out of the detail page therefore landed on a row holding the type placeholder until that fetch finished, even though the full-size cover was already decoded and in the cache. The two pages cache under different keys because they request different sizes, and `fallbackCacheKey` only ever resolved one way: detail borrowing the overview's small image. It is now symmetric — whichever size is missing borrows the other. The row shows the hero image on its first frame (the memory-cache lookup in `CachedAsyncImage.init` is synchronous), then swaps in the 256 without a fade, because the fade keys on nil-to-non-nil and the slot was never empty. Nothing new is downloaded: this reuses an image already in the cache rather than prefetching a second one ahead of time. `fallbackCacheKey` moved onto `JobArtworkLoader` so the direction it picks is testable, and the literal 256 became `overviewPixelSize` — it is part of a cache key, not just a request size. Private playlists are unaffected: both sizes share one unsized key, the two lookups return the same string, and the fallback stays nil rather than having a view borrow from itself. Signed-off-by: LYJW131 <lyjw2007@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: LYJW131 <lyjw2007@gmail.com>
1 parent 97b4d0b commit deae57e

2 files changed

Lines changed: 71 additions & 5 deletions

File tree

amdl-ios/DownloadArtworkView.swift

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import UIKit
88

99
@MainActor
1010
enum JobArtworkLoader {
11+
/// 概览列表那档尺寸。行里的槽位只有 56pt,但两个页面各存各的一份,所以它
12+
/// 同时是一个缓存 key 的组成部分 —— 改动它会让所有已缓存的列表封面失效。
13+
static let overviewPixelSize = 256
14+
1115
/// 与详情页 Hero 封面一致:按显示原生像素的 2 倍请求并缓存大图。
1216
static var heroPixelSize: Int {
1317
let windowScenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
@@ -29,6 +33,24 @@ enum JobArtworkLoader {
2933
return ""
3034
}
3135

36+
/// 本尺寸还没到位时可以先顶上的另一档尺寸的 key,没有可借的就是 nil。
37+
///
38+
/// **两个方向都要**:
39+
///
40+
/// - 概览 → 详情:大图还在下,先用列表里那张小的,免得 zoom 转场结束后短暂
41+
/// 或永久露出任务类型占位图。
42+
/// - 详情 → 概览:分享拓展和完成通知都是 `amdl://download/<id>` 深链,直接把
43+
/// 详情页压进导航栈,概览列表一次都没出现过 —— 那张 256 于是从来没人取过。
44+
/// 退回列表时若不借详情页已经下好的大图,就得从占位图重新等一次。
45+
///
46+
/// 私人歌单两边共用一个不带尺寸的 key,两次算出来是同一个,这里返回 nil,
47+
/// 不会自己借自己。
48+
static func fallbackCacheKey(for job: Job, pixelSize: Int) -> String? {
49+
let otherPixelSize = pixelSize == overviewPixelSize ? heroPixelSize : overviewPixelSize
50+
let otherKey = cacheKey(for: job, pixelSize: otherPixelSize)
51+
return otherKey == cacheKey(for: job, pixelSize: pixelSize) ? nil : otherKey
52+
}
53+
3254
static func prefetch(job: Job, pixelSize: Int) async {
3355
let primaryURL = job.artworkURL(pixelSize: pixelSize)
3456
if let request = PrivatePlaylistArtworkStore.request(for: job, pixelSize: pixelSize) {
@@ -49,7 +71,7 @@ enum JobArtworkLoader {
4971
/// 其他任务直接使用后端的 artwork_url。
5072
struct JobArtworkView: View {
5173
let job: Job
52-
var pixelSize: Int = 256
74+
var pixelSize: Int = JobArtworkLoader.overviewPixelSize
5375

5476
@State private var fallbackURL: URL?
5577
@State private var artworkRevision = 0
@@ -70,11 +92,8 @@ struct JobArtworkView: View {
7092
JobArtworkLoader.cacheKey(for: job, pixelSize: pixelSize)
7193
}
7294

73-
/// 详情大图尚未准备好时先复用概览封面的缓存,避免 zoom 动画结束后
74-
/// 短暂或永久露出任务类型占位图。
7595
private var fallbackCacheKey: String? {
76-
let overviewKey = JobArtworkLoader.cacheKey(for: job, pixelSize: 256)
77-
return overviewKey == cacheKey ? nil : overviewKey
96+
JobArtworkLoader.fallbackCacheKey(for: job, pixelSize: pixelSize)
7897
}
7998

8099
var body: some View {

amdl-iosTests/amdl_iosTests.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,53 @@ struct amdl_iosTests {
7575
try assert(small.imageCacheKey != large.imageCacheKey, "template image cache should retain size")
7676
}
7777

78+
/// 概览和详情各存各的尺寸,所以谁先拿到图,另一边都得能先借来顶上。
79+
///
80+
/// 借的方向以前只有一个:详情借概览。分享拓展和完成通知走 `amdl://download/<id>`
81+
/// 深链直接进详情页,概览列表压根没出现过,那张 256 从来没人取过 —— 退回列表
82+
/// 时没得借,就得从占位图重新等一次。
83+
@Test @MainActor func artworkFallsBackBetweenOverviewAndHeroSizes() throws {
84+
let job = try DownloadsAPI.decodeDownloadDetail(from: """
85+
{
86+
"job": {
87+
"id": "job_art", "input": "https://music.apple.com/cn/album/example/1", "type": "album",
88+
"force": false, "status": "running", "total_items": 1, "done_items": 0, "failed_items": 0,
89+
"artwork_url": "https://is1-ssl.mzstatic.com/image/thumb/x/{w}x{h}bb.jpg",
90+
"created_at": "2026-07-30T00:00:00Z", "updated_at": "2026-07-30T00:00:00Z"
91+
},
92+
"items": []
93+
}
94+
""".data(using: .utf8)!).job
95+
96+
let overview = JobArtworkLoader.overviewPixelSize
97+
let hero = JobArtworkLoader.heroPixelSize
98+
try assert(hero != overview, "hero and overview must be different sizes for this to matter")
99+
100+
let overviewKey = JobArtworkLoader.cacheKey(for: job, pixelSize: overview)
101+
let heroKey = JobArtworkLoader.cacheKey(for: job, pixelSize: hero)
102+
try assert(overviewKey != heroKey, "each size caches separately")
103+
104+
// 详情 → 概览:这一条以前是 nil,正是深链进来后退回列表要等图的原因。
105+
try assert(
106+
JobArtworkLoader.fallbackCacheKey(for: job, pixelSize: overview) == heroKey,
107+
"overview borrows the hero image"
108+
)
109+
// 概览 → 详情:原有方向,不能改坏。
110+
try assert(
111+
JobArtworkLoader.fallbackCacheKey(for: job, pixelSize: hero) == overviewKey,
112+
"hero borrows the overview image"
113+
)
114+
115+
// 私人歌单两档尺寸共用一个 key,没有另一份可借,不能自己借自己。
116+
let privateJob = privatePlaylistJob(
117+
artworkURL: "https://example-bucket.s3.amazonaws.com/cover.jpg?X-Amz-Expires=86400"
118+
)
119+
try assert(
120+
JobArtworkLoader.fallbackCacheKey(for: privateJob, pixelSize: overview) == nil,
121+
"a shared cache key has nothing to borrow"
122+
)
123+
}
124+
78125
@Test func downloadDetailDecodesJobItems() throws {
79126
let json = """
80127
{

0 commit comments

Comments
 (0)