1

I'm a newbie to Swift and XCode, taking a class in iOS development this summer. A lot of projects we're doing and examples I'm seeing for UI elements like PickerViews, TableViews, etc. are defining everything in the ViewController.swift file that acts as the controller for the main view. This works fine, but I'm starting to get to the point of project complexity where I'd really like all of my code to not be crammed into the same Swift file. I've talked to a friend who does iOS development on the side, he said this is sane and reasonable and well in-line with proper object-oriented programming... but I just can't seem to get it to work. Through trial and error I've gotten to this situation: the app runs in the simulator, the UITableView appears, but I'm not getting it populated with entries. I can get it working just fine when all the code is in the ViewController, but once I start trying to create a new controller class and make an instance of that class the dataSource/delegate of the UITableView I start getting nothing. I feel like I'm either missing some core understanding of Swift here, or doing something wrong with the Interface Builder in XCode.

My end result should be a UITableView with three entries in it; currently I'm getting a UITableView with no entries. I'm following along with a few different examples I've Googled, but primarily this other SO question: UITableView example for Swift

ViewController.swift:

import UIKit

class ViewController: UIViewController{

    @IBOutlet var stateTableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        var viewController = StateViewController()
        self.stateTableView.delegate = viewController
        self.stateTableView.dataSource = viewController
    }
}

StateViewController.swift:

import UIKit

class StateViewController: UITableViewController{
    var states = ["Indiana", "Illinois", "Nebraska"]

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

    func tableView(cellForRowAttableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
        let cell = UITableViewCell(style:UITableViewCellStyle.default, reuseIdentifier:"cell")
        cell.textLabel?.text = states[indexPath.row]
        return cell
    }
}

In XCode I have the UITableView hooked up to the View Controller; the outlets are set to dataSource and delegate and the referencing outlet is stateTableView.

I'm not getting any errors; I do get a warning on my `var viewController = StateViewController()' statement in ViewController.swift where it wants me to use a constant, but switching it to a constant doesn't change the behavior (this is as it should be, I assume).

Originally I assumed that the error was in my StateViewController.swift file, where I'm not creating an object that adheres to the UITableViewDataSource or UITableViewDelegate protocol, but if I even add them into the class statement I immediately get errors like "Redundant conformance of 'StateViewController' to protocol 'UITableViewDataSource'" - I'm reading that this is because inheriting from UITableViewController automatically inherits the other protocols as well.

The last thing I tried was instead referring to self.states in the StateViewController's tableView functions, but I'm pretty sure self in Swift works the same as it does in Python and it feels like I'm just trying to add magic words at this point.

I've investigated as far as my currently-limited Swift knowledge can take me, so any answer that explains what I'm doing wrong rather than just telling me what to fix would be very appreciated.

rmaddy
  • 298,130
  • 40
  • 468
  • 517
epsilonplus
  • 15
  • 1
  • 5
  • If you want to separate the class that should be responsible for implementing the datasource/delegate, it does not even have to be a `UITableViewController` – Ahmad F Jun 27 '17 at 02:01
  • if you want to display a tableview embedded in other viewController suppose vc2 and want it to show in vc1 why don't you make use of containerView. secondly you are using tableViewController. No, make use of simple TableView – iOS Geek Jun 27 '17 at 03:16
  • I wrote a solution (full code) in my answer on this page. It may help people who are still confused of how all the communication between the table view and the view controller would happens once we start separating concerns in separate classes. – ppalancica Nov 05 '18 at 10:13

5 Answers5

3

Your issue is being caused by a memory management problem. You have the following code:

override func viewDidLoad() {
    super.viewDidLoad()
    var viewController = StateViewController()
    self.stateTableView.delegate = viewController
    self.stateTableView.dataSource = viewController
}

Think about the lifetime of the viewController variable. It ends when the end of viewDidLoad is reached. And since a table view's dataSource and delegate properties are weak, there is no strong reference to keep your StateViewController alive once viewDidLoad ends. The result, due to the weak references, is that the dataSource and delegate properties of the table view revert back to nil after the end of viewDidLoad is reached.

The solution is to create a strong reference to your StateViewController. Do this by adding a property to your view controller class:

class ViewController: UIViewController{
    @IBOutlet var stateTableView: UITableView!
    let viewController = StateViewController()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.stateTableView.delegate = viewController
        self.stateTableView.dataSource = viewController
    }
}

Now your code will work.

Once you get that working, review the answer by Ahmed F. There is absolutely no reason why your StateViewController class should be a view controller. It's not a view controller in any sense. It's simply a class that implements the table view data source and delegate methods.

