Search code examples
iosswiftprotocols

Protocol Design in Swift


I have a single view controller class that I reuse on multiple screens, on different screens I want to supply a different data source. My VC looks something like this:

class SpotViewController: UIViewController {

    var dataSource: SpotDataSource!
}

Now I want to have two different Data Source Classes

class PersonalSpotDataSource {}
class ExploreSpotDataSource {}

Now I want to create some sort of a shared protocol that makes the above two classes implmenent the following properties

var spots: [Spot]
var title: String

And then I want to that shared construct to conform UITableViewDataSource since it has everything it needs which is just the list of spots and the title. And then going back to my first class I can supply either class (Personal or explore) as my VC's data source.

Is this possible?


Solution

  • So, first define your protocol:

    protocol SpotDataSource {
        var spots: [Spot] { get }
        var title: String { get }
    }
    

    Then define your two classes to conform to that protocol:

    class PersonalSpotDataSource: SpotDataSource {
        let spots = [Spot(name: "foo"), Spot(name: "bar")]
        let title = "Foobar"
    }
    
    class ExploreSpotDataSource: SpotDataSource {
        let spots = [Spot(name: "baz"), Spot(name: "qux")]
        let title = "Bazqux"
    }
    

    Clearly, your implementations of these will be more sophisticated than the above, but this illustrates the basic idea.

    Anyway, having done that, define a UITableViewDataSource that uses this protocol:

    class SpotTableViewDataSource: NSObject {
        let spotDataSource: SpotDataSource
    
        init(spotDataSource: SpotDataSource) {
            self.spotDataSource = spotDataSource
            super.init()
        }
    }
    
    extension SpotTableViewDataSource: UITableViewDataSource {
    
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return spotDataSource.spots.count
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "SpotCell", for: indexPath) as! SpotCell
            cell.spotLabel.text = spotDataSource.spots[indexPath.row].name
            return cell
        }
    }
    

    Finally, define you table view controller to use this UITableViewDataSource, passing it whatever SpotDataSource you want:

    class PersonalSpotTableViewController: UITableViewController {
    
        private let dataSource = SpotTableViewDataSource(spotDataSource: PersonalSpotDataSource())
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            tableView.dataSource = dataSource
        }
    
    }
    

    Note, in that table view controller class, I defined the data source to be stored property because table view controllers don't keep strong reference to their data sources and/or delegates, but we want it to stay around.

    Obviously, it would have been more graceful if we could have just put the UITableViewDataSource methods inside the SpotDataSource protocol's default implementation, but sadly we can't. So you have to create a concrete object that conforms to UITableViewDataSource and uses your SpotDataSource protocol, like outlined above.