mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:02:10 +00:00
Automatically try reloading failed images on network changes (#3170)
* Add the networkMonitor to the NSE and delete the old MockMediaLoader * Generate a MediaLoaderMock through AutoMockable and use it in the MediaProviderTests * Implement an ImageProvider mechanism that automatically tries reloading images when connectivity is established again and use it for LoadableImages * Merge the ImageProvider protocol back into the MediaProvider * Address PR review comments * Provide a default sdk client request configuration * Address tasks not automatically cancelling themselves when views get deallocated
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
@@ -28,7 +29,7 @@ struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
|
||||
private let mediaType: LoadableImageMediaType
|
||||
private let blurhash: String?
|
||||
private let size: CGSize?
|
||||
private let imageProvider: ImageProviderProtocol?
|
||||
private let mediaProvider: MediaProviderProtocol?
|
||||
private let transformer: (AnyView) -> TransformerView
|
||||
private let placeholder: () -> PlaceholderView
|
||||
|
||||
@@ -44,14 +45,14 @@ struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
|
||||
mediaType: LoadableImageMediaType = .generic,
|
||||
blurhash: String? = nil,
|
||||
size: CGSize? = nil,
|
||||
imageProvider: ImageProviderProtocol?,
|
||||
mediaProvider: MediaProviderProtocol?,
|
||||
transformer: @escaping (AnyView) -> TransformerView = { $0 },
|
||||
placeholder: @escaping () -> PlaceholderView) {
|
||||
self.mediaSource = mediaSource
|
||||
self.mediaType = mediaType
|
||||
self.blurhash = blurhash
|
||||
self.size = size
|
||||
self.imageProvider = imageProvider
|
||||
self.mediaProvider = mediaProvider
|
||||
self.transformer = transformer
|
||||
self.placeholder = placeholder
|
||||
}
|
||||
@@ -60,14 +61,14 @@ struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
|
||||
mediaType: LoadableImageMediaType = .generic,
|
||||
blurhash: String? = nil,
|
||||
size: CGSize? = nil,
|
||||
imageProvider: ImageProviderProtocol?,
|
||||
mediaProvider: MediaProviderProtocol?,
|
||||
transformer: @escaping (AnyView) -> TransformerView = { $0 },
|
||||
placeholder: @escaping () -> PlaceholderView) {
|
||||
self.init(mediaSource: MediaSourceProxy(url: url, mimeType: nil),
|
||||
mediaType: mediaType,
|
||||
blurhash: blurhash,
|
||||
size: size,
|
||||
imageProvider: imageProvider,
|
||||
mediaProvider: mediaProvider,
|
||||
transformer: transformer,
|
||||
placeholder: placeholder)
|
||||
}
|
||||
@@ -77,7 +78,7 @@ struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
|
||||
mediaType: mediaType,
|
||||
blurhash: blurhash,
|
||||
size: size,
|
||||
imageProvider: imageProvider,
|
||||
mediaProvider: mediaProvider,
|
||||
transformer: transformer,
|
||||
placeholder: placeholder)
|
||||
// Binds the lifecycle of the LoadableImage to the associated URL.
|
||||
@@ -99,28 +100,20 @@ private struct LoadableImageContent<TransformerView: View, PlaceholderView: View
|
||||
mediaType: LoadableImageMediaType,
|
||||
blurhash: String? = nil,
|
||||
size: CGSize? = nil,
|
||||
imageProvider: ImageProviderProtocol?,
|
||||
mediaProvider: MediaProviderProtocol?,
|
||||
transformer: @escaping (AnyView) -> TransformerView,
|
||||
placeholder: @escaping () -> PlaceholderView) {
|
||||
assert(imageProvider != nil, "Missing image provider, make sure one has been supplied to the view model.")
|
||||
assert(mediaProvider != nil, "Missing image provider, make sure one has been supplied to the view model.")
|
||||
|
||||
self.mediaSource = mediaSource
|
||||
self.mediaType = mediaType
|
||||
self.blurhash = blurhash
|
||||
self.transformer = transformer
|
||||
self.placeholder = placeholder
|
||||
_contentLoader = StateObject(wrappedValue: ContentLoader(mediaSource: mediaSource, size: size, imageProvider: imageProvider))
|
||||
_contentLoader = StateObject(wrappedValue: ContentLoader(mediaSource: mediaSource, size: size, mediaProvider: mediaProvider))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let _ = Task {
|
||||
guard contentLoader.content == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
await contentLoader.load()
|
||||
}
|
||||
|
||||
ZStack {
|
||||
switch contentLoader.content {
|
||||
case .image(let image):
|
||||
@@ -140,6 +133,20 @@ private struct LoadableImageContent<TransformerView: View, PlaceholderView: View
|
||||
}
|
||||
}
|
||||
.animation(mediaType == .avatar ? .noAnimation : .elementDefault, value: contentLoader.content)
|
||||
.task(id: mediaSource) {
|
||||
guard contentLoader.content == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
await contentLoader.load()
|
||||
}
|
||||
.onDisappear {
|
||||
guard contentLoader.content == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
contentLoader.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ImageDataProvider
|
||||
@@ -163,9 +170,10 @@ private class ContentLoader: ObservableObject {
|
||||
case gifData(Data)
|
||||
}
|
||||
|
||||
private let imageProvider: ImageProviderProtocol?
|
||||
private let mediaProvider: MediaProviderProtocol?
|
||||
private let mediaSource: MediaSourceProxy
|
||||
private let size: CGSize?
|
||||
private var imageLoadingCancellable: AnyCancellable?
|
||||
|
||||
@Published private var cachedContent: Content?
|
||||
|
||||
@@ -175,36 +183,47 @@ private class ContentLoader: ObservableObject {
|
||||
}
|
||||
|
||||
if isGIF {
|
||||
if let image = imageProvider?.imageFromSource(mediaSource),
|
||||
if let image = mediaProvider?.imageFromSource(mediaSource),
|
||||
let data = image.kf.data(format: .GIF) {
|
||||
return .gifData(data)
|
||||
}
|
||||
} else if let image = imageProvider?.imageFromSource(mediaSource, size: size) {
|
||||
} else if let image = mediaProvider?.imageFromSource(mediaSource, size: size) {
|
||||
return .image(image)
|
||||
}
|
||||
|
||||
return cachedContent
|
||||
}
|
||||
|
||||
init(mediaSource: MediaSourceProxy, size: CGSize?, imageProvider: ImageProviderProtocol?) {
|
||||
init(mediaSource: MediaSourceProxy, size: CGSize?, mediaProvider: MediaProviderProtocol?) {
|
||||
self.mediaSource = mediaSource
|
||||
self.size = size
|
||||
self.imageProvider = imageProvider
|
||||
self.mediaProvider = mediaProvider
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func load() async {
|
||||
if isGIF {
|
||||
if case let .success(data) = await imageProvider?.loadImageDataFromSource(mediaSource) {
|
||||
if case let .success(data) = await mediaProvider?.loadImageDataFromSource(mediaSource) {
|
||||
cachedContent = .gifData(data)
|
||||
}
|
||||
} else {
|
||||
if case let .success(image) = await imageProvider?.loadImageFromSource(mediaSource, size: size) {
|
||||
guard let task = mediaProvider?.loadImageRetryingOnReconnection(mediaSource, size: size) else {
|
||||
MXLog.error("Failed loading image, invalid reconnection retry task.")
|
||||
return
|
||||
}
|
||||
|
||||
imageLoadingCancellable = task.asCancellable()
|
||||
|
||||
if let image = try? await task.value {
|
||||
cachedContent = .image(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
imageLoadingCancellable?.cancel()
|
||||
}
|
||||
|
||||
private var isGIF: Bool {
|
||||
mediaSource.mimeType == "image/gif"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user