Compiles with strict concurrency checking
This commit is contained in:
parent
c489d018bd
commit
ba60f92223
|
@ -51,9 +51,18 @@ public extension MainActor {
|
|||
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
||||
return try MainActor.assumeIsolated(body)
|
||||
}
|
||||
|
||||
|
||||
dispatchPrecondition(condition: .onQueue(.main))
|
||||
return try withoutActuallyEscaping(body) { fn in
|
||||
try unsafeBitCast(fn, to: (() throws -> T).self)()
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
@_unavailableFromAsync
|
||||
@available(*, deprecated, message: "Tool of last resort, do not use this.")
|
||||
static func runUnsafelyMaybeIntroducingDataRace<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
||||
return try withoutActuallyEscaping(body) { fn in
|
||||
try unsafeBitCast(fn, to: (() throws -> T).self)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,10 @@ struct ImageGrayscalifier {
|
|||
private static let cache = NSCache<NSURL, UIImage>()
|
||||
|
||||
static func convertIfNecessary(url: URL?, image: UIImage) -> UIImage? {
|
||||
if Preferences.shared.grayscaleImages,
|
||||
let grayscale = MainActor.runUnsafelyMaybeIntroducingDataRace {
|
||||
Preferences.shared.grayscaleImages
|
||||
}
|
||||
if grayscale,
|
||||
let source = image.cgImage {
|
||||
// todo: should this return the original image if conversion fails?
|
||||
return convert(url: url, cgImage: source)
|
||||
|
@ -35,6 +38,21 @@ struct ImageGrayscalifier {
|
|||
return doConvert(CIImage(cgImage: cgImage), url: url)
|
||||
}
|
||||
|
||||
static func convert(url: URL?, image: UIImage) async -> UIImage? {
|
||||
if let url,
|
||||
let cached = cache.object(forKey: url as NSURL) {
|
||||
return cached
|
||||
}
|
||||
guard let cgImage = image.cgImage else {
|
||||
return nil
|
||||
}
|
||||
return await withCheckedContinuation { continuation in
|
||||
queue.async {
|
||||
continuation.resume(returning: doConvert(CIImage(cgImage: cgImage), url: url))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func convert(url: URL?, data: Data) -> UIImage? {
|
||||
if let url = url,
|
||||
let cached = cache.object(forKey: url as NSURL) {
|
||||
|
@ -56,6 +74,18 @@ struct ImageGrayscalifier {
|
|||
return doConvert(CIImage(cgImage: cgImage), url: url)
|
||||
}
|
||||
|
||||
static func convert(url: URL?, cgImage: CGImage) async -> UIImage? {
|
||||
if let url = url,
|
||||
let cached = cache.object(forKey: url as NSURL) {
|
||||
return cached
|
||||
}
|
||||
return await withCheckedContinuation { continuation in
|
||||
queue.async {
|
||||
continuation.resume(returning: doConvert(CIImage(cgImage: cgImage), url: url))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func doConvert(_ source: CIImage, url: URL?) -> UIImage? {
|
||||
guard let filter = CIFilter(name: "CIColorMonochrome") else {
|
||||
return nil
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import SwiftUI
|
||||
import MessageUI
|
||||
|
||||
@MainActor
|
||||
struct AboutView: View {
|
||||
@State private var logData: Data?
|
||||
@State private var isGettingLogData = false
|
||||
|
|
|
@ -30,7 +30,7 @@ class AttachmentView: GIFImageView {
|
|||
var attachment: Attachment!
|
||||
var index: Int!
|
||||
|
||||
private var attachmentRequest: ImageCache.Request?
|
||||
private var loadAttachmentTask: Task<Void, Never>?
|
||||
private var source: Source?
|
||||
|
||||
private var autoplayGifs: Bool {
|
||||
|
@ -45,11 +45,13 @@ class AttachmentView: GIFImageView {
|
|||
|
||||
self.attachment = attachment
|
||||
self.index = index
|
||||
loadAttachment()
|
||||
self.loadAttachmentTask = Task {
|
||||
await self.loadAttachment()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
attachmentRequest?.cancel()
|
||||
loadAttachmentTask?.cancel()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
@ -76,7 +78,9 @@ class AttachmentView: GIFImageView {
|
|||
gifPlaybackModeChanged()
|
||||
|
||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||
self.displayImage()
|
||||
Task {
|
||||
await displayImage()
|
||||
}
|
||||
}
|
||||
|
||||
if getBadges().isEmpty != Preferences.shared.showAttachmentBadges {
|
||||
|
@ -107,34 +111,34 @@ class AttachmentView: GIFImageView {
|
|||
}
|
||||
}
|
||||
|
||||
func loadAttachment() {
|
||||
private func loadAttachment() async {
|
||||
let blurHashTask: Task<Void, any Error>?
|
||||
if let hash = attachment.blurHash {
|
||||
AttachmentView.queue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
blurHashTask = Task {
|
||||
guard var preview = UIImage(blurHash: hash, size: self.blurHashSize()) else {
|
||||
return
|
||||
}
|
||||
try Task.checkCancellation()
|
||||
|
||||
if Preferences.shared.grayscaleImages,
|
||||
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: preview.cgImage!) {
|
||||
preview = grayscale
|
||||
}
|
||||
try Task.checkCancellation()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self, self.image == nil else { return }
|
||||
self.image = preview
|
||||
}
|
||||
self.image = preview
|
||||
}
|
||||
} else {
|
||||
blurHashTask = nil
|
||||
}
|
||||
|
||||
createBadgesView(getBadges())
|
||||
|
||||
switch attachment.kind {
|
||||
case .image:
|
||||
loadImage()
|
||||
await loadImage()
|
||||
case .video:
|
||||
loadVideo()
|
||||
await loadVideo()
|
||||
case .audio:
|
||||
loadAudio()
|
||||
case .gifv:
|
||||
|
@ -142,6 +146,8 @@ class AttachmentView: GIFImageView {
|
|||
case .unknown:
|
||||
createUnknownLabel()
|
||||
}
|
||||
|
||||
blurHashTask?.cancel()
|
||||
}
|
||||
|
||||
private func getBadges() -> Badges {
|
||||
|
@ -182,66 +188,27 @@ class AttachmentView: GIFImageView {
|
|||
}
|
||||
}
|
||||
|
||||
func loadImage() {
|
||||
let attachmentURL = attachment.url
|
||||
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, image) in
|
||||
guard let self = self,
|
||||
self.attachment.url == attachmentURL else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.attachmentRequest = nil
|
||||
|
||||
if attachmentURL.pathExtension == "gif",
|
||||
let data {
|
||||
self.source = .gifData(attachmentURL, data, image)
|
||||
if self.autoplayGifs {
|
||||
let controller = GIFController(gifData: data)
|
||||
controller.attach(to: self)
|
||||
controller.startAnimating()
|
||||
} else {
|
||||
self.displayImage()
|
||||
}
|
||||
} else if let image {
|
||||
self.source = .image(attachmentURL, image)
|
||||
self.displayImage()
|
||||
}
|
||||
private func loadImage() async {
|
||||
let (data, image) = await ImageCache.attachments.get(attachment.url)
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
if attachment.url.pathExtension == "gif",
|
||||
let data {
|
||||
source = .gifData(attachment.url, data, image)
|
||||
if autoplayGifs {
|
||||
let controller = GIFController(gifData: data)
|
||||
controller.attach(to: self)
|
||||
controller.startAnimating()
|
||||
} else {
|
||||
await displayImage()
|
||||
}
|
||||
} else if let image {
|
||||
source = .image(attachment.url, image)
|
||||
await displayImage()
|
||||
}
|
||||
}
|
||||
|
||||
func loadVideo() {
|
||||
if let previewURL = self.attachment.previewURL {
|
||||
attachmentRequest = ImageCache.attachments.get(previewURL, completion: { [weak self] (_, image) in
|
||||
guard let self, let image else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.attachmentRequest = nil
|
||||
self.source = .image(previewURL, image)
|
||||
self.displayImage()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let attachmentURL = self.attachment.url
|
||||
AttachmentView.queue.async { [weak self] in
|
||||
let asset = AVURLAsset(url: attachmentURL)
|
||||
let generator = AVAssetImageGenerator(asset: asset)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
#if os(visionOS)
|
||||
#warning("Use async AVAssetImageGenerator.image(at:)")
|
||||
#else
|
||||
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
||||
UIImage(cgImage: image).prepareForDisplay { [weak self] image in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self, let image else { return }
|
||||
self.source = .image(attachmentURL, image)
|
||||
self.displayImage()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private func loadVideo() async {
|
||||
let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
|
||||
playImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(playImageView)
|
||||
|
@ -251,9 +218,40 @@ class AttachmentView: GIFImageView {
|
|||
playImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
playImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
])
|
||||
|
||||
if let previewURL = attachment.previewURL {
|
||||
guard let image = await ImageCache.attachments.get(previewURL).1,
|
||||
!Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
source = .image(previewURL, image)
|
||||
await displayImage()
|
||||
} else {
|
||||
let asset = AVURLAsset(url: attachment.url)
|
||||
let generator = AVAssetImageGenerator(asset: asset)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
let image: CGImage?
|
||||
#if os(visionOS)
|
||||
image = try? await generator.image(at: .zero).image
|
||||
#else
|
||||
if #available(iOS 16.0, *) {
|
||||
image = try? await generator.image(at: .zero).image
|
||||
} else {
|
||||
image = try? generator.copyCGImage(at: .zero, actualTime: nil)
|
||||
}
|
||||
#endif
|
||||
guard let image,
|
||||
let prepared = await UIImage(cgImage: image).byPreparingForDisplay(),
|
||||
!Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
source = .image(attachment.url, prepared)
|
||||
await displayImage()
|
||||
}
|
||||
}
|
||||
|
||||
func loadAudio() {
|
||||
private func loadAudio() {
|
||||
let label = UILabel()
|
||||
label.text = "Audio Only"
|
||||
let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
|
||||
|
@ -274,23 +272,8 @@ class AttachmentView: GIFImageView {
|
|||
])
|
||||
}
|
||||
|
||||
func loadGifv() {
|
||||
let attachmentURL = self.attachment.url
|
||||
let asset = AVURLAsset(url: attachmentURL)
|
||||
AttachmentView.queue.async {
|
||||
let generator = AVAssetImageGenerator(asset: asset)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
#if os(visionOS)
|
||||
#warning("Use async AVAssetImageGenerator.image(at:)")
|
||||
#else
|
||||
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.source = .cgImage(attachmentURL, image)
|
||||
self.displayImage()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func loadGifv() {
|
||||
let asset = AVURLAsset(url: attachment.url)
|
||||
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
|
||||
self.gifvView = gifvView
|
||||
gifvView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -306,7 +289,7 @@ class AttachmentView: GIFImageView {
|
|||
])
|
||||
}
|
||||
|
||||
func createUnknownLabel() {
|
||||
private func createUnknownLabel() {
|
||||
backgroundColor = .appSecondaryBackground
|
||||
let label = UILabel()
|
||||
label.text = "Unknown Attachment Type"
|
||||
|
@ -325,8 +308,7 @@ class AttachmentView: GIFImageView {
|
|||
])
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func displayImage() {
|
||||
private func displayImage() async {
|
||||
isGrayscale = Preferences.shared.grayscaleImages
|
||||
|
||||
switch source {
|
||||
|
@ -335,12 +317,7 @@ class AttachmentView: GIFImageView {
|
|||
|
||||
case let .image(url, sourceImage):
|
||||
if isGrayscale {
|
||||
ImageGrayscalifier.queue.async { [weak self] in
|
||||
let grayscale = ImageGrayscalifier.convert(url: url, image: sourceImage)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.image = grayscale
|
||||
}
|
||||
}
|
||||
self.image = await ImageGrayscalifier.convert(url: url, image: sourceImage)
|
||||
} else {
|
||||
self.image = sourceImage
|
||||
}
|
||||
|
@ -348,26 +325,16 @@ class AttachmentView: GIFImageView {
|
|||
case let .gifData(url, _, sourceImage):
|
||||
if isGrayscale,
|
||||
let sourceImage {
|
||||
ImageGrayscalifier.queue.async { [weak self] in
|
||||
let grayscale = ImageGrayscalifier.convert(url: url, image: sourceImage)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.image = grayscale
|
||||
}
|
||||
}
|
||||
self.image = await ImageGrayscalifier.convert(url: url, image: sourceImage)
|
||||
} else {
|
||||
self.image = sourceImage
|
||||
}
|
||||
|
||||
case let .cgImage(url, cgImage):
|
||||
if isGrayscale {
|
||||
ImageGrayscalifier.queue.async { [weak self] in
|
||||
let grayscale = ImageGrayscalifier.convert(url: url, cgImage: cgImage)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.image = grayscale
|
||||
}
|
||||
}
|
||||
self.image = await ImageGrayscalifier.convert(url: url, cgImage: cgImage)
|
||||
} else {
|
||||
image = UIImage(cgImage: cgImage)
|
||||
self.image = UIImage(cgImage: cgImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue