5

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.

muescha
  • 1,544
  • 2
  • 12
  • 22
Erik Westwig
  • 713
  • 4
  • 16
  • what is the idea behind all this code? cause it might be you are doing it wrong. – JuicyFruit Mar 28 '17 at 18:41
  • I almost certainly am doing it wrong. :) – Erik Westwig Mar 28 '17 at 18:43
  • This is a testing app for my learning of iOS and Swift. I have a table view that is filled with a large number of little tests. For example, in one little test, I make a new view controller that shows off custom drawing views. In another I try making a collection view. Basically I'm writing a big app where I try lots of different things. This code is a dispatcher for calling all those little tests. At runtime, when I click on the table element, I run that little test in question. – Erik Westwig Mar 28 '17 at 18:45
  • if you are just starting and testing, then just make `switch indexPath.row {case 0: callFunc1() case1: callFunc2() default: CallSomeDefault()}` and call function you need this way. because passing function to a model is wrong. – JuicyFruit Mar 28 '17 at 18:49
  • Well that's what I WAS doing... I was irritated by the fact that I had multiple switch statements going on. One where I displayed the text for the table, and one where I dispatched out to the functions. I wanted that logic all in one place (hence the dispatchItems array). – Erik Westwig Mar 28 '17 at 18:59
  • What exactly _is_ the question? What precisely do you want to know / do? – matt Mar 28 '17 at 19:17
  • just a note: name your struct uppercased to distinct it from a variable (usage of your struct definition) – muescha Mar 28 '17 at 20:18
  • @muescha - Yes, of course. Stupid oversight. Thanks. – Erik Westwig Mar 28 '17 at 20:23

3 Answers3

2

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).

Community
  • 1
  • 1
Hamish
  • 78,605
  • 19
  • 187
  • 280
1

The problem here is that Swift treats class methods and functions differently under the hood. Class methods get a hidden self parameter (similar to how it works in Python), which allows them to know on which class instance they were called. That's why even though you declared testEditableCode as () -> (), the actual function has type (MasterDispatchController) -> () -> (). It needs to know on which object instance it was called.

The correct way to do what you're trying to do would be to create a closure which calls the correct method, like such:

class MasterDispatchController: UITableViewController {

   let dispatchItems = [
      dispatchItem(description: "Static Table", f: {() in
        self.testStaticTable()
      }),
      dispatchItem(description: "Editable Table", f: {() in
        self.testEditableTable()
      })
   ]

    func testEditableTable() {
        //some code
    }

    func testStaticTable() {
        //some code
    }

If you're familiar with JavaScript, the {() in ...code...} notation is the same as function() { ...code... } or the same as lambda: ...code... in Python.

Pedro Castilho
  • 10,174
  • 2
  • 28
  • 39
  • 1
    What do you mean "*You can't pass `self` to your methods either*"? With a given `(MasterDispatchController) -> () -> ()`, you can totally apply it with `self`, given that `self` is a `MasterDispatchController` instance. You just have to call the resultant function, e.g `f(self)()`. – Hamish Mar 28 '17 at 19:27
  • I'm not sure if that works, but I'm not able to test right now. I'll remove the part about not being able to pass self. I still think using closures would be more idiomatic/readable in this case. – Pedro Castilho Mar 28 '17 at 19:30
  • @Hamish - but I didn't know that syntax! I just wrote f(self). I didn't write f(self)()... And by the way, when I did that, it worked! – Erik Westwig Mar 28 '17 at 19:37
  • I'll admit I didn't know class methods behaved as curried functions, but that does make your error make sense. When you did f(self), it returned a function of type `() -> ()` which you never called, so that's what the compiler was complaining about. – Pedro Castilho Mar 28 '17 at 19:39
  • Thank you for all your help. f(self)() was the thing I was missing. That makes perfect sense now. – Erik Westwig Mar 28 '17 at 19:54
0

Here is the way to achieve what you want.

Implement protocol with parameters you want:

protocol Testable {
    func perfromAction()
    var description: String { get set }
    weak var viewController: YourViewController? { get set } //lets assume it is fine for testing
}

Accessing your UIViewController like this is not quite right, but for now it is ok. You can access labels, segues, etc.

Create Class for each test you want:

class TestA: Testable {
    var description: String
    weak var viewController: YourViewController?
    func perfromAction() {
        print(description) //do something
        viewController?.testCallback(description: description) //accessing your UIViewController
    }
    init(viewController: YourViewController, description: String) {
        self.viewController = viewController
        self.description = description
    }
}

class TestB: Testable {
    var description: String
    weak var viewController: YourViewController?
    func perfromAction() {
        print(description) //do something
        viewController?.testCallback(description: description) //accessing your UIViewController
    }
    init(viewController: YourViewController, description: String) {
        self.viewController = viewController
        self.description = description
    }
}

You can add some custom parameters for each Class, but those 3 from protocol are required.

Then your UIViewController would be like

class YourViewController: UIViewController {
    var arrayOfTest: [Testable] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        arrayOfTest.append(TestA(viewController: self, description: "testA"))
        arrayOfTest.append(TestB(viewController: self, description: "testB"))
        arrayOfTest[0].perfromAction()
        arrayOfTest[1].perfromAction()
    }

    func testCallback(description: String) {
        print("I am called from \(description)")
    }
}
JuicyFruit
  • 2,638
  • 2
  • 18
  • 35