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: improve image caching #3498

Merged
merged 12 commits into from
Dec 12, 2023
Merged

Paywalls: improve image caching #3498

merged 12 commits into from
Dec 12, 2023

Conversation

NachoSoto
Copy link
Contributor

Supersedes #3477.

This replaces AsyncImage with a custom implementation that allows us to ensure caching works reliably by explicitly using a custom URLSession with a shared URLCache.

expect(self.urlSession.cachePolicy) == .returnCacheDataElseLoad
}

func testLoadingImageResetsPreviousResult() async throws {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test is pretty complicated but it was the simplest way I could confirm that the image is reset before loading the new one, without any race conditions.

Copy link
Member

Choose a reason for hiding this comment

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

makes sense. At least the code is very clear

@NachoSoto NachoSoto force-pushed the nacho/image-caching branch 2 times, most recently from 1fd50ad to bffe57d Compare December 7, 2023 21:41

// Load images in a background thread
return await Task<Value, Never>
.detached(priority: .utility) { data.toImage() }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is new

Copy link
Member

Choose a reason for hiding this comment

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

Feels like utility might be a bit low?

I'd imagined we'd have the pre-warming loader use utility or background and then userInitiated default when loading the paywall, given the potential impact on conversion for an image taking too long to load, and that there's probably not a lot more going on that's more important than the image at the time of displaying a paywall.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The pre-warming is actually happening with .default QOS because that's what OperationDispatcher.workerQueue is set up with :/
We should talk about changing that, maybe having multiple queues.

But I agree, I'll change this to default.

return nil
}

if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is slightly different than the original implementation to allow responses for local files.

.data(for: .init(url: url, cachePolicy: .returnCacheDataElseLoad))

do {
try Task.checkCancellation()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This allows preventing the image from being decoded if the task is already cancelled.

Comment on lines +104 to +109
#if os(macOS)
if let image = NSImage(data: self) {
return .success(.init(nsImage: image))
} else {
return .failure(.invalidImage)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't support macOS yet, but this makes the class compile, and it'll be ready when we do.

Copy link
Member

Choose a reason for hiding this comment

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

great catch, I keep forgetting macOS

}
}
.transition(Self.transition)
.task(id: self.url) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This automatically cancels the task when the url changes.

Copy link
Member

Choose a reason for hiding this comment

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

🤔 let's maybe add a comment? Feels like that would be easy to miss

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's the canonical way of implementing task: making the ID whatever parameter the task depends on. But yeah I'll add a comment 👍🏻

Copy link

codecov bot commented Dec 8, 2023

Codecov Report

Attention: 10 lines in your changes are missing coverage. Please review.

Comparison is base (27138c4) 86.05% compared to head (cd6b37b) 86.01%.

Files Patch % Lines
Sources/Paywalls/PaywallCacheWarming.swift 0.00% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3498      +/-   ##
==========================================
- Coverage   86.05%   86.01%   -0.04%     
==========================================
  Files         237      237              
  Lines       17226    17231       +5     
==========================================
- Hits        14823    14821       -2     
- Misses       2403     2410       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@@ -16,6 +16,7 @@ import RevenueCat

// swiftlint:disable identifier_name

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
Copy link
Member

Choose a reason for hiding this comment

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

I realize it's not going to make a difference in practice, but our entire SDK is technically 6.2+ on watchOS, might as well set that as the min deployment target. Actually, cocoapods might complain if we don't? Not sure

Copy link
Member

Choose a reason for hiding this comment

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

and given that RCUI is 15+, maybe it'll complain further about that for all OS versions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops thanks, not sure where I got this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh this is because some method I needed from URLSession wasn't available until specifically these versions. So having a lower watchOS one doesn't matter, it's just overly available. But I'll update for consistency.


// Load images in a background thread
return await Task<Value, Never>
.detached(priority: .utility) { data.toImage() }
Copy link
Member

Choose a reason for hiding this comment

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

Feels like utility might be a bit low?

I'd imagined we'd have the pre-warming loader use utility or background and then userInitiated default when loading the paywall, given the potential impact on conversion for an image taking too long to load, and that there's probably not a lot more going on that's more important than the image at the time of displaying a paywall.

What do you think?

Comment on lines +104 to +109
#if os(macOS)
if let image = NSImage(data: self) {
return .success(.init(nsImage: image))
} else {
return .failure(.invalidImage)
}
Copy link
Member

Choose a reason for hiding this comment

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

great catch, I keep forgetting macOS

}
}
.transition(Self.transition)
.task(id: self.url) {
Copy link
Member

Choose a reason for hiding this comment

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

🤔 let's maybe add a comment? Feels like that would be easy to miss

expect(self.urlSession.cachePolicy) == .returnCacheDataElseLoad
}

func testLoadingImageResetsPreviousResult() async throws {
Copy link
Member

Choose a reason for hiding this comment

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

makes sense. At least the code is very clear

@aboedo
Copy link
Member

aboedo commented Dec 8, 2023

couple of small questions, looking great though

@NachoSoto
Copy link
Contributor Author

@aboedo all comments addressed.

@NachoSoto NachoSoto requested a review from a team December 11, 2023 04:16
Copy link
Contributor

@tonidero tonidero left a comment

Choose a reason for hiding this comment

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

LGTM!

@NachoSoto NachoSoto merged commit 59a877a into main Dec 12, 2023
19 of 20 checks passed
@NachoSoto NachoSoto deleted the nacho/image-caching branch December 12, 2023 15:24
NachoSoto pushed a commit that referenced this pull request Dec 12, 2023
**This is an automatic release.**

### RevenueCatUI
* `Paywalls`: improve image caching (#3498) via NachoSoto (@NachoSoto)
* `Paywalls`: change style of CTA button to be consistent with other
platforms (#3507) via NachoSoto (@NachoSoto)
* `Paywalls`: open footer links on Safari on Catalyst (#3508) via
NachoSoto (@NachoSoto)
* `Paywalls`: fix compilation on Xcode < 14.3 (#3513) via NachoSoto
(@NachoSoto)
* Fix typo in zh-Hans localization of RevenueCatUI (#3512) via Francis
Feng (@francisfeng)
* `Paywalls`: fix PaywallViewControllerDelegate access from Objective-C
(#3510) via noncenz (@noncenz)
* `Paywalls`: open Terms and Privacy Policy links in-app (#3475) via
Andy Boedo (@aboedo)
* `Paywalls`: fix empty description when using `custom` package type and
`{{ sub_period }}` (#3495) via Andy Boedo (@aboedo)
* `Paywalls`: use `HEIC` images (#3496) via NachoSoto (@NachoSoto)
* Paywalls: disable the close button when an action is in progress
(#3474) via Andy Boedo (@aboedo)
* `Paywalls`: adjusted German translations (#3476) via Tensei (@tensei)
* Paywalls: Improve Chinese localization (#3489) via Andy Boedo
(@aboedo)
### Other Changes
* `CircleCI`: add git credentials to snapshot generation (#3506) via
NachoSoto (@NachoSoto)
* `StoreProduct`: improve `priceFormatter` documentation (#3500) via
NachoSoto (@NachoSoto)
* `Paywalls`: fix tests (#3499) via NachoSoto (@NachoSoto)
* `Optimization`: changed `first` to `first(where:)` (#3467) via
NachoSoto (@NachoSoto)
@vegaro vegaro added pr:other and removed pr:perf labels Sep 17, 2024
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.

4 participants