Compiles with strict concurrency checking

This commit is contained in:
Shadowfacts 2024-01-27 11:40:03 -05:00
parent c489d018bd
commit ba60f92223
4 changed files with 120 additions and 113 deletions

View File

@ -56,4 +56,13 @@ public extension MainActor {
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)()
}
}
}

View File

@ -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

View File

@ -9,6 +9,7 @@
import SwiftUI
import MessageUI
@MainActor
struct AboutView: View {
@State private var logData: Data?
@State private var isGettingLogData = false

View File

@ -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
}
}
} 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
}
private func loadImage() async {
let (data, image) = await ImageCache.attachments.get(attachment.url)
guard !Task.isCancelled else { return }
DispatchQueue.main.async {
self.attachmentRequest = nil
if attachmentURL.pathExtension == "gif",
if attachment.url.pathExtension == "gif",
let data {
self.source = .gifData(attachmentURL, data, image)
if self.autoplayGifs {
source = .gifData(attachment.url, data, image)
if autoplayGifs {
let controller = GIFController(gifData: data)
controller.attach(to: self)
controller.startAnimating()
} else {
self.displayImage()
await displayImage()
}
} else if let image {
self.source = .image(attachmentURL, image)
self.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
source = .image(attachment.url, image)
await displayImage()
}
}
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
}
func loadAudio() {
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()
}
}
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)
}
}
}