UIKit 클론코딩을 하다 보면 여러가지 상황에서 Delegate Protocol을 채택하고 Protocol 내부에서 제공하는 method를 사용하게 된다. UITextField, UITableView에서도 기능을 구현할 때 흔하게 사용되는 방식이다. Apple에서 우리가 유용하게 사용할 만한 기능들을 함수에 담아 미리 구현해두고 우리가 추가적인 코드를 작성함으로서 우리가 원하는 방식대로 앱이 동작하게 할 수 있다.
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)
}
}
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)
}
}
구현하고자 하는 method를 Protocol을 생성하고 그 내부에 만들어 주어야 한다. Protocol은 class을 Type으로 가진다. class Type을 가지게 되면 이후 생성할 delegate 인스턴스를 weak 형태로 생성할 수 있다.
// 1. 프로토콜 정의 (class 전용)
protocol ChatDelegate: AnyObject {
func didReceiveMessage(_ message: String)
}
// 2. 메시지를 받는 쪽 (delegate를 호출하는 쪽)
class ChatRoom {
// 2.1 순환 참조 방지를 위해 반드시 weak 사용
// 프로토콜에 AnyObject 또는 class 제약을 반드시 붙여야 함
weak var delegate: ChatDelegate?
func receiveNewMessage() {
let message = "안녕하세요!"
delegate?.didReceiveMessage(message)
}
}
// 3. 메시지를 표시할 화면 (delegate를 구현하는 쪽)
class ChatViewController: UIViewController, ChatDelegate {
let chatRoom = ChatRoom()
override func viewDidLoad() {
super.viewDidLoad()
chatRoom.delegate = self // 위임 설정
}
func didReceiveMessage(_ message: String) {
print("받은 메시지: \(message)")
}
}
이 실습에서는 SecondViewController 의 UITextField 에 입력받은 내용을 FirstViewController 의 UILabel 에 전달해야하므로 우리가 구현할 method 는 String 을 Parameter 로 전달 받는다.
protocol CustomTextFieldDelegate: class {
func textDidInput(didInput text: String)
}
SecondViewController에서 delegate 인스턴스를 생성한다. delegate인스턴스는 CustomTextFieldDelegate를 타입으로 가짐으로서 이 인스턴스에 접근해서 우리가 Protocol 내부에 작성해두었던 함수에 접근할 수 있게 된다.
// weak 을 사용해 ARC 가 증가하지 않도록 만들어줌으로서 메모리 Leak 이 발생하지 않도록 방지해주는 것이 중요
weak var delegate: CustomTextFieldDelegate?
Protocol을 채택시 우리가 생성한 Protocol을 채택하고 method까지 함께 구현한다.
Protocol을 채택할 때는 반드시 Extension으로 주어야 하는 것은 아니지만 필자는 가독성이 좋다고 판단하여 Extension으로 Delegate를 채택하는 것을 선호한다.
Protocol 이 채택되고 나면 경고창이 뜨면서 필수 함수를 구현하라고 나오는데 이 때 Xcode 가 지원하는 자동에러처리를 사용하면 함수 하나가 생성된다.
extension FirstViewController: CustomTextFieldDelegate {
func textDidInput(didInput text: String) {
label.text = text
}
}
delegate 인스턴스를 생성했을 때 Optional 형태로 생성하였다. 이 값에 CustomTextFieldDelegate를 채택한 FirstViewController를 할당해준다.
실제 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)
}
우리가 원하는 타이밍에 함수가 작동할 수 있도록 코드 구현. 화면을 전달하면서 값을 전달하면된다. delegate의 textDidInput함수를 호출하는 시점이 결정되었고 이제 버튼이 눌리게 되면 View 가 Dismiss 되기 전 이 함수를 호출하며 FirstViewController 가 값을 전달받게 된다. 이게 가능한 이유는 SecondViewController 가 present 되기 전 우리가 FirstViewController 를 delegate 로 설정해주었기 때문이다.
@objc
private func handleButton(_ sender: UIButton) {
let text = textField.text ?? ""
self.delegate?.textDidInput(didInput: text)
self.dismiss(animated: true, completion: nil)
}
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
}
}
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)
}
}