How can a ViewController communicate an action to a button in a View

msa_128

Alex

Posted on May 8, 2023

How can a ViewController communicate an action to a button in a View

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

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.

// 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?()
    }

}
Enter fullscreen mode Exit fullscreen mode
// 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!")
    }

}
Enter fullscreen mode Exit fullscreen mode

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:

guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
let viewController = ViewController()
window.rootViewController = viewController
self.window = window
window.makeKeyAndVisible()
Enter fullscreen mode Exit fullscreen mode

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

terminal result tap

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.

💖 💪 🙅 🚩
msa_128
Alex

Posted on May 8, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related