How can a ViewController communicate an action to a button in a View
Published: 2023-05-08 04:39:08

As a beginner, I'm constantly struggling with communication between objects. So I decided to try some approaches to facilitate this communication.

I'm still in the beginning of my journey, so please feel free to correct my approach if you see something wrong.

## UI Object Communication

I find separating `Views` from `ViewControllers` to be one of the most challenging aspects. In this tutorial, we will explore various methods for declaring an action method in a `UIViewController` and setting it for a private `UIButton` inside a `UIView`.

![View-ViewModel handler injection problem](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v6gn9fc1msgf1jhnmbz3.png)

By following the principle of Separation of Concerns, our View should be only responsible for the presentation layer, any action that is not related to the presentation logic must be delegated to another layer. 

In this scenario a button action should not be handled by the View itself, so we need to find a way to "inject" this functionality somehow from the outside.

## Using callbacks (closures)

One way to do this is to inject a function from the `ViewController` inside our `View`.

Let's imagine we have the following classes for our `View` and `ViewController` that represent the user interface and the logic for what to do when an event happens.

```swift
// View.swift
import UIKit

final class CustomView: UIView {

    // We define the callback method we want to use in our button
    var callback: (() -> Void)?

    private lazy var button: UIButton = {
        var button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        button.setTitle("Tap Me", for: .normal)
        button.backgroundColor = .systemBlue
        return button
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.addSubview(button)
        // setting the target method to the button
        button.addTarget(self, action: #selector(buttonHandler), for: .touchUpInside)
        positioningButton()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func positioningButton() {
        button.translatesAutoresizingMaskIntoConstraints = false
        // centering the button to the center of the custom view
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: self.centerYAnchor)
        ])
    }

    @objc
    private func buttonHandler(_ sender: UIButton) {
        // Calling the outside callback for the handler
        // if the callback doesn't exist then it won't do anything
        callback?()
    }

}
```

```swift
// ViewController.swift
import UIKit

final class ViewController: UIViewController {
    let customView = CustomView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Setting the callback to the button
        customView.callback = customAction
        view.addSubview(customView)
        customView.backgroundColor = .cyan
        setupCustomViewConstrains()
    }
    
    func setupCustomViewConstrains() {
        customView.translatesAutoresizingMaskIntoConstraints = false
        // Making the customView to take all screen space
        NSLayoutConstraint.activate([
            customView.topAnchor.constraint(equalTo: view.topAnchor),
            customView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            customView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            customView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    func customAction() {
        print("Button from View was tapped!")
    }
    
}
```

Since we are not using Interface builder, we also need to tell `SceneDelegate` to use our `ViewController` as the root view controller, if we don't do this our screen will be black.

To solve this, let us open `SceneDelegate.swift` and locate the method called `func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)`.

Then replace the body of the method (the code between the curly braces) with this:

```swift
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
let viewController = ViewController()
window.rootViewController = viewController
self.window = window
window.makeKeyAndVisible()
```

Once we have all this code ready we can run the app. The button will appear in the center of the screen and if we tap it, the closure defined in the ViewController will be executed.

![interface on simulator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/34kt2ueglazasodryl1c.png)

![terminal result tap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uujuv4hbbri5o356wjx3.png)

## Conclusion

In this small tutorial, we created an app that communicates the view and the viewController programmatically using closures. This approach provides an effective way to facilitate communication between our objects. While there are other mechanisms to achieve this, such as delegation (which I'll cover in another post), closures offer a convenient solution.

This mechanism of communication is not restricted to only Views and ViewControllers, you can use with any type of objects you need.

I hope this tutorial helps you gain a better understanding of this topic and sheds some light on your learning process.

Thanks for reading.