8

I am creating a Swift project and I want to define a specific protocol that enforces other components to implement a animate method:

protocol AnimatableBehavior {
    @IBAction func animate()
}

The problem is I want this method to be an IBAction, but I get this error from XCode:

Only instance methods can be declared 'IBAction'

My question is, how would you implement such a thing?

I have considered:

  1. Remove @IBAction, but then I need to remember adding it in every class that implements. Not very elegant and error prone.
  2. Create a base class instead of protocol, but then I am enforcing all components to subclass my base class instead of their own choice ones, so it is not a valid option.

Any other ideas?


EDIT: Response to comments below.

The idea of the IBAction on the protocol is because in the project there will be many different devs implementing small UI components, all of which have the animate method. The components can be added programatically or by Interface Builder and it is very convenient that they are always IBAction because I plan to compose them from IB files to simplify the View Controllers to the maximum extent (and this is clearly a View only task).

Therefore, the solution proposed below of adding a method in the controller that just calls the animate of the component is not good because it is redundant code and makes your Controller more dependent on your View.

The idea of letting the dev to remember adding the IBAction keyword on the method is workable, but as I said it is error prone (and by that I mean that there will be some forgetting about it), and I want to make sure that this is always accessible from IB. It also adds extra cognitive load, because I will need to document this lack of IBAction on the protocol and request the implementor to add it manually.

I know is not the common way of working in iOS and UIKit, but that was why I posted the question, maybe someone has an alternative idea.

Angel G. Olloqui
  • 8,045
  • 3
  • 33
  • 31
  • I don't think there is another solution to this. Maybe look at the big picture, what are you trying to achieve with this? – Rengers Apr 11 '15 at 10:31
  • I don't see how letting the actual implementers determine whether or not `animate()` is an `@IBAction` is error prone at all. Please read my answer and let me know if you have any question. I think this may be an XY problem, but making a method in a protocol an `@IBAction` can't possibly make any sense at all if you actually understand what `@IBAction` is and does. – nhgrif Apr 11 '15 at 12:30
  • I have read your comment and I understand what `IBAction` does, but it still makes sense. See my edit :) – Angel G. Olloqui Apr 12 '15 at 10:39
  • Any fix for this yet? I understand the issue, ran into the same thing. Storyboard IBOutlets need to be "detectable" in protocols. It really sucks you can't do that. Wish it would just look at the method signature. – datWooWoo Dec 02 '19 at 20:10

2 Answers2

11

It doesn't make any sense to have an @IBAction in a protocol. @IBAction is nothing more than a keyword for Interface Builder to have a hook when you're control+dragging from Interface Builder to your actual source code.

