1

If have the following code, which should transform a [String: any] document from Firestore into a struct.

When I debug at that time all requirements are met but after returning the value is nil.

I tried changing the init? to a regular init and an else { fatalError() } on the guard. This works and returns a valid struct if data is valid.

What am I doing wrong with the failable initializer?

This does not work (always returns nil, even with valid data):

 struct Banner {
        let destinationUrl: URL
        let imageUrl: URL
        let endTime: Date
        let startTime: Date
        let priority: Int
        let trackingKeyClicked: String
        let trackingKeyDismissed: String

        init?(document: [String: Any]) {
            guard
                let destinationUrlString = document["destinationUrl"] as? String,
                let destinationUrl = URL(string: destinationUrlString),
                let imageUrlString = document["imageUrl"] as? String,
                let imageUrl = URL(string: imageUrlString),
                let priority = document["priority"] as? Int,
                let trackingKeyClicked = document["trackingKeyClicked"] as? String,
                let trackingKeyDismissed = document["trackingKeyDismissed"] as? String,
                let startTime = document["startTime"] as? Date,
                let endTime = document["endTime"] as? Date
                else { return nil }
            self.destinationUrl = destinationUrl
            self.imageUrl = imageUrl
            self.priority = priority
            self.trackingKeyClicked = trackingKeyClicked
            self.trackingKeyDismissed = trackingKeyDismissed
            self.endTime = endTime
            self.startTime = startTime
        }
    }

// using it like this
let bannerStructs = querySnapshot.documents.map { Banner(document: $0.data()) }

This works with valid data (but crashes on wrong data instead of returning nil):

 struct Banner {
        let destinationUrl: URL
        // ...
        let endTime: Date

        init(document: [String: Any]) {
            guard
                let destinationUrlString = document["destinationUrl"] as? String,
                let destinationUrl = URL(string: destinationUrlString),
                // ....
                let endTime = document["endTime"] as? Date
                else { fatalError() }
            self.destinationUrl = destinationUrl
            // ...
            self.endTime = endTime
        }
    }

Esben von Buchwald
  • 2,772
  • 1
  • 29
  • 37
  • If it doesn't work when its not optional init and it crashes, then the init is failing, you need to debug the fields and casts that you do. Put a breakpoint in your else return nil statement and it will hit it. You might be better off using Codable – Scriptable Jan 21 '20 at 13:27
  • It seems that you are dealing with JSON. Why shouldn't your struct implement Codable ? – Zyigh Jan 21 '20 at 13:34
  • "When I debug, i can see that all requirements are met" But we can't see that, as you have shown no data. If it fails, returning nil, that's because the guard statement failed. I believe the runtime, not you. – matt Jan 21 '20 at 13:39
  • you can use optional chaining instead of guard. you will be able to find exact problem. – Aqeel Ahmad Jan 21 '20 at 14:28
  • I have doubts that either ```startTime``` or ```endTime``` get casted into ```Date```. Can you put a break point on the start of your guard statement, then hit ```Step over``` and check which of your variables fail to be casted? I make an assumption that it is your ```startTime``` and ```endTime``` variables which fail and thus, return ```nil```. – Starsky Jan 21 '20 at 15:21

3 Answers3

1

If none of your guard let conditions are met it will fail and eventually trigger a fatalError or return nil, depending on which implementation you use. Please debug well which of the data is not parsed/casted correctly.

I setup a good example on how you might expect it to work and a bad example to let you know how one attribute that is not expected in that format can make the initialiser return nil:

import Foundation

struct Banner {
    let destinationUrl: URL
    let imageUrl: URL
    let endTime: Date
    let startTime: Date
    let priority: Int
    let trackingKeyClicked: String
    let trackingKeyDismissed: String

    init?(document: [String: Any]) {
        guard
            let destinationUrlString = document["destinationUrl"] as? String,
            let destinationUrl = URL(string: destinationUrlString),
            let imageUrlString = document["imageUrl"] as? String,
            let imageUrl = URL(string: imageUrlString),
            let priority = document["priority"] as? Int,
            let trackingKeyClicked = document["trackingKeyClicked"] as? String,
            let trackingKeyDismissed = document["trackingKeyDismissed"] as? String,
            let startTime = document["startTime"] as? Date,
            let endTime = document["endTime"] as? Date
            else { return nil }
        self.destinationUrl = destinationUrl
        self.imageUrl = imageUrl
        self.priority = priority
        self.trackingKeyClicked = trackingKeyClicked
        self.trackingKeyDismissed = trackingKeyDismissed
        self.endTime = endTime
        self.startTime = startTime
    }
}

// using it like this
let goodData:[String:Any] = [
    "destinationUrl": "http://destination--url",
    "imageUrl": "http://image-url",
    "priority": 17,
    "trackingKeyClicked": "Tracking Key Clicked",
    "trackingKeyDismissed": "Tracking Key Dismissed",
    "startTime": Date(),
    "endTime": Date()
    ]
let goodBannerStructs = Banner(document: goodData)

let badData:[String:Any] = [
    "destinationUrl": "http://destination--url",
    "imageUrl": "http://image-url",
    "priority": 17,
    "trackingKeyClicked": "Tracking Key Clicked",
    "trackingKeyDismissed": "Tracking Key Dismissed",
    "startTime": "17 December",
    "endTime": Date()
    ]
let badBannerStructs = Banner(document: badData)

print("Good banner: \(goodBannerStructs)")
print("Bad banner: \(badBannerStructs)")

This is what it prints out:

Good banner: Optional(Banner(destinationUrl: http://destination--url, imageUrl: http://image-url, endTime: 2020-01-21 17:45:27 +0000, startTime: 2020-01-21 17:45:27 +0000, priority: 17, trackingKeyClicked: "Tracking Key Clicked", trackingKeyDismissed: "Tracking Key Dismissed"))
Bad banner: nil

You can try this code on: http://online.swiftplayground.run/

It can be one the dictionary keys of document might be incorrect with what is coming from the query, might be that priority might not be an Int or the dates might be String. You have to debug it.

denis_lor
  • 6,212
  • 4
  • 31
  • 55
  • Of course. But can you tell me why it returns nil with valid data? – Esben von Buchwald Jan 21 '20 at 14:57
  • Try to transform that guard let..,let...,let... in separated if statements in order for you to better have an overview when you debug. And try ti check where it doesn't work. Otherwise you could try with the debugger and the console `po` command if you are familiar with it and you won't need to temporary change your guard statement. – denis_lor Jan 21 '20 at 17:22
  • Please re-check my answer, I included an example that fits your struct initialisation, a bad and a good example. Make sure you get your error with the debugger or by printing out values in order to check why one or more of the casting/parsing is not behaving correctly. – denis_lor Jan 21 '20 at 17:49
1

If the failable initialiser is returning nil and the normal initialiser is crashing because of bad data then that points me towards the guard statement in the failable initialiser failing leading to it returning nil. Place a breakpoint on the return nil line within the guard statement and see if this is being hit.

GetSwifty
  • 13
  • 1
  • 5
0

This does not work (always returns nil, even with valid data)

Since your guard is always failing, the data seems to be incorrect. I guess that startDate and endDate aren't that easy to be converted to Date. Could you please post a example of the json data?

If this is the cause, here is someone describing how to use a DateFormatter to create a date from a string. If your dates follow ISO8601 you can use Apples ISO8601DateFormatter to do it.

Paul Schröder
  • 1,440
  • 1
  • 10
  • 19