본 포스팅은 Swift 5.3 기준으로 작성되었습니다.

Intro

약 두달간 진행하던 Market Kurly 서비스를 클론하는 프로젝트가 끝나, 오늘부터는 기존에 어느정도 공부하고 사용도하고 있었지만 완벽하게 숙지를 한 것은 아닌 부분들을 다시 복습하고 이해도를 높이는 것을 목표로 한동안 공부해보려고 합니다. 그럼 오늘은 처음 배울 때 이해하기 까다로웠던 부분 중 하나인 Delegate Pattern 에 대해 복습하는 시간을 가져보도록 하겠습니다. iOS 개발 에서는 정말 자주 쓰이고 중요한 개념이므로 여러분들도 혹시 아직 완벽히 이해한게 아니라면 같이 공부해보도록 해요~


Delegate 개념 이해하기

iOS 개발에 어느정도 익숙해졌다면 여러가지 상황에서 Delegate Protocol 을 채택하고 Protocol 내부에서 제공하는 method 를 사용해본 적이 있을텐데요. UITextFieldUITableView 에서도 기능을 구현할 때 흔하게 사용되는 방식입니다. Apple 에서 우리가 유용하게 사용할 수 있을만한 기능들을 함수에 담아 미리 구현해놓고 그 코드 위에 우리가 추가적인 코드를 작성함으로서 우리가 원하는 방식대로 앱이 동작하게 할 수 있습니다.

Delegate Pattern 을 사용하기 위해서는 아래의 내용을 순차적으로 구현하면 됩니다. 동작방식이 처음에는 이해가 어려울 수도 있지만 반복해서 사용하다보면 금방 익숙해질 수 있으니 이해가 잘 가지 않는다면 계속 반복해서 연습해보세요 ㅎㅎ 저도 처음에는 개념을 이해하는게 어려웠지만 어느 순간부터 그냥 자연스레 사용할 수 있게 되더라고요.

Delegate Pattern 은 다음과 같은 조건을 충족함으로서 사용할 수 있습니다.

Delegate 를 생성하는 뷰

  1. Protocol 을 생성하고, 구현하고 싶은 기능을 해당 Protocol 의 method 로 생성
  2. Protocol 을 Type 으로 갖는 Delegate 인스턴스 생성
  3. 생성한 method 가 동작해야하는 상황에 코드 작성

Delegate 를 위임받는 뷰

  1. ViewController 에 Delegate Protocol 채택
  2. Protocol 필수 method 구현
  3. Delegate 위임

이렇게 각 뷰에서 3 가지 조건, 총 6가지 조건을 만족하면 Delegate 가 정상적으로 작동합니다.


Prerequisite

이제 새로운 프로젝트를 생성하고 직접 Delegate Pattern 을 구현하면서 실습해 볼게요.

실습을 위해 총 2개의 ViewController 를 생성하고

  • 첫번째 Controller 에는 결과값을 표시할 Label 과 두번째 Controller 를 띄울 Button
  • 두번째 Controller 에는 결과값을 입력받을 TextField 와 첫번째 Controller 로 돌아갈 수 있는 Button

을 배치했습니다.

FirstViewController

class FirstViewController: UIViewController {

    let label = UILabel()
    let button = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
    }

    // MARK: - UI
    private func configureUI() {
        view.backgroundColor = .white
        setAttributes()
        setContraints()
    }

    private func setAttributes() {
        label.text = "Sample Text"

        button.setTitle("Present", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        button.addTarget(self, action: #selector(handleButton(_:)), for: .touchUpInside)
    }

    private func setContraints() {
        [label, button].forEach {
            view.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
        }

        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0),

            button.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
            button.bottomAnchor.constraint(equalTo: label.topAnchor, constant: -30)
        ])
    }

    // MARK: - Selectors
    @objc
    private func handleButton(_ sender: UIButton) {
        let nextVC = SecondViewController()
        self.present(nextVC, animated: true, completion: nil)
    }
}

SecondViewController

class SecondViewController: UIViewController {

