한국어로 직역하면 '대리자 패턴'이다.
델리게이트는 프로토콜을 활용하여 객체 간 소통이 필요할 때 사용하는 디자인 패턴이다.
이벤트가 일어나는 객체의 정보를 다른 객체가 알고 싶을 때 사용할 수 있다.
콜렉션뷰를 써본 사람은 알겠지만 didSelectItemAt
또한 델리게이트 패턴으로 사용되고 있다.
뷰컨과 뷰의 관계에 따라 델리게이트 프로토콜을 직접 정의해야 할 필요가 있으며, 객체 간 참조가 강한 순환 참조를 일으킬 수 있기 때문에 메모리 누수에 주의해야 한다.
개인적으로 델리게이트 패턴은 설명을 들었을 때보다 실제로 사용할 때가 굉장히 헷갈렸다.
델리게이트 패턴은 정말정말 다양한 방법으로 활용할 수 있다.
이번엔 필자가 진행하고 있는 사이드 프로젝트의 예시를 가져와서 살펴보겠다.
주어진 상황은 다음과 같다.
GameViewController
는 GameCollectionView
를 갖고 있다.GameCollectionView
는 25개의 GameViewCell
을 갖고 있다.GameViewCell
은 각각의 UITextField
를 갖고 있다.GameViewCell
의 UITextField
에 입력된 값을 GameViewController
에서 확인하고자 한다.이벤트가 일어나는 곳은 GameViewCell
의 UITextField
고 변화를 알고 싶은 곳은 GameViewController
다.
델리게이트 패턴의 핵심은 이벤트 발생 객체에서 delegate
속성을 정의하고, 이벤트 수신 객체에서 delegate
의 메소드를 정의하는 것이다.
이 관계를 중개해주는 프로토콜이 있기 때문에 이러한 응용이 가능하다.
protocol
정의아래에 정의된 프로토콜로 속성과 메소드는 서로 다른 객체에서 소통할 수 있게 된다.
이 프로토콜은 UITextField
와 GameViewCell
을 파라미터 타입으로 요구하는 메소드 하나를 갖는다.
클래스 객체만 채택할 수 있도록 AnyObject
를 더한다.
직접 커스터마이징한 특정한 셀에서의 활동을 받아오기 때문에 구체화 했다.
protocol CustomCollectionViewCellDelegate: AnyObject {
func collectionViewCell(valueChangedIn textField: UITextField,
delegatedFrom cell: GameViewCell)
}
delegate
속성 정의이벤트가 일어나는 객체에 delegate
속성을 정의한다.
위에서 정의한 프로토콜 타입으로 정의하여 delegate
에 할당되는 또 다른 프로토콜 타입의 메소드를 호출할 수 있는 연관성을 얻는다.
UITextField
의 값 입력이 끝날 때 그 값을 전달해야 하기 떄문에 addTarget
을 함께 활용한다.
addTarget
메소드가 트리거하는 메소드는 delegate
프로토콜의 메소드다.
이 시점까지 delegate
의 메소드는 정의되어 있지 않기 때문에 아무런 일도 일어나지 않는다!
CharacterTextField
와 self
가 아규먼트로 전달되어 이제 delegate
에 할당되는 객체는 이 이벤트에 접근할 수 있을 것이다.
또한 이벤트에 접근하고 처리하는 그 객체에서 프로토콜을 채택하고 구체화함으로써 delegate
메소드는 올바르게 동작할 것이다.
그러나 이 구조는 delegate
가 참조하는 객체 내부의 메소드를 호출하는 것이다.
클래스 객체가 온다면 retain
이 1회 발생할 것이다.
반대로 클래스 객체는 GameViewCell
의 delegate
에 자기 자신을 할당하게 될테니 여기서도 retain
이 발생한다.
서로 참조하는 강한 참조 순환이 일어날 수 있기 때문에 delegate
속성은 weak
으로 선언했다.
// MARK: GameViewCell
public weak var delegate: CustomCollectionViewCellDelegate?
public let chracterTextField: UITextField = {
let tf = UITextField()
tf.backgroundColor = .systemGray4
tf.layer.borderColor = UIColor.black.cgColor
tf.layer.borderWidth = 0.5
tf.font = UIFont.boldSystemFont(ofSize: 16)
tf.autocapitalizationType = .allCharacters
tf.autocorrectionType = .no
tf.keyboardType = .alphabet
tf.textAlignment = .center
tf.addTarget(self, action: #selector(valueChanged), for: .editingDidEnd)
return tf
}()
@objc func valueChanged(_ sender: UITextField) {
delegate?.collectionViewCell(valueChangedIn: chracterTextField, delegatedFrom: self)
}
delegate
메소드 정의위의 delegate
가 전달받은 이벤트로 무엇을 할 것인지 정의하는 것으로 커스텀 델리게이트 패턴은 마무리된다.
이벤트를 수신하는 객체는 GameViewController
다.
GameViewController
에서 이벤트를 수신하고 처리하기 때문에 프로토콜은 이 곳에서 채택된다.
위에서 정의한 GameViewCell
을 생성하는 UICollectionViewDataSource
에서 delegate
를 GameViewController
로 할당한다.
GameViewController
를 확장해서 CustomCollectionViewCellDelegate
를 채택하고 수행할 메소드를 정의한다.
gameView
의 indexPath
를 받아오고, 25개의 셀이 갖고 있는 textField
의 값이 어느 위치의 indexPath
에 해당하는지 확인할 수 있다.
// MARK: GameViewController
private var gameView: UICollectionView?
// MARK: GameViewController: UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! GameViewCell
cell.delegate = self
return cell
}
extension GameViewController: CustomCollectionViewCellDelegate {
func collectionViewCell(valueChangedIn textField: UITextField, delegatedFrom cell: GameViewCell) {
guard let gameView = gameView else { return }
if let indexPath = gameView.indexPath(for: cell),
let text = textField.text, text != "" {
print("textField text: \(text) from cell: \(indexPath))")
}
}
}
NotificationCenter
로 구현하는 옵저버 패턴과 달리 일대일, 단방향 소통이다(일반적으로는).retain count
가 증가한다.220822