Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift function as parameter within a class

Tags:

function

swift

I'm a swift beginner, so be gentle...

I'm having trouble assigning a function as a parameter.

I have defined this struct:

struct dispatchItem {
   let description: String
   let f: ()->Void

   init(description: String, f: @escaping ()->()) {
      self.description = description
      self.f = f
   }
}

I make use of this within a class called MasterDispatchController like so:

class MasterDispatchController: UITableViewController {

   let dispatchItems = [
      dispatchItem(description: "Static Table", f: testStaticTable),
      dispatchItem(description: "Editable Table", f: testEditableTable)
   ]

    func testEditableTable() {
        //some code
    }

    func testStaticTable() {
        //some code
    }

etc.

Then I have a table view in my code that dispatches out to whichever function was clicked on (there are more than just the two I showed in the code above, but that's unimportant) like so

   override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      dispatchItems[indexPath.row].f()
   }

So... The compiler is not happy with this. It says when I am defining the dispatchItems let statement:

Cannot convert value of type '(MasterDispatchController) -> () -> ()' to expected argument type '() -> ()'

I figure... ok... I'm not sure I exactly understand this, but it seems like the compiler wants to know what class the callback functions are going to come from. I can see why it might need that. So I kind of blindly follow the pattern the compiler gives me, and change my struct to:

struct dispatchItem {
   let description: String
   let f: (MasterDispatchController)->()->Void

   init(description: String, f: @escaping (MasterDispatchController)->()->()) {
      self.description = description
      self.f = f
   }
}

Great the compiler is happy there, but now when I try to call the function with dispatchItems[indexPath.row].f() it says:

Missing parameter #1 in call

The function has no parameters, so I got confused...

I thought maybe it was asking me for the instance of the object in question which would make some sense... that would be "self" in my example, so I tried dispatchItems[indexPath.row].f(self) but then I got an error:

Expression resolves to an unused function

So I'm kind of stuck.

Sorry if this is a stupid question. Thanks for your help.

like image 371
Erik Westwig Avatar asked Oct 18 '22 15:10

Erik Westwig


1 Answers

The problem is that you're trying to refer to the instance methods testStaticTable and testEditableTable in your instance property's initialiser before self is fully initialised. Therefore the compiler cannot partially apply these methods with self as the implicit parameter, but can instead only offer you the curried versions – of type (MasterDispatchController) -> () -> ().

One might be tempted then to mark the dispatchItems property as lazy, so that the property initialiser runs on the first access of the property, when self is fully initialised.

class MasterDispatchController : UITableViewController {

    lazy private(set) var dispatchItems: [DispatchItem] = [
        DispatchItem(description: "Static Table", f: self.testStaticTable),
        DispatchItem(description: "Editable Table", f: self.testEditableTable)
    ]

    // ...
}

(Note that I renamed your struct to conform to Swift naming conventions)

This now compiles, as you now can refer to the partially applied versions of the methods (i.e of type () -> Void), and can call them as:

dispatchItems[indexPath.row].f()

However, you now have a retain cycle, because you're storing closures on self which are strongly capturing self. This is because when used as a value, self.someInstanceMethod resolves to a partially applied closure that strongly captures self.

One solution to this, which you were already close to achieving, is to instead work with the curried versions of the methods – which don't strongly capture self, but instead have to be applied with a given instance to operate on.

struct DispatchItem<Target> {

    let description: String
    let f: (Target) -> () -> Void

    init(description: String, f: @escaping (Target) -> () -> Void) {
        self.description = description
        self.f = f
    }
}

class MasterDispatchController : UITableViewController {

    let dispatchItems = [
        DispatchItem(description: "Static Table", f: testStaticTable),
        DispatchItem(description: "Editable Table", f: testEditableTable)
    ]

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        dispatchItems[indexPath.row].f(self)()
    }

    func testEditableTable() {}
    func testStaticTable() {}
}

These functions now take a given instance of MasterDispatchController as a parameter, and give you back the correct instance method to call for that given instance. Therefore, you need to first apply them with self, by saying f(self) in order to get the instance method to call, and then call the resultant function with ().

Although it may be inconvenient constantly applying these functions with self (or you may not even have access to self). A more general solution would be to store self as a weak property on DispatchItem, along with the curried function – then you can apply it 'on-demand':

struct DispatchItem<Target : AnyObject> {

    let description: String

    private let _action: (Target) -> () -> Void
    weak var target: Target?

    init(description: String, target: Target, action: @escaping (Target) -> () -> Void) {
        self.description = description
        self._action = action
    }

    func action() {
        // if we still have a reference to the target (it hasn't been deallocated),
        // get the reference, and pass it into _action, giving us the instance
        // method to call, which we then do with ().
        if let target = target {
            _action(target)()
        }
    }
}

class MasterDispatchController : UITableViewController {

    // note that we've made the property lazy again so we can access 'self' when
    // the property is first accessed, after it has been fully initialised.
    lazy private(set) var dispatchItems: [DispatchItem<MasterDispatchController>] = [
        DispatchItem(description: "Static Table", target: self, action: testStaticTable),
        DispatchItem(description: "Editable Table", target: self, action: testEditableTable)
    ]

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        dispatchItems[indexPath.row].action()
    }

    func testEditableTable() {}
    func testStaticTable() {}
}

This ensures that you have no retain cycles, as DispatchItem doesn't have a strong reference to self.

Of course, you may be able to use unowned references to self, such as shown in this Q&A. However, you should only do so if you can guarantee that your DispatchItem instances don't outlive self (you would want to make dispatchItems a private property for one).

like image 127
Hamish Avatar answered Oct 21 '22 04:10

Hamish