1

I am implementing a picture posting app by using parse.com. several days ago, I had an error on my app, which is

2015-10-22 14:36:52.501 bany[2595:874380] * Terminating app due to uncaught exception 'NSRangeException', reason: '* -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array’

but my array was fine. then I posted stackOverFlow, someone told me this problem occurs that I did it wrong with thread safety and UI updating.

this is the post - index 0 beyond bounds for empty array, sometime works, sometime does not

I read about thread safety from apple reference, I figured out that using Mutable array is thread unsafe class type.

here is my code.

class MainTVC: UITableViewController {

@IBOutlet weak var categorySegment: UISegmentedControl!

var postsArray : NSMutableArray = NSMutableArray()
var filterdArray : NSMutableArray = NSMutableArray()
var objectTwo : PFObject!

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(true)
    
    bringAllDatafromParse()
}

@IBAction func segmentTapped(sender: AnyObject) {

    // Empty postArray
    postsArray = []
  
    // get post's data by categories
    switch categorySegment.selectedSegmentIndex {
    case 0 :
        bringAllDatafromParse()
    case 1 :
        bringCategoryDataFromParse(1)
        
    case 2 :
        bringCategoryDataFromParse(2)
        
    case 3 :
        bringCategoryDataFromParse(3)
        
    default :
        bringAllDatafromParse()
   }
    self.tableView.reloadData()
}


override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()

}

// MARK: - Table view data source

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {

    return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    
    return postsArray.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! MainTVCE
    let postObjects = self.postsArray.objectAtIndex(indexPath.row) as! PFObject
    
    
    // IndexPath for comment button on tableView
    cell.didRequestToShowComment = { (cell) in
        let indexPath = tableView.indexPathForCell(cell)
        let objectToSend = self.postsArray[indexPath!.row] as? PFObject
        // Show your Comment view controller here, and set object to send here
        self.objectTwo = objectToSend!
        self.performSegueWithIdentifier("mainToComment", sender: self)
    }
   
    
    // Show sold label or not
    cell.soldLabel.hidden = true
    
    if (postObjects.objectForKey("sold") as! Bool) == true {
        cell.soldLabel.hidden = false
    }
    
    
    // title Label of post
    cell.titleLabel.text = postObjects.objectForKey("titleText") as? String

    
    // nick name of user
    if let nickNameExists = postObjects.objectForKey("nickName") as? String {
       cell.nickNameLabel.text = nickNameExists
    }else {
        cell.nickNameLabel.text = postObjects.objectForKey("username") as? String
    }
    
    
    // time label for posts
    let dateFormatter:NSDateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "MM /dd /yy"
    cell.timeLabel.text = (dateFormatter.stringFromDate(postObjects.createdAt!))
    
   
    // price label
    let price = (postObjects.objectForKey("priceText") as! String)
            cell.priceLable.text = "   $\(price)"
    
   
    // main Image for post
    let mainImages = postObjects.objectForKey("front_image") as! PFFile
    mainImages.getDataInBackgroundWithBlock { (imageData, error) -> Void in
        let image = UIImage(data: imageData!)
        cell.mainPhoto.image = image
    }
    
    
    //profile picture for user
    if let profileImages = (postObjects.objectForKey("profile_picture") as? PFFile){
                profileImages.getDataInBackgroundWithBlock { (imageData, error) -> Void in
                    let image = UIImage(data: imageData!)
                    cell.profilePhoto.image = image
        }
    }else{ cell.profilePhoto.image = UIImage(named: "AvatarPlaceholder")
    }
    circularImage(cell.profilePhoto)
    
    
    
    return cell
}



func bringAllDatafromParse() {

    //empty postArray
    postsArray = []
    
    //bring data from parse
    let query = PFQuery(className: "Posts")
    query.orderByAscending("createdAt")
    query.findObjectsInBackgroundWithBlock { (objects: [PFObject]?, error) -> Void in
        if error == nil && objects != nil{
            for object : PFObject in objects! {

                self.postsArray.addObject(object)
            }
            let array : Array = self.postsArray.reverseObjectEnumerator().allObjects
            self.postsArray = array as! NSMutableArray
        }
     self.tableView.reloadData()
   
    }

}


