0

I have data stored in NSUserDefaults as an array. I need to take this data and store it on a database using Parse Server. When I try to loop through the array and use saveInBackgroundWithBlock the loop runs again setting new values before the block completes. What is the best way to save this data as individual objects on the database?

let other = PFObject(className: "Other")
if (NSUserDefaults.standardUserDefaults().objectForKey("otherTypes") != nil) && (NSUserDefaults.standardUserDefaults().objectForKey("otherCosts") != nil) {
        otherCosts = NSUserDefaults.standardUserDefaults().objectForKey("otherCosts") as! [Double]

        otherTypes = NSUserDefaults.standardUserDefaults().objectForKey("otherTypes") as! [String]

        for costs in otherCosts {

            other.setObject(PFUser.currentUser()!.objectId!, forKey: "userId")
            other.setObject(otherTypes[i], forKey: "otherName")
            let cost = String(costs)
            other.setObject(cost, forKey: "otherCost")
            i = i + 1
            other.saveInBackgroundWithBlock({ (success, error) -> Void in
                if error == nil {
                    print("Success")
                    NSUserDefaults.standardUserDefaults().setObject(nil, forKey: "otherTypes")
                    NSUserDefaults.standardUserDefaults().setObject(nil, forKey: "otherCosts")
                } else {
                    print("Fail")
                }
            })
        } 
bobthegoalie
  • 201
  • 3
  • 14
  • 1
    Look into NSCondition class, which allows a thread to wait for the completion of other thread. Using that you can wait for the previous save() to complete before starting the loop again. – Gaurav Pal Mar 15 '16 at 04:48
  • Do you want multiple `Other` objects in Parse or one that contains two arrays? – Paulw11 Mar 15 '16 at 06:07

2 Answers2

0

You can run all of this on a background thread and use a semaphore for blocking:

let semaphore = dispatch_semaphore_create(0)
for costs in otherCosts {
    //...
    other.saveInBackgroundWithBlock({ (success, error) -> Void in
        // ...
        dispatch_semaphore_signal(semaphore)
    })
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}

Note that since this is a long running operation, you definitely do NOT want to do this saving in the foreground thread.

One other possibility is to convert the whole thing to a work queue arrangement, which looks something like:

func tryToSend() {
    let defaults = NSUserDefaults.standardUserDefaults()
    if let otherTypes = defaults.objectForKey("otherTypes") as? [String],
        let otherCosts = defaults.objectForKey("otherCosts") as? [Double] {
            if otherTypes.count > 0 {
                let other = PFObject(className: "Other")
                other.setObject(PFUser.currentUser()!.objectId!, forKey: "userId")
                other.setObject(otherTypes[0], forKey:"otherName")
                other.setObject("\(otherCosts[0])", forKey:"otherCost")
                other.saveInBackgroundWithBlock({ (success, error) -> Void in
                    if error == nil {
                        print("Success")
                        otherTypes = otherTypes[1..<otherTypes.count]
                        otherCosts = otherCosts[1..<otherCosts.count]
                        defaults.setObject(otherTypes, forKey: "otherTypes")
                        defaults.setObject(otherCosts, forKey: "otherCosts")
                        tryToSend()
                    } else {
                        print("Fail")
                    }
                })
            }
    }
}

The advantage of this is that you don't have to worry about spinning off another thread, AND you can dynamically add more entries to otherTypes and otherCosts (with appropriate synchronization to insure that NSUserDefaults isn't modified while reading)

One final idea in this case, is to use PFObject.saveAllInBackground method to do the whole thing in one network operation, which would look something like:

let defaults = NSUserDefaults.standardUserDefaults()
if let otherTypes = defaults.objectForKey("otherTypes") as? [String],
    let otherCosts = defaults.objectForKey("otherCosts") as? [Double] {
        let others = zip(otherTypes, otherCosts).map { type, cost in
            let other = PFObject(className:"Other")
            other.setObject(PFUser.currentUser()!.objectId!, forKey: "userId")
            other.setObject(type, forKey: "otherName")
            other.setObject("\(cost)", forKey: "otherCost")
            return other
        }

        PFObject.saveAllInBackground(others) { (success, error) in
            if error == nil {
                print("Success")
                NSUserDefaults.standardUserDefaults().setObject(nil, forKey: "otherTypes")
                NSUserDefaults.standardUserDefaults().setObject(nil, forKey: "otherCosts")
            } else {
                print("Fail")
            }
        }
}
David Berry
  • 40,941
  • 12
  • 84
  • 95
  • 1
    Also, note that as written, your code will clear the entire otherTypes and otherCosts arrays when the first value is successfully saved. – David Berry Mar 15 '16 at 05:44
  • PaulW11's answer was sufficient for this case. I appreciate the multiple different ways of handling this and giving me options for the future. – bobthegoalie Mar 15 '16 at 13:39
0

You need to allocate the PFObject inside the for loop otherwise you are simply messing with the same object over and over.

if (NSUserDefaults.standardUserDefaults().objectForKey("otherTypes") != nil) && (NSUserDefaults.standardUserDefaults().objectForKey("otherCosts") != nil) {
        otherCosts = NSUserDefaults.standardUserDefaults().objectForKey("otherCosts") as! [Double]

        otherTypes = NSUserDefaults.standardUserDefaults().objectForKey("otherTypes") as! [String]

        for costs in otherCosts {
            let other = PFObject(className: "Other")
            other.setObject(PFUser.currentUser()!, forKey: "userId")
            other.setObject(otherTypes[i], forKey: "otherName")
            let cost = String(costs)
            other.setObject(cost, forKey: "otherCost")
            i = i + 1
            other.saveInBackgroundWithBlock({ (success, error) -> Void in
                if error == nil {
                    print("Success")
                    NSUserDefaults.standardUserDefaults().setObject(nil, forKey: "otherTypes")
                    NSUserDefaults.standardUserDefaults().setObject(nil, forKey: "otherCosts")
                } else {
                    print("Fail")
                }
            })
        }

As

Paulw11
  • 108,386
  • 14
  • 159
  • 186