Files
purchases-ios/RevenueCatUI/Views/RemoteImage.swift
T
Josh Holtz 91e695869a [Paywalls V2] Add root paywall background (color and image) (#4502)
* Added root paywall background

* Added some previews

* Undo comment about fallback

* Fix some lint

* Fit lint... again

* Added backgroud style to text and stack and fixed preview
2024-11-21 10:46:15 -06:00

163 lines
4.7 KiB
Swift

//
// 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
//
// RemoteImage.swift
//
// Created by Nacho Soto on 7/19/23.
import SwiftUI
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct RemoteImage<Content: View>: View {
@Environment(\.colorScheme)
private var colorScheme
let url: URL
let lowResUrl: URL?
let darkUrl: URL?
let darkLowResUrl: URL?
let aspectRatio: CGFloat?
let maxWidth: CGFloat?
let content: (Image, CGSize) -> Content
@StateObject
private var highResLoader: ImageLoader = .init()
@StateObject
private var lowResLoader: ImageLoader = .init()
var fetchLowRes: Bool {
lowResUrl != nil
}
private let transition: AnyTransition = .opacity.animation(Constants.defaultAnimation)
init(
url: URL,
lowResUrl: URL? = nil,
darkUrl: URL? = nil,
darkLowResUrl: URL? = nil,
@ViewBuilder content: @escaping (Image, CGSize) -> Content
) {
self.url = url
self.lowResUrl = lowResUrl
self.darkUrl = darkUrl
self.darkLowResUrl = darkLowResUrl
self.content = content
self.aspectRatio = nil
self.maxWidth = nil
}
init(
url: URL,
lowResUrl: URL? = nil,
darkUrl: URL? = nil,
darkLowResUrl: URL? = nil,
aspectRatio: CGFloat? = nil,
maxWidth: CGFloat? = nil
) where Content == AnyView {
self.url = url
self.lowResUrl = lowResUrl
self.darkUrl = darkUrl
self.darkLowResUrl = darkLowResUrl
self.maxWidth = maxWidth
self.aspectRatio = aspectRatio
self.content = { (image, _) in
if let aspectRatio {
return AnyView(
image
.fitToAspect(aspectRatio, contentMode: .fill)
.frame(maxWidth: maxWidth)
.accessibilityHidden(true)
)
} else {
return AnyView(
image
.resizable()
.accessibilityHidden(true)
)
}
}
}
var body: some View {
Group {
if case let .success(result) = highResLoader.result {
content(result.image, result.size)
} else if case let .success(result) = lowResLoader.result {
content(result.image, result.size)
} else if case let .failure(highResError) = highResLoader.result {
if !fetchLowRes {
emptyView(error: highResError)
} else if case .failure = lowResLoader.result {
emptyView(error: highResError)
} else {
emptyView(error: nil)
}
} else {
emptyView(error: nil)
}
}
.transition(self.transition)
.task(id: self.url) { // This cancels the previous task when the URL changes.
switch self.colorScheme {
case .dark:
await loadImages(
url: self.darkUrl ?? self.url,
lowResUrl: self.darkLowResUrl ?? self.lowResUrl
)
case .light:
fallthrough
@unknown default:
await loadImages(
url: self.url,
lowResUrl: self.lowResUrl
)
}
}
}
private func loadImages(url: URL, lowResUrl: URL?) 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)
}
}
@ViewBuilder
private func emptyView(error: Error?) -> some View {
let placeholderView = Rectangle()
.hidden()
Group {
if let aspectRatio {
placeholderView
.aspectRatio(aspectRatio, contentMode: .fit)
} else {
placeholderView
}
}
.frame(maxWidth: self.maxWidth)
.overlay {
Group {
if let error {
DebugErrorView("Error loading image from '\(self.url)': \(error)", releaseBehavior: .emptyView)
.font(.footnote)
.textCase(.none)
}
}
}
}
}