Hot questions for Using Eureka in firebase

Question:

I'm writing an extension to bridge the dictionary values between FirebaseDatabase and Eureka.

private extension Dictionary {
    func firebaseFriendlyDictionary() -> [String: Any?] {
        return self.map({ (key: String, value: Any?) -> (String, Any?) in
            if value is NSDate {
                return (key, (value as! NSDate).timeIntervalSince1970)
            }
            return (key, value)
        })
    }
}

But I get thrown this error when I try to build:

map produces '[T]', not the expected contextual result type '[String: Any?]'


Answer:

Your problem lies with the fact, that map always returns an Array, even when applied on a Dictionary. Your error message basically means, that you declared your method as returning a Dicitonary, but the statement inside returns an Array ([T] - means an Array with objects of some type T). In your case, the array returned by map will contain tuples (more about them here). In this case it looks like a key value pair, but its not equivalent to a key-value pair inside a Dictionary. Basically, tuples enable you to return more than one value/object from method. You can think of them as of anonymous structures.

In my opinion, there is no need to use a map to accomplish what you need - the solution provided by Mr. Xcoder is the way to go.

Question:

I have a user profile that is an Eureka form view. In this space, users can update their profile. If the user clicks 'Save' it calls this function.

    func saveProfileSettings(){

    let userID = user?.uid
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"

    let PROFILE_DISPLAY_NAME_REF: NameRow? = form.rowBy(tag: Constants.PROFILE_DISPLAY_NAME)
    let PROFILE_DISPLAY_NAME = PROFILE_DISPLAY_NAME_REF?.value

    let PROFILE_EMAIL_REF: EmailRow? = form.rowBy(tag: Constants.PROFILE_EMAIL)
    let PROFILE_EMAIL = PROFILE_EMAIL_REF?.value

    let PROFILE_PHONENUMBER_REF: PhoneRow? = form.rowBy(tag: Constants.PROFILE_PHONENUMBER)
    let PROFILE_PHONENUMBER = PROFILE_PHONENUMBER_REF?.value

    let PROFILE_BIRTH_AGE_REF: DateRow? = form.rowBy(tag: Constants.PROFILE_BIRTH_AGE)
    let PROFILE_BIRTH_AGE_CONVERT = PROFILE_BIRTH_AGE_REF?.value
    let PROFILE_BIRTH_AGE = formatter.string(from: PROFILE_BIRTH_AGE_CONVERT!)
    let PROFILE_BIRTH_AGE_String = String(PROFILE_BIRTH_AGE)

    let PROFILE_GENDER_REF: SegmentedRow<String>! = form.rowBy(tag: Constants.PROFILE_GENDER)
    let PROFILE_GENDER = PROFILE_GENDER_REF.value

    let PROFILE_GENDER_INTEREST_REF: SegmentedRow<String>! = form.rowBy(tag: Constants.PROFILE_GENDER_INTEREST)
    let PROFILE_GENDER_INTEREST = PROFILE_GENDER_INTEREST_REF.value

    let uploadPath = databaseRef.child("profiles").child(userID!)

    let dataBlock = ["\(Constants.PROFILE_DISPLAY_NAME)": PROFILE_DISPLAY_NAME!, "\(Constants.PROFILE_EMAIL)": PROFILE_EMAIL!, "\(Constants.PROFILE_PHONENUMBER)": PROFILE_PHONENUMBER!, "\(Constants.PROFILE_BIRTH_AGE)": PROFILE_BIRTH_AGE_String!, "\(Constants.PROFILE_GENDER)": PROFILE_GENDER!, "\(Constants.PROFILE_GENDER_INTEREST)": PROFILE_GENDER_INTEREST!]

    print("DataBlock \(dataBlock)")

    uploadPath.setValue(dataBlock) { (error, datebaseRef) in
        if error != nil{
            print("Error Occured")
        }
        print("Profile Updated!")
    }



}

This functions job is to pull all the data from the form, put it into an array and then upload it to firebase. If the user clicks save and doesnt update any information all is fine however if they edit their profile and clicks save it causes an error. See below.

assertion failed: Duplicate tag profileDisplayName: file /Users/brandonmayhew/Documents/Programming/X-Code Project's/ChristinaApp/Pods/Eureka/Source/Core/BaseRow.swift, line 172
2017-08-21 12:19:19.258586-0700 ChristinaApp[2615:926887] assertion failed: Duplicate tag profileDisplayName: file /Users/brandonmayhew/Documents/Programming/X-Code Project's/ChristinaApp/Pods/Eureka/Source/Core/BaseRow.swift, line 172

