1

I created a tableView with custom cells that each cell has an image.

In the model class, I created a func mainPulatesData() to use URLSession dataTask method to retrieve data from url, and convert data into UIImage in the completion handler block, then add image into an variable of array of UIImage.

The process of retrieve data and adding them into UIImage array was perform in DispatchQueue.global(qos: .userInitiated).async block. based on the print message, the images did be added into array.

however, even I created an instance of model class in tableView controller, and invokes the mainPulatesData() in viewDidlLoad, the image didn't show up in the table.

Based on other print message in table view controller class, I found even it can be added into array in model class, but it seems like doesn't work on the instance of model class in tableView controller.

that's the code in model class to gain image data:

func mainPulatesData() {
    let session = URLSession.shared
    if myURLs.count > 0{
        print("\(myURLs.count) urls")
        for url in myURLs{
            let task = session.dataTask(with: url, completionHandler: { (data,response, error)  in
                let imageData = data
                DispatchQueue.global(qos: .userInitiated).async {
                    if imageData != nil{
                        if let image = UIImage(data: imageData!){
                            self.imageList.append(image)
                            print("\(self.imageList.count) images added.")
                        }
                    }
                    else{
                        print("nil")
                    }
                }
            })
            task.resume()   
        }
    }
}

that's the code in view controller to create instance of model:

override func viewDidLoad() {
    super.viewDidLoad()
    myModel.mainPulatesURLs()
    myModel.mainPulatesData()
    loadImages()
}

private func loadImages(){
    if myModel.imageList.count > 0{
        tableView.reloadData()
    }
    else{
        print("data nil")
    }
}

override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

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

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath) as! ImageTableViewCell
    if myModel.imageList.count > 0{
        let image = myModel.imageList[indexPath.row]
        cell.tableImage = image
        return cell

    }
    return cell
}
CEz
  • 43
  • 1
  • 7

3 Answers3

0

You should take note of the sequence of your function call here:

myModel.mainPulatesURLs() --> populates myURLs
myModel.mainPulatesData() --> populates image from myURLs in forloop asynchronously.
loadImages() --> called asynchronously.

while you're loading your images from myModel.mainPulatesData() you already called loadImages() which myModel.imageList was still empty.

you should call loadImages() after a callback from myModel.mainPulatesData() or when you're sure that the images where already loaded.

you can use dispatch_group_t to configure the callbacks.

here as requested:

import UIKit


var myURLs: [String] = ["urlA", "urlB", "urlC", "urlD"]

// we define the group for our asynchronous fetch. also works in synchronous config
var fetchGroup = DispatchGroup()

for urlString in myURLs {

    // for every url fetch we define, we will call an 'enter' to issue that there is a new block for us to wait or monitor
    fetchGroup.enter()

    // the fetch goes here
    let url = URL(string: urlString)!
    URLSession.shared.downloadTask(with: URLRequest(url: url), completionHandler: { (urlReq, urlRes, error) in

        // do your download config here...

        // now that the block has downloaded the image, we are to notify that it is done by calling 'leave'
        fetchGroup.leave()
    }).resume()
}

// now this is where our config will be used.
fetchGroup.notify(queue: DispatchQueue.main) { 

    // reload your table here as all of the image were fetched regardless of download error.
}
eNeF
  • 3,241
  • 2
  • 18
  • 41
  • could you tell me more about `dispatch_group_t`? – CEz May 11 '17 at 10:55
  • @CEz I added a sample with comments as requested. also here's your reference: https://developer.apple.com/reference/dispatch/dispatch_group_t – eNeF May 12 '17 at 07:25
0

Images are not displayed because you download them in background thread (asynchronously), and loadImages() is called synchronously. That means loadImage() is called before myModel.mainPulatesData() is executed, so when your images are downloaded, tableview is not being updated (reloadData() is not called). You should create Protocol to notify UIViewController, that data has been downloaded, or use Completion Handler.

Simple example of handler I am using, I call this in my viewDidLoad, it requests data from server and return an array of [Reservation]?

 ReservationTableModule.requestAllReservations { [weak self] reservations in
            guard let `self` = self else {
                return
            }

            guard let `reservations` = `reservations` else {
                return
            }
            self.reservations = reservations
            .reservationsTableView.reloadData()
        }

this is actual request function

class func requestAllReservations(handler: @escaping ([Reservation]?) -> Void) {

    let url = "reservations/all"

    APIModel.shared.requestWithLocation(.post, URL: url, parameters: nil) { data in
        let reservations = data?["reservations"].to(type: Reservation.self) as? [Reservation]
        handler(reservations)
    }
}

handler: @escaping ([Reservation]?) -> Void is called completion handler, you should, I guess make it handler: @escaping ([UIImage]?) -> Void and after your data downloaded call handler(reservations)

JuicyFruit
  • 2,638
  • 2
  • 18
  • 35
  • could you give some example code? I'm not familiar with protocol and completion handler. btw, it is asked to handle data in model class, controller shouldn't know anything about data. – CEz May 11 '17 at 10:57
  • @CEz added an example – JuicyFruit May 11 '17 at 11:25
  • so do you mean I need create a class called `ReservationTableModule` with `class func requestAllReservations` method? then call it in `viewDidLoad()`? – CEz May 11 '17 at 11:54
  • @CEz no, this is an example of how you can use function with callback – JuicyFruit May 11 '17 at 11:58
0

The reason is that the images or imageList isn't ready by the time cellForRowAt is called after you reloadData().

A good practice is to use placeholder images in the beginning and only load image when a table view cell is visible instead of everything at once. Something like:

// VC class
private var modelList = [MyModel(url: url1), MyModel(url: url2), ...]

override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

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

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath) as! ImageTableViewCell
    cell.update(model: modelList[indexPath.row])
    return cell
}

// Cell class
@IBOutlet weak var cellImageView: UIImageView!

func update(model: MyModel) {
    model.fetchImage(callback: { image in
        self.cellImageView.image = image
    })
}

// Model class
final class MyModel: NSObject {
  let url: URL
  private var _imageCache: UIImage?

  init(url: URL) {
    self.url = url
  }

  func fetchImage(callback: @escaping (UIImage?) -> Void) {
    if let img = self._imageCache {
      callback(img)
      return
    }

    let task = URLSession.shared.dataTask(with: self.url, completionHandler: { data, _, _  in
      if let imageData = data, let img = UIImage(data: imageData) {
        self._imageCache = img
        callback(img)
      } else {
        callback(nil)
      }
    })
    task.resume()
  }
}
xiangxin
  • 409
  • 6
  • 18
  • thanks for your suggestion, but it is asked to use model class to handle data, controller can't know anything about data. in this case, what should I do? – CEz May 11 '17 at 10:54
  • @CEz I've updated my answer. Hope that's what you want. – xiangxin May 12 '17 at 08:00