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:
Stefan Ceriu
2024-08-15 18:20:19 +03:00
committed by GitHub
parent cf35056c78
commit f89c3e54b4
92 changed files with 618 additions and 332 deletions
@@ -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"
}