Understanding Delegate Pattern

swift pattern uikit
Written on Aug 1, 2023

Introduce Delegate Pattern

UIKit에서는 여러가지 UI 관련 View를 제공합니다. 이 중에선 동작에 필요한 세부 구현은 숨겨진 채로 특정 상황이 발생했을 때만 ViewController에게 데이터를 전달하는 UIView가 있습니다.

예를 들어 UITextField의 경우, 사용자가 입력창을 눌렀을 때 OS에게 키보드를 열 것을 명령하며 사용자가 입력한 값을 UITextField에 전달하는 동작이 구현되어 있을 것입니다. 하지만 ViewController에선 이러한 세부 동작은 알 필요 없이 특정 상황이 발생했을 때만 데이터를 전달 받아 이용하면 됩니다.

UIkit에서는 이렇게 세부 구현은 숨긴 채로 특정 상황이 발생했음을 알리거나 동작의 제어를 위임하기 위해 Delegate Pattern을 활용하고 있습니다. UITextFieldUITextFieldDelegate 프로토콜을 통해 이를 구현하고 있으며, 이를 채택한 ViewController는 다음의 메서드를 구현하여 데이터를 전달받고 동작을 제어할 수 있습니다.

UITextFieldDelegate를 채택하여 구현할 수 있는 메서드 목록은 다음과 같습니다.

Language:swift
// 편집을 시작할 때 호출합니다. Bool 값을 반환하여 편집을 허용할지 여부를 결정할 수 있습니다.
optional func textFieldShouldBeginEditing(_:) -> Bool

// 편집이 시작된 직후 호출됩니다.
optional func textFieldDidBeginEditing(_:)

// 편집이 종료될 때 호출됩니다. Bool 값을 반환하여 편집을 중지할지 여부를 결정할 수 있습니다.
optional func textFieldShouldEndEditing(_:) -> Bool

// 편집이 종료된 직후 호출됩니다.
optional func textFieldDidEndEditing(_:)

// 텍스트 필드의 문자열이 변경될 때 호출됩니다. Bool 값을 반환하여 변경을 허용할지 여부를 결정할 수 있습니다.
optional func textField(_:shouldChangeCharactersIn:replacementString:) -> Bool

// 텍스트 필드의 선택 영역이 변경될 때 호출됩니다.
optional func textFieldDidChangeSelection(_:)

// 텍스트를 삭제할 때 호출됩니다. Bool 값을 반환하여 삭제를 허용할지 여부를 결정할 수 있습니다.
optional func textFieldShouldClear(_:) -> Bool

// 텍스트 필드의 리턴 키가 눌렸을 때 호출됩니다. Bool 값을 반환하여 리턴 키를 허용할지 여부를 결정할 수 있습니다.
optional func textFieldShouldReturn(_:) -> Bool

optional 키워드로 정의되어 있으니 ViewController는 이 중에서 필요한 메서드만 구현하면 됩니다.

정의한 메서드가 호출되려면 UITextField 인스턴스의 delegate 프로퍼티에 ViewController를 할당해야 합니다.

Language:swift
final class ViewController: UIViewController {
    private let textField = UITextField()

    override func viewDidLoad() {
        super.viewDidLoad()
        textField.delegate = self
    }
}

extension ViewController: UITextFieldDelegate {
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        // ...
    }

    func textFieldDidBeginEditing(_ textField: UITextField) {
        // ...
    }
}

이런 식으로 Delegate Pattern을 통해 동작의 제어권을 위임받을 수 있습니다.

Understand Delegate Pattern

그렇다면 Delegate Pattern은 어떤 식으로 동작하기에 다른 객체에게 동작의 제어권을 위임할 수 있는걸까요? 간단한 Delegate 프로토콜을 구현하여 동작을 확인해보겠습니다.

Language:swift
protocol UITextFieldDelegate {
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool
    func textFieldDidBeginEditing(_ textField: UITextField)
}

class UITextField {
    weak var delegate: UITextFieldDelegate?

    func userTextFieldTapped() {
        guard let delegate else { return }
        if delegate.textFieldShouldBeginEditing(self) {
            // OS에게 키보드 열기 요청
            delegate.textFieldDidBeginEditing(self)
        }
    }
}

class ViewController {
    let textField = UITextField()

    func viewDidLoad() {
        super.viewDidLoad()
        textField.delegate = self
    }
}

extension ViewController: UITextFieldDelegate {
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
       print(#function)
       return true
   }

   func textFieldDidBeginEditing(_ textField: UITextField) {
       print(#function)
   }
}

let controller = ViewController()

controller.viewDidLoad()

controller.textField.userTextFieldTapped()

// output:
// textFieldShouldBeginEditing(_:)
// textFieldDidBeginEditing(_:)

소스 코드가 공개되어 있지 않으므로 정확한 세부 구현은 알 수 없으나 Delegate 패턴을 활용한 내부 모습은 대략 위와 비슷할 것으로 추정합니다.

UITextField의 경우 외부로부터 주입 받은 delegate 객체를 통해 textFieldShouldBeginEditing를 호출하여 동작의 제어권을 위임한 모습입니다. textFieldShouldBeginEditing에서 false를 반환하면 textFieldDidBeginEditing는 호출되지 않을 것입니다.

이러한 원리를 응용하여 Custom Delegate를 추가할 수 있습니다. Delegate Pattern 적용하기