    let textField = UITextField()
    let button = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
    }

    // MARK: - UI
    private func configureUI() {
        view.backgroundColor = .white
        setAttributes()
        setContraints()
    }

    private func setAttributes() {
        textField.layer.borderColor = UIColor.lightGray.cgColor
        textField.layer.borderWidth = 0.5

        button.setTitle("Dismiss", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        button.addTarget(self, action: #selector(handleButton(_:)), for: .touchUpInside)
    }

    private func setContraints() {
        [textField, button].forEach {
            view.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
        }

        NSLayoutConstraint.activate([
            textField.heightAnchor.constraint(equalToConstant: 50),
            textField.widthAnchor.constraint(equalToConstant: 350),
            textField.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
            textField.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0),

            button.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
            button.bottomAnchor.constraint(equalTo: textField.topAnchor, constant: -30)
        ])
    }

    // MARK: - Selectors
    @objc
    private func handleButton(_ sender: UIButton) {
        self.dismiss(animated: true, completion: nil)
    }
}

저는 이렇게 기본 세팅을 해주었는데요. 함께 실습하고 싶은 분은 위 코드를 복사해서 사용하세요~


Delegate Pattern 연습하기

이제 기본적인 세팅을 마쳤으니 Delegate Pattern 개념 이해하기에서 설명했었던 6가지 단계를 직접 구현해보도록 하겠습니다. Delegate Pattern 을 구현할 때는 지금 실습할 이 단계들만 잘 기억한다면 어려울 것이 없습니다.


Protocol 구현하기

가장 먼저 우리가 구현하고자하는 method 를 Protocol 을 생성하고 그 내부에 만들어 주어야 하는데요. Protocol 은 class 를 Type 으로 가집니다. 이것은 ARC, 메모리 관리와 관련이 있는 부분이며 지금 설명하기에는 복잡한 내용이라 나중에 다른 포스팅에서 자세히 알아보도록 하겠습니다. 아무튼 class Type 을 가지게되면 이후 생성할 delegate 인스턴스를 weak 형태로 생성할 수 있게 됩니다.

이 실습에서는 SecondViewControllerUITextField 에 입력받은 내용을 FirstViewControllerUILabel 에 전달해야하므로 우리가 구현할 method 는 StringParameter 로 전달 받습니다.

protocol CustomTextFieldDelegate: class {
    func textDidInput(didInput text: String)
}

간단하죠? method 에 구현될 코드는 나중에 우리가 Protocol 을 채택하고 입력하는 형식이므로 지금 당장은 어떤 내용도 입력할 필요가 없습니다.


Delegate 인스턴스 생성하기

SecondViewControllerdelegate 인스턴스를 생성합니다. delegate 인스턴스는 CustomTextFieldDelegateType 을 가짐으로서 이 인스턴스에 접근해서 우리가 Protocol 내부에 작성해두었던 함수에 접근할 수 있게 됩니다.

weak var delegate: CustomTextFieldDelegate?

위에서 한번 설명했듯이 weak 을 사용해 ARC 가 증가하지 않도록 만들어줌으로서 메모리 Leak 이 발생하지 않도록 방지해주는 것이 중요합니다.


Protocol 채택 및 필수 method 구현하기

이제 FirstViewController 로 돌아와서 우리가 생성한 Protocol 을 채택하고 method 까지 함께 구현해보도록 할게요.

Protocol 을 채택할 때는 반드시 Extension 을 통해 만들어 주어야하는 것은 아니지만 저는 Extension 으로 Delegate 를 채택하는 것을 선호하고, 실제로도 많은 개발자들이 이 형식을 따르므로, 이번 실습에서도 extension 을 사용해 CustomTextFieldDelegate 를 채택했습니다. Protocol 이 채택되고 나면 경고창이 뜨면서 필수 함수를 구현하라고 나오는데요. 이 때 Xcode 가 지원하는 자동에러처리를 사용하면 함수 하나가 생성됩니다.

이 함수는 우리가 Protocol 생성 시에 만들어 둔 함수로, 실제로 UITableView 등에서 Delegate Protocol 을 채택했을 때 필수로 구현해야하는 함수도 이런 방식으로 자동완성이 되는 것입니다.

extension FirstViewController: CustomTextFieldDelegate {
    func textDidInput(didInput text: String) {
        label.text = text
    }
}

함수 내부의 코드를 간단하게 설명해보면, 함수가 실제로 호출되는 곳에서 어떤 String 값을 가지고 들어오게되고, 우리는 Parameter 를 전달받아 사용하기만 하면 됩니다. 이 실습에서는 전달받은 값을 UILabel 에 표현하는 것이 목적이므로 위와 같이 코드를 구현했습니다.


Delegate 위임하기

