Skip to content

Commit

Permalink
RemoteImage Low Res Image support (#3906)
Browse files Browse the repository at this point in the history
`RemoteImage` now uses two images: a high res image (as before), and a
new low res image which will be used if it is available before the high
res image has been loaded.

To enable this, it now uses two `ImageLoader` instances, one for to
fetch the low res image, a second for high res image.

It assumes that the low res image url is the same as the high res image
url, with a suffix appended (default `_low_res`) to the file name, eg
`/images/myImage.jpg` is assumed to have a low res equivalent at
`/images/myImage_low_res.jpg`.

It requests the high and low res images concurrently, and displays an
error when both requests fail. A low res image will never replace a high
res image even if it is received last.

Here is a video of it loading a "low res" image, following by high res
image. For this video the low-res image was intentionally set to a
completely different image to make the change easy to see. If you move
the slider manually you can control the speed.


https://github.com/RevenueCat/purchases-ios/assets/109382862/97521803-5c12-43d0-92a8-596b23cdcff6

---------

Co-authored-by: Josh Holtz <[email protected]>
  • Loading branch information
jamesrb1 and joshdholtz authored May 27, 2024
1 parent c6e185f commit 0a627d7
Show file tree
Hide file tree
Showing 27 changed files with 206 additions and 35 deletions.
30 changes: 30 additions & 0 deletions RevenueCatUI/Data/TemplateViewConfiguration+Images.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ extension TemplateViewConfiguration {
var backgroundImageURL: URL? { self.url(for: \.background) }
var iconImageURL: URL? { self.url(for: \.icon) }

var headerLowResImageURL: URL? { self.url(forLowRes: \.header) }
var backgroundLowResImageURL: URL? { self.url(forLowRes: \.background) }
var iconLowResImageURL: URL? { self.url(forLowRes: \.icon) }

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
Expand All @@ -32,6 +36,12 @@ extension TemplateViewConfiguration {
return self.backgroundImageURL
}

var backgroundLowResImageToDisplay: URL? {
guard self.mode.shouldDisplayBackground else { return nil }

return self.backgroundLowResImageURL
}

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
Expand All @@ -45,6 +55,14 @@ private extension TemplateViewConfiguration {
)
}

func url(forLowRes lowResImage: KeyPath<PaywallData.Configuration.Images, String?>) -> URL? {
return PaywallData.url(
for: lowResImage,
in: self.configuration.imagesLowRes,
assetBaseURL: self.assetBaseURL
)
}

}

