Esben von Buchwald
Esben von Buchwald

Reputation: 3112

Failable initializer always returns nil even though requirements are met

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
        }
    }

Upvotes: 1

Views: 201

Answers (3)

denis_lor
denis_lor

Reputation: 6547

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.

Upvotes: 1

Paul Schröder
Paul Schröder

Reputation: 1540

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.

Upvotes: 0

GetSwifty
GetSwifty

Reputation: 13

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.

Upvotes: 1

Related Questions