func bringCategoryDataFromParse(category : Int) {

    let query = PFQuery(className: "Posts")
    query.whereKey("category", equalTo: category)
    query.orderByAscending("createdAt")
    query.findObjectsInBackgroundWithBlock { (objects: [PFObject]?, error) -> Void in
        if error == nil && objects != nil{
            for object : PFObject in objects! {
                self.postsArray.addObject(object)
            }
            let array : Array = self.postsArray.reverseObjectEnumerator().allObjects
            
            self.postsArray = array as! NSMutableArray
            self.tableView.reloadData()
        }
    }
}


override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // send object to commentViewController
    if (segue.identifier == "mainToComment") {
        let destViewController : CommentVC = segue.destinationViewController as! CommentVC
        destViewController.object = objectTwo
    }
    
    // send object to DetailViewController
    if (segue.identifier == "mainToDetail") {
        let selectedRowIndex = self.tableView.indexPathForSelectedRow
        let destViewController : DetailVC = segue.destinationViewController as! DetailVC
            destViewController.object = (postsArray[(selectedRowIndex?.row)!] as? PFObject)

    }
}

func circularImage(image : UIImageView) {
    image.layer.cornerRadius = image.frame.size.width / 2
    image.clipsToBounds  = true
    image.layer.borderColor  = UIColor.blackColor().CGColor
    image.layer.borderWidth = 1
}


    

Here are thoughts about what I did wrong on my code during look into other's code. So please comment these are right or not.

  1. I should use NSArray instead of using NSMutableArray for thread safety. Will it be okay if I use NSArray?

  2. I will have separate data class for retrieving data includes checking nil or there is a value, instead of putting in the method "cellForRowAtIndexPath" which would have 'only' define of cell for reuse.

If I do it this, should I retrieve data from array in most ViewControllers instead of using prepareSegue method to send specific(selected) object from mainViewcontroller which has function retrievingAllData?

  1. should I use cache and dispatch_async in "cellForRowAtIndexPath" Is it mandatory?

4.UI part, I am really confused. Could anyone tip for this? For example, how does make UI showing progressing until data retrived done? You can tip just 'a' comment. It will be helpful a lot for me. I really appreciate that.

please give me advise.

Community
  • 1
  • 1
Janghyup Lee
  • 167
  • 3
  • 17
  • try commenting out this line: var objectTwo : PFObject!. you can't instantiate a PFObject at the top of the class unless it is a PFObject class (and your class is uitableviewcontroller) – jjjjjjjj Oct 29 '15 at 02:49

1 Answers1

0

As a general tip, by using NSArray/NSMutableArray (which can contain a collection of any type of object) you're loosing a lot of the benefit of using Swift's Array type (which can only contain one type of object).

The Parse findObjectsInBackgroundWithBlock method is already returning an Array of PFObjects so there isn't really any reason to switch to using an NSArray.

Like the name suggests findObjectsInBackgroundWithBlock is performing the block on a background thread (because it might take a while to complete and you don't want it to lock up your main thread where you UI updates are being performed).

Calling self.tableView.reloadData() starts of a chain of events that update your UI, so this should be called on the main thread. You can do this pretty easily with a GCD function dispatch_async. Here's an update of one of your methods to show both of these changes:

query.findObjectsInBackgroundWithBlock { (objects:[PFObject]?, error:NSError?) -> Void in

    // unwrap the optional array of PFObjects
    if let objects = objects {

        // reverse your objects
        self.postsArray = objects.reverse()

        // trigger update to table on the main thread
        dispatch_async(dispatch_get_main_queue(),{
            self.tableView.reloadData()
        })
    }
    // if objects is nil that means there was an some error
    else {
        // unwrap the optional error
        if let error = error {
            // do something about the error - present feedback etc
        }
    }
}

As you can see, just directly using the objects array and using the reverse method makes the code a lot clearer.

You're also using ! to force unwrap an optional, which isn't a good habit to get into. Optionals are a pretty core part of Swift and it would make sense to make sure you wrap your head around them and your code will be a lot safer and less likely to crash :) http://roadfiresoftware.com/2014/12/wrap-your-head-around-optionals-in-swift/

Subclassing PFObject

It's a completely optional step, but one of the most interesting things about using the parse API is the ability to subclass PFObject. So instead always having to access your object attributes like this:

postObjects.objectForKey("titleText") as? String

you can just do this:

postObjects.titleText

It takes a little setup which is described for swift in this answer and also in the parse.com docs.

Hope this helps!

Community
  • 1
  • 1
MathewS
  • 2,267
  • 2
  • 20
  • 31