If you want to see what the code is for creating the form it is in my 'viewDidLoad'

override func viewDidLoad() {
    super.viewDidLoad()

    self.startAnimating(Constants.animationSize, message: "Finding Your Profile", type: .ballZigZag)

    //findImageURL()


    //START

    let userID = user?.uid
    print("Logged in user: \(userID!)")


    databaseRef.child("profiles").child(userID!).observe(.value, with: { (snapshot) in


        let data = snapshot.value as? NSDictionary

        let PROFILE_DISPLAY_NAME = data?[Constants.PROFILE_DISPLAY_NAME] as? String
        let PROFILE_UID = data?[Constants.PROFILE_UID] as? String
        let PROFILE_IMAGE = data?[Constants.PROFILE_IMAGE] as? String
        let PROFILE_EMAIL = data?[Constants.PROFILE_EMAIL] as? String
        let PROFILE_PHONENUMBER = data?[Constants.PROFILE_PHONENUMBER] as? String
        let PROFILE_GENDER = data?[Constants.PROFILE_GENDER] as? String
        let PROFILE_GENDER_INTEREST = data?[Constants.PROFILE_GENDER_INTEREST] as? String
        let PROFILE_BIRTH_AGE = data?[Constants.PROFILE_BIRTH_AGE] as? String
        let PROFILE_LATITUDE = data?[Constants.PROFILE_LATITUDE] as? String
        let PROFILE_lONGITUDE = data?[Constants.PROFILE_lONGITUDE] as? String

        self.form +++ Section("Personal Information")

            <<< NameRow(){ row in

                row.title = "Name"
                row.placeholder = "Enter Name"
                row.value = PROFILE_DISPLAY_NAME
                row.tag = "\(Constants.PROFILE_DISPLAY_NAME)"

                }

            <<< EmailRow(){ row in

                row.title = "Email"
                row.placeholder = "Enter Email"
                row.value = PROFILE_EMAIL
                row.tag = "\(Constants.PROFILE_EMAIL)"

            }

            <<< PhoneRow(){ row in

                row.title = "Phone Number"
                row.placeholder = "Enter Phone Number"
                row.value = PROFILE_PHONENUMBER
                row.tag = "\(Constants.PROFILE_PHONENUMBER)"

            }

            <<< DateRow(){ row in
                row.title = "Your Birth Year"

                //Convert 'PROFILE_BIRTH_AGE' string to NSDATE
                let date = NSDate()
                let dateFormatter = DateFormatter()
                dateFormatter.dateFormat = "yyyy-MM-dd"
                let formattedDate = dateFormatter.date(from: PROFILE_BIRTH_AGE!)

                row.value = formattedDate
                row.tag = "\(Constants.PROFILE_BIRTH_AGE)"
            }



            +++ Section("Dating Settings")

            <<< SegmentedRow <String> (){ row in
                row.title = "I am"
                row.options = ["Male", "Female"]
                row.value = PROFILE_GENDER
                row.tag = "\(Constants.PROFILE_GENDER)"
            }

            <<< SegmentedRow <String> (){ row in
                row.title = "I'm interested in"
                row.options = ["Male", "Female"]
                row.value = PROFILE_GENDER_INTEREST
                row.tag = "\(Constants.PROFILE_GENDER_INTEREST)"
            }

            +++ Section("Save Profile")
            <<< ButtonRow() {
                $0.title = "Save"
                }
                .onCellSelection {  cell, row in

                    self.saveProfileSettings()

            }

            +++ Section("Come Back Later")
            <<< ButtonRow() {
                $0.title = "Sign Out"
                }
                .onCellSelection {  cell, row in

                    self.signOut()
            }




        self.stopAnimating()


    })

    //STOP


}

I can not figure out what is making this error occur!


Answer:

The issue you're facing is caused by adding rows or sections with the same tag String value. In Eureka Forms the tags for cells or even sections should be unique, so if you are adding 2 rows with the same tag this will cause the assert exception

You have this type of behaviour in several parts of your code but I will take the name row as example

 <<< NameRow(){ row in

                row.title = "Name"
                row.placeholder = "Enter Name"
                row.value = PROFILE_DISPLAY_NAME
                row.tag = "\(Constants.PROFILE_DISPLAY_NAME)"  

                }

as you can see row.tag = "\(Constants.PROFILE_DISPLAY_NAME)" this is the line causing the issue, I am assuming that Constants.PROFILE_DISPLAY_NAME is a String defined in some part of your project, this string don't change and when you are about to add your second result from this method

databaseRef.child("profiles").child(userID!).observe(.value, with: { (snapshot) in

your app crash because you are adding the Constants.PROFILE_DISPLAY_NAME by second time with the same value "profileDisplayName"

a way to fix this

As a possible solution for this issue, You can append a unique value for every user fetched let say userID

Fixed code for NameRow case

 <<< NameRow(){ row in

                row.title = "Name"
                row.placeholder = "Enter Name"
                row.value = PROFILE_DISPLAY_NAME
                row.tag = "\(Constants.PROFILE_DISPLAY_NAME)\(userID)"  

                }

Question:

I am attempting to collect the date value from a Eureka DateTimeRow to then store it into Firebase but to store it I would need it to be in a string format. I have attempted this conversion but I receive the error 'Could not cast value of type 'Foundation.Date' (0x108af27e8) to 'Swift.String' (0x1086e99f8).' I would like to know if there is something I am missing from my conversion method.

DateTimeRow:

<<< DateTimeRow("startDate"){
            $0.title = "Start Date"
            $0.value = NSDate() as Date
            $0.cellUpdate { (cell, row) in
                cell.datePicker.minimumDate = Date()
            }
                $0.onChange { row in
                    start = row.value!
            }

        }

Code getting the values of the Erueka form and converting:

let valuesDictionary = form.values()   
let formatter = DateFormatter()
                formatter.dateFormat = "yyyy-MM-dd"
                let formattedDate = formatter.date(from: valuesDictionary["startDate"] as! String)

Thank you all feedback welcomed.


Answer:

As you want to convert from a Date (Eureka) to a String (Firebase), you should use the string(from:) method of the DateFormatter, whereas you are attempting to use the date(from:) method.

// Date to String
func string(from date: Date) -> String

// String to Date
func date(from string: String) -> Date?

Question:

This forEach loop works sometimes and sometimes it skips. I am not sure what I am doing wrong here. The loop will skip the last item and will never exit. So the completion block does not get fired at all.

I am using firebase, Eureka forms and it's ImageRow extension.

I would appreciate some help here.

//MARK: - Get Form Values
var returnedValues: [String: Any] = [:]
fileprivate func getFormValues(values: [String: Any], completion: @escaping ([String:Any])->()) {

    if let name = values["name"] as? String,
        let description = values["description"] as? String,
        let images = values["images"] as? [UIImage],
        let category = values["category"] as? String,
        let price = values["price"] as? Double,
        let deliveryFee = values["deliveryFee"] as? Double,
        let deliveryAreas = values["deliveryArea"] as? Set<String>,
        let deliveryTime = values["deliveryTime"] as? String {

        guard let uid = Auth.auth().currentUser?.uid else { return }
        var imagesData = [[String: Any]]()
        var counter = 0

        images.forEach({ (image) in

            let imageName = NSUUID().uuidString
            let productImageStorageRef = Storage.storage().reference().child("product_images").child(uid).child("\(imageName).jpg")
            var resizedImage = UIImage()

            if image.size.width > 800 {
                resizedImage = image.resizeWithWidth(width: 800)!
            }

            if let uploadData = UIImageJPEGRepresentation(resizedImage, 0.5) {
                productImageStorageRef.putData(uploadData, metadata: nil, completion: { (metadata, error) in
                    if error != nil {
                        print("Failed to upload image: \(error?.localizedDescription ?? "")")
                        return
                    }

                    //Successfully uploaded product Image
                    print("Successfully uploaded product Image")
                    if let productImageUrl = metadata?.downloadURL()?.absoluteString {
                        counter += 1
                        let imageData: [String: Any] = [imageName: productImageUrl]
                        imagesData.append(imageData)

                        if counter == images.count {
                            let deliveryAreasArr = Array(deliveryAreas)
                            self.returnedValues = ["name": name, "description": description, "images": imagesData , "category": category, "price": price, "deliveryFee": deliveryFee, "deliveryArea": deliveryAreasArr, "deliveryTime": deliveryTime, "creationDate": Date().timeIntervalSince1970, "userId": uid]
                            completion(self.returnedValues)
                        }

                    }

                })
            }
        })

    } else {

        let alert = UIAlertController(title: "Missing Information", message: "All fields are required. Please fill all fields.", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in
            alert.dismiss(animated: true, completion: nil)
        }))
        UIActivityIndicatorView.stopActivityIndicator(indicator: self.activityIndicator, container: self.activityIndicatorContainer, loadingView: self.activityIndicatorLoadingView)
        self.present(alert, animated: true, completion: nil)
    }
}

