Skip to content

Accept asset catalog entries in localResources#465

Open
yusuftor wants to merge 10 commits intodevelopfrom
feat/local-resources-asset-catalog
Open

Accept asset catalog entries in localResources#465
yusuftor wants to merge 10 commits intodevelopfrom
feat/local-resources-asset-catalog

Conversation

@yusuftor
Copy link
Copy Markdown
Collaborator

@yusuftor yusuftor commented Apr 24, 2026

Changes in this pull request

Generalises SuperwallOptions.localResources from [String: URL] to [String: AssetResource] so customers can register paywall assets that live inside an asset catalog (.xcassets), achieving parity with the Android SDK's PaywallResource.FromResources(R.drawable.*).

Customer ask (Pylon): customers whose assets live in .xcassets had no clean way to register them because asset catalog entries don't expose a file URL.

API

New AssetResource protocol is the dictionary's value type. Three conforming types:

  • URL — existing behavior, a file on disk. Conforms out of the box, so existing call sites that assign URL dictionaries compile and behave unchanged.
  • UIImage — register an in-memory image directly; served to the webview as image/png via pngData().
  • CatalogAsset(name:bundle:) — deferred lookup into an .xcassets. Tries UIImage(named:in:compatibleWith:) first (Image Set, re-encoded as PNG), then falls back to NSDataAsset(name:bundle:) (Data Set, raw bytes + UTI-derived MIME type). Handles both image assets and non-image Data Sets (video, Lottie JSON, etc.).
options.localResources = [
  "hero-image": Bundle.main.url(forResource: "hero", withExtension: "png")!,
  "logo":       UIImage(named: "Logo")!,
  "hero-video": CatalogAsset(name: "HeroVideo")
]

Implementation notes

  • LocalFileSchemeHandler switches on the resource type. UIImage and CatalogAsset's Image Set path resolve through UIImage.pngData(); CatalogAsset's Data Set path uses NSDataAsset with a UTI → MIME type conversion.
  • UTI → MIME uses UTType.preferredMIMEType on iOS 14+ and falls back to UTTypeCopyPreferredTagWithClass from MobileCoreServices on iOS 13, so Data Sets get a real MIME type across the supported deployment range.
  • ObjC keeps a URL-only shim under the same localResources name (@objc(localResources)). Asset catalog and UIImage entries are Swift-only.
  • SWLocalResourcesViewController (debug view) previews URL, UIImage, and catalog entries. Non-image catalog assets (e.g. video, JSON) show their UTI and byte count instead of a blank cell.
  • CatalogAsset tries UIImage(named:) before NSDataAsset so lookups against Image Sets don't trigger a CoreUI "wrong type" log warning.
  • Unrelated cleanup: dropped a stale trailing_closure lint violation in ConfigLogic.swift:310 while in the area.

Tests

Extends LocalFileSchemeHandlerTests:

  • URL registered through the unified dictionary still loads bytes and a MIME type.
  • UIImage registered directly is served as image/png with bytes matching pngData().
  • CatalogAsset whose name is missing from the bundle throws fileNotFound.
  • Existing 9 tests continue to pass with the new value type.

CatalogAsset's Data Set success path isn't covered because adding a real .xcassets to the test bundle is non-trivial; the code is small (NSDataAsset.data + UTI MIME lookup) and will be exercised end-to-end via the demo app.

Checklist

  • All unit tests pass (12/12 in LocalFileSchemeHandlerTests).
  • All UI tests pass.
  • Demo project builds and runs on iOS.
  • Demo project builds and runs on Mac Catalyst.
  • Demo project builds and runs on visionOS.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run swiftlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

🤖 Generated with Claude Code

Generalises SuperwallOptions.localResources from `[String: URL]` to
`[String: AssetResource]`. URL conforms to AssetResource so existing
call sites are unaffected. New `CatalogAsset(name:bundle:)` registers
a Data Set entry from an .xcassets, resolved at load time via
NSDataAsset — the iOS equivalent of Android's R.raw.* resource IDs.

