Skip to content

Cache decoded images by file URL in FileImageLoader#6697

Merged
JZDesign merged 5 commits into
jzdesign/prevent-redrawsfrom
pallares/cache-decoded-images
Apr 24, 2026
Merged

Cache decoded images by file URL in FileImageLoader#6697
JZDesign merged 5 commits into
jzdesign/prevent-redrawsfrom
pallares/cache-decoded-images

Conversation

@ajpallares

@ajpallares ajpallares commented Apr 24, 2026

Copy link
Copy Markdown
Member

Cache UIImage/NSImage results in FileImageLoader so repeated RemoteImage lookups for the same file URL don't re-decode on every access. Backed by an NSCache (evicts under memory pressure) with a concurrent queue + barrier writes for thread safety.

Noticeable on paywalls with many RemoteImage instances (e.g. looping carousels) where UIImage(contentsOfFile:) was being called hundreds of times for only a handful of unique URLs.

Made with Cursor


Note

Medium Risk
Moderate risk: introduces new process-wide image cache and changes reward-verification status models to carry reward payloads, which could affect UI memory usage and internal ad-reward verification behavior if decoding/mapping is incorrect.

Overview
File image loading now reuses decoded images across view lifecycles. FileImageLoader routes URL decoding through a new DecodedImageCache (NSCache + concurrent queue) so repeated UIImage/NSImage decoding for the same file URL is avoided.

Reward verification is upgraded to return a typed reward payload when verified. The backend response and polling APIs change from verified to verified(VerifiedReward) with new VerifiedReward/VirtualCurrencyReward types, stricter decoding + warning logs for malformed/unsupported reward payloads, and updated unit tests.

Paywalls V2 button actions add workflowTrigger. Button component decoding/view-model analytics include the new action, cache warming ignores it, and tests validate decoding/interaction value behavior.

Removes the .github/workflows/claude.yml GitHub Actions workflow.

Reviewed by Cursor Bugbot for commit b2f7026. Bugbot is set up for automated code reviews on this repo. Configure here.

facumenzella and others added 4 commits April 24, 2026 15:51
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Decode reward payload in RewardVerification poll response

Plumbs the verified-reward payload from the backend response through the
internal `pollRewardVerificationStatus` SPI so RC ad adapters can dispatch
meaningful "verified" events with the granted reward.

* Adds `VirtualCurrencyReward` and `VerifiedReward` (`@_spi(Internal) public`)
  under `Sources/Ads/RewardVerification/`.
* Extends `RewardVerificationStatusResponse` to optionally decode a typed nested
  reward payload (`{ type, code, amount }`) on `verified` responses.
  - `reward` absent or null → `.noReward`
  - `type == "virtual_currency"` with valid `code`+`amount` → `.virtualCurrency`
  - any other `type` (or malformed `virtual_currency`) → `.unsupportedReward` + warn
* `RewardVerificationPollStatus.verified` now carries the `VerifiedReward`.
* `Purchases.pollRewardVerificationStatus` returns `.verified(reward)`.
* Test mocks injected via a new `BasePurchasesTests.MockBackend` convenience
  init so `backend.adsAPI` resolves to `MockAdsAPI`.

* Add unit tests for VerifiedReward and VirtualCurrencyReward

Co-locates the unit tests next to the types this PR introduces under
`Sources/Ads/RewardVerification/`:

- `Tests/UnitTests/Ads/RewardVerification/VirtualCurrencyRewardTests.swift`
  Covers field storage, equality (both fields must match), and decimal
  precision preservation.

- `Tests/UnitTests/Ads/RewardVerification/VerifiedRewardTests.swift`
  Covers `.virtualCurrency(_)` associated-value carrying, `.noReward`
  vs `.unsupportedReward` distinct-case identity, equality (matching
  associated reward required), and an exhaustive switch coverage test
  to guard against silently adding cases.

Both tests pass against the existing `Sources/Ads/RewardVerification/`
types and require no changes to them.

(Originally written on top of #6663; moved to this base PR so the type
and its tests ship together rather than splitting across PRs.)

* fix: warn when reward verification reward value is not a JSON object

Previously, `decodeVerifiedReward` silently returned `.noReward` for any
of: missing reward key, null reward, or non-object reward value (e.g. a
string/number/array). The first two are expected; the last indicates a
backend mismatch and should be surfaced.

Now distinguish:
- absent or null reward → `.noReward` (silent, expected)
- present but not a JSON object → `.unsupportedReward` + `Logger.warn`

This matches the existing convention in this file of logging warnings
when decoding unrecognized/malformed values into fallback cases.

* Fix virtual currency reward amount type to integer.

Align reward payload modeling with backend semantics by decoding `amount` as Int and updating tests to reject fractional values as malformed rewards.

* fix: address review — couple verified status to reward

Make the reward payload part of the verified status case so invalid state combinations are unrepresentable and remove the verified fallback mapping.

* fix: address review — reject non-positive reward amounts

Treat virtual-currency rewards with non-positive amounts as malformed payloads and return unsupported reward with warning logs.

* fix: address review — reject empty reward currency code

Validate virtual-currency payloads require a non-empty code and treat empty code values as malformed unsupported rewards.

* fix: address review — remove non-actionable enum switch test

Drop the exhaustive switch smoke test in VerifiedRewardTests since it did not validate behavior and duplicated compiler guarantees.

* fix: address review — remove synthesized equatable assertions

Drop VerifiedReward tests that only mirrored compiler-synthesized Equatable behavior and keep behavior-focused coverage.
* Add workflowTrigger case to ButtonComponent.Action

Mirrors Android's rename of Action.Workflow → Action.WorkflowTrigger
(purchases-android#3380). Decodes backend "type": "workflow" into
.workflowTrigger so workflow buttons are rendered (previously decoded
as .unknown and hidden) and track as "workflow_trigger" in analytics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add tests for workflowTrigger action decoding and analytics value

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
`URL.asImageAndSize` (used by `FileImageLoader.loadFromCache`) previously
called `UIImage(contentsOfFile:)` / `NSImage(contentsOfFile:)` on every
access. A SwiftUI paywall with many `RemoteImage` instances (e.g. a looping
carousel that materialises multiple copies of each page) ends up hitting
this path hundreds of times because `StateObject(wrappedValue:)` evaluates
its closure on every host view re-init. UIKit/AppKit's internal caches
still cost ~0.5–1 ms per call, which adds up to ~100+ ms of main-thread
blocking for only a handful of unique URLs.

Introduce a process-wide `DecodedImageCache` backed by `NSCache<NSURL, _>`
so repeated lookups for the same URL become a dictionary hit. `NSCache`
evicts on memory pressure, so we don't pin images forever. Reads/writes
are serialised through a concurrent dispatch queue with a barrier on
writes to keep the cache thread-safe.

Made-with: Cursor
@ajpallares ajpallares force-pushed the pallares/cache-decoded-images branch from ca9e1ca to b2f7026 Compare April 24, 2026 16:23
Comment on lines +113 to +115
static let shared = DecodedImageCache()

private let cache = NSCache<NSURL, Entry>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine. Simplifies things a lot.

@JZDesign JZDesign changed the base branch from main to jzdesign/prevent-redraws April 24, 2026 19:56
@JZDesign JZDesign marked this pull request as ready for review April 24, 2026 19:56
@JZDesign JZDesign requested review from a team as code owners April 24, 2026 19:56
@JZDesign JZDesign merged commit 5c9d66b into jzdesign/prevent-redraws Apr 24, 2026
16 of 18 checks passed
@JZDesign JZDesign deleted the pallares/cache-decoded-images branch April 24, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants