Search code examples
iosswiftfirebase-realtime-databaseeureka-forms

forEach Loop skips the last item


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

Solution

  • 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.