ObjC keeps a URL-only shim under the same `localResources` name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread Sources/SuperwallKit/Debug/SWLocalResourcesViewController.swift
Comment thread Sources/SuperwallKit/Config/Options/AssetResource.swift
yusuftor and others added 3 commits April 24, 2026 16:25
`UTType.preferredMIMEType` is iOS 14+. On iOS 13, fall back to
`UTTypeCopyPreferredTagWithClass` from MobileCoreServices so catalog
assets are served with a real MIME type (an `<img>` or `<video>` is
refused by WKWebView when the MIME is `application/octet-stream`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SWLocalResourcesViewController: when a catalog asset exists but isn't
  image-decodable, show its UTI and byte count instead of a blank cell.
- AssetResource: remove the unused UIKit import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Satisfies the `trailing_closure` SwiftLint rule — the only remaining
project-level lint violation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yusuftor
Copy link
Copy Markdown
Collaborator Author

@greptileai

yusuftor and others added 5 commits April 24, 2026 16:47
- LocalFileSchemeHandler: remove unreferenced UIKit import.
- SuperwallOptions: ObjC setter now replaces only the URL subset of
  localResources, so CatalogAsset entries registered from Swift survive
  a subsequent ObjC assignment in mixed Swift/ObjC codebases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
localResources is documented as set once before configure(), so the
"mixed Swift/ObjC post-configure mutation" scenario the merge was
guarding against isn't part of the intended usage pattern. The setter
goes back to replacing the full dict.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NSDataAsset only resolves Data Sets, so a typical Image Set logo would
fail with "File not found". Try NSDataAsset first (lossless, any file
type), then fall back to UIImage(named:in:compatibleWith:)?.pngData()
so existing Image Sets work without restructuring the asset catalog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Calling NSDataAsset on an Image Set triggers a CoreUI log about a
wrong-typed lookup. Flip the order so UIImage(named:) runs first —
Image Sets resolve cleanly, and NSDataAsset is only reached when
there's no Image Set to mistype.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets customers register an in-memory image directly:
  "logo": UIImage(named: "Logo")!
Served to the webview as image/png via pngData(). Complements
CatalogAsset for cases where eager decoding is acceptable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yusuftor
Copy link
Copy Markdown
Collaborator Author

@greptileai

Comment on lines 265 to +279
}

private func configureCatalogAsset(id: String, catalog: CatalogAsset) {
idLabel.text = "\(id) (asset: \(catalog.name))"
spinner.startAnimating()
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let asset = NSDataAsset(name: catalog.name, bundle: catalog.bundle)
let image = asset.flatMap { UIImage(data: $0.data) }
DispatchQueue.main.async {
self?.spinner.stopAnimating()
if let image = image {
self?.imageView.image = image
} else if let asset = asset {
let byteCount = ByteCountFormatter.string(fromByteCount: Int64(asset.data.count), countStyle: .file)
self?.showErrorText("No preview\n\(asset.typeIdentifier) · \(byteCount)")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Image-Set CatalogAsset shows "Asset not found" in debug view

configureCatalogAsset loads asset data exclusively via NSDataAsset, but NSDataAsset only resolves Data Set entries — it returns nil for Image Sets. The scheme handler in LocalFileSchemeHandler.load(resource:key:) tries UIImage(named:in:compatibleWith:) first, so an Image Set CatalogAsset is served successfully in the webview, but the debug preview shows "Asset not found" because NSDataAsset finds nothing. The fix is to mirror the scheme handler's resolution order: try UIImage(named:) first, and only then fall back to NSDataAsset.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/SuperwallKit/Debug/SWLocalResourcesViewController.swift
Line: 265-279

Comment:
**Image-Set `CatalogAsset` shows "Asset not found" in debug view**

`configureCatalogAsset` loads asset data exclusively via `NSDataAsset`, but `NSDataAsset` only resolves *Data Set* entries — it returns `nil` for *Image Sets*. The scheme handler in `LocalFileSchemeHandler.load(resource:key:)` tries `UIImage(named:in:compatibleWith:)` first, so an Image Set CatalogAsset is served successfully in the webview, but the debug preview shows "Asset not found" because `NSDataAsset` finds nothing. The fix is to mirror the scheme handler's resolution order: try `UIImage(named:)` first, and only then fall back to `NSDataAsset`.

How can I resolve this? If you propose a fix, please make it concise.

Debug view was loading catalog assets via NSDataAsset only, so an
Image Set CatalogAsset previewed as "Asset not found" even though
the scheme handler resolved it successfully. Try UIImage(named:)
first, fall through to NSDataAsset for Data Sets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant