swifticloud-documents

Images would not be synchronized


The user can add a image, which name would be stored in the database under "imagename". the database would be synchronized in a second on iPhone and example iPad of the user. A image get a new Name and be saved.

Here is the function for this:

private func saveImage() -> String? {
    guard let image = inputImage,
          let imageData = image.jpegData(compressionQuality: 0.5) else {
        return nil
    }
    
    let timestamp = Date().timeIntervalSince1970
    let imageName = "\(UUID().uuidString)-\(timestamp)"
    
    if let existingImageName = tool?.imagename {
        // Delete the existing image
        if let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents") {
            let existingImagePath = iCloudDocumentsURL.appendingPathComponent(existingImageName)
            try? FileManager.default.removeItem(at: existingImagePath)
        } else {
            let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            let existingImagePath = documentsDirectory.appendingPathComponent(existingImageName)
            try? FileManager.default.removeItem(at: existingImagePath)
        }
    }
    
    var imagePath: URL
    if let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents") {
        // iCloud storage is available
        imagePath = iCloudDocumentsURL.appendingPathComponent(imageName)
    } else {
        // iCloud storage is not available, use local storage
        let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        imagePath = documentsDirectory.appendingPathComponent(imageName)
    }
    
    do {
        try imageData.write(to: imagePath)
        print("Image saved with name: \(imageName)")
        return imageName
    } catch {
        print("Error saving image: \(error.localizedDescription)")
        return nil
    }
}

When I like to view the picture then I use the loadImage() Function:

func loadImage() {
    if let inputImage = inputImage {
        // An image has already been selected, so just display it
        self.inputImage = inputImage
    } else if let imageName = tool?.imagename {
        // Check both local and iCloud Documents directories for the image
        let localImagePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(imageName)

        var iCloudImagePath: URL?
        if let iCloudURL = FileManager.default.url(forUbiquityContainerIdentifier: nil) {
            iCloudImagePath = iCloudURL.appendingPathComponent("Documents").appendingPathComponent(imageName)
        }

        if FileManager.default.fileExists(atPath: localImagePath.path) {
            // Image exists in local directory, load it from there
            do {
                let imageData = try Data(contentsOf: localImagePath)
                inputImage = UIImage(data: imageData)
            } catch {
                print("Error loading image from local directory: \(error.localizedDescription)")
                inputImage = nil
            }
        } else if let iCloudImagePath = iCloudImagePath, FileManager.default.fileExists(atPath: iCloudImagePath.path) {
            // Image exists in iCloud Documents directory, load it from there
            do {
                let imageData = try Data(contentsOf: iCloudImagePath)
                inputImage = UIImage(data: imageData)
            } catch {
                print("Error loading image from iCloud Documents directory: \(error.localizedDescription)")
                inputImage = nil
            }
        } else {
            // Image not found in either directory
            inputImage = nil
        }
    } else {
        // No image selected
        inputImage = nil
    }
}

This is the persistanceController:

struct PersistenceController {
    
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        do {

            try viewContext.save()

        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    let container: NSPersistentCloudKitContainer

    
    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "HereIsTheContainerName")
        
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
    }
}

I can save the Image and on the local System it would viewed. When I watch the value of imagename at the iPad, then the correct name is there, but the image would not be viewed. It seems like that the image would not be synchronized to the iClouddrive.

Both devices have the option enabled for the app "iCloud" and "iCloudDrive".

Can you find a fail of my code?


Solution

  • My assumption why the images are only visible locally is, that within the saveImage() function the call url(forUbiquityContainerIdentifier:) returns nil (according to the doc: "if the container could not be located or if iCloud storage is unavailable for the current user or device"), hence with the else statement the image gets saved in your local storage (not within the iCloud container).

    Note: the function url(forUbiquityContainerIdentifier:) does not depend on network connectivity, meaning you can save the image in the container and let iOS synchronize them.

    Design hint: the function should not be called directly from the main thread (use something like DispatchQueue.global to retrieve the url).

    Refere to doc

    func url(forUbiquityContainerIdentifier:)

    Important

    Do not call this method from your app’s main thread. Because this method might take a nontrivial amount of time to set up iCloud and return the requested URL, you should always call it from a secondary thread. To determine if iCloud is available, especially at launch time, check the value of the ubiquityIdentityToken property instead.

    PS: if you want to force the download have a look at startDownloadingUbiquitousItem(at:) and to delete the local copy only evictUbiquitousItem(at:)