user5090684
user5090684

Reputation: 21

Swift 2.0 new 'perform changes' of PHPPhotoLibrary won't work

I just downloaded Xcode 7 Beta 2 and am trying to use my knowledge of Swift to make an app that deletes a photo from the user's camera roll. I know how to do this normally with Swift 1.2, but I can't seem to get it in Swift 2.0. I tried searching through the documentation to learn how to use the 'performChange' function in Swift 2.0, but it won't work. Here's my Swift :

PHPhotoLibrary.sharedPhotoLibrary().performChanges({
    PHAssetChangeRequest.deleteAssets(arrayToDelete)
}, completionHandler: { (success, error) -> Void in
    NSLog("Finished deleting asset. %@", (success ? "Success" : error))
})

Here's my error:

Cannot invoke performChanges with an argument list of type (() -> _, completionHandler: (_, _) -> Void)

Any help is appreciated!!

Upvotes: 2

Views: 1222

Answers (1)

rickster
rickster

Reputation: 126167

The Swift compiler generally has issues with reporting the correct root cause of a type-checking failure in a complex expression. Rather than simply show you the correct form of this code, I'll walk through how I found the way there, so you can reuse that process for future debugging. (Skip down for the TLDR if you must.)


First, you've got an error of the form:

Cannot invoke 'function' with an argument list of type 'params'

That means that something about your function call has failed to type-check. Because you're calling a function where the parameters include closures, you'll need to look at the behavior of the closures to narrow down the type-checking issues.

Let's start by making the closures explicitly return Void:

PHPhotoLibrary.sharedPhotoLibrary().performChanges({
    PHAssetChangeRequest.deleteAssets(arrayToDelete)
    return
}, completionHandler: { (success, error) -> Void in
    NSLog("Finished deleting asset. %@", (success ? "Success" : error))
    return
})

What's going on here? You've already declared the type of the completion handler as returning Void, so why the extra return statement? Swift type checking works both bottom-up and top-down, and if there's an error along either way, it can't make assumptions about the other. Here, you have two single-expression closure, so Swift has to consider that each one's single expression could be an implicit return statement.

If one of those statements has a type-checking error, that statement's return type becomes <<error type>>, and because it's a single-statement closure, the closure's return type becomes <<error type>>, and therefore the function call to which the closure is a parameter fails, because the function is expecting a closure that returns Void, not a closure that returns <<error type>>.

Indeed that's what's happening — once we make the above change, we get a different error, on the NSLog statement (with "Success" highlighted):

'_' is not convertible to 'StringLiteralConvertible'

That's a bit unclear still, but we're closer to the root of the problem. If you replace the (success ? "Success" : error) part of the log statement with something static (say, just "Success"), it compiles. So, let's separate and dissect that ternary operation to see what's going wrong inside it.

let successString = "Success"
let report = (success ? successString : error)
NSLog("Finished deleting asset. %@", report)

This gets us a new error, on the ? of the ternary operator:

'NSString' is not a subtype of 'NSError'

Huh? Something to do with automatic conversion of Swift.String to NSString, maybe? Let's make that conversion explicit to be sure:

let successString = "Success" as NSString
let report = (success ? successString : error)

type of expression is ambiguous without more context

Now we reach the crux of the matter. What, indeed, is the type of report supposed to be? If success is true, it's NSString, but if false, it's NSError? (the type of error, inferred from the declaration of performChanges(_:completionHandler:)). This sort of type hand-waviness will fly in C, but Swift is much more strict about such things. (Someone very wise once said, "Incomplete type specification leads to unclear memory layout, unclear memory layout leads to undefined behavior, undefined behavior leads to suffering." Or something like that.)

The only supertype of both NSString and NSError? is Any, and Swift is reluctant to infer that type. (Because if you infer that everything can be anything, your type information is worthless.) And if you try to use that type manually, you get errors when you try to pass it to NSLog:

let report: Any = (success ? successString : error)
NSLog("Finished deleting asset. %@", report)

cannot invoke 'NSLog' with an argument list of type '(String, Any)'

expected an argument list of type '(String, [CVarArgType])'

Those errors take us off down the rabbit hole of C vararg functions, so let's step back a bit — what type does NSLog really want for this argument? NSLog is an ObjC function, with format string substitution (the %@ business) built on NSString stringWithFormat. Per the docs, when you use a %@ token, NSString looks for an object in the corresponding parameter and calls its description method. Because that implementation is ObjC and part of the Cocoa frameworks (dating back to before the dinosaurs were wiped out), not a Swift thing, it stands to reason that a pure-Swift type like Any won't work here.

NSObject would be a good Cocoa type to pass to the NSLog function. However, declaring that as the type of report won't fly, either — you can't implicitly convert both branches of the ternary operator to NSObject. The error parameter is an optional — its inferred type is NSError?, remember? And that's a Swift type, not a Cocoa type.

So that's the final problem — you have a ternary operator that's trying to have one branch be a perfectly sensible object, and the other branch a still-wrapped optional. Indeed, force-unwrapping the optional clears all the compiler errors:

let report = (success ? successString : error!) // report now type-infers NSObject
NSLog("Finished deleting asset. %@", report)

Now that we've fixed everything, we can put the wheels back on and collapse everything back down...


TLDR: Unwrap your optionals.

PHPhotoLibrary.sharedPhotoLibrary().performChanges({
    PHAssetChangeRequest.deleteAssets(arrayToDelete)
}, completionHandler: { success, error in
    NSLog("Finished deleting asset. %@", (success ? "Success" : error!))
})

We know it's safe to force-unwrap here because of the API contract: if success is true, error will be nil, but if success is false, there will actually be an error.

(This may have even worked for you without unwrapping on previous SDK versions because the closure type in the performChanges(_:completionHandler:) would have used an implicitly-unwrapped optional before Apple audited all their APIs for nullability.)

Upvotes: 6

Related Questions