This is just a simple misunderstanding of what @IBAction actually is and does.

  1. A method does not have to be marked as @IBAction in order for it to be the target of a UI element's actions. You programmatically hook up any method to any action using the addTarget set of methods that UI elements have. The method does not have to be marked as an @IBAction to do this.

  2. Regardless of whether or not a protocol defines a method as @IBAction, the class conforming to the protocol can add it (and still be conforming to the protocol.

    protocol FooProtocol {
        func doSomething()
    }
    
    class ViewControllerA: UIViewController, FooProtocol {
        @IBAction func doSomething() {
            // do something
        }
    } 
    
    class ViewControllerB: UIViewController, FooProtocol {
        func doSomething() {
            // do something
        }
    }
    

    Both of these view controller subclasses conform to the protocol, and having @IBAction there is ONLY necessary if you intend to hook up an action from interface builder!


Ultimately, whatever you're trying to do, if you think an @IBAction is necessary in your protocol, I think you're taking the wrong approach to something. It's hard to say what the right approach would be without knowing more details about what you're actually doing, but it never makes sense for @IBAction to belong in a protocol.

To me, it seems like the methods your protocol enforces shouldn't at all be tied to @IBAction methods. Instead, whatever user interaction should trigger the animation, should in turn call the animate method. For example, if we weren't talking about the protocol, my recommendation would be this sort of set up:

class ViewController: UIViewController {
    @IBAction func buttonThatStartsAnimation {
        self.animate()
    }

    func animate {
        // code that does all the animation
    }
}

So, with the protocol, we should take the same seperation of duties between the method that's actually initiating the animation code (which in the case of protocols, this is obviously some other outside class), and the animate method should only ever handle doing the relevant animations.

Importantly, just as a general rule, you shouldn't be directly referring to your @IBAction methods or your @IBOutlet variables directly from outside the class which defines them.

nhgrif
  • 61,578
  • 25
  • 134
  • 173
  • 5
    Thanks for the time you spent answering. However, I must disagree with some of your comments. I am totally aware what `IBAction` means, but that does not change the question. Check my edit. – Angel G. Olloqui Apr 12 '15 at 10:36
  • 1
    BTW, you said "whatever user interaction should trigger the animation, should in turn call the animate method.". That is exactly what I want, but by doing "that glue" it in the VC you are actually going from your View layer to your Controller layer and back to the View layer, when you could keep it all in the View layer in IB (where it should be as it is just a UI change). View Controllers are always abused in Cocoa, and this is a clear example IMO. – Angel G. Olloqui Apr 12 '15 at 10:46
  • 4
    With all due respect I totally disagree. Declaring some feature set in a protocol makes perfectly sense. For example, across your app, half of the `ViewControllers` may have a feature to present user profile controller in multiple different places, and putting an `@IBAction func actionPresentProfile(sender:)` in a protocol could be very useful. – superarts.org Jun 12 '17 at 14:47
  • I agree with @superarts.org. I have a situation where a number of view controllers conform to a protocol and I would like to have certain actions available to all of them to wire up in their respective storyboards. I'm using a where clause to define the default version of the method in an extension of the protocol. Being able to expose it to IB would be super helpful since it's an action all of those view controllers should be able to take. – Christopher Griffith Oct 17 '19 at 21:53
0

I totally agree with OP, although until Swift 3.1 you can't really declare anything as @IBOutlet, @IBAction, @objc etc in a protocol. As a workaround, I chose to build something based on pod 'ActionKit' and wrote something like:

protocol RequiresAnimation {
    var animateButton: UIButton! { get }
    func enableAnimateButton()
    func actionAnimate()
}

extension RequiresAnimation where Self: UIViewController {
    func enableAnimateButton() {
        animateButton.addControlEvent(.touchUpInside) {
            self.actionAnimate()
        }   
    }
    func actionAnimate() {
        //  animate here
    }
}

And make your view controller:

class MyViewController: UIViewController, RequiresAnimation {
    @IBOutlet var animateButton: UIButton!
    override func viewDidLoad() {
        super.viewDidLoad()
        enableAnimateButton()
    }
}

I wish there would be any easier approach, but so far you make need to do these 2 things manually: declaring your button as @IBOutlet and call a setup function. The reason why we need to import ActionKit is that we can't addTarget in protocol extension.

nhgrif
  • 61,578
  • 25
  • 134
  • 173
superarts.org
  • 7,009
  • 1
  • 58
  • 44
  • Is this actually preferred to simply not making the outlet for the button and instead hooking the button to an action in your view controller, and in that action simply call the `actionAnimate` method from the protocol? I don't see how this approach is preferred? – nhgrif Jun 13 '17 at 15:04
  • 1
    Nothing is always better, it depends on your use case. Your approach is more flexible that's for sure, but in my case, I would like to specify that I want MyViewController to "have a button that does nothing but perform a certain animation". Your approach introduces hidden dependency, like if you put something like presenting another view controller, you wouldn't know whether it will break anything before or after calling `animate`. But in the other hand, if you do want to do something before or after `animate`, my approach would be hard to extend. – superarts.org Jun 13 '17 at 18:19