fix(client/apple): a fallback banner sized the poster tile it was supposed to be cropped into
Reported as the library "breaking" on iOS whenever a title showed a banner instead of a cover: the tile expanded out of its bounds. `PosterImage` already meant to prevent exactly this — its own comment said a banner "would otherwise report a much wider intrinsic size than the card and overflow into neighboring cards" — and the guard it used, a flexible frame plus `.clipped()`, does not do it. `scaledToFill` answers a proposal with a size that COVERS it, i.e. larger; `.frame(maxWidth: .infinity, maxHeight: .infinity)` then clamps that answer to infinity, which is no clamp at all, so the image's size propagates straight up through `aspectRatio(2/3, .fit)` and decides the tile. Measured offscreen with ImageRenderer, one LazyVGrid column at 170pt: 460x215 header, before poster 1750.9 x 818.4 tile 545.6 x 255.0 460x215 header, after poster 170.0 x 255.0 tile 170.0 x 255.0 300x450 cover, either poster 170.0 x 255.0 tile 170.0 x 255.0 A 545pt tile in a 170pt column is what the report is describing, and the cover case coming out right either way is why this only ever showed on the titles whose cover was missing. The fix gives the sizing role to something that has no opinion about it: `Color.clear` takes the proposal, and the art rides as its overlay, where it can be drawn but never measured. `LibraryCoverflowView` reuses `PosterImage` directly, so it is fixed by the same change.
This commit is contained in:
@@ -63,9 +63,10 @@ private extension Image {
|
||||
/// Sequentially tries cover-art URLs over `loader` (so a paired client can reach the host's own
|
||||
/// art proxy, not just public CDNs — see `LibraryArtLoader`), advancing past any that fail to
|
||||
/// load, then a placeholder. The loaded image is hard-clipped to fill the card's actual frame
|
||||
/// regardless of its own aspect ratio: a portrait capsule fills it as intended, but a fallback
|
||||
/// banner (wide hero/header art, used when a title has no portrait capsule) would otherwise report
|
||||
/// a much wider intrinsic size than the card and overflow into neighboring cards. Not `private` —
|
||||
/// regardless of its own aspect ratio: a portrait capsule fills it as intended, and a fallback
|
||||
/// banner (wide hero/header art, used when a title has no portrait capsule) is cropped to the same
|
||||
/// tile rather than allowed to size it — see the `Color.clear` in `body` for why that takes more
|
||||
/// than a `.frame(maxWidth:)` and a `.clipped()`. Not `private` —
|
||||
/// the gamepad coverflow (`LibraryCoverflowView`) reuses it directly rather than re-fetching art.
|
||||
struct PosterImage: View {
|
||||
let candidates: [URL]
|
||||
@@ -84,9 +85,20 @@ struct PosterImage: View {
|
||||
var body: some View {
|
||||
Group {
|
||||
if let image {
|
||||
Image(platformImage: image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
// `Color.clear` is what takes the proposed size; the art rides along as its
|
||||
// overlay, where it can be DRAWN but never MEASURED. Handing the image the sizing
|
||||
// role instead is what let a fallback banner escape the tile: `scaledToFill`
|
||||
// reports a size that covers the proposal, and the flexible frame below clamps it
|
||||
// to `.infinity` — i.e. not at all. Measured offscreen, a 460×215 `header.jpg` in a
|
||||
// 170pt grid column resolved the tile to 545×255 and overran its neighbours, while
|
||||
// a 300×450 cover in the same chain came out correct — which is why this only ever
|
||||
// showed on the titles whose cover was missing.
|
||||
Color.clear
|
||||
.overlay {
|
||||
Image(platformImage: image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
}
|
||||
.transition(.opacity)
|
||||
} else if index < candidates.count {
|
||||
ZStack { placeholder; ProgressView() }
|
||||
|
||||
Reference in New Issue
Block a user