AlmoDev
AlmoDev

Reputation: 969

RealTime Reload UITableview without "Index out of Range" swift

I'm trying to reload my tableview every second. what I have now reload tableview objects but since I'm clearing Order array before reloading, it crashes due to index out of range.

This is my current code

 var orders = [Order]()

 override func viewDidLoad() {
    super.viewDidLoad()

    // table stuff
    tableview.dataSource = self
    tableview.delegate = self

    // update orders
    var timer = Timer.scheduledTimer(timeInterval: 4, target: self, selector: "GetOrders", userInfo: nil, repeats: true)

    GetOrders()

}

 func numberOfSections(in tableView: UITableView) -> Int {
    if orders.count > 0 {
        self.tableview.backgroundView = nil
        self.tableview.separatorStyle = .singleLine
        return 1
    }

    let rect = CGRect(x: 0,
                      y: 0,
                      width: self.tableview.bounds.size.width,
                      height: self.tableview.bounds.size.height)
    let noDataLabel: UILabel = UILabel(frame: rect)

    noDataLabel.text = "no orders"
    noDataLabel.textColor = UIColor.white
    noDataLabel.textAlignment = NSTextAlignment.center
    self.tableview.backgroundView = noDataLabel
    self.tableview.separatorStyle = .none


    return 0
}

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

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
     let cell = tableView.dequeueReusableCell(withIdentifier: "OrderCell", for: indexPath) as! OrderCell

    let entry = orders[indexPath.row]

    cell.DateLab.text = entry.date
     cell.shopNameLab.text = entry.shopname
    cell.shopAddLab.text = entry.shopaddress
    cell.nameClientLab.text = entry.clientName
    cell.clientAddLab.text = entry.ClientAddress
    cell.costLab.text = entry.Cost
    cell.perefTimeLab.text = entry.PerferTime
    cell.Shopimage.hnk_setImage(from: URL(string: entry.Logo))

    return cell
}

here is how I get data from API:

func GetOrders (){

orders = []
// get data by Alamofire

       let info = Order(shopname: shopname, shopaddress: shopaddr,
 clientName: cleintName,ClientAddress: clientAddres, PerferTime: time,
Cost: subtotal , date : time , Logo : logoString ,id : id)

      self.orders.append(info)

  // some if statements 
   DispatchQueue.main.async {
                self.tableview.reloadData()

            }

And here is if the range is out of index

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        let order =  orders[indexPath.row]
        guard orders.count > indexPath.row else {
            print("Index out of range")
            return
        }

        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        var viewController = storyboard.instantiateViewController(withIdentifier: "viewControllerIdentifer") as! OrderDetailsController
        viewController.passedValue = order.id
        self.present(viewController, animated: true , completion: nil)


}

Upvotes: 5

Views: 4643

Answers (8)

Shabbir Ahmad
Shabbir Ahmad

Reputation: 615

You are getting index out of Range because every 4 second you are appending info data to the orders array and it is not initialize.

You can try this..

override func viewDidLoad() {
    super.viewDidLoad()

    // table stuff
    tableview.dataSource = self
    tableview.delegate = self

    // update orders
    startTimer()

}

Timer portion is:

func startTimer() {

    let mySelector = #selector(self.GetOrders)
    var timer = Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(mySelector), userInfo: nil, repeats: true)

  timer.fire()

}

order method:

 func GetOrders(){

orders.removeAll()

// get data by Alamofire

       let info = Order(shopname: shopname, shopaddress: shopaddr,
 clientName: cleintName,ClientAddress: clientAddres, PerferTime: time,
Cost: subtotal , date : time , Logo : logoString ,id : id)

      self.orders.append(info)

  // some if statements 
   DispatchQueue.main.async {
                self.tableview.reloadData()

            }

Upvotes: 2

RinaLiu
RinaLiu

Reputation: 400

just use callback closure

func getOrders() {            
 Alamofire.request("https://api.ivi.ru/mobileapi//geocheck/whoami/v6?app_version=5881&user_agent=ios").validate().responseJSON(completionHandler: { response in
                 //handle response
                 guard everythingsOkay else { return }
                 DispatchQueue.main.async {
                  self.tableView.reloadData()
                }
                })
}

Upvotes: 1

Subramanian P
Subramanian P

Reputation: 4375

You are getting indexOutOfRange because you are modifying the tableview dataSource array every 4sec based on api response. At the same time you are trying to access the data from the same array.

Change : 1:

Create separate array for tableView DataSource and one for api response.

func getOrders() {

    var tempOrdersList = [Order]()


   // get data by Alamofire

   let info = Order(shopname: shopname, shopaddress: shopaddr, clientName: cleintName,ClientAddress: clientAddres, PerferTime: time,Cost: subtotal , date : time , Logo : logoString ,id : id)

   tempOrdersList.append(info)

  // some if statements 
   DispatchQueue.main.async {

        //Assign temporary array to class variable, which you are using as tableview data source. 

        self.orders = tempOrdersList

        self.tableview.reloadData()
   }
}

Change 2 :

Don't simply trigger the api for every 4 seconds. Schedule the timer once you got the response of the api.

func getOrders() {

        var tempOrdersList = [Order]()


       // get data by Alamofire

       let info = Order(shopname: shopname, shopaddress: shopaddr, clientName: cleintName,ClientAddress: clientAddres, PerferTime: time,Cost: subtotal , date : time , Logo : logoString ,id : id)

       tempOrdersList.append(info)

      // some if statements 
       DispatchQueue.main.async {

            //Assign temporary array to class variable, which you are using as tableview data source. 

            self.orders = tempOrdersList

            self.tableview.reloadData()


             timer =  Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(self. getOrder), userInfo: nil, repeats: false)

       }
  }


 // Invalidate the timer at the view controller deinit
 deinit {
    timer.invalidate()
 }