rmaddy
  • 298,130
  • 40
  • 468
  • 517
  • Thanks for the clear, detailed answer; I think my main problem with comprehending everything was the lifetime of that `viewController` variable, like you said. – epsilonplus Jun 27 '17 at 07:42
  • Oh, I didn't noticed that, thank you for providing such a useful answer. – Ahmad F Jun 27 '17 at 15:56
1

Although I find it more readable and understandable to implement dataSource/delegate methods in the same viewcontroller, what are you trying to achive is also valid. However, StateViewController class does not have to be a subclass of UITableViewController (I think that is the part that you are misunderstanding it), for instance (adapted from another answer for me):

import UIKit

// ViewController File
class ViewController: UIViewController {
    var handler: Handler!

    @IBOutlet weak var tableView: UITableView!
    override func viewDidLoad() {
        super.viewDidLoad()

        handler = Handler()
        tableView.dataSource = handler
    }
}

Handler Class:

import UIKit

class Handler:NSObject, UITableViewDataSource {
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("myCell")

        cell?.textLabel?.text = "row #\(indexPath.row + 1)"

        return cell!
    }
}
Ahmad F
  • 26,570
  • 13
  • 76
  • 124
0

You are trying to set datasource and delegate of UITableView as UITableViewController. As @Ahmad mentioned its more understandable in same class i.e. ViewController, you can take clear approach separating datasource and delegate of UITableView from UIViewController. You can make subclass of NSObject preferably and use it as datasource and delgate class of your UITableView.

You can also also use a container view and embed a UITableViewController. All your table view code will move to your UITableViewController subclass.Hence seprating your table view logic from your View Controller

Hope it helps. Happy Coding!!

luckyShubhra
  • 2,606
  • 1
  • 9
  • 18
0

You can also use adapter to resolve this with super clean code and easy to understand, Like

protocol MyTableViewAdapterDelegate: class {
    func myTableAdapter(_ adapter:MyTableViewAdapter, didSelect item: Any)
}

class MyTableViewAdapter: NSObject {
    private let tableView:UITableView
    private weak var delegate:MyTableViewAdapterDelegate!

    var items:[Any] = []

    init(_ tableView:UITableView, _ delegate:MyTableViewAdapterDelegate) {
        self.tableView = tableView
        self.delegate = delegate
        super.init()
        tableView.dataSource = self
        tableView.delegate = self
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }

    func setData(data:[Any]) {
        self.items = data
        reloadData()
    }

    func reloadData() {
        tableView.reloadData()
    }
}

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

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = "Hi im \(indexPath.row)"
        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        delegate?.myTableAdapter(self, didSelect: items[indexPath.row])
    }
}

Use Plug and Play

class ViewController: UIViewController, MyTableViewAdapterDelegate {

    @IBOutlet var stateTableView: UITableView!
    var myTableViewAdapter:MyTableViewAdapter!

    override func viewDidLoad() {
        super.viewDidLoad()
        myTableViewAdapter = MyTableViewAdapter(stateTableView, self)
    }

    func myTableAdapter(_ adapter: MyTableViewAdapter, didSelect item: Any) {
        print(item)
    }
}
SPatel
  • 3,869
  • 4
  • 26
  • 47
0

The way I separate those concerns in my projects, is by creating a class to keep track of the state of the app and do the required operations on data. This class is responsible for getting the actual data (either creating it hard-coded or getting it from the persistent store). This is a real example:

import Foundation

class CountriesStateController {

    private var countries: [Country] = [
        Country(name: "United States", visited: true),
        Country(name: "United Kingdom", visited: false),
        Country(name: "France", visited: false),
        Country(name: "Italy", visited: false),
        Country(name: "Spain", visited: false),
        Country(name: "Russia", visited: false),
        Country(name: "Moldova", visited: false),
        Country(name: "Romania", visited: false)
    ]

    func toggleVisitedCountry(at index: Int) {
        guard index > -1, index < countries.count else {
            fatalError("countryNameAt(index:) - Error: index out of bounds")
        }
        let country = countries[index]
        country.visited = !country.visited
    }

    func numberOfCountries() -> Int {
        return countries.count
    }

    func countryAt(index: Int) -> Country {
        guard index > -1, index < countries.count else {
            fatalError("countryNameAt(index:) - Error: index out of bounds")
        }
        return countries[index]
    }
}

Then, I create separate classes that implement the UITableViewDataSource and UITableViewDelegate protocols:

import UIKit

class CountriesTableViewDataSource: NSObject {

    let countriesStateController: CountriesStateController
    let tableView: UITableView

    init(stateController: CountriesStateController, tableView: UITableView) {
        countriesStateController = stateController
        self.tableView = tableView
        self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")
        super.init()
        self.tableView.dataSource = self
    }
}

