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
}
}
getArtwork(for:)seems to be called only from within a Task which is created in the methodconfigure(with:)which is isolated, and thus the Task's closure and thusgetArtwork(for:)should run on this actor as well, i.e. on the main thread. I fail to see why addingnonisolatedto this function should fix it, unless you call it directly from elsewhere. IMHO, the problems you see coming from the weaknesses in your design. So, you should tackle this first ;)nonisolatedasyncfunction explicitly runs on a generic executor, i.e. off the main actor in this case. See SE-0338. (And, as an aside, Swift 6 gives even more control, via SE-0417.)static, they will all use a single, shared cache.