diff --git a/.mapping.json b/.mapping.json index d3f6b16ac..fca0d4519 100644 --- a/.mapping.json +++ b/.mapping.json @@ -18738,6 +18738,7 @@ "client/ios/DivKit/Form/DivSubmitter.swift":"divkit/public/client/ios/DivKit/Form/DivSubmitter.swift", "client/ios/DivKit/Form/SubmitRequest.swift":"divkit/public/client/ios/DivKit/Form/SubmitRequest.swift", "client/ios/DivKit/IdToPath.swift":"divkit/public/client/ios/DivKit/IdToPath.swift", + "client/ios/DivKit/Images/AsyncDataImageHolder.swift":"divkit/public/client/ios/DivKit/Images/AsyncDataImageHolder.swift", "client/ios/DivKit/Images/CachedRemoteImageHolder.swift":"divkit/public/client/ios/DivKit/Images/CachedRemoteImageHolder.swift", "client/ios/DivKit/Images/DivImageHolderFactory.swift":"divkit/public/client/ios/DivKit/Images/DivImageHolderFactory.swift", "client/ios/DivKit/LayoutProvider/DivLayoutProviderHandler.swift":"divkit/public/client/ios/DivKit/LayoutProvider/DivLayoutProviderHandler.swift", diff --git a/client/ios/DivKit/Extensions/DivVideoExtensions.swift b/client/ios/DivKit/Extensions/DivVideoExtensions.swift index 1932d7a53..3291c066e 100644 --- a/client/ios/DivKit/Extensions/DivVideoExtensions.swift +++ b/client/ios/DivKit/Extensions/DivVideoExtensions.swift @@ -38,7 +38,13 @@ extension DivVideo: DivBlockModeling { let elapsedTime: Binding? = elapsedTimeVariable.flatMap { context.makeBinding(variableName: $0, defaultValue: 0) } - let preview: Image? = resolvePreview(resolver).flatMap(_makeImage(base64:)) + let preview: ImageHolder = context + .imageHolderFactory.make( + nil, + resolvePreview(resolver) + .map { .imageData(ImageData(base64: $0)) } + ) + let videoData = VideoData(videos: videoSources? .compactMap { $0.makeVideo(resolver: resolver) } ?? [] ) @@ -124,18 +130,3 @@ extension DivVideoScale { } } } - -private func _makeImage(base64: String) -> Image? { - decode(base64: base64).flatMap(Image.init(data:)) -} - -fileprivate func decode(base64: String) -> Data? { - if let data = Data(base64Encoded: base64) { - return data - } - if let url = URL(string: base64), - let dataHoldingURL = try? Data(contentsOf: url) { - return dataHoldingURL - } - return nil -} diff --git a/client/ios/DivKit/Images/AsyncDataImageHolder.swift b/client/ios/DivKit/Images/AsyncDataImageHolder.swift new file mode 100644 index 000000000..b8197274d --- /dev/null +++ b/client/ios/DivKit/Images/AsyncDataImageHolder.swift @@ -0,0 +1,85 @@ +import Foundation +import VGSL + +@preconcurrency @MainActor +final class AsyncDataImageHolder: ImageHolder { + private(set) weak var image: Image? + + let placeholder: ImagePlaceholder? + + private let imageData: ImageData + private let imageProcessingQueue: OperationQueueType + private let imageDecoder: (@Sendable (_ data: Data) -> Image?)? + + nonisolated var debugDescription: String { + onMainThreadSync { + let imagePart = if let image { + String(format: "%.0fx%.0f", image.size.width, image.size.height) + } else { + "nil" + } + return "AsyncDataImageHolder(image=\(imagePart), placeholder=\(dbgStr(placeholder?.debugDescription)), customDecoder=\(imageDecoder != nil), queue=\(imageProcessingQueue), imageData=\(String(describing: imageData)))" + } + } + + init( + data: ImageData, + imageProcessingQueue: OperationQueueType, + imageDecoder: (@Sendable (_ data: Data) -> Image?)? = nil, + placeholder: ImagePlaceholder? = nil + ) { + self.imageData = data + self.imageProcessingQueue = imageProcessingQueue + self.imageDecoder = imageDecoder + self.placeholder = placeholder + } + + func requestImageWithCompletion( + _ completion: @escaping @MainActor (Image?) -> Void + ) -> Cancellable? { + imageData.makeImage( + queue: imageProcessingQueue, + decoder: imageDecoder + ) { [weak self] image in + guard let self else { return } + completion(image) + self.image = image + } + + return nil + } + + func reused( + with placeholder: ImagePlaceholder?, + remoteImageURL url: URL? + ) -> (any ImageHolder)? { + (self.placeholder === placeholder && url == nil) ? self : nil + } + + func equals(_ other: any ImageHolder) -> Bool { + guard let other = other as? Self else { + return false + } + + return self.imageData == other.imageData && self.imageProcessingQueue === other + .imageProcessingQueue + } +} + +extension ImagePlaceholder { + public func toAsyncDataImageHolder( + imageProcessingQueue: OperationQueueType, + decoder: (@Sendable (Data) -> Image?)? = nil + ) -> ImageHolder { + switch self { + case .image, .color, .view: + toImageHolder() + case let .imageData(imageData): + AsyncDataImageHolder( + data: imageData, + imageProcessingQueue: imageProcessingQueue, + imageDecoder: decoder + ) + } + } +} diff --git a/client/ios/DivKit/Images/DivImageHolderFactory.swift b/client/ios/DivKit/Images/DivImageHolderFactory.swift index 1d3ebcc51..7c9164242 100644 --- a/client/ios/DivKit/Images/DivImageHolderFactory.swift +++ b/client/ios/DivKit/Images/DivImageHolderFactory.swift @@ -80,7 +80,9 @@ final class DefaultImageHolderFactory: DivImageHolderFactory { func make(_ url: URL?, _ placeholder: ImagePlaceholder?) -> ImageHolder { guard let url else { - return placeholder?.toImageHolder() ?? NilImageHolder() + return placeholder?.toAsyncDataImageHolder( + imageProcessingQueue: imageProcessingQueue + ) ?? NilImageHolder() } return RemoteImageHolder( url: url, diff --git a/client/ios/DivKitSVG/SVGImageHolderFactory.swift b/client/ios/DivKitSVG/SVGImageHolderFactory.swift index d4043f2ef..072f6b27e 100644 --- a/client/ios/DivKitSVG/SVGImageHolderFactory.swift +++ b/client/ios/DivKitSVG/SVGImageHolderFactory.swift @@ -29,17 +29,25 @@ public final class SVGImageHolderFactory: DivImageHolderFactory { } public func make(_ url: URL?, _ placeholder: ImagePlaceholder?) -> ImageHolder { - guard let url else { - return placeholder?.toImageHolder() ?? NilImageHolder() + let decoder: ( + @Sendable (_ data: Data) -> Image? + )? = { [weak self] in + self?.svgDecoder.decode(data: $0) + } + + return if let url { + RemoteImageHolder( + url: url, + placeholder: placeholder, + requester: requester, + imageProcessingQueue: imageProcessingQueue, + imageDecoder: decoder + ) + } else { + placeholder?.toAsyncDataImageHolder( + imageProcessingQueue: imageProcessingQueue, + decoder: decoder + ) ?? NilImageHolder() } - return RemoteImageHolder( - url: url, - placeholder: placeholder, - requester: requester, - imageProcessingQueue: imageProcessingQueue, - imageDecoder: { [weak self] in - self?.svgDecoder.decode(data: $0) - } - ) } } diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlock+UIViewRenderableBlock.swift index 708641628..01dcdfe88 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/VideoBlock+UIViewRenderableBlock.swift @@ -157,8 +157,14 @@ private final class VideoBlockView: BlockView, VisibleBoundsTrackingContainer { player?.set(isMuted: model.playbackConfig.isMuted) } - if let previewImage = model.preview { - preview.value.image = previewImage + if let preview = model.preview { + if !compare(preview, oldValue.preview) { + preview.requestImageWithCompletion { [weak self] image in + self?.updatePreview(image) + } + } + } else { + self.preview.value.image = nil } if let elapsedTime = model.elapsedTime?.value, @@ -168,6 +174,11 @@ private final class VideoBlockView: BlockView, VisibleBoundsTrackingContainer { } } + private func updatePreview(_ image: Image?) { + preview.value.image = image + preview.value.frame = adjustPreviewFrame() + } + private func adjustPreviewFrame() -> CGRect { guard let videoView, let videoRatio = videoView.videoRatio else { diff --git a/client/ios/LayoutKit/LayoutKit/ViewModels/VideoBlockViewModel.swift b/client/ios/LayoutKit/LayoutKit/ViewModels/VideoBlockViewModel.swift index 32779db97..51089ca42 100644 --- a/client/ios/LayoutKit/LayoutKit/ViewModels/VideoBlockViewModel.swift +++ b/client/ios/LayoutKit/LayoutKit/ViewModels/VideoBlockViewModel.swift @@ -5,7 +5,7 @@ public struct VideoBlockViewModel: Equatable { public let playbackConfig: PlaybackConfig public var elapsedTime: Binding? public var duration: Binding? - public let preview: Image? + public let preview: ImageHolder? public let resumeActions: [UserInterfaceAction] public let pauseActions: [UserInterfaceAction] public let bufferingActions: [UserInterfaceAction] @@ -18,7 +18,7 @@ public struct VideoBlockViewModel: Equatable { public init( videoData: VideoData, playbackConfig: PlaybackConfig, - preview: Image? = nil, + preview: ImageHolder? = nil, elapsedTime: Binding? = nil, duration: Binding? = nil, resumeActions: [UserInterfaceAction] = [],