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

Fixes reloading paywall images after they've been scrolled off screen #4423

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
503b431
Adds RootComponent and StickyFooterComponent
JayShortway Oct 24, 2024
9c8bc3f
Removes RootComponent
JayShortway Oct 25, 2024
bea0f0e
RootView shows the root stack.
JayShortway Oct 25, 2024
8f4b258
RootView shows the root stack.
JayShortway Oct 25, 2024
6267557
RootView shows the sticky footer.
JayShortway Oct 25, 2024
0542778
Merge branch 'sticky-footer-component-1' into sticky-footer-component-2
JayShortway Oct 25, 2024
63319ca
Adds long sample paywall with sticky footer.
JayShortway Oct 25, 2024
d596de9
Merge branch 'main' into sticky-footer-component-2
JayShortway Oct 28, 2024
0c08831
Merge branch 'sticky-footer-component-2' into sticky-footer-component-3
JayShortway Oct 28, 2024
f689a30
Merge branch 'main' into sticky-footer-component-3
JayShortway Oct 28, 2024
c95c4ac
Fixes the sticky footer not drawing in the bottom safe area.
JayShortway Oct 29, 2024
54c0738
Merge branch 'main' into sticky-footer-component-3
JayShortway Oct 29, 2024
f5c4185
Merge branch 'sticky-footer-component-3' into sticky-footer-component-4
JayShortway Oct 29, 2024
f7e642e
ImageLoader doesn't reload if the URL hasn't changed.
JayShortway Oct 29, 2024
5ea6b3f
Merge branch 'main' into sticky-footer-component-3
JayShortway Oct 29, 2024
0b0d80f
Merge branch 'sticky-footer-component-3' into sticky-footer-component-4
JayShortway Oct 29, 2024
94486a7
Merge branch 'sticky-footer-component-4' into fix-image-reload-on-scroll
JayShortway Oct 29, 2024
b221fef
Rephrases a comment
JayShortway Oct 29, 2024
9165eb8
Fixes ImageLoaderTests and adds a new one
JayShortway Oct 30, 2024
d03a77e
Merge branch 'main' into fix-image-reload-on-scroll
JayShortway Oct 30, 2024
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
33 changes: 25 additions & 8 deletions RevenueCatUI/Helpers/ImageLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ final class ImageLoader: ObservableObject {

typealias Value = Result<(image: Image, size: CGSize), Error>

// We want to remember the URL used for a successful load, so we can avoid loading it again if we get asked for
// the same URL.
private var resultWithURL: ValueWithURL? {
didSet {
self.result = resultWithURL.map { result in
result.map { (image: $0.image, size: $0.size) }
}
}
}

@Published
private(set) var result: Value? {
didSet {
Expand All @@ -58,16 +68,21 @@ final class ImageLoader: ObservableObject {

@available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *)
func load(url: URL) async {
// Only reload if the new URL is different from the current one.
if case let .success((_, _, currentUrl))? = resultWithURL,
currentUrl == url {
return
}
Logger.verbose(Strings.image_starting_request(url))

// Reset previous image before loading new one
self.result = nil
self.result = await self.loadImage(url)
self.resultWithURL = nil
self.resultWithURL = await self.loadImage(url)
}

/// - Returns: `nil` if the Task was cancelled.
@available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *)
private func loadImage(_ url: URL) async -> Value? {
private func loadImage(_ url: URL) async -> ValueWithURL? {
do {
let (data, response) = try await self
.urlSession
Expand All @@ -84,8 +99,8 @@ final class ImageLoader: ObservableObject {
}

// Load images in a background thread
return await Task<Value, Never>
.detached(priority: .medium) { data.toImage() }
return await Task<ValueWithURL, Never>
.detached(priority: .medium) { data.toImage(url: url) }
.value
} catch let error {
return .failure(.responseError(error as NSError))
Expand All @@ -96,18 +111,20 @@ final class ImageLoader: ObservableObject {

extension URLSession: URLSessionType {}

private typealias ValueWithURL = Result<(image: Image, size: CGSize, url: URL), ImageLoader.Error>

private extension Data {

func toImage() -> ImageLoader.Value {
func toImage(url: URL) -> ValueWithURL {
#if os(macOS)
if let image = NSImage(data: self) {
return .success((.init(nsImage: image), image.size))
return .success((.init(nsImage: image), image.size, url))
} else {
return .failure(.invalidImage)
}
#else
if let image = UIImage(data: self) {
return .success((.init(uiImage: image), image.size))
return .success((.init(uiImage: image), image.size, url))
} else {
return .failure(.invalidImage)
}
Expand Down
49 changes: 48 additions & 1 deletion Tests/RevenueCatUITests/ImageLoaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class ImageLoaderTests: TestCase {
let newImageRequested = self.expectation(description: "Image request")
Task {
newImageRequested.fulfill()
await self.loader.load(url: Self.url)
await self.loader.load(url: Self.url2)
}

// 5. Verify image is reset
Expand All @@ -152,6 +152,53 @@ class ImageLoaderTests: TestCase {
expect(self.loader.result).to(beSuccess())
}

func testReloadingSameImageDoesNotResetPreviousResult() async throws {
let session = MockAsyncURLSession()
self.loader = .init(urlSession: session)

func returnImage() async throws {
let resultSet = self.expectation(description: "Result set")
resultSet.assertForOverFulfill = false

let cancellable = self.loader.$result
.filter { $0 != nil }
.sink { _ in resultSet.fulfill() }
defer { cancellable.cancel() }

let completionSet = self.expectation(that: \.completionSet, on: session, willEqual: true)
await self.fulfillment(of: [completionSet], timeout: 1)

session.completion?(.success(try Self.createValidResponse(httpResponse: true)))
await self.fulfillment(of: [resultSet], timeout: 1)
}

// 1. Request image
Task {
await self.loader.load(url: Self.url)
}

// 2. Return image
try await returnImage()

// 3. Verify result
expect(self.loader.result).to(beSuccess())
let firstResult = self.loader.result

// 4. Request the same image again
let newImageRequested = self.expectation(description: "Image request")
Task {
newImageRequested.fulfill()
await self.loader.load(url: Self.url)
}

// 5. Verify image is not reset
await self.fulfillment(of: [newImageRequested], timeout: 1)
expect(self.loader.result).toNot(beNil())

// 6. Verify the image is unchanged
expect(self.loader.result?.value) == firstResult?.value
}

private static let url = URL(string: "https://assets.revenuecat.com/test")!
private static let url2 = URL(string: "https://assets.revenuecat.com/test2")!

Expand Down