Compiles with strict concurrency checking
This commit is contained in:
parent
c489d018bd
commit
ba60f92223
|
@ -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)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue