Search code examples
iosswiftparse-platform

How can I properly pass a child class to a Swift 'inout' method for updating?


I would like to create a generic 'refreshInBackground' method for my project that allows updating of my various PFObject subclasses. I can't just use PFObject.refreshInBackground because I want to 'include' several 'keys' (pointers to other objects)

The problem is that when I pass my subclass as an 'inout' parameter I am told

Cannot pass immutable value as inout argument: implicit conversion from 'ParseUser' to 'PFObject' requires a temporary

1) Why is 'currentUser' immutable?? Is that because it is trying to do the implicit conversion?

My subclass is simple

class ParseUser : PFUser {
    @NSManaged var teams : [ParseTeam]           // Teams that the user is a member of

    ..more stuff..
}

The call to update it

var currentUser : ParseUser?
if currentUser != nil {
    // utilize the local user cache... but refresh the user
    // to get the teams
    self.refreshInBackground(parseObject: &currentUser!, withKeys: ["teams"], withCompletion: nil)
}

Finally, the generic update function:

// fetch and refresh an object in the background, including various pointers to included keys
// this is necessary because the native Parse fetchInBackground does not allow 'includeKeys'
func refreshInBackground(inout parseObject object : PFObject, withKeys includeKeys : [String]?, withCompletion completion : ((error : NSError) -> Void)?) {
    // make sure our object has been stored on the server before refershing
    // if it has an objectId, it has been stored
    if let objectId = object.objectId {
        let query = PFQuery(className:object.parseClassName)
        query.whereKey("objectId", equalTo: objectId)
        if let keys = includeKeys {
            query.includeKeys(keys)
        }
        query.getFirstObjectInBackgroundWithBlock({ (updatedObject, error) in
            if error == nil, let update = updatedObject {
                object = update
            }
            completion?(error: error)
        })
    }
    else {
        // oops the object hasn't been saved yet, so don't refresh it
        completion?(error: NSError(domain: "xxxxx", code: 911, userInfo: ["message":"Object Not saved"]))
    }
}

I tried working around it by setting a temporary variable, casting it and passing it in, but that of course doesn't update the currentUser pointer... just the temporary variable

// Clearly doesn't work as the assignment is made to a placeholder
var user = currentUser! as PFObject
self.refreshInBackground(parseObject: &user, withKeys: ["teams"], withCompletion: nil)

Lastly, this can be solved by just returning the updated object and then setting it in the completion handler... but I would like to understand how to do this in Swift such that I don't have to do that each time. Ideally the 'refresh' call is self contained.

thanks


Solution

  • The reason that you can't pass a derived type as an inout parameter of the base type is that this would allow the caller to break type safety. Consider this example (that doesn't work):

    class Base {}
    class Derived1: Base {}
    class Derived2: Base {}
    
    func updateFoo(inout foo: Base) {
        foo = Derived2()
    }
    
    var derived1: Derived1 = Derived1()
    updateFoo(&derived1)
    // derived1 is of type Derived2???
    

    As far as error messages go, Swift's error messages are not great yet. You will often get misleading messages and it's possible that you're hitting one of these cases.

    The other thing that cannot work is that inout arguments shouldn't be included in an escaping closure. Your inout reference of a variable is only valid until the end of the function that accepted it, and my understanding is that getFirstObjectInBackground will easily outlive refreshInBackground (and violate the lifetime of the reference). SE-0035 explains why it doesn't work right now and how it will work with Swift 3.

    In summary, inout parameters create a "shadow copy" of the variable and assign that potentially modified copy back to the original at the end of the function call, so any modification that you want to make to the variable must happen before the end of the function call (which is incompatible with background tasks, that are meant to end at an indefinite point in the future). It will be a compile-time error in Swift 3 to use an inout parameter in an escaping closure. It currently compiles but it doesn't work.