Answer:

There are a number of if statements inside your for loop that can result in counter not being incremented. If any of these fail then you will never call the completion handler.

I understand that you are using the counter in an attempt to know when all of the asynchronous tasks are complete, but a dispatch group is a better solution for this.

It is also important that your completion handler is called in all paths; such as when the initial guard fails or in the else clause of the initial if - Your completion handler should probably accept an Error parameter so that it knows that there was a problem.

//MARK: - Get Form Values

fileprivate func getFormValues(values: [String: Any], completion: @escaping ([String:Any]?)->()) {
    var returnedValues: [String: Any] = [:]

    if let name = values["name"] as? String,
        let description = values["description"] as? String,
        let images = values["images"] as? [UIImage],
        let category = values["category"] as? String,
        let price = values["price"] as? Double,
        let deliveryFee = values["deliveryFee"] as? Double,
        let deliveryAreas = values["deliveryArea"] as? Set<String>,
        let deliveryTime = values["deliveryTime"] as? String {

        guard let uid = Auth.auth().currentUser?.uid else {
            completion(nil) 
            return 
        }
        var imagesData = [[String: Any]]()

        let dispatchGroup = DispatchGroup() // Create a Dispatch Group

        images.forEach({ (image) in

            let imageName = NSUUID().uuidString
            let productImageStorageRef = Storage.storage().reference().child("product_images").child(uid).child("\(imageName).jpg")
            var resizedImage = UIImage()

            if image.size.width > 800 {
                resizedImage = image.resizeWithWidth(width: 800)!
            }

            if let uploadData = UIImageJPEGRepresentation(resizedImage, 0.5) {

                dispatchGroup.enter()  // Enter the group

                productImageStorageRef.putData(uploadData, metadata: nil, completion: { (metadata, error) in
                   guard error == nil else {
                        print("Failed to upload image: \(error?.localizedDescription ?? "")")
                        dispatchGroup.leave()  // Leave the dispatch group if there was an error
                        return
                    }

                    //Successfully uploaded product Image
                    print("Successfully uploaded product Image")
                    if let productImageUrl = metadata?.downloadURL()?.absoluteString {
                        let imageData: [String: Any] = [imageName: productImageUrl]
                        imagesData.append(imageData)
                    }
                    dispatchGroup.leave() // Leave the dispatch group in normal circumstances
                })
            }
        })

        // Schedule a notify closure for execution when the dispatch group is empty

        dispatchGroup.notify(queue: .main) {
            let deliveryAreasArr = Array(deliveryAreas)
            returnedValues = ["name": name, "description": description, "images": imagesData , "category": category, "price": price, "deliveryFee": deliveryFee, "deliveryArea": deliveryAreasArr, "deliveryTime": deliveryTime, "creationDate": Date().timeIntervalSince1970, "userId": uid]
            completion(self.returnedValues)
        }

    } else {
        completion(nil)
        let alert = UIAlertController(title: "Missing Information", message: "All fields are required. Please fill all fields.", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in
            alert.dismiss(animated: true, completion: nil)
        }))
        UIActivityIndicatorView.stopActivityIndicator(indicator: self.activityIndicator, container: self.activityIndicatorContainer, loadingView: self.activityIndicatorLoadingView)
        self.present(alert, animated: true, completion: nil)
    }
}

Some other points:

  • It would be better to pass structs rather than dictionaries. Using a struct for your input would get rid of that massive if let at the start of your function since you would know the types of the values and by making them non-optional properties of the struct you would know that the values were present.
  • It is unusual for a function such as this to present an alert; it would normally just return an error via the completion or perhaps throw back to the caller to indicate that there was a problem and let the caller handle it
  • I don't see why imagesData needs to be an array of dictionaries. Each dictionary in the array only has one entry, so you could just use a dictionary of [String:String] (There is no need to use Any when you know what the type will be.