ParseFile init without previous fetch

Is there a way how to init an existing ParseFile without previously fetching it in ParseSwift SDK or set the url property?

I would like to keep an array of ParseFile in a field of parent document. For the case when I would change the content on one ParseFile (image) in the array or delete any from the array, I believe I have to call .delete() or .save() on initialised ParseFile. As I am using realm database for local store, I can store there name and url of the ParseFile after it gets saved. I just can’t do init and set the url property as it is marked as public internal(set) var url: URL?

Is there a security reason why url setter is private?

The url is set from the parse server. It sounds like you are trying to set properties manually.

If you really want to save a ParseFile locally and keep data in sync, why don’t you just encode the ParseFile with a json encoder and save it as Data in your local database? When you fetch the file from your local db, decode it and the url will be intact.

1 Like

Thank you! Saving it in Data form does the job.

There is I guess no batch command to save array of ParseFile, right? Something like .saveAll() on ParseObject

I don’t believe the server supports batch saving an array of files, but I could be wrong. As a workaround you can write a ParseFile extension called saveAll that calls save for each element realizing this will become slower the larger your parse files become and may bog down your server.

If you had an array of ParseObjects that each had a ParseFile, batch saving the array of ParseObjects will work. Similarly, if a ParseObject had multiple properties that each contain a file, saving that object should save all of those files.

As it is closely related to ParseFile init I would like to rise another question here:

Is there a way how to encode and decode a ParseObject that has ParseFile as one of the key into and from JSON?

I am trying to implement “pending saves” on various Realm objects and I got it working well with help od JSON code:

class PendingSave: EmbeddedObject {
    
    /// date when the save operation was triggered
    @objc dynamic var date: Date!
    
    /// Original value before any field god modified
    @objc dynamic var originalValue: String!
    
    /// Object with pending values to be saved at given `date`
    @objc dynamic var pendingSaveValue: String!
    
    
    convenience init<T:Codable>(pendingValue: T, originalValue: T?) {
        self.init()
        self.date = Date()
        let jsonEncoder = JSONEncoder()
        if let originalJsonData = try? jsonEncoder.encode(originalValue) {
            let string = String(data: originalJsonData, encoding: String.Encoding.utf8)
            self.originalValue = string
        }
        if let pendingJsonData = try? jsonEncoder.encode(pendingValue) {
            let string = String(data: pendingJsonData, encoding: String.Encoding.utf8)
            self.pendingSaveValue = string
        }
    }
    
    func originalObjectValues<T:Codable>(objectType: T.Type) -> T? {
        let jsonDecoder = JSONDecoder()
        if let data = originalValue.data(using: .utf8) {
            return try? jsonDecoder.decode(T.self, from: data)
        } else {
            return nil
        }
    }
    
    func pendingObjectValues<T:Codable>(objectType: T.Type) -> T? {
        let jsonDecoder = JSONDecoder()
        if let data = pendingSaveValue.data(using: .utf8) {
            return try? jsonDecoder.decode(T.self, from: data)
        } else {
            return nil
        }
    }
    
    /// Has to be called in realm.write{} transaction
    func setNewPendingValue<T:Codable>(object: T) {
        let jsonEncoder = JSONEncoder()
        if let pendingJsonData = try? jsonEncoder.encode(object) {
            let string = String(data: pendingJsonData, encoding: String.Encoding.utf8)
            self.pendingSaveValue = string
        } else {
            assertionFailure("failed to set updated pending value")
        }
    }

    static func merge<T:Codable>(object: T, withNewer: T/*, uniquingKeysWith conflictResolver: (Any, Any) throws -> Any*/) -> T {
        let encoder = JSONEncoder()
        let selfData = try! encoder.encode(object)
        let withData = try! encoder.encode(withNewer)

        var selfDict = try! JSONSerialization.jsonObject(with: selfData) as! [String: Any]
        let withDict = try! JSONSerialization.jsonObject(with: withData) as! [String: Any]

        selfDict.merge(withDict) { (_, new) in new }

        let final = try! JSONSerialization.data(withJSONObject: selfDict)
        return try! JSONDecoder().decode(T.self, from: final)        
    }