Upvotes: 4

Gokul G
Gokul G

Reputation: 2096

I don't know whether it works or nor, however give it a try.

  1. I think your are emptying your orders (orders = []) array before you get data from API, meanwhile your previous call tries to reload tableview here comes index out of range .
  2. Change your GetOrders() function as follows

    func GetOrders (){
    
       //orders = [] remove this line
       // get data by Alamofire
    
       // some if statements 
       DispatchQueue.main.async {
          //empty your array in main queue most importantly just after getting data from API and just before appending new data's
          orders = [] 
          let info = Order(shopname: shopname, shopaddress: shopaddr,  clientName: cleintName,ClientAddress: clientAddres, PerferTime: time,
             Cost: subtotal , date : time , Logo : logoString ,id : id)
          self.orders.append(info)
          self.tableview.reloadData()
     }
    

** Empty your array in main queue most importantly just after getting data from API and just before appending new data's

  1. if it fails to solve your problem, As @o15a3d4l11s2 said make sure GetOrders() function is called only after getting response for previous call

     override func viewDidLoad() {
        super.viewDidLoad()
        tableview.dataSource = self
        tableview.delegate = self
        GetOrders()
     }
    
     func GetOrders (){
    
       //orders = [] remove this line
       // get data by Alamofire
    
       // some if statements 
       DispatchQueue.main.async {
          //empty your array in main queue most importantly just after getting data from API and just before appending new data's
          orders = [] 
          let info = Order(shopname: shopname, shopaddress: shopaddr,  clientName: cleintName,ClientAddress: clientAddres, PerferTime: time,
             Cost: subtotal , date : time , Logo : logoString ,id : id)
          self.orders.append(info)
          self.tableview.reloadData()
    
          Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self. getOrder), userInfo: nil, repeats: false)
     }
    

Upvotes: 7

o15a3d4l11s2
o15a3d4l11s2

Reputation: 4059

Here is a proposal for the logic to refresh the orders and keep away from out of bound exceptions - let getOrders() to schedule the next call to itself only when it is finished. Here is an example:

func getOrders() {
    asyncLoadOrders(onComplete: { loadedOrders
        self.orders = loadedOrders
        self.tableView.reloadData()
        Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self. getOrder), userInfo: nil, repeats: false)
    }
}

The idea of this logic is that one second after the orders are really loaded, only then the next getOrders will be called.

Please note that it might be needed to wrap the reload of the table (as in your example) with DispatchQueue.main.async

Upvotes: 6

I would suggest you to reload the tableView in a completionBlock rather than the way you are calling

Swift 3

let say you are getting data through NSURLsession

func getDataFromJson(url: String, parameter: String, completion: @escaping (_ success: [String : AnyObject]) -> Void) {

//@escaping...If a closure is passed as an argument to a function and it is invoked after the function returns, the closure is @escaping.

var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "POST"
let postString = parameter

request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { Data, response, error in

    guard let data = Data, error == nil else {  // check for fundamental networking error

        print("error=\(error)")
        return
    }

    if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {  // check for http errors

        print("statusCode should be 200, but is \(httpStatus.statusCode)")
        print(response!)
        return

    }

    let responseString  = try! JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String : AnyObject]
    completion(responseString)



}
task.resume()

}

Since you are using Alomofire you can change accordingly

    getDataFromJson(url: "http://....", parameter: "....", completion: { response in
        print(response)

//You are 100% sure that you received your data then go ahead a clear your Array , load the newData then reload the tableView in main thread as you are doing orders = []

       let info = Order(shopname: shopname, shopaddress: shopaddr,
 clientName: cleintName,ClientAddress: clientAddres, PerferTime: time,
Cost: subtotal , date : time , Logo : logoString ,id : id)

      self.orders.append(info)

   DispatchQueue.main.async {
                self.tableview.reloadData()

            }

    })

Completion blocks should solve your problem

Upvotes: 3

J. Doe
J. Doe

Reputation: 13063

Maybe it is because you reload the data on the main sync. You are not showing your full function GetOrder(). If you are using dispatch there without waiting for completion, and using the main sync to reload the data, the data will try to reload when the orders are not even downloaded.

Are you working with dispatch groups? When will the data be reloaded? Are you sure you only reload the data when you have all the data?

If this is the case show us your full code and I will try to add dispatch groups their that will wait for completion.

Upvotes: 1

Maybe the problem is that you`re trying to fill the TableView before you have data to do so. If thats the case, you need to set a local variable to 0 and when you call GetOrders in the ViewDidLoad you set it to orders.count. Then you use this variable in numberOfRowsInSection, like that :

 var orders = [Order]()
 var aux = 0

 override func viewDidLoad(){
     GetOrders() 
     aux = Orders.count
 }

 func tableView(_ tableView: UITableView, numberOfRowsInSectionsection: Int) -> Int {
     return aux
 }

Upvotes: 2

Related Questions