Home > Software engineering >  Using protocol with generic data type to pass data between screens
Using protocol with generic data type to pass data between screens

Time:10-08

I am Android Developer that started learning iOS. I am trying to pass data between the master-detail style app. I got controller1 that has a list of ToDo items, and controller2 that allows to create a new ToDo item and add it to the list on controller1.

I have created a protocol:

protocol ListDataHolder {
    
    associatedtype T
    
    func addItem(item: T)
    
    func reloadData()
}

Assigned self in prepare of controller1:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let controller2 = segue.destination as? Controller2{
            controller2.toDoDataHolder = self
        }
    } 

Declared delegate in controller2

var toDoDataHolder: ListDataHolder? = nil

And use it like this:

@IBAction func onAddClicked(_ sender: Any) {
        let toDo = ToDo()
        ...
        toDoDataHolder?.addItem(item: toDo)
        toDoDataHolder?.reloadData()
        navigationController?.popViewController(animated: true)
    }

I got a few errors when going this way:

For delegate declaration:

Protocol 'ListDataHolder' can only be used as a generic constraint because it has Self or associated type requirements

When using addItem() :

Cannot convert value of type 'ToDo' to expected argument type 'ListDataHolder.T'
Insert ' as! ListDataHolder.T'
Member 'addItem' cannot be used on value of protocol type 'ListDataHolder'; use a generic constraint instead

When I remove generic from protocol and just have addItem(item: ToDo), everything works fine. But I want to be able to use ListDataHolder with any data type.

This is just experimentation for me, I am not looking for a correct way to pass data between controllers.

CodePudding user response:

Issue arises with the statement

var toDoDataHolder: ListDataHolder? = nil

Thats because ListDataHolder has an associatedtype, now this associatedtype can be anything, ToDo, ToBe, ToSee etc etc (assuming those are different classes).

Swift likes to resolve the type information at the time of compilation to impose type safety and check and your declaration of toDoDataHolder does not provide any way to evaluate type information of its associatedtype T at the time of compilation.

You can make use of Generics for this, In case of generics all the type information are resolved at the time of compilation itself.

Because you haven't provided the whole code, I made few assumptions in the code below

struct ToDo {
    let name: String
}

protocol ListDataHolder {

    associatedtype T

    func addItem(item: T)

    func reloadData()
}

class ViewController1: UIViewController, ListDataHolder {
    func addItem(item: T) {
        self.array.append(item)
    }

    func reloadData() {
        debugPrint("Called")
    }

    typealias T = ToDo
    var array = [T]()

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let controller2 = segue.destination as? ViewController2<ViewController1> {
            controller2.toDoDataHolder = self
        }
    }
}

class ViewController2<M: ListDataHolder>: UIViewController {
    var toDoDataHolder: M?

    @IBAction func onAddClicked(_ sender: Any) {
        let toDo = ToDo(name: "Sandeep")
        toDoDataHolder?.addItem(item: toDo as! M.T)
        toDoDataHolder?.reloadData()
        navigationController?.popViewController(animated: true)
    }
}

Why does it work?

With var toDoDataHolder: M? There is no confusion, swift can obviously infer or evaluate type of M at the time of compilation because you have already resolved it for swift using ViewController2<ViewController1>

Extending the idea further

you can have your ToBe class used in ViewController3

struct ToBe {
    let name: String
}

class ViewController3: UIViewController, ListDataHolder {
    func addItem(item: T) {
        self.array.append(item)
    }

    func reloadData() {
        debugPrint("Called")
    }

    typealias T = ToBe
    var array = [T]()

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let controller2 = segue.destination as? ViewController2<ViewController3> {
            controller2.toDoDataHolder = self
        }
    }
}

You can set your type of your choice as associatedtype and have generic delegate called toDoDataHolder in controller2. I hope that matches your need.

CodePudding user response:

Instead of using associated type in ListDataHolder protocol use ToDo if your are always passing ToDo instance to the addItem method.

If you want this protocol addItem method will work with any genericType use following code.

protocol ListDataHolder {
    func addItem<T:Decodable>(item: T)
    func reloadData()
}

struct ToDo: Decodable {

}

class A: ListDataHolder {
   
    func addItem<T:Decodable>(item: T) {
          print("Add Item called")
    }
    
    func reloadData(){

    }
}

Here you need to implement Delegation design pattern.
Please understand Delegation design pattern in detail to understand this concept.
  • Related