-
Notifications
You must be signed in to change notification settings - Fork 265
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix image loading for MC and PC (#2845)
- Loading branch information
Showing
5 changed files
with
145 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/* Copyright Airship and Contributors */ | ||
|
||
import Combine | ||
import Foundation | ||
import SwiftUI | ||
|
||
public struct ThomasAsyncImage<Placeholder: View, ImageView: View>: View { | ||
|
||
let url: String | ||
let imageLoader: AirshipImageLoader | ||
let image: (Image, CGSize) -> ImageView | ||
let placeholder: () -> Placeholder | ||
|
||
public init( | ||
url: String, | ||
imageLoader: AirshipImageLoader = AirshipImageLoader(), | ||
image: @escaping (Image, CGSize) -> ImageView, | ||
placeholder: @escaping () -> Placeholder | ||
) { | ||
self.url = url | ||
self.imageLoader = imageLoader | ||
self.image = image | ||
self.placeholder = placeholder | ||
} | ||
|
||
@State private var loadedImage: AirshipImageData? = nil | ||
@State private var currentImage: UIImage? | ||
@State private var imageIndex: Int = 0 | ||
@State private var animationTask: Task<Void, Never>? | ||
@State private var cancellable: AnyCancellable? | ||
|
||
@Environment(\.isVisible) var isVisible: Bool // we use this value not for updating view tree, but for starting stopping animation, | ||
//that's why we need to store the actual value in a separate @State variable | ||
@State private var isImageVisible: Bool = false | ||
|
||
public var body: some View { | ||
content | ||
.onAppear { | ||
if self.loadedImage != nil { | ||
animateIfNeeded() | ||
} else { | ||
self.cancellable = self.imageLoader.load(url: self.url) | ||
.receive(on: DispatchQueue.main) | ||
.sink( | ||
receiveCompletion: { completion in | ||
if case let .failure(error) = completion { | ||
AirshipLogger.error( | ||
"Unable to load image \(error)" | ||
) | ||
} | ||
}, | ||
receiveValue: { image in | ||
self.loadedImage = image | ||
animateIfNeeded() | ||
} | ||
) | ||
} | ||
} | ||
.onChange(of: isVisible, perform: { newValue in | ||
self.isImageVisible = newValue | ||
animateIfNeeded() | ||
}) | ||
} | ||
|
||
private var content: some View { | ||
Group { | ||
if let image = currentImage { | ||
self.image(Image(uiImage: image), image.size) | ||
.animation(nil, value: self.imageIndex) | ||
.onDisappear { | ||
animationTask?.cancel() | ||
} | ||
} else { | ||
self.placeholder() | ||
} | ||
} | ||
} | ||
|
||
private func animateIfNeeded() { | ||
if isImageVisible { | ||
self.animationTask?.cancel() | ||
self.animationTask = Task { | ||
await animateImage() | ||
} | ||
} else { | ||
self.animationTask?.cancel() | ||
} | ||
} | ||
|
||
@MainActor | ||
private func animateImage() async { | ||
guard let loadedImage = self.loadedImage else { return } | ||
|
||
guard loadedImage.isAnimated else { | ||
self.currentImage = loadedImage.loadFrames().first?.image | ||
return | ||
} | ||
|
||
let frameActor = loadedImage.getActor() | ||
|
||
imageIndex = 0 | ||
var frame = await frameActor.loadFrame(at: imageIndex) | ||
|
||
self.currentImage = frame?.image | ||
|
||
while !Task.isCancelled { | ||
let duration = frame?.duration ?? AirshipImageData.minFrameDuration | ||
|
||
async let delay: () = Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) | ||
|
||
let nextIndex = (imageIndex + 1) % loadedImage.imageFramesCount | ||
|
||
do { | ||
let (_, nextFrame) = try await (delay, frameActor.loadFrame(at: nextIndex)) | ||
frame = nextFrame | ||
} catch {} // most likely it's a task cancelled exception when animation is stopped | ||
|
||
imageIndex = nextIndex | ||
|
||
if !Task.isCancelled { | ||
self.currentImage = frame?.image | ||
} | ||
} | ||
} | ||
} |