extension CountriesTableViewDataSource: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // return the number of items in the section(s)
        return countriesStateController.numberOfCountries()
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // return a cell of type UITableViewCell or another subclass
        let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
        let country = countriesStateController.countryAt(index: indexPath.row)
        let countryName = country.name
        let visited = country.visited
        cell.textLabel?.text = countryName
        cell.accessoryType = visited ? .checkmark : .none
        return cell
    }
}

import UIKit

protocol CountryCellInteractionDelegate: NSObjectProtocol {

    func didSelectCountry(at index: Int)
}

class CountriesTableViewDelegate: NSObject {

    weak var interactionDelegate: CountryCellInteractionDelegate?

    let countriesStateController: CountriesStateController
    let tableView: UITableView

    init(stateController: CountriesStateController, tableView: UITableView) {
        countriesStateController = stateController
        self.tableView = tableView
        super.init()
        self.tableView.delegate = self
    }
}

extension CountriesTableViewDelegate: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("Selected row at index: \(indexPath.row)")
        tableView.deselectRow(at: indexPath, animated: false)
        countriesStateController.toggleVisitedCountry(at: indexPath.row)
        tableView.reloadRows(at: [indexPath], with: .none)
        interactionDelegate?.didSelectCountry(at: indexPath.row)
    }
}

And this is how easy is to use them from the ViewController class now:

import UIKit

class ViewController: UIViewController, CountryCellInteractionDelegate {

    public var countriesStateController: CountriesStateController!
    private var countriesTableViewDataSource: CountriesTableViewDataSource!
    private var countriesTableViewDelegate: CountriesTableViewDelegate!
    private lazy var countriesTableView: UITableView = createCountriesTableView()

    func createCountriesTableView() -> UITableView {
        let tableViewOrigin = CGPoint(x: 0, y: 0)
        let tableViewSize = view.bounds.size
        let tableViewFrame = CGRect(origin: tableViewOrigin, size: tableViewSize)
        let tableView = UITableView(frame: tableViewFrame, style: .plain)
        return tableView
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        guard countriesStateController != nil else {
            fatalError("viewDidLoad() - Error: countriesStateController was not injected")
        }

        view.addSubview(countriesTableView)
        configureCountriesTableViewDelegates()
    }

    func configureCountriesTableViewDelegates() {
        countriesTableViewDataSource = CountriesTableViewDataSource(stateController: countriesStateController, tableView: countriesTableView)
        countriesTableViewDelegate = CountriesTableViewDelegate(stateController: countriesStateController, tableView: countriesTableView)
        countriesTableViewDelegate.interactionDelegate = self
    }

    func didSelectCountry(at index: Int) {
        let country = countriesStateController.countryAt(index: index)
        print("Selected country: \(country.name)")
    }
}

Note that ViewController didn't create the countriesStateController object, so it must be injected. We can do that from the Flow Controller, from the Coordinator or Presenter, etc. I did it from AppDelegate like so:

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    let countriesStateController = CountriesStateController()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        if let viewController = window?.rootViewController as? ViewController {

            viewController.countriesStateController = countriesStateController
        }

        return true
    }
    /* ... */
}

If it's never injected - we get a runt-time crash, so we know we must fix it straight away.

This is the Country class:

import Foundation

class Country {

    var name: String
    var visited: Bool

    init(name: String, visited: Bool) {
        self.name = name
        self.visited = visited
    }
}

Note how clean and slim the ViewController class is. It's less than 50 lines, and if create the table view from Interface Builder - it becomes 8-9 lines smaller.

ViewController above does what it's supposed to do, and that's to be a mediator between View and Model objects. It doesn't really care if the table displays one type or many types of cells, so the code to register the cell(s) belongs to CountriesTableViewDataSource class, which is responsible to create each cell as needed.

Some people combine CountriesTableViewDataSource and CountriesTableViewDelegate in one class, but I think it breaks the Single Responsibility Principle. Those two classes both need access to the same DataProvider / State Controller object, and ViewController needs access to that as well.

Note that View Controller had now way to know when didSelectRowAt was called, so we needed to create an additional protocol inside UITableViewDelegate:

protocol CountryCellInteractionDelegate: NSObjectProtocol {

    func didSelectCountry(at index: Int)
}

And we also need a delegate property to make the communication possible:

weak var interactionDelegate: CountryCellInteractionDelegate?

Note that neither CountriesTableViewDataSource not CountriesTableViewDelegate class knows about the existence of the ViewController class. Using Protocol-Oriented-Programming - we could even remove the tight-coupling between those two classes and the CountriesStateController class.

ppalancica
  • 4,098
  • 4
  • 25
  • 39