Files
divkit/client/ios/DivKit/Images/DivImageHolderFactory.swift
T
burstein 826829e0a1 Added optional bundle param to divkit-assets
commit_hash:575bfbbb7f0f1ab54dd506ce1ec2b4d6ba3885d6
2026-02-21 14:38:39 +03:00

152 lines
4.2 KiB
Swift

import Foundation
import VGSL
/// Loads images for `DivKit` views.
public protocol DivImageHolderFactory {
/// Provides `ImageHolder` for an image with given `URL`.
func make(_ url: URL?, _ placeholder: ImagePlaceholder?) -> ImageHolder
}
extension DivImageHolderFactory {
public func make(_ url: URL?) -> ImageHolder {
make(url, nil)
}
public func withCache(_ cachedImageHolders: [ImageHolder]) -> DivImageHolderFactory {
if cachedImageHolders.isEmpty {
return self
}
return CachedImageHolderFactory(
imageHolderFactory: self,
cachedImageHolders: cachedImageHolders
)
}
public func withAssets() -> DivImageHolderFactory {
AssetsImageProvider(
imageHolderFactory: self
)
}
}
extension DivImageHolderFactory {
public func withInMemoryCache(imageCountLimit: Int) -> ImageHolderFactory {
let memoizedFactory = memoizeNonSendable(
sizeLimit: UInt(imageCountLimit),
keyMapper: calculateKey,
size: { _ in 1 },
makeCached
)
return ImageHolderFactory(make: { url, placeholder in
if let url {
memoizedFactory((url, placeholder))
} else {
make(url, placeholder)
}
})
}
private func makeCached(url: URL, placeholder: ImagePlaceholder?) -> ImageHolder {
let imageHolder = make(url, placeholder)
return if let remoteImageHolder = imageHolder as? RemoteImageHolder {
CachedRemoteImageHolder(wrapped: remoteImageHolder)
} else {
imageHolder
}
}
private func calculateKey(url: URL, placeholder: ImagePlaceholder?) -> String {
url.absoluteString + (placeholder?.key ?? "")
}
}
extension ImageHolderFactory: DivImageHolderFactory {}
final class DefaultImageHolderFactory: DivImageHolderFactory {
private let requester: URLResourceRequesting
private let imageProcessingQueue = OperationQueue(
name: "tech.divkit.image-processing",
qos: .userInitiated
)
init(
requestPerformer: URLRequestPerforming
) {
self.requester = NetworkURLResourceRequester(performer: requestPerformer)
}
func make(_ url: URL?, _ placeholder: ImagePlaceholder?) -> ImageHolder {
guard let url else {
return placeholder?.toImageHolder() ?? NilImageHolder()
}
return RemoteImageHolder(
url: url,
placeholder: placeholder,
requester: requester,
imageProcessingQueue: imageProcessingQueue
)
}
}
private final class CachedImageHolderFactory: DivImageHolderFactory {
private let imageHolderFactory: DivImageHolderFactory
private let cachedImageHolders: [ImageHolder]
init(
imageHolderFactory: DivImageHolderFactory,
cachedImageHolders: [ImageHolder]
) {
self.imageHolderFactory = imageHolderFactory
self.cachedImageHolders = cachedImageHolders
}
func make(_ url: URL?, _ placeholder: ImagePlaceholder?) -> ImageHolder {
let cachedImageHolder = cachedImageHolders.first {
$0.reused(with: placeholder, remoteImageURL: url) != nil
}
return cachedImageHolder ?? imageHolderFactory.make(url, placeholder)
}
}
private struct AssetsImageProvider: DivImageHolderFactory {
private let imageHolderFactory: DivImageHolderFactory
init(
imageHolderFactory: DivImageHolderFactory
) {
self.imageHolderFactory = imageHolderFactory
}
func make(_ url: URL?, _ placeholder: ImagePlaceholder?) -> ImageHolder {
var localImage: ImageHolder?
if let url, url.scheme == "divkit-asset", let name = url.host {
let bundle = URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?.first { $0.name == "bundle" }?.value
.flatMap { Bundle(identifier: $0) }
// To restrict access to resources, all divkit asset images must start with the 'divkit.'
// prefix.
localImage = Image(named: "divkit.\(name)", in: bundle)
}
return localImage ?? imageHolderFactory.make(url, placeholder)
}
}
extension ImagePlaceholder {
fileprivate var key: String {
switch self {
case let .imageData(imageData):
String(imageData.hashValue)
case let .image(image):
String(ObjectIdentifier(image).hashValue)
case let .color(color):
String(color.hashValue)
case let .view(viewProvider):
String((viewProvider as? AnyHashable).hashValue)
}
}
}