mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Add animated gif support (#825)
This commit is contained in:
@@ -36,15 +36,24 @@ struct LoadableAvatarImage: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
LoadableImage(url: url,
|
||||
size: avatarSize.scaledSize,
|
||||
imageProvider: imageProvider) { image in
|
||||
image
|
||||
.scaledToFill()
|
||||
} placeholder: {
|
||||
avatar
|
||||
.frame(width: frameSize, height: frameSize)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var avatar: some View {
|
||||
if let url {
|
||||
LoadableImage(url: url,
|
||||
size: avatarSize.scaledSize,
|
||||
imageProvider: imageProvider) { image in
|
||||
image
|
||||
.scaledToFill()
|
||||
} placeholder: {
|
||||
PlaceholderAvatarImage(name: name, contentID: contentID)
|
||||
}
|
||||
} else {
|
||||
PlaceholderAvatarImage(name: name, contentID: contentID)
|
||||
}
|
||||
.frame(width: frameSize, height: frameSize)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,18 +14,19 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
|
||||
private let mediaSource: MediaSourceProxy?
|
||||
struct LoadableImage<TransformerView: View, PlaceholderView: View>: View, ImageDataProvider {
|
||||
private let mediaSource: MediaSourceProxy
|
||||
private let blurhash: String?
|
||||
private let size: CGSize?
|
||||
private let imageProvider: ImageProviderProtocol?
|
||||
|
||||
private var transformer: (Image) -> TransformerView
|
||||
private var transformer: (AnyView) -> TransformerView
|
||||
private let placeholder: () -> PlaceholderView
|
||||
|
||||
@State private var cachedImage: UIImage?
|
||||
@StateObject private var contentLoader: ContentLoader
|
||||
|
||||
/// A SwiftUI view that automatically fetches images
|
||||
/// It will try fetching the image from in-memory cache and if that's not available
|
||||
@@ -35,11 +36,11 @@ struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
|
||||
/// - blurhash: an optional blurhash
|
||||
/// - transformer: entry point for configuring the resulting image view
|
||||
/// - placeholder: a view to show while the image or blurhash are not available
|
||||
init(mediaSource: MediaSourceProxy?,
|
||||
init(mediaSource: MediaSourceProxy,
|
||||
blurhash: String? = nil,
|
||||
size: CGSize? = nil,
|
||||
imageProvider: ImageProviderProtocol?,
|
||||
transformer: @escaping (Image) -> TransformerView = { $0 },
|
||||
transformer: @escaping (AnyView) -> TransformerView = { $0 },
|
||||
placeholder: @escaping () -> PlaceholderView) {
|
||||
self.mediaSource = mediaSource
|
||||
self.blurhash = blurhash
|
||||
@@ -48,17 +49,17 @@ struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
|
||||
|
||||
self.transformer = transformer
|
||||
self.placeholder = placeholder
|
||||
|
||||
_contentLoader = StateObject(wrappedValue: ContentLoader(mediaSource: mediaSource, size: size, imageProvider: imageProvider))
|
||||
}
|
||||
|
||||
init(url: URL?,
|
||||
init(url: URL,
|
||||
blurhash: String? = nil,
|
||||
size: CGSize? = nil,
|
||||
imageProvider: ImageProviderProtocol?,
|
||||
transformer: @escaping (Image) -> TransformerView = { $0 },
|
||||
transformer: @escaping (AnyView) -> TransformerView = { $0 },
|
||||
placeholder: @escaping () -> PlaceholderView) {
|
||||
let mediaSource = url.map { MediaSourceProxy(url: $0, mimeType: nil) }
|
||||
|
||||
self.init(mediaSource: mediaSource,
|
||||
self.init(mediaSource: MediaSourceProxy(url: url, mimeType: nil),
|
||||
blurhash: blurhash,
|
||||
size: size,
|
||||
imageProvider: imageProvider,
|
||||
@@ -69,34 +70,101 @@ struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
|
||||
var body: some View {
|
||||
let _ = Task {
|
||||
// Future improvement: Does guarding against a nil image prevent the image being updated when the URL changes?
|
||||
guard image == nil, let mediaSource else { return }
|
||||
|
||||
if case let .success(image) = await imageProvider?.loadImageFromSource(mediaSource, size: size) {
|
||||
cachedImage = image
|
||||
guard contentLoader.content == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
await contentLoader.load()
|
||||
}
|
||||
|
||||
ZStack {
|
||||
if let image {
|
||||
switch contentLoader.content {
|
||||
case .image(let image):
|
||||
transformer(
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
AnyView(Image(uiImage: image).resizable())
|
||||
)
|
||||
} else if let blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
transformer(
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
)
|
||||
} else {
|
||||
placeholder()
|
||||
case .gifData:
|
||||
transformer(AnyView(KFAnimatedImage(source: .provider(self))))
|
||||
case .none:
|
||||
if let blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
transformer(AnyView(Image(uiImage: image).resizable()))
|
||||
} else {
|
||||
placeholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.elementDefault, value: image)
|
||||
.animation(.elementDefault, value: contentLoader.content)
|
||||
}
|
||||
|
||||
private var image: UIImage? {
|
||||
cachedImage ?? imageProvider?.imageFromSource(mediaSource, size: size)
|
||||
// MARK: - ImageDataProvider
|
||||
|
||||
var cacheKey: String {
|
||||
mediaSource.url.absoluteString
|
||||
}
|
||||
|
||||
func data(handler: @escaping (Result<Data, Error>) -> Void) {
|
||||
guard case let .gifData(data) = contentLoader.content else {
|
||||
fatalError("Shouldn't reach this point without any gif data")
|
||||
}
|
||||
|
||||
handler(.success(data))
|
||||
}
|
||||
}
|
||||
|
||||
private class ContentLoader: ObservableObject {
|
||||
enum Content: Equatable {
|
||||
case image(UIImage)
|
||||
case gifData(Data)
|
||||
}
|
||||
|
||||
private let imageProvider: ImageProviderProtocol?
|
||||
private let mediaSource: MediaSourceProxy
|
||||
private let size: CGSize?
|
||||
|
||||
@Published private var cachedContent: Content?
|
||||
|
||||
var content: Content? {
|
||||
if cachedContent != nil {
|
||||
return cachedContent
|
||||
}
|
||||
|
||||
if let image = imageProvider?.imageFromSource(mediaSource) {
|
||||
let isGIF = mediaSource.mimeType == "image/gif"
|
||||
|
||||
if isGIF {
|
||||
if let data = image.kf.data(format: .GIF) {
|
||||
return .gifData(data)
|
||||
}
|
||||
} else {
|
||||
return .image(image)
|
||||
}
|
||||
}
|
||||
|
||||
return cachedContent
|
||||
}
|
||||
|
||||
init(mediaSource: MediaSourceProxy, size: CGSize?, imageProvider: ImageProviderProtocol?) {
|
||||
self.mediaSource = mediaSource
|
||||
self.size = size
|
||||
self.imageProvider = imageProvider
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func load() async {
|
||||
if isGIF {
|
||||
if case let .success(data) = await imageProvider?.loadImageDataFromSource(mediaSource) {
|
||||
cachedContent = .gifData(data)
|
||||
}
|
||||
} else {
|
||||
if case let .success(image) = await imageProvider?.loadImageFromSource(mediaSource, size: size) {
|
||||
cachedContent = .image(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isGIF: Bool {
|
||||
mediaSource.mimeType == "image/gif"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user