Cache decoded images by file URL in FileImageLoader#6697
Merged
JZDesign merged 5 commits intoApr 24, 2026
Conversation
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
ca9e1ca to
b2f7026
Compare
JZDesign
reviewed
Apr 24, 2026
Comment on lines
+113
to
+115
| static let shared = DecodedImageCache() | ||
|
|
||
| private let cache = NSCache<NSURL, Entry>() |
Contributor
There was a problem hiding this comment.
I think this is fine. Simplifies things a lot.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cache
UIImage/NSImageresults inFileImageLoaderso repeatedRemoteImagelookups for the same file URL don't re-decode on every access. Backed by anNSCache(evicts under memory pressure) with a concurrent queue + barrier writes for thread safety.Noticeable on paywalls with many
RemoteImageinstances (e.g. looping carousels) whereUIImage(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.
FileImageLoaderroutes URL decoding through a newDecodedImageCache(NSCache+ concurrent queue) so repeatedUIImage/NSImagedecoding 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
verifiedtoverified(VerifiedReward)with newVerifiedReward/VirtualCurrencyRewardtypes, 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.ymlGitHub Actions workflow.Reviewed by Cursor Bugbot for commit b2f7026. Bugbot is set up for automated code reviews on this repo. Configure here.