Main actor-isolated property can not be reference from a Sendable closure

I am working thru the issues of turning on Strict Concurrency Checking. I have a SwiftData application, and I am compressing images before saving them as data. My save function is pretty simple

    private func save() {
        ImageCompressor.compress(image: (frontImageSelected?.asUIImage())!, maxByte: 1_048_576) { image in
            guard image != nil else {
                print("Error compressing image")
                return
            }
            if let greetingCard {
                greetingCard.cardName = cardName
                greetingCard.cardFront = image?.pngData()
                greetingCard.cardManufacturer = cardManufacturer
                greetingCard.cardURL = cardURL
                greetingCard.eventType = eventType
                
            } else {
                let newGreetingCard = GreetingCard(cardName: cardName, cardFront: image?.pngData(), eventType: eventType, cardManufacturer: cardManufacturer, cardURL: cardURL)
                modelContext.insert(newGreetingCard)
            }
        }
    }

I compress the selected image, I had to change my ImageCompressor.compress closure to Sendable, but now every assignment above is flagging with the above warning. I define the greetingCard as var greetingCard: GreetingCard? in my view, since I can have it passed in for edit, or generated if new.

I also get the same warning on modelContext, which is defined as @Environment(\.modelContext) private var modelContext.

It's not clear to me how to address this warning. Any pointers would be helpful.

Answered by Michaelrowe01 in 792583022

Figured it out..

struct ImageCompressor {
    
    /// This is a static method that takes three parameters:
    /// image: The input UIImage that needs to be compressed.
    /// maxByte: The maximum allowable size for the compressed image in bytes.
    /// completion: A closure that will be called when the compression is complete, passing the resulting compressed UIImage or nil if an error occurs.
    static func compress(image: UIImage, maxByte: Int,
                         completion: @Sendable @MainActor @escaping (UIImage?) -> Void) {
        MainActor.assumeIsolated {
            guard let currentImageSize = image.jpegData(compressionQuality: 1.0)?.count else {
                return completion(nil)
            }

            var iterationImage: UIImage? = image
            var iterationImageSize = currentImageSize
            var iterationCompression: CGFloat = 1.0

            while iterationImageSize > maxByte && iterationCompression > 0.01 {
                let percentageDecrease: CGFloat
                                switch iterationImageSize {
                                case 0..<3000000: percentageDecrease = 0.05
                                case 3000000..<10000000: percentageDecrease = 0.1
                                default: percentageDecrease = 0.2
                                }

                let canvasSize = CGSize(width: image.size.width * iterationCompression,
                                        height: image.size.height * iterationCompression)
                UIGraphicsBeginImageContextWithOptions(canvasSize, false, image.scale)
                defer { UIGraphicsEndImageContext() }
                image.draw(in: CGRect(origin: .zero, size: canvasSize))
                iterationImage = UIGraphicsGetImageFromCurrentImageContext()

                guard let newImageSize = iterationImage?.jpegData(compressionQuality: 1.0)?.count else {
                    return completion(nil)
                }
                iterationImageSize = newImageSize
                iterationCompression -= percentageDecrease
            }
            completion(iterationImage)
        }
    }
}

Once I changed my ImageCompressor the save function looks like this -

    private func save() {
        ImageCompressor.compress(image: (frontImageSelected?.asUIImage())!, maxByte: 1_048_576) { image in
            guard image != nil else {
                print("Error compressing image")
                return
            }
            if let greetingCard {
                greetingCard.cardName = cardName
                greetingCard.cardFront = image?.pngData()
                greetingCard.cardManufacturer = cardManufacturer
                greetingCard.cardURL = cardURL
                greetingCard.eventType = eventType
                
            } else {
                let newGreetingCard = GreetingCard(cardName: cardName, cardFront: image?.pngData(), eventType: eventType, cardManufacturer: cardManufacturer, cardURL: cardURL)
                modelContext.insert(newGreetingCard)
            }
        }
    }
Accepted Answer

Figured it out..

struct ImageCompressor {
    
    /// This is a static method that takes three parameters:
    /// image: The input UIImage that needs to be compressed.
    /// maxByte: The maximum allowable size for the compressed image in bytes.
    /// completion: A closure that will be called when the compression is complete, passing the resulting compressed UIImage or nil if an error occurs.
    static func compress(image: UIImage, maxByte: Int,
                         completion: @Sendable @MainActor @escaping (UIImage?) -> Void) {
        MainActor.assumeIsolated {
            guard let currentImageSize = image.jpegData(compressionQuality: 1.0)?.count else {
                return completion(nil)
            }

            var iterationImage: UIImage? = image
            var iterationImageSize = currentImageSize
            var iterationCompression: CGFloat = 1.0

            while iterationImageSize > maxByte && iterationCompression > 0.01 {
                let percentageDecrease: CGFloat
                                switch iterationImageSize {
                                case 0..<3000000: percentageDecrease = 0.05
                                case 3000000..<10000000: percentageDecrease = 0.1
                                default: percentageDecrease = 0.2
                                }

                let canvasSize = CGSize(width: image.size.width * iterationCompression,
                                        height: image.size.height * iterationCompression)
                UIGraphicsBeginImageContextWithOptions(canvasSize, false, image.scale)
                defer { UIGraphicsEndImageContext() }
                image.draw(in: CGRect(origin: .zero, size: canvasSize))
                iterationImage = UIGraphicsGetImageFromCurrentImageContext()

                guard let newImageSize = iterationImage?.jpegData(compressionQuality: 1.0)?.count else {
                    return completion(nil)
                }
                iterationImageSize = newImageSize
                iterationCompression -= percentageDecrease
            }
            completion(iterationImage)
        }
    }
}

Once I changed my ImageCompressor the save function looks like this -

    private func save() {
        ImageCompressor.compress(image: (frontImageSelected?.asUIImage())!, maxByte: 1_048_576) { image in
            guard image != nil else {
                print("Error compressing image")
                return
            }
            if let greetingCard {
                greetingCard.cardName = cardName
                greetingCard.cardFront = image?.pngData()
                greetingCard.cardManufacturer = cardManufacturer
                greetingCard.cardURL = cardURL
                greetingCard.eventType = eventType
                
            } else {
                let newGreetingCard = GreetingCard(cardName: cardName, cardFront: image?.pngData(), eventType: eventType, cardManufacturer: cardManufacturer, cardURL: cardURL)
                modelContext.insert(newGreetingCard)
            }
        }
    }
Main actor-isolated property can not be reference from a Sendable closure
 
 
Q