우리가 이전에 delegate 인스턴스를 생성했을 때 optional 형태로 생성해주었잖아요? 이제 이 값에 CustomTextFieldDelegate 를 채택한 Controller 를 할당해주면 됩니다. 실제 InputSecondViewController 에서 이루어지지만 그 값에 대한 처리는 FirstViewController 에서 대행하겠다하는 일종의 명시입니다. 위임방법은 FirstViewController 에서 화면을 present 할 때 구현해 놓은 nextVC 의 delegate 에 접근해서 설정할 수 있습니다.

@objc
private func handleButton(_ sender: UIButton) {
    let nextVC = SecondViewController()
    nextVC.delegate = self
    self.present(nextVC, animated: true, completion: nil)
}

PresentController 를 생성하고 present 되기 직전 delegate 에 값을 넣어주는 방식입니다. 넘어갈 View 에 데이터를 전송할 떄 많이 써봤던 방식일 거에요 ㅎㅎ


함수가 동작해야하는 시점 설정하기

이제 우리가 원하는 타이밍에 함수가 작동할 수 있도록 코드를 구현해볼게요. 화면이 내려가면서 값을 전달하면 되니까 Dismiss 되는 타이밍에 작동할 수 있도록 만들어 주었습니다.

@objc
private func handleButton(_ sender: UIButton) {
    let text = textField.text ?? ""
    self.delegate?.textDidInput(didInput: text)
    self.dismiss(animated: true, completion: nil)
}

delegatetextDidInput() 함수를 호출하는 시점이 결정되었고, 이제 버튼이 눌리게 되면 View 가 Dismiss 되기 전 이 함수를 호출하며 FirstViewController 가 값을 전달받게 됩니다. 이게 가능한 이유는 SecondViewController 가 present 되기 전 우리가 FirstViewController 를 delegate 로 설정해주었기 때문입니다.


Wrap Up

이렇게 Delegate Pattern 실습을 마쳤습니다. 이 개념이 이해하고나면 의외로 간단하지만 초반에는 완전히 이해하는 것이 생각보다 까다롭더라고요. 처음배울 때는 왜 그렇게 어려웠었는지…ㅎㅎ 아무튼 만약 지금 이 개념이 잘 이해가 가지 않더라도 지속적으로 반복 연습하다보면 어느순간 당연하게 생각되는 부분이니까 열심히 공부해보세요!


Entire Code

FirstViewController

class FirstViewController: UIViewController {

    let label = UILabel()
    let button = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
    }

    // MARK: - UI
    private func configureUI() {
        view.backgroundColor = .white
        setAttributes()
        setContraints()
    }

    private func setAttributes() {
        label.text = "Sample Text"

        button.setTitle("Present", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        button.addTarget(self, action: #selector(handleButton(_:)), for: .touchUpInside)
    }

    private func setContraints() {
        [label, button].forEach {
            view.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
        }

        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0),

            button.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
            button.bottomAnchor.constraint(equalTo: label.topAnchor, constant: -30)
        ])
    }

    // MARK: - Selectors
    @objc
    private func handleButton(_ sender: UIButton) {
        let nextVC = SecondViewController()
        nextVC.delegate = self
        self.present(nextVC, animated: true, completion: nil)
    }
}

extension FirstViewController: CustomTextFieldDelegate {
    func textDidInput(didInput text: String) {
        label.text = text
    }
}

SecondViewController

class SecondViewController: UIViewController {

    weak var delegate: CustomTextFieldDelegate?

    let textField = UITextField()
    let button = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
    }

    // MARK: - UI
    private func configureUI() {
        view.backgroundColor = .white
        setAttributes()
        setContraints()
    }

    private func setAttributes() {
        textField.layer.borderColor = UIColor.lightGray.cgColor
        textField.layer.borderWidth = 0.5

        button.setTitle("Dismiss", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        button.addTarget(self, action: #selector(handleButton(_:)), for: .touchUpInside)
    }

    private func setContraints() {
        [textField, button].forEach {
            view.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
        }

        NSLayoutConstraint.activate([
            textField.heightAnchor.constraint(equalToConstant: 50),
            textField.widthAnchor.constraint(equalToConstant: 350),
            textField.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
            textField.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0),

            button.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
            button.bottomAnchor.constraint(equalTo: textField.topAnchor, constant: -30)
        ])
    }

    // MARK: - Selectors
    @objc
    private func handleButton(_ sender: UIButton) {
        let text = textField.text ?? ""
        self.delegate?.textDidInput(didInput: text)
        self.dismiss(animated: true, completion: nil)
    }
}