Orphaning a CKAsset

I'm running into a problem in my attempt to clear CKAssets on the iCloud server. The documentation for CKAsset says:

If you no longer require an asset that’s on the server, you don’t delete it. Instead, orphan the asset by setting any fields that contain the asset to nil and then saving the record. CloudKit periodically deletes orphaned assets from the server.

I'm deleting image file assets which are properties on an ImageReference type (largeImage and thumbNailImage properties). When I delete an image, I am setting those properties to nil and sending the record for the ImageReference to iCloud using the async CKDatabase.modifyRecords method.

This always results in an error: <CKError 0x600000d92a60: "Asset File Not Found" (16/3002); "open error: 2 (No such file or directory)">

And of course the assets still appear in the CloudKit dashboard.

What is the proper way of orphaning the assets on the CloudKit server?

Answered by deeje in 879969022

Make this function handle nil values for largeImageURL and thumbnailURL…

    override func addAssetsToRecord(_ record: CKRecord) {
        // add the image data as CKAssets
if let largeImageURL, let thumbnailURL {
        let largeAsset = CKAsset(fileURL: largeImageURL)
        let thumbAsset = CKAsset(fileURL: thumbnailURL)
        record.setObject(largeAsset, forKey: "largeImage")
        record.setObject(thumbAsset, forKey: "thumbnailImage")
} else {
        record.setObject(nil, forKey: "largeImage")
        record.setObject(nil, forKey: "thumbnailImage")
    }

another way is to just delete the ImageAsset CKRecord itself, which will also orphan the CKAssets.

can you show some code?

can you show some code?

I put together a barebones demo that goes through the same flow, with many shortcuts for brevity sake.

When I need to delete an ImageReference instance, I call the DataManager's deleteImage(_) method. When I do so, it falls through to the failure case of the CloudKitController's pushToICloud method.

//  CloudObject

import Foundation
import CloudKit

class CloudObject: NSObject {
    var properties: [String] = []
    var className: String = ""
    let cloudIdentifier = UUID().uuidString
    
    func cloudRecord() -> CKRecord {
        let recordID = CKRecord.ID(recordName: cloudIdentifier)
        let record = CKRecord(recordType: className, recordID: recordID)
        for property in properties {
            let value = self.value(forKey: property)
            record.setValue(value, forKey: property)
        }
        
        addAssetsToRecord(record)
        
        return record
    }
    
    func addAssetsToRecord(_ record: CKRecord) {
        // let subclasses use for adding assets or other content as needed
    }
}

//  ImageReference

import Foundation
import CloudKit
import UniformTypeIdentifiers

class ImageReference: CloudObject {
    @objc var largeImage: Data?
    @objc var thumbnailImage: Data?
    
    private var largeImageURL: URL {
        return ImageReference.imageFolderURL.appendingPathComponent(cloudIdentifier + "-large.jpg", conformingTo: .image)
    }

    private var thumbnailURL: URL {
        return ImageReference.imageFolderURL.appendingPathComponent(cloudIdentifier + "-thumbnail.jpg", conformingTo: .image)
    }

    static let imageFolderURL: URL = URL.documentsDirectory.appendingPathComponent("Images", conformingTo: .folder)
    
    override init() {
        super.init()
        className = "ImageReference"
        properties.append(contentsOf: ["largeImage", "thumbnailImage"])
    }
    
    override func addAssetsToRecord(_ record: CKRecord) {
        // add the image data as CKAssets
        let largeAsset = CKAsset(fileURL: largeImageURL)
        let thumbAsset = CKAsset(fileURL: thumbnailURL)
        record.setObject(largeAsset, forKey: "largeImage")
        record.setObject(thumbAsset, forKey: "thumbnailImage")
    }
}


//  DataManager

import Foundation

class DataManager {
    var cloudKitController = CloudKitController()
    
    func deleteImage(_ imageRef: ImageReference) async {
        // delete the image reference from the local database...
        // then:
        imageRef.largeImage = nil
        imageRef.thumbnailImage = nil
        
        await cloudKitController.pushToICloud(object: imageRef)
    }
}


//  CloudKitController

import Foundation
import CloudKit
import os.log

class CloudKitController {
    private var ckDatabase: CKDatabase

    init() {
        let container = CKContainer(identifier: "Demo Container Name")
        ckDatabase = container.privateCloudDatabase
    }
    
    func pushToICloud(object: CloudObject) async {
        // get a CKRecord for the object
        let record = object.cloudRecord()
        
        do {
            let response = try await ckDatabase.modifyRecords(saving: [record], deleting: [], atomically: false)
            let resultsDict = response.saveResults
            for result in resultsDict {
                switch result.value {
                    case .success(let record):
                        // call code that handles writing record to local DB
                        break // switch requires code execution of some sort, so for this demo...
                    case .failure(let error):
                        if let ckError = (error as? CKError) {
                            os_log("CK - CloudKit failure with error: \(ckError) in \(#function)")
                        }
                }
            }
        } catch {
            //handle the error
        }
    }
}```

Accepted Answer

Make this function handle nil values for largeImageURL and thumbnailURL…

    override func addAssetsToRecord(_ record: CKRecord) {
        // add the image data as CKAssets
if let largeImageURL, let thumbnailURL {
        let largeAsset = CKAsset(fileURL: largeImageURL)
        let thumbAsset = CKAsset(fileURL: thumbnailURL)
        record.setObject(largeAsset, forKey: "largeImage")
        record.setObject(thumbAsset, forKey: "thumbnailImage")
} else {
        record.setObject(nil, forKey: "largeImage")
        record.setObject(nil, forKey: "thumbnailImage")
    }

another way is to just delete the ImageAsset CKRecord itself, which will also orphan the CKAssets.

Make this function handle nil values for largeImageURL and thumbnailURL…

Yes! That was the problem. Thanks for spotting that for me. I actually need to check for nil Data (not URL) properties, but that fixed the problem.

Orphaning a CKAsset
 
 
Q