8
votes

I am using Firebase on iOS with Swift 3.

When I use

FIRDatabase.database().reference().child("child").setValue("value") { 
  (error: Error?, databaseReference: FIRDatabaseReference) in
    print("Error while setting value \(error)")
}   

The app crashes on runtime with the following log:

*** Terminating app due to uncaught exception 'InvalidFirebaseData', reason: '(nodeFrom:priority:) Cannot store object of type _SwiftValue at . Can only store objects of type NSNumber, NSString, NSDictionary, and NSArray.'

I tried to use the same function but without the trailing closure and for some reason, it works!

FIRDatabase.database().reference().child("child").setValue("value", 
  withCompletionBlock: { 
    (error: Error?, databaseReference: FIRDatabaseReference) in
      print("Error while setting value \(error)")
})

Is there something special about trailing closures and Swift 3?

2
Crash is because you can only set values / store object of type NSNumber, NSString, NSDictionary, and NSArray & the value which you are storing is not of any such type. - iYoung
Not sure if that is the problem. I tried to use an NSString variable and pass it to setValue and it would still crash. But either case, not using a closure with regular Swift String seems to work. - Emad Toukan
@EmadToukan How did you get the Crash log on runtime? It may be a silly question to ask! - Anurag Sharma
@AnuragSharma run the firebase app on a device and you should be able to see it in the Xcode console. - Emad Toukan
@EmadToukan obviously I know this but I was getting a crash after I sent build through testflight. And even the crash was not appearing while I debug. Now it is resolved! My NSTimer implementation was causing the crash and I got this report from Crashlytics. - Anurag Sharma

2 Answers

8
votes

tl;dr: Firebase provides a setValue(_ value: Any?, andPriority priority: Any?) which is incorrectly matched when using a trailing closure with setValue(_ value: Any?, withCompletionBlock: (Error?, FIRDatabaseReference) -> Void).

Solution: When using an API that has many varieties, avoid using trailing closures. In this case, prefer setValue(myValue, withCompletionBlock: { (error, dbref) in /* ... */ }); do not use setValue(myValue) { (error, dbref) in /* ... */ }.

Explanation

This appears to be a Swift bug. As in other languages, such as Java, Swift generally chooses the most specific overload. E.g.,

class Alpha {}
class Beta : Alpha {}

class Charlie {
    func charlie(a: Alpha) {
        print("\(#function)Alpha")
    }
    func charlie(a: Beta) {
        print("\(#function)Beta")
    }
}

Charlie().charlie(a: Alpha()) // outputs: charlie(a:)Alpha
Charlie().charlie(a: Beta() as Alpha) // outputs: charlie(a:)Alpha
Charlie().charlie(a: Beta()) // outputs: charlie(a:)Beta

However, when overloaded functions match a trailing closure, Swift (at least, sometimes) selects the more general type. E.g.,

class Foo {
    func foo(completion: () -> Void) {
        print(#function)
    }
    func foo(any: Any?) {
        print(#function)
    }
}

func bar() {}
Foo().foo(completion: bar) // outputs: foo(completion:)
Foo().foo(any: bar) // outputs: foo(any:)
Foo().foo() { () in } // outputs: foo(any:)
// ^---- Here lies the problem
// Foo().foo(bar) will not compile; can't choose between overrides.

Any? is a more general type than () -> Void -- i.e., "anything, even null" is more broad than "a function receiving 0 parameters and returning something of type Void". However, the trailing closure matches Any?; this is the opposite of what you would expect from a language that matches the most specific type.

-3
votes

While there is an accepted answer, which provides some clarity, explaining that it's a Swift bug is not really accurate. That being said, the explanation is accurate but not for this issue.

Allowing the closure to be added to setValue in the first place is the real bug.

A more accurate answer is that there is no completion block/closure for the setValue function, which is why it fails.

The specific function -setValue: does NOT have a closure, which is why it's crashing. i.e. it's an incorrect implementation in your code. Per the docs:

func setValue(_ value: Any?)

note that the setValue function does NOT have a closure and if you add one the function will have no idea what to do with that data.

To use a completion block/closure, you must call the correct function which is

-setValue:withCompletionBlock:

Bottom line is you can't randomly add a parameter or call to a function that's not designed to accept it.

This is obviously not valid but conceptually it's the same error.

    let a = "myString"
    a.append("x") { (error: Error?) }

In this case the compiler knows the the string.append function doesn't have a closure option and catches it before compiling.

So go a tad further, this code complies & runs but also generates an error

ref.child("child").setValue("value") { }

Again, setValue doesn't have a closure so this code is improperly implemented.

To clarify, given a class

class MyClass {
    var s = ""
    var compHandler = {}

    func setValue(aString: String) {
        self.s = aString
    }

    func setValue(aString: String, someCompletionHandler: completionHandler) {
        self.s = aString
        self.compHandler = someCompletionHandler
    }
}

Note that setValue:aString is a totally different function than setValue:aString:someCompletionHandler

The only parameter that can be based to setValue:aString is a String as the first and only parameter.

The setValue:aString:someCompletionHandler will be passed two parameters, a String in the first position and a completionHandler in the second position.

The actual completion block is the second parameter passed.

param1, param2 ------, --------------- string, completionBlock

This is why

setValue(value) {}

is improperly formatted whereas

setValue(value, withBlock: {})

is properly formatted.