    static func dirtyKeysDictBetween<T:Codable>(object1: T, object2: T) -> [String] {
        let encoder = JSONEncoder()
        
        let object1Data = try! encoder.encode(object1)
        let object2Data = try! encoder.encode(object2)
        
        let object1Dict = try! JSONSerialization.jsonObject(with: object1Data) as! [String: Any]
        let object2Dict = try! JSONSerialization.jsonObject(with: object2Data) as! [String: Any]
        
        let dirtyDict = object1Dict.merging(object2Dict) { (value1, value2) in
            //if the values are same, return nil --> reducing dictionary only to dirty keys
            return (value1 as! AnyHashable == value2 as! AnyHashable ? nil : value2) as Any
        }
        return Array(dirtyDict.keys)
    }

    static func remove<T:Codable>(keys: [String], from object: inout T) {
        let encoder = JSONEncoder()
        let objectData = try! encoder.encode(object)
        var objectDict = try! JSONSerialization.jsonObject(with: objectData) as! [String: Any]
        for key in keys {
            //setting nil to the dirty keys as the fetched updateAt is newer than pending upload. This way the fetched value wins
            objectDict[key] = nil
        }
        let reducedObjectData = try! JSONSerialization.data(withJSONObject: objectDict)
        object = try! JSONDecoder().decode(T.self, from: reducedObjectData)
    }
    
}

But the following decode function returns nil if the ParseObject struct has ParseFile there:

func pendingObjectValues<T:Codable>(objectType: T.Type) -> T? {
    let jsonDecoder = JSONDecoder()
    if let data = pendingSaveValue.data(using: .utf8) {
       //This does not decode ParseFile
       return try? jsonDecoder.decode(T.self, from: data) 
    } else {
        return nil
    }
}

Here is the PendingSave Realm object that it takes data in JSON string (ttl is encrypted string base64data):

PendingSave {
date = 2021-10-17 10:09:03 +0000;
originalValue = {“createdAt”:656158141.30808997,“ttl”:“ps/i3INnqerjxIWCPiNLsc9oqOMezhxE6vrhmsjcnpM/+0H85j9UDfHO”,“objectId”:“sFGyKan1yH”};
pendingSaveValue = {“createdAt”:656158141.32706594,“ifl”:{“name”:“hi_70cIWkCcEP.jpg”,"__type":“File”},“objectId”:“sFGyKan1yH”};
}

If I remove “ifl”:{“name”:“hi_70cIWkCcEP.jpg”,"__type":“File”} from that pendingSaveValue, the merging, decoding and encoding works as intended

I don’t fully understand your question, but a ParseFile along with ParseObject is Encodable and Decodable via:

Every ParseObject allows you to retrieve the proper encoded form with the className prepended using description or debugDescription:

My guess is you are running into issues because your JSON encoder/decoder isn’t configured to handle ParseObject's correctly. I say this because if ParseFile wasn’t encodable/decodable, it would mean you can’t save/retrieve files from a Parse Server, a number of tests on the SDK should have failed, and the Playgrounds wouldn’t work properly. It could also be because you are using Codable which technically ParseTypes are, but they don’t conform to Codable directly. Instead, they are all Encodable and Decodable, not sure if the compiler resolves this for you… To encode any ParseObject or Parse Type, you should be using the included Parse encoders/decoder. These can be accessed by looking at some code from one of my projects:

You can use any instance of one of your ParseObject’s as they will all give you the same encoders/decoders. The ones it seems you are interested in are: jsonEncoder and decoder. You shouldn’t use encoder for your local storage is this is the required encoded needed to send objects to the server.

I was suspecting that the JSON encoder/decoder is not correctly configured and were not aware that there is publicly accessible ParseSwift one. I’ll have a look at that. Thank you for a great help!

1 Like

If you make T:Codable, T: ParseObject instead, you can use the encoders/decoder directly from the objects.

1 Like