2

I have a UITableView and in its prototype cell have a UICollectionView.

MainViewController is delegate for UITableView and MyTableViewCell class is delegate for UICollectionView.

On updating each TableViewCell contents I call cell.reloadData() to make the collectionView inside the cell reloads its contents.

When I use reusable cells, as each cell appears, it has contents of the last cell disappeared!. Then it loads the correct contents from a URL.

I'll have 5 to 10 UITableViewCells at most. So I decided not to use reusable cells for UITableView. I changed the cell creation line in tableView method to this:

let cell = MyTableViewCell(style: .default, reuseIdentifier:nil)

Then I got an error in MyTableViewCell class (which is delegate for UICollectionView), in this function:

override func layoutSubviews() {
    myCollectionView.dataSource = self
}

EXC_BAD_INSTRUCTION CODE(code=EXC_I386_INVOP, subcode=0x0)
fatal error: unexpectedly found nil while unwrapping an Optional value

MyTableViewCell.swift

import UIKit
import Kingfisher
import Alamofire

class MyTableViewCell: UITableViewCell, UICollectionViewDataSource {


    struct const {
        struct api_url {
            static let category_index = "http://example.com/api/get_category_index/";
            static let category_posts = "http://example.com/api/get_category_posts/?category_id=";
        }
    }

    @IBOutlet weak var categoryCollectionView: UICollectionView!

    var category : IKCategory?
    var posts : [IKPost] = []

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code

        if category != nil {
            self.updateData()
        }
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

    override func layoutSubviews() {
            categoryCollectionView.dataSource = self
    }

    func updateData() {
        if let id = category?.id! {
            let url = const.api_url.category_posts + "\(id)"
            Alamofire.request(url).responseObject { (response: DataResponse<IKPostResponse>) in
                if let postResponse = response.result.value {
                    if let posts = postResponse.posts {
                        self.posts = posts
                        self.categoryCollectionView.reloadData()
                    }
                }
            }
        }
    }

    internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "postCell", for: indexPath as IndexPath) as! MyCollectionViewCell

        let post = self.posts[indexPath.item]
        cell.postThumb.kf.setImage(with: URL(string: post.thumbnail!))
        cell.postTitle.text = post.title

        return cell
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        //You would get something like "model.count" here. It would depend on your data source
        return self.posts.count
    }

    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }

}

MainViewController.swift

import UIKit
import Alamofire

class MainViewController: UITableViewController {

    struct const {
        struct api_url {
            static let category_index = "http://example.com/api/get_category_index/";
            static let category_posts = "http://example.com/api/get_category_posts/?category_id=";
        }
    }


    var categories : [IKCategory] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        self.updateData()
    }

    func updateData() {
        Alamofire.request(const.api_url.category_index).responseObject { (response: DataResponse<IKCategoryResponse>) in
            if let categoryResponse = response.result.value {
                if let categories = categoryResponse.categories {
                    self.categories = categories
                    self.tableView.reloadData()
                }
            }
        }
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return self.categories.count
    }

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

    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return self.categories[section].title
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//        let cell = tableView.dequeueReusableCell(withIdentifier: "CollectionHolderTableViewCell") as! MyTableViewCell
        let cell = MyTableViewCell(style: .default, reuseIdentifier:nil)


        cell.category = self.categories[indexPath.section]
        cell.updateData()

        return cell
    }

}

MyCollectionViewCell.swift

import UIKit

class MyCollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var postThumb: UIImageView!
    @IBOutlet weak var postTitle: UILabel!

    var category : IKCategory?

}

Why not reusing cells caused this? Why am I doing wrong?

Hadi Sharghi
  • 903
  • 16
  • 33
  • There is no issue using reusable cells, you just need to clear the data source before the next cell appears to keep it from showing the old data. Please post all your code for your table cell, it will be easier to troubleshoot the issue. layoutSubviews() is probably not where you want to be setting the dataSource – JAB Jul 21 '17 at 23:05
  • @BJHStudios posted the source code – Hadi Sharghi Jul 21 '17 at 23:20

1 Answers1

6

There are a few things to do that should get you up to speed.

First, uncomment the line that uses reusable cells and remove the line of code that creates the non-reusable cells. It is safe to use reusable cells here.

Second, in MyTableViewCell, set the dataSource for the collection view right after the super.awakeFromNib() call. You only need to set the dataSource once, but layoutSubviews() will potentially get called multiple times. It's not the right place to set the dataSource for your needs.

override func awakeFromNib() {
    super.awakeFromNib()
    categoryCollectionView.dataSource = self
}

I have removed the call to updateData() from awakeFromNib(), as you are already calling it at cell creation. You can also delete the layoutSubviews() override, but as a general rule, you should be careful to call super.layoutSubviews() when overriding it.

Lastly, the reason the posts seemed to re-appear in the wrong cells is that the posts array wasn't being emptied as the cells were reused. To fix this issue, add the following method to MyTableViewCell:

func resetCollectionView {
    guard !posts.isEmpty else { return }
    posts = []
    categoryCollectionView.reloadData()
}

This method empties the array and reloads your collection view. Since there are no posts in the array now, the collection view will be empty until you call updateData again. Last step is to call that function in the cell's prepareForReuse method. Add the following to MyTableViewCell:

override func prepareForReuse() {
    super.prepareForReuse()
    resetCollectionView()
}

Let me know how it goes!

JAB
  • 3,165
  • 16
  • 29
  • Thanks man, everything's working as expected! One minor thing is when a new cell appears it's empty, but it show contents of one other cell for a moment then reloads its correct contents. Do you have any idea what's going on? – Hadi Sharghi Jul 22 '17 at 00:06
  • 1
    @Hadu - you can reset the cell prior to reuse by overriding `prepareForReuse()` in your UITableViewCell/UICollectionViewCell subclass: https://developer.apple.com/documentation/uikit/uitableviewcell/1623223-prepareforreuse If you do override it, make sure you call super - see the discussion in the link above. – siburb Jul 22 '17 at 00:56
  • 1
    Edited to move the call to prepareForReuse(), although it should be noted that Apple specifically states in the documentation not to use prepareForReuse for cell content adjustments. However if it's not working properly in cellForRowAt, then I don't really see a problem putting it in prepareForReuse – JAB Jul 22 '17 at 02:36