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 return try withoutActuallyEscaping(body) { fn in
try unsafeBitCast(fn, to: (() throws -> T).self)() 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>() private static let cache = NSCache<NSURL, UIImage>()
static func convertIfNecessary(url: URL?, image: UIImage) -> 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 { let source = image.cgImage {
// todo: should this return the original image if conversion fails? // todo: should this return the original image if conversion fails?
return convert(url: url, cgImage: source) return convert(url: url, cgImage: source)
@ -35,6 +38,21 @@ struct ImageGrayscalifier {
return doConvert(CIImage(cgImage: cgImage), url: url) 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? { static func convert(url: URL?, data: Data) -> UIImage? {
if let url = url, if let url = url,
let cached = cache.object(forKey: url as NSURL) { let cached = cache.object(forKey: url as NSURL) {
@ -56,6 +74,18 @@ struct ImageGrayscalifier {
return doConvert(CIImage(cgImage: cgImage), url: url) 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? { private static func doConvert(_ source: CIImage, url: URL?) -> UIImage? {
guard let filter = CIFilter(name: "CIColorMonochrome") else { guard let filter = CIFilter(name: "CIColorMonochrome") else {
return nil return nil

View File

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

View File

@ -30,7 +30,7 @@ class AttachmentView: GIFImageView {
var attachment: Attachment! var attachment: Attachment!
var index: Int! var index: Int!
private var attachmentRequest: ImageCache.Request? private var loadAttachmentTask: Task<Void, Never>?
private var source: Source? private var source: Source?
private var autoplayGifs: Bool { private var autoplayGifs: Bool {
@ -45,11 +45,13 @@ class AttachmentView: GIFImageView {
self.attachment = attachment self.attachment = attachment
self.index = index self.index = index
loadAttachment() self.loadAttachmentTask = Task {
await self.loadAttachment()
}
} }
deinit { deinit {
attachmentRequest?.cancel() loadAttachmentTask?.cancel()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
@ -76,7 +78,9 @@ class AttachmentView: GIFImageView {
gifPlaybackModeChanged() gifPlaybackModeChanged()
if isGrayscale != Preferences.shared.grayscaleImages { if isGrayscale != Preferences.shared.grayscaleImages {
self.displayImage() Task {
await displayImage()
}
} }
if getBadges().isEmpty != Preferences.shared.showAttachmentBadges { 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 { if let hash = attachment.blurHash {
AttachmentView.queue.async { [weak self] in blurHashTask = Task {
guard let self = self else { return }
guard var preview = UIImage(blurHash: hash, size: self.blurHashSize()) else { guard var preview = UIImage(blurHash: hash, size: self.blurHashSize()) else {
return return
} }
try Task.checkCancellation()
if Preferences.shared.grayscaleImages, if Preferences.shared.grayscaleImages,
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: preview.cgImage!) { let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: preview.cgImage!) {
preview = grayscale 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()) createBadgesView(getBadges())
switch attachment.kind { switch attachment.kind {
case .image: case .image:
loadImage() await loadImage()
case .video: case .video:
loadVideo() await loadVideo()
case .audio: case .audio:
loadAudio() loadAudio()
case .gifv: case .gifv:
@ -142,6 +146,8 @@ class AttachmentView: GIFImageView {
case .unknown: case .unknown:
createUnknownLabel() createUnknownLabel()
} }
blurHashTask?.cancel()
} }
private func getBadges() -> Badges { private func getBadges() -> Badges {
@ -182,66 +188,27 @@ class AttachmentView: GIFImageView {
} }
} }
func loadImage() { private func loadImage() async {
let attachmentURL = attachment.url let (data, image) = await ImageCache.attachments.get(attachment.url)
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, image) in guard !Task.isCancelled else { return }
guard let self = self,
self.attachment.url == attachmentURL else {
return
}
DispatchQueue.main.async { if attachment.url.pathExtension == "gif",
self.attachmentRequest = nil
if attachmentURL.pathExtension == "gif",
let data { let data {
self.source = .gifData(attachmentURL, data, image) source = .gifData(attachment.url, data, image)
if self.autoplayGifs { if autoplayGifs {
let controller = GIFController(gifData: data) let controller = GIFController(gifData: data)
controller.attach(to: self) controller.attach(to: self)
controller.startAnimating() controller.startAnimating()
} else { } else {
self.displayImage() await displayImage()
} }
} else if let image { } else if let image {
self.source = .image(attachmentURL, image) source = .image(attachment.url, image)
self.displayImage() 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")) let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
playImageView.translatesAutoresizingMaskIntoConstraints = false playImageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(playImageView) addSubview(playImageView)
@ -251,9 +218,40 @@ class AttachmentView: GIFImageView {
playImageView.centerXAnchor.constraint(equalTo: centerXAnchor), playImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
playImageView.centerYAnchor.constraint(equalTo: centerYAnchor), 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() let label = UILabel()
label.text = "Audio Only" label.text = "Audio Only"
let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill")) let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
@ -274,23 +272,8 @@ class AttachmentView: GIFImageView {
]) ])
} }
func loadGifv() { private func loadGifv() {
let attachmentURL = self.attachment.url let asset = AVURLAsset(url: 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
}
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill) let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
self.gifvView = gifvView self.gifvView = gifvView
gifvView.translatesAutoresizingMaskIntoConstraints = false gifvView.translatesAutoresizingMaskIntoConstraints = false
@ -306,7 +289,7 @@ class AttachmentView: GIFImageView {
]) ])
} }
func createUnknownLabel() { private func createUnknownLabel() {
backgroundColor = .appSecondaryBackground backgroundColor = .appSecondaryBackground
let label = UILabel() let label = UILabel()
label.text = "Unknown Attachment Type" label.text = "Unknown Attachment Type"
@ -325,8 +308,7 @@ class AttachmentView: GIFImageView {
]) ])
} }
@MainActor private func displayImage() async {
private func displayImage() {
isGrayscale = Preferences.shared.grayscaleImages isGrayscale = Preferences.shared.grayscaleImages
switch source { switch source {
@ -335,12 +317,7 @@ class AttachmentView: GIFImageView {
case let .image(url, sourceImage): case let .image(url, sourceImage):
if isGrayscale { if isGrayscale {
ImageGrayscalifier.queue.async { [weak self] in self.image = await ImageGrayscalifier.convert(url: url, image: sourceImage)
let grayscale = ImageGrayscalifier.convert(url: url, image: sourceImage)
DispatchQueue.main.async { [weak self] in
self?.image = grayscale
}
}
} else { } else {
self.image = sourceImage self.image = sourceImage
} }
@ -348,26 +325,16 @@ class AttachmentView: GIFImageView {
case let .gifData(url, _, sourceImage): case let .gifData(url, _, sourceImage):
if isGrayscale, if isGrayscale,
let sourceImage { let sourceImage {
ImageGrayscalifier.queue.async { [weak self] in self.image = await ImageGrayscalifier.convert(url: url, image: sourceImage)
let grayscale = ImageGrayscalifier.convert(url: url, image: sourceImage)
DispatchQueue.main.async { [weak self] in
self?.image = grayscale
}
}
} else { } else {
self.image = sourceImage self.image = sourceImage
} }
case let .cgImage(url, cgImage): case let .cgImage(url, cgImage):
if isGrayscale { if isGrayscale {
ImageGrayscalifier.queue.async { [weak self] in self.image = await ImageGrayscalifier.convert(url: url, cgImage: cgImage)
let grayscale = ImageGrayscalifier.convert(url: url, cgImage: cgImage)
DispatchQueue.main.async { [weak self] in
self?.image = grayscale
}
}
} else { } else {
image = UIImage(cgImage: cgImage) self.image = UIImage(cgImage: cgImage)
} }
} }
} }