// MARK: -
Expand All @@ -56,6 +74,10 @@ extension PaywallData {
var backgroundImageURL: URL? { self.url(for: \.background) }
var iconImageURL: URL? { self.url(for: \.icon) }

var headerLowResImageURL: URL? { self.url(forLowRes: \.header) }
var backgroundLowResImageURL: URL? { self.url(forLowRes: \.background) }
var iconLowResImageURL: URL? { self.url(forLowRes: \.icon) }

private func url(for image: KeyPath<PaywallData.Configuration.Images, String?>) -> URL? {
return PaywallData.url(
for: image,
Expand All @@ -64,6 +86,14 @@ extension PaywallData {
)
}

private func url(forLowRes lowResImage: KeyPath<PaywallData.Configuration.Images, String?>) -> URL? {
return PaywallData.url(
for: lowResImage,
in: self.config.imagesLowRes,
assetBaseURL: self.assetBaseURL
)
}

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ struct WatchTemplateView: TemplateViewType {
var body: some View {
ScrollView {
VStack(spacing: self.defaultVerticalPaddingLength) {
if let url = self.configuration.headerImageURL {
RemoteImage(url: url, aspectRatio: Self.imageAspectRatio, maxWidth: .infinity)
if let headerImageURL = self.configuration.headerImageURL {
let headerLowResImageURL = self.configuration.headerLowResImageURL
RemoteImage(url: headerImageURL,
lowResUrl: headerLowResImageURL,
aspectRatio: Self.imageAspectRatio,
maxWidth: .infinity)
.clipped()
.roundedCorner(Self.imageRoundedCorner, corners: [.bottomLeft, .bottomRight])
.padding(.bottom)
Expand Down
3 changes: 2 additions & 1 deletion RevenueCatUI/Templates/Template1View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ struct Template1View: TemplateViewType {
@ViewBuilder
private var asyncImage: some View {
if let headerImage = self.configuration.headerImageURL {
RemoteImage(url: headerImage, aspectRatio: self.imageAspectRatio)
let headerImageLowRes = self.configuration.headerLowResImageURL
RemoteImage(url: headerImage, lowResUrl: headerImageLowRes, aspectRatio: self.imageAspectRatio)
.frame(maxWidth: .infinity)
.aspectRatio(self.imageAspectRatio, contentMode: .fit)
}
Expand Down
7 changes: 4 additions & 3 deletions RevenueCatUI/Templates/Template2View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,10 @@ struct Template2View: TemplateViewType {
private var iconImage: some View {
Group {
#if canImport(UIKit)
if let url = self.configuration.iconImageURL {
if let iconUrl = self.configuration.iconImageURL {
let iconLowResURL = self.configuration.iconLowResImageURL
Group {
if url.pathComponents.contains(PaywallData.appIconPlaceholder) {
if iconUrl.pathComponents.contains(PaywallData.appIconPlaceholder) {
if let appIcon = Bundle.main.appIcon {
Image(uiImage: appIcon)
.resizable()
Expand All @@ -297,7 +298,7 @@ struct Template2View: TemplateViewType {
self.placeholderIconImage
}
} else {
RemoteImage(url: url, aspectRatio: 1, maxWidth: self.iconSize)
RemoteImage(url: iconUrl, lowResUrl: iconLowResURL, aspectRatio: 1, maxWidth: self.iconSize)
}
}
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
Expand Down
5 changes: 3 additions & 2 deletions RevenueCatUI/Templates/Template3View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,9 @@ struct Template3View: TemplateViewType {

@ViewBuilder
private var headerIcon: some View {
if let url = self.configuration.iconImageURL {
RemoteImage(url: url, aspectRatio: 1)
if let iconImageURL = self.configuration.iconImageURL {
let iconImageLowResURL = self.configuration.iconLowResImageURL
RemoteImage(url: iconImageURL, lowResUrl: iconImageLowResURL, aspectRatio: 1)
.frame(width: self.iconSize, height: self.iconSize)
.cornerRadius(8)
.matchedGeometryEffect(id: Geometry.icon, in: self.namespace)
Expand Down
6 changes: 4 additions & 2 deletions RevenueCatUI/Templates/Template5View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,10 @@ struct Template5View: TemplateViewType {

@ViewBuilder
private var headerImage: some View {
if let header = self.configuration.headerImageURL {
RemoteImage(url: header,
if let headerImageURL = self.configuration.headerImageURL {
let headerLowResImageURL = self.configuration.headerLowResImageURL
RemoteImage(url: headerImageURL,
lowResUrl: headerLowResImageURL,
aspectRatio: self.headerAspectRatio,
maxWidth: .infinity)
.clipped()
Expand Down
1 change: 1 addition & 0 deletions RevenueCatUI/Views/LoadingPaywallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ struct LoadingPaywallView: View {
.background {
TemplateBackgroundImageView(
url: Self.defaultPaywall.backgroundImageURL,
lowResUrl: Self.defaultPaywall.backgroundLowResImageURL,
blurred: true,
ignoreSafeArea: self.mode.shouldDisplayBackground
)
Expand Down
72 changes: 52 additions & 20 deletions RevenueCatUI/Views/RemoteImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,76 @@ import SwiftUI
struct RemoteImage: View {

let url: URL
let lowResUrl: URL?
let aspectRatio: CGFloat?
let maxWidth: CGFloat?

var fetchLowRes: Bool {
lowResUrl != nil
}

@StateObject
private var highResLoader: ImageLoader = .init()

@StateObject
private var loader: ImageLoader = .init()
private var lowResLoader: ImageLoader = .init()

init(url: URL, aspectRatio: CGFloat? = nil, maxWidth: CGFloat? = nil) {
init(url: URL, lowResUrl: URL? = nil, aspectRatio: CGFloat? = nil, maxWidth: CGFloat? = nil) {
self.url = url
self.aspectRatio = aspectRatio
self.maxWidth = maxWidth
self.lowResUrl = lowResUrl
}

var body: some View {
Group {
switch self.loader.result {
case .none:
self.emptyView(error: nil)

case let .success(image):
if let aspectRatio {
image
.fitToAspect(aspectRatio, contentMode: .fill)
.frame(maxWidth: self.maxWidth)
.accessibilityHidden(true)

if case let .success(image) = highResLoader.result {
displayImage(image)
} else if case let .success(image) = lowResLoader.result {
displayImage(image)
} else if case let .failure(highResError) = highResLoader.result {
if !fetchLowRes {
emptyView(error: highResError)
} else if case .failure = lowResLoader.result {
emptyView(error: highResError)
} else {
image
.resizable()
.accessibilityHidden(true)
emptyView(error: nil)
}

case let .failure(error):
self.emptyView(error: error)
} else {
emptyView(error: nil)
}
}
.transition(Self.transition)
.task(id: self.url) { // This cancels the previous task when the URL changes.
await self.loader.load(url: self.url)
await loadImages()
}
}

private func displayImage(_ image: Image) -> some View {
if let aspectRatio {
return AnyView(
image
.fitToAspect(aspectRatio, contentMode: .fill)
.frame(maxWidth: self.maxWidth)
.accessibilityHidden(true)
)
} else {
return AnyView(
image
.resizable()
.accessibilityHidden(true)
)
}
}

private func loadImages() async {
if fetchLowRes, let lowResLoc = lowResUrl {
async let lowResLoad: Void = lowResLoader.load(url: lowResLoc)
async let highResLoad: Void = highResLoader.load(url: url)
_ = await (lowResLoad, highResLoad)

} else {
await highResLoader.load(url: url)
}
}

Expand Down
17 changes: 12 additions & 5 deletions RevenueCatUI/Views/TemplateBackgroundImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ import SwiftUI
struct TemplateBackgroundImageView: View {

private let url: URL?
private let urlLowRes: URL?
private let blurred: Bool
private let ignoreSafeArea: Bool

init(configuration: TemplateViewConfiguration) {
self.init(url: configuration.backgroundImageURLToDisplay,
lowResUrl: configuration.backgroundLowResImageToDisplay,
blurred: configuration.configuration.blurredBackgroundImage)
}

init(url: URL?, blurred: Bool, ignoreSafeArea: Bool = true) {
init(url: URL?, lowResUrl: URL?, blurred: Bool, ignoreSafeArea: Bool = true) {
self.url = url
self.urlLowRes = lowResUrl
self.blurred = blurred
self.ignoreSafeArea = ignoreSafeArea
}

var body: some View {
if let url = self.url {
let image = self.image(url)
let image = self.image(url, lowResUrl: self.urlLowRes)
.unredacted()

if self.ignoreSafeArea {
Expand All @@ -49,9 +52,9 @@ struct TemplateBackgroundImageView: View {
}

@ViewBuilder
private func image(_ url: URL) -> some View {
private func image(_ url: URL, lowResUrl: URL?) -> some View {
if self.blurred {
RemoteImage(url: url)
RemoteImage(url: url, lowResUrl: lowResUrl)
.blur(radius: 40)
.opacity(0.7)
.background {
Expand All @@ -64,7 +67,7 @@ struct TemplateBackgroundImageView: View {
#endif
}
} else {
RemoteImage(url: url)
RemoteImage(url: url, lowResUrl: lowResUrl)
}
}

Expand All @@ -82,24 +85,28 @@ struct TemplateBackgroundImageView_Previews: PreviewProvider {
static var previews: some View {
TemplateBackgroundImageView(
url: TestData.paywallAssetBaseURL.appendingPathComponent(TestData.paywallHeaderImageName),
lowResUrl: nil,
blurred: false
)
.previewDisplayName("Wrong aspect ratio not blured")

TemplateBackgroundImageView(
url: TestData.paywallAssetBaseURL.appendingPathComponent(TestData.paywallHeaderImageName),
lowResUrl: nil,
blurred: true
)
.previewDisplayName("Wrong aspect ratio blured")

TemplateBackgroundImageView(
url: TestData.paywallAssetBaseURL.appendingPathComponent(TestData.paywallBackgroundImageName),
lowResUrl: nil,
blurred: false
)
.previewDisplayName("Correct aspect ratio not blured")

TemplateBackgroundImageView(
url: TestData.paywallAssetBaseURL.appendingPathComponent(TestData.paywallBackgroundImageName),
lowResUrl: nil,
blurred: true
)
.previewDisplayName("Correct aspect ratio blured")
Expand Down
10 changes: 10 additions & 0 deletions Sources/Paywalls/PaywallData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ extension PaywallData {
}
}

/// Low resolution images for this template.
public var imagesLowRes: Images {
get { self._imagesHeicLowRes ?? Images() }
set { self._imagesHeicLowRes = newValue }
}

/// Whether the background image will be blurred (in templates with one).
public var blurredBackgroundImage: Bool {
get { self._blurredBackgroundImage }
Expand Down Expand Up @@ -233,6 +239,7 @@ extension PaywallData {
packages: [String],
defaultPackage: String? = nil,
images: Images,
imagesLowRes: Images = Images(),
colors: ColorInformation,
blurredBackgroundImage: Bool = false,
displayRestorePurchases: Bool = true,
Expand All @@ -242,6 +249,7 @@ extension PaywallData {
self.packages = packages
self.defaultPackage = defaultPackage
self._imagesHeic = images
self._imagesHeicLowRes = imagesLowRes
self.colors = colors
self._blurredBackgroundImage = blurredBackgroundImage
self._displayRestorePurchases = displayRestorePurchases
Expand All @@ -251,6 +259,7 @@ extension PaywallData {

var _legacyImages: Images?
var _imagesHeic: Images?
var _imagesHeicLowRes: Images?

@DefaultDecodable.False
var _blurredBackgroundImage: Bool
Expand Down Expand Up @@ -479,6 +488,7 @@ extension PaywallData.Configuration: Codable {
case defaultPackage
case _legacyImages = "images"
case _imagesHeic = "imagesHeic"
case _imagesHeicLowRes = "imagesHeicLowRes"
case _blurredBackgroundImage = "blurredBackgroundImage"
case _displayRestorePurchases = "displayRestorePurchases"
case _termsOfServiceURL = "tosUrl"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
"header" : null,
"icon" : "revenuecatui_default_paywall_app_icon"
},
"images_heic_low_res" : {
"background" : null,
"header" : null,
"icon" : null
},
"packages" : [
"$rc_annual",
"$rc_monthly"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
"header" : null,
"icon" : "revenuecatui_default_paywall_app_icon"
},
"images_heic_low_res" : {
"background" : null,
"header" : null,
"icon" : null
},
"packages" : [
"$rc_annual",
"$rc_monthly"
Expand Down
Loading

0 comments on commit 0a627d7

Please sign in to comment.