본 포스팅은 Swift 5.3 기준으로 작성되었습니다.
Intro
약 두달간 진행하던 Market Kurly 서비스를 클론하는 프로젝트가 끝나, 오늘부터는 기존에 어느정도 공부하고 사용도하고 있었지만 완벽하게 숙지를 한 것은 아닌 부분들을 다시 복습하고 이해도를 높이는 것을 목표로 한동안 공부해보려고 합니다. 그럼 오늘은 처음 배울 때 이해하기 까다로웠던 부분 중 하나인 Delegate Pattern 에 대해 복습하는 시간을 가져보도록 하겠습니다. iOS 개발 에서는 정말 자주 쓰이고 중요한 개념이므로 여러분들도 혹시 아직 완벽히 이해한게 아니라면 같이 공부해보도록 해요~
Delegate 개념 이해하기
iOS 개발에 어느정도 익숙해졌다면 여러가지 상황에서 Delegate Protocol 을 채택하고 Protocol 내부에서 제공하는 method 를 사용해본 적이 있을텐데요. UITextField 나 UITableView 에서도 기능을 구현할 때 흔하게 사용되는 방식입니다. Apple 에서 우리가 유용하게 사용할 수 있을만한 기능들을 함수에 담아 미리 구현해놓고 그 코드 위에 우리가 추가적인 코드를 작성함으로서 우리가 원하는 방식대로 앱이 동작하게 할 수 있습니다.
Delegate Pattern 을 사용하기 위해서는 아래의 내용을 순차적으로 구현하면 됩니다. 동작방식이 처음에는 이해가 어려울 수도 있지만 반복해서 사용하다보면 금방 익숙해질 수 있으니 이해가 잘 가지 않는다면 계속 반복해서 연습해보세요 ㅎㅎ 저도 처음에는 개념을 이해하는게 어려웠지만 어느 순간부터 그냥 자연스레 사용할 수 있게 되더라고요.
Delegate Pattern 은 다음과 같은 조건을 충족함으로서 사용할 수 있습니다.
Delegate 를 생성하는 뷰
- Protocol 을 생성하고, 구현하고 싶은 기능을 해당 Protocol 의 method 로 생성
- Protocol 을 Type 으로 갖는 Delegate 인스턴스 생성
- 생성한 method 가 동작해야하는 상황에 코드 작성
Delegate 를 위임받는 뷰
- ViewController 에 Delegate Protocol 채택
- Protocol 필수 method 구현
- 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 형태로 생성할 수 있게 됩니다.
이 실습에서는 SecondViewController 의 UITextField 에 입력받은 내용을 FirstViewController 의 UILabel 에 전달해야하므로 우리가 구현할 method 는 String 을 Parameter 로 전달 받습니다.
protocol CustomTextFieldDelegate: class {
func textDidInput(didInput text: String)
}
간단하죠? method 에 구현될 코드는 나중에 우리가 Protocol 을 채택하고 입력하는 형식이므로 지금 당장은 어떤 내용도 입력할 필요가 없습니다.
Delegate 인스턴스 생성하기
SecondViewController 에 delegate 인스턴스를 생성합니다. delegate 인스턴스는 CustomTextFieldDelegate 를 Type 을 가짐으로서 이 인스턴스에 접근해서 우리가 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 를 할당해주면 됩니다. 실제 Input 은 SecondViewController 에서 이루어지지만 그 값에 대한 처리는 FirstViewController 에서 대행하겠다하는 일종의 명시입니다. 위임방법은 FirstViewController 에서 화면을 present 할 때 구현해 놓은 nextVC 의 delegate 에 접근해서 설정할 수 있습니다.
@objc
private func handleButton(_ sender: UIButton) {
let nextVC = SecondViewController()
nextVC.delegate = self
self.present(nextVC, animated: true, completion: nil)
}
Present 할 Controller 를 생성하고 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)
}
delegate 의 textDidInput() 함수를 호출하는 시점이 결정되었고, 이제 버튼이 눌리게 되면 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)
}
}