I am using a Task block to fetch images from disk to be displayed in a UIImageView.
I noticed that as I scroll the collection view, there’s noticeable lag caused by the getArtwork method meaning it is blocking the main actor.
Marking it as “nonisolated” fixes the issue which if I understand correctly, means the method is not isolated to the current actor context but most examples I have seen uses the keyword in actors NOT classes thus I am wondering if it is even appropriate in a class?
class ListViewCell: UICollectionViewListCell {
private lazy var thumbnailImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 5
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var placeholderImageView: UIImageView = {
let cfg = UIImage.SymbolConfiguration(scale: .small)
let image = UIImage(systemName: "music.note", withConfiguration: cfg)
let imageView = UIImageView(image: image)
imageView.contentMode = .scaleAspectFit
imageView.tintColor = .tertiaryLabel
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var nameLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.lineBreakMode = .byTruncatingTail
return label
}()
private lazy var artistLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .subheadline)
label.textColor = .secondaryLabel
label.lineBreakMode = .byTruncatingTail
return label
}()
var audioFile: AudioFile?
private var thumbnailTask: Task<Void, Never>?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
thumbnailTask?.cancel()
thumbnailImageView.image = nil
thumbnailImageView.backgroundColor = nil
}
func configure(with audioFile: AudioFile) {
self.audioFile = audioFile
if let artwork = audioFile.artwork {
thumbnailTask = Task { [weak self] in
var thumbnail: UIImage?
if let cachedImage = self?.imageCache.object(forKey: artwork as NSString) {
thumbnail = cachedImage
} else {
thumbnail = await self?.getArtwork(for: artwork)
}
await MainActor.run { [weak self] in
// Check if the cell's audioFile is still the same
if audioFile == self?.audioFile {
self?.thumbnailImageView.image = thumbnail
}
}
}
} else {
thumbnailImageView.addSubview(placeholderImageView)
NSLayoutConstraint.activate([
placeholderImageView.widthAnchor.constraint(equalToConstant: 30),
placeholderImageView.heightAnchor.constraint(equalToConstant: 30),
placeholderImageView.centerXAnchor.constraint(equalTo: thumbnailImageView.centerXAnchor),
placeholderImageView.centerYAnchor.constraint(equalTo: thumbnailImageView.centerYAnchor)
])
thumbnailImageView.bringSubviewToFront(placeholderImageView)
thumbnailImageView.backgroundColor = .quaternarySystemFill
}
}
private var imageCache = NSCache<NSString, UIImage>()
nonisolated func getArtwork(for name: String) async -> UIImage? {
let url = FileManager.artworksFolderURL.appendingPathComponent(name)
do {
let data = try Data(contentsOf: url)
if let image = UIImage(data: data) {
let targetSize = CGSize(width: 50, height: 50)
let imageSize = image.size
let widthRatio = targetSize.width / imageSize.width
let heightRatio = targetSize.height / imageSize.height
let scaleFactor = min(widthRatio, heightRatio)
let scaledImageSize = CGSize(width: imageSize.width * scaleFactor, height: imageSize.height * scaleFactor)
let renderer = UIGraphicsImageRenderer(size: targetSize)
let centeredImage = renderer.image { context in
let origin = CGPoint(
x: (targetSize.width - scaledImageSize.width) / 2,
y: (targetSize.height - scaledImageSize.height) / 2
)
context.cgContext.addPath(UIBezierPath(roundedRect: CGRect(origin: origin, size: scaledImageSize), cornerRadius: 5).cgPath)
context.cgContext.clip()
image.draw(in: CGRect(origin: origin, size: scaledImageSize))
}
await MainActor.run {
imageCache.setObject(centeredImage, forKey: name as NSString)
}
return centeredImage
}
} catch {
print("Error loading image data: (error)")
return nil
}
return nil
}
}