21

I'm trying to keep my view controllers clean as described in this article objc.io Issue #1 Lighter View Controllers. I tested this method in Objective-C and it works fine. I have a separate class which implements UITableViewDataSource methods.

#import "TableDataSource.h"

@interface TableDataSource()

@property (nonatomic, strong) NSArray *items;
@property (nonatomic, strong) NSString *cellIdentifier;

@end

@implementation TableDataSource

- (id)initWithItems:(NSArray *)items cellIdentifier:(NSString *)cellIdentifier {
    self = [super init];
    if (self) {
        self.items = items;
        self.cellIdentifier = cellIdentifier;
    }
    return self;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.items.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.cellIdentifier forIndexPath:indexPath];

    cell.textLabel.text = self.items[indexPath.row];

    return cell;
}

@end

From the tableview controller, all I have to do is instantiate a instance of this class and set it as the tableview's data source and it works perfectly.

self.dataSource = [[TableDataSource alloc] initWithItems:@[@"One", @"Two", @"Three"] cellIdentifier:@"Cell"];
self.tableView.dataSource = self.dataSource;

Now I'm trying to do the same in Swift. First here's my code. Its pretty much of a translation of the Objective-C code above.

import Foundation
import UIKit

public class TableDataSource: NSObject, UITableViewDataSource {

    var items: [AnyObject]
    var cellIdentifier: String

    init(items: [AnyObject]!, cellIdentifier: String!) {
        self.items = items
        self.cellIdentifier = cellIdentifier

        super.init()
    }

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

    public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as UITableViewCell

        cell.textLabel?.text = items[indexPath.row] as? String

        return cell
    }

}

And I call it like this.

let dataSource = TableDataSource(items: ["One", "Two", "Three"], cellIdentifier: "Cell")
tableView.dataSource = dataSource

But the app crashes with the following error.

-[NSConcreteNotification tableView:numberOfRowsInSection:]: unrecognized selector sent to instance

I checked the init method of TableDataSource and the items and the cell identifier gets passed fine. I had to declare the UITableViewDataSource methods public and remove the override keyword otherwise it would give compile time errors.

I'm clueless on what's going wrong here. Can anyone please help me out?

Thank you.

Isuru
  • 27,485
  • 56
  • 174
  • 278
  • Can you show the stack trace at the point of crashing? – Mike Pollard Sep 11 '14 at 13:56
  • 2
    It looks like your data source isn't being retained. Where are you storing the reference? – jlehr Sep 11 '14 at 14:39
  • 3
    @jlehr That was indeed the problem. I _wasn't_ storing it! I made a property `var dataSource: TableDataSource!` and assigned it to the `dataSource` property of the tableview and it works now :) Thank you. – Isuru Sep 11 '14 at 16:20
  • 5
    This happens because the `delegate` and `dataSource` properties of `UITableView` (and other views/controls) are declared as `unowned` in Swift, so you need your own reference to the delegate/data source (if it isn't the view controller itself). – Nate Cook Sep 11 '14 at 17:41
  • @NateCook I did see the `unowned` thing when I checked the type of the `dataSource` property but had no idea what it was. Thank you for clearing that up. – Isuru Sep 11 '14 at 18:33
  • How do you update datasource with new items. is there any method to update the table list? – iPhone Guy Nov 22 '16 at 17:29

3 Answers3

28

Create a property for data source and use it with your tableview.

class ViewController: UIViewController {

  @IBOutlet weak var tableView: UITableView!
  var dataSource:TableDataSource!

  override func viewDidLoad() {
    super.viewDidLoad()

    dataSource = TableDataSource(items: ["One", "Two", "Three"], cellIdentifier: "Cell")

   tableView.dataSource = dataSource

  }
}
ayalcinkaya
  • 2,963
  • 25
  • 25
6

I used the below code, for more generic approach, as a try..

import UIKit

class CustomDataSource<ItemsType, CellType:UITableViewCell>: NSObject, UITableViewDataSource {

    typealias ConfigureCellClosure = (_ item: ItemsType, _ cell: CellType) -> Void
    private var items: [ItemsType]
    private let identifier: String
    private var configureCellClosure: ConfigureCellClosure


    init(withData items: [ItemsType], andId identifier: String, withConfigBlock config:@escaping ConfigureCellClosure) {

        self.identifier = identifier
        self.items      = items
        self.configureCellClosure = config
    }

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

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: self.identifier, for: indexPath) as! CellType
        configureCellClosure(items[indexPath.row], cell)
        return cell
    }

    func item(at indexpath: IndexPath) -> ItemsType {

        return items[indexpath.row]
    }

}

In view controller

   var dataSource: CustomDataSource<CellObject, CustomTableViewCell>?

    override func viewDidLoad() {
        super.viewDidLoad()

         dataSource = CustomDataSource<CellObject, CustomTableViewCell>(withData: customDataStore.customData(), andId: CustomTableViewCell.defaultReuseIdentifier) { (cellObject, cell) in
            cell.configureCell(with: cellObject)
        }
        customTableView.dataSource = dataSource

        // Do any additional setup after loading the view.
}

Used this approach in my small project WorldCountriesSwift

anoop4real
  • 6,737
  • 4
  • 44
  • 48
  • I placed the DataSource to separate file but don't know how to "listen to the click event & send clicked item to DetailViewController". ```class PicturesDataSource: NSObject, UITableViewDataSource{ }``` inside PicturesDataSource "storyboard" & "navigationController" are not available & therefore ```if let detailViewController = storyboard.instantiateViewController(withIdentifier: "PictureDetailView") as? PictureDetailViewController & navigationController?.pushViewController(detailViewController, animated: true)``` not working. – Hassan Tareq Sep 10 '17 at 12:16
  • Try adding the dataSource?.item(at: indexPath) in the didselectrow method of viewcontroller and then try to present the details pictureVC.present(PictureDetailViewController.....) – anoop4real Sep 10 '17 at 15:37
5

Extending the accepted answer by "ayalcinkaya", which explains the how but not the why:

Most probably what is happening is that your TableDataSource is being deallocated as tableview.dataSource is a weak reference, that is why creating a property solves the problem, as it creates a strong reference and avoids the dataSource delegate being deallocated.

PakitoV
  • 2,384
  • 24
  • 34