Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Paywalls V2] Prefetch low res images #4658

Merged
merged 6 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
03C72FC22D349BAE00297FEC /* IconComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C72FC02D349BAE00297FEC /* IconComponentViewModel.swift */; };
03C72FC32D349BAE00297FEC /* IconComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C72FBF2D349BAE00297FEC /* IconComponentView.swift */; };
03C7305B2D35985900297FEC /* TextComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C7305A2D35985500297FEC /* TextComponentTests.swift */; };
03C730F32D35F14600297FEC /* PaywallV2CacheWarming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C730F22D35F13F00297FEC /* PaywallV2CacheWarming.swift */; };
03E37BEA2D30B32200CD9678 /* PaywallTabsComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E37BE92D30B32200CD9678 /* PaywallTabsComponent.swift */; };
03E37BED2D30B73400CD9678 /* TabsComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E37BEC2D30B72E00CD9678 /* TabsComponentViewModel.swift */; };
03E37BEF2D30B73A00CD9678 /* TabsComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E37BEE2D30B73800CD9678 /* TabsComponentView.swift */; };
Expand Down Expand Up @@ -1279,6 +1280,7 @@
03C72FBF2D349BAE00297FEC /* IconComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconComponentView.swift; sourceTree = "<group>"; };
03C72FC02D349BAE00297FEC /* IconComponentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconComponentViewModel.swift; sourceTree = "<group>"; };
03C7305A2D35985500297FEC /* TextComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextComponentTests.swift; sourceTree = "<group>"; };
03C730F22D35F13F00297FEC /* PaywallV2CacheWarming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallV2CacheWarming.swift; sourceTree = "<group>"; };
03E37BE92D30B32200CD9678 /* PaywallTabsComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallTabsComponent.swift; sourceTree = "<group>"; };
03E37BEC2D30B72E00CD9678 /* TabsComponentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsComponentViewModel.swift; sourceTree = "<group>"; };
03E37BEE2D30B73800CD9678 /* TabsComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsComponentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4844,6 +4846,7 @@
88AD010B2C740CF400AA1F2B /* Components */ = {
isa = PBXGroup;
children = (
03C730F22D35F13F00297FEC /* PaywallV2CacheWarming.swift */,
2CC791612CC0493600FBE120 /* Common */,
7707A94B2CAD93AC006E0313 /* PaywallButtonComponent.swift */,
88AD01032C740CF400AA1F2B /* PaywallImageComponent.swift */,
Expand Down Expand Up @@ -5981,6 +5984,7 @@
1ED4CA492CC148CF0021AB8F /* PostRedeemWebPurchaseOperation.swift in Sources */,
2D00A41D2767C08300FC3DD8 /* ManageSubscriptionsStrings.swift in Sources */,
2DD9F4BE274EADC20031AE2C /* Purchases+async.swift in Sources */,
03C730F32D35F14600297FEC /* PaywallV2CacheWarming.swift in Sources */,
B3C4AAD526B8911300E1B3C8 /* Backend.swift in Sources */,
35AAEB452BBB14D000A12548 /* DiagnosticsFileHandler.swift in Sources */,
B34D2AA626976FC700D88C3A /* ErrorCode.swift in Sources */,
Expand Down
141 changes: 141 additions & 0 deletions Sources/Paywalls/Components/PaywallV2CacheWarming.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// PaywallV2CacheWarming.swift
//
// Created by Josh Holtz on 1/13/25.

import Foundation

#if PAYWALL_COMPONENTS

extension PaywallComponentsData {

var allImageURLs: [URL] {
var imageUrls = self.componentsConfig.base.allImageURLs

for (_, localeValues) in self.componentsLocalizations {
for (_, value) in localeValues {
switch value {
case .string:
break
case .image(let image):
imageUrls += image.imageUrls
}
}
}
Comment on lines +23 to +32
Copy link
Member

Choose a reason for hiding this comment

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

This made me think: should all images be in componentsLocalizations, including the "base" images?

Copy link
Member Author

Choose a reason for hiding this comment

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

I had a reason against putting the base image in localization at one point and it made sense so I'm going to say... no. But now I need to remember that good argument that I had 😛

But TBH, I did think the same thing as you while writing this code how that would be easier but... we'd have to do the same thing for icons anyway 🤷‍♂️


return imageUrls
}

}

extension PaywallComponentsData.PaywallComponentsConfig {

var allImageURLs: [URL] {
let rootStackImageURLs = self.collectAllImageURLs(in: self.stack)
let stickFooterImageURLs = self.stickyFooter.flatMap { self.collectAllImageURLs(in: $0.stack) } ?? []

return rootStackImageURLs + stickFooterImageURLs
}

// swiftlint:disable:next cyclomatic_complexity
private func collectAllImageURLs(in stack: PaywallComponent.StackComponent) -> [URL] {
Copy link
Member

Choose a reason for hiding this comment

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

Traversing the tree of components is not the most performant, especially since we're already doing that to build the paywall. But I guess we need to do it in a separate traversal, because if we do it at the same time as building the paywall it's too late? (It wouldn't be prefetch anymore ha.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeaahhhh, I figured this could be improved in the future at some point if we wanted 🤷‍♂️ The thing this does differently than paywalls is that it doesn't load the entire view models and very localization stuff. Its pretty specific in how/what its traversing.

But tbh... precaching a bunch of images is LESS performant than traversing this free but its leading to better performance of the paywall 😛 🥴

Copy link
Member

Choose a reason for hiding this comment

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

That's true, good perspective!


var urls: [URL] = []
for component in stack.components {
switch component {
case .text:
()
case .icon(let icon):
guard let baseUrl = URL(string: icon.baseUrl) else {
break
}

urls += icon.formats.imageUrls(base: baseUrl)
urls += icon.overrides?.imageUrls(base: baseUrl) ?? []
case .image(let image):
urls += [
image.source.light.heicLowRes,
image.source.dark?.heicLowRes
].compactMap { $0 }

if let overides = image.overrides {
urls += [
overides.introOffer?.source?.imageUrls ?? [],
overides.states?.selected?.source?.imageUrls ?? [],
overides.conditions?.compact?.source?.imageUrls ?? [],
overides.conditions?.medium?.source?.imageUrls ?? [],
overides.conditions?.expanded?.source?.imageUrls ?? []
].flatMap { $0 }
}
case .stack(let stack):
urls += self.collectAllImageURLs(in: stack)
case .button(let button):
urls += self.collectAllImageURLs(in: button.stack)
case .package(let package):
urls += self.collectAllImageURLs(in: package.stack)
case .purchaseButton(let purchaseButton):
urls += self.collectAllImageURLs(in: purchaseButton.stack)
case .stickyFooter(let stickyFooter):
urls += self.collectAllImageURLs(in: stickyFooter.stack)
case .tabs(let tabs):
for tab in tabs.tabs {
urls += self.collectAllImageURLs(in: tab.stack)
}
case .tabControl:
break
case .tabControlButton(let controlButton):
urls += self.collectAllImageURLs(in: controlButton.stack)
case .tabControlToggle:
break
}
}

return urls
}

}

extension PaywallComponent.IconComponent.Formats {

func imageUrls(base: URL) -> [URL] {
return [
base.appendingPathComponent(heic)
]
}

}

extension PaywallComponent.ComponentOverrides where T == PaywallComponent.PartialIconComponent {

func imageUrls(base: URL) -> [URL] {
return [
self.introOffer?.formats?.imageUrls(base: base) ?? [],
self.states?.selected?.formats?.imageUrls(base: base) ?? [],
self.conditions?.compact?.formats?.imageUrls(base: base) ?? [],
self.conditions?.medium?.formats?.imageUrls(base: base) ?? [],
self.conditions?.expanded?.formats?.imageUrls(base: base) ?? []
].flatMap { $0 }
}

}

private extension PaywallComponent.ThemeImageUrls {

var imageUrls: [URL] {
return [
self.light.heicLowRes,
self.dark?.heicLowRes
].compactMap { $0 }
}

}

#endif
34 changes: 34 additions & 0 deletions Sources/Paywalls/PaywallCacheWarming.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ actor PaywallCacheWarming: PaywallCacheWarmingType {
Logger.error(Strings.paywalls.error_prefetching_image(url, error))
}
}

}

}
Expand Down Expand Up @@ -150,7 +151,21 @@ private extension Offerings {
)
}

#if PAYWALL_COMPONENTS

var allImagesInPaywalls: Set<URL> {
return self.allImagesInPaywallsV1 + self.allImagesInPaywallsV2
}

#else

var allImagesInPaywalls: Set<URL> {
return self.allImagesInPaywallsV1
}

#endif

private var allImagesInPaywallsV1: Set<URL> {
return .init(
self
.offeringsToPreWarm
Expand All @@ -160,6 +175,25 @@ private extension Offerings {
)
}

#if PAYWALL_COMPONENTS

private var allImagesInPaywallsV2: Set<URL> {
// Attempting to warm up all low res images for all offerings for Paywalls V2.
// Paywalls V2 paywall are explicitly published so anything that
// is here is intended to be displayed.
// Also only prewarming low res urls
return .init(
self
.all
.values
.lazy
.compactMap(\.paywallComponents)
.flatMap(\.data.allImageURLs)
)
}

#endif

}

private extension Offering {
Expand Down