ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • UIKit에서 delegate 패턴이 어떻게 활용될까?
    Apple🍎/UIKit 2024. 9. 18. 22:39

     

    Delegate 패턴이란

    객체가 어떤(What)일들을 하는지를 정의해두고 해당 객체를 사용할 때
    앞서 정의해놓은 사항들을 바탕으로 그 일들을 어떻게(How) 할지를 구현하여 실제 객체가 동작하는 방식을 결정합니다.

     

    Protocol 이란?

    프로토콜이란 특정 작업이나 기능을 수행하기 위해 어떠한 것들이 필요한지를 사전에 정의해놓은 것을 의미합니다.
    Drivable 한 객체가 되기 위해서는 start()stop()을 할 수 있어야합니다.
    이와 같이 객체가 어떠한 기능을 수행하기 위해서 할 수 있어야하는 목록들을 사전에 정해놓은 것을 프로토콜이라고 합니다.

    protocol Drivable {
        func start()
        func stop()
    }
    
    struct Car: Drivable {
        func start() {
            print("차가 출발합니다.")
        }
    
        func stop() {
            print("차가 멈춥니다.")
        }
    
    }
    
    struct Airplane: Drivable {
        func start(){
            print("비행기가 출발합니다")
        }
    
        func stop(){
            print("비행기가 멈춥니다")
        }
    }

    Delegate 패턴으로 해당 컴포넌트가 어떤 역할을 하는지 정해놓는다.

    • 프로토콜을 통해 MessageView어떤 일을 하는지를 명시합니다.
    • 다른말로 MessageView가 제대로 작동하기 위해서는 프로토콜에 나와 있는 일들을 할 수 있어야한다는 말입니다.
    • MessageView가 어떤일들을 하는지는 알려줄 테니까 이 MessageView를 사용하기 위해서는 이 어떤일을 어떻게 해야할지를 알려줘야한다를 의미합니다.
    • 여기서는 MessageView라는 뷰를 사용하기 위해서는 messageView라는 동작을 “어떻게” 수행할지를 알려주어야한다고 명시하고 있습니다.

    1 . 프로토콜 정의

    protocol MessageViewDelegate: AnyObject {
        func messageView(_ messageView: MessageView, didEnterMessage message: String)
    }
    • MessageView 내부에 delegate 프로퍼티는 앞서 프로토콜을 통해 정의한 타입입니다.
    • delegate 프로퍼티를 통해 언제 , 프로콜에서 정의한 동작을 수행할지를 명시합니다. 여기서는 sendButtonTapped 메서드 내부에서 해당 버튼이 눌렸을 때 messageView라는 메서드를 실행합니다.
    • 실행되는 messageView 메서드의 구체적인 동작을 MesssageView 를 사용하는 곳에서 delegate 메서드를 구현함으로써 알려줍니다.

    2. 컴포넌트 정의

    class MessageView: UIView {
        private let textField: UITextField
        private let sendButton: UIButton
        private let displayLabel: UILabel
    
        weak var delegate: MessageViewDelegate?
    
        ...
        ...
        ...
    
        @objc private func sendButtonTapped() {
            guard let message = textField.text, !message.isEmpty else { return }
            delegate?.messageView(self, didEnterMessage: message)
            textField.text = ""
        }
    
        func displayMessage(_ message: String) {
            displayLabel.text = message
        }
    }
    
    • MessageViewControllerMessageView를 사용하고 있습니다.
    • 앞서 MessageViewMessageViewDelegate로 자신을 이용하기 위해 필요한 사항들을 프로토콜을 통해 설명하고 있었습니다.
    • MessageViewControllerMessageView를 사용하기 위해서 MessageViewDelegate를 채택한다음에 MessageViewDelegate에서 명시하고 있는 messageView 메서드를 구현할 책임이 있습니다.
    • MessageViewController는 extension을 통해 MessageViewDelegate를 채택하고 있고 messageView가 실제 어떠한 기능을 할지를 구현해줍니다.
    • 아래에서는 message라는 파라미터로 넘어온 String 값을 이용해 Message라는 인스턴스를 만든 휘에 MessageView가 내부적으로 가지고 있는 메서드를 호출하여 라벨에 입력받은 String을 보여줍니다.

    3. 컴포넌트 사용

    class MessageViewController: UIViewController {
        private let messageView = MessageView()
        private var currentMessage: Message?
    
        override func loadView() {
            view = messageView
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            messageView.delegate = self
        }
    }
    
    extension MessageViewController: MessageViewDelegate {
        func messageView(_ messageView: MessageView, didEnterMessage message: String) {
            currentMessage = Message(content: message)
            messageView.displayMessage("You entered: \(message)")
        }
    }
    

    전체 흐름을 정리해보면
    첫번째로 MessageView가 하나의 컴포넌트로써 역할을 하기 위해 수행할 수 있어야하는 기능목록을 MessageViewDelegate 라는 프로토콜을 통해 명시해둡니다.
    두번째로 MessageView에서 delegate 프로퍼티를 통해 어떤 시점에(예를 들어 뷰가 처음 떴을때, 버튼이 눌렸을때 등) 앞에서 프로토콜을 통해 명시해 놓은 기능이 작동하는 시점을 표시해둡니다.
    세번째로 MessageViewController에서 MessageView를 사용하기 위해 MessageViewDelegate를 채택해서 실제 어떠한 동작을 수행할지를 구현하고 messageView.delegate = self를 통해 MessageViewdelegate에다가 자신히 구현한 실제 동작을 넘겨 줍니다. 이렇게 함으로써 MessageViewdelegate가 동작하는 시점에 MessageViewController에서 구현한 내용을 실행할 수 있습니다.

    UIKit에서의 Delegate 패턴

    애플이 UIKit 프레임워크를 만들때도 위와 같은 Delegate 패턴을 따릅니다.

    UITableView를 사용하다고 가정해봅시다.

    애플에서는 UITableView가 하나의 컴포넌트로써 기능하기 위해 갖추어야하는 사항들 또는 해당 컴포넌트가 할 수 있는 사항들을 프로토콜을 통해 정의해놓았습니다.

    1 . 프로토콜 정의

    • UITableViewDataSource : UITableView에 어떠한 데이터들을 보여줄지 결정하는 방법을 명시해둔 프로토콜
    • // MARK: - UITableViewDataSource protocol UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { } }
    • UITableViewDelegate : UITableView와 사용자가 상호작용할때 어떻게 동작할지를 명시해둔 프로토콜

    // MARK: - UITableViewDelegate
    protocol UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    }

    }

    
    그러면 앞서 봤던 `MessageView`에서 `MessageViewDelegate`를 통해 정의한 동작이 언제 실행될지를  `delegate` 프로퍼티를 이용해 정해놓았던 것처럼 `UITableView` 는 내부적으로 어떤시점에 해당 프로토콜에서 명시한 동작이 실행될지를 정해 놓았을 겁니다.
    ### 2. 컴포넌트 정의 
    ```swift
    class UITableView {
        ...
        weak var source: UITableViewDataSource
        weak var delegate: UITableViewDelegate?
        ...
        ...
        ...
    }

    MemoListView에서 UITableView를 컴포넌트를 사용하고 있고, MemoViewController에서 MemoListView를 사용하고 있습니다. 최종적으로 UITableView를 사용하기 위해 UITableView가 기능하기 위해 실제 동작을 구현해야하는 책임은 MemoViewController에게 있습니다. ( 사용하는 구조에 따라 책임의 위치는 달라질수도 있습니다.) 따라서 현재 MemoViewController에서는 extension을 통해 UITableViewDataSourceUITableViewDelegate를 구현하며 실제 동작을 구현하고 있습니다.

    3. 컴포넌트 사용

    import UIKit
    
    class MemoListView: UIView {
        ...
    
        let tableView: UITableView = {
            let table = UITableView()
            table.register(MemoTableViewCell.self, forCellReuseIdentifier: MemoTableViewCell.identifier)
            table.translatesAutoresizingMaskIntoConstraints = false
            return table
        }()
    
        ...
    }
    
    import UIKit
    
    class MemoViewController: UIViewController {
    
        private let memoListView = MemoListView()
    
        override func viewDidLoad() {
               ...
    
            memoListView.tableView.delegate = self
            memoListView.tableView.dataSource = self
        }
    }
    
    // MARK: - UITableViewDataSource
    extension MemoViewController: UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return memos.count
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: MemoTableViewCell.identifier, for: indexPath) as? MemoTableViewCell else {
                fatalError("Failed to dequeue MemoTableViewCell")
            }
    
            let memo = memos[indexPath.row]
            cell.configure(with: memo)
    
            return cell
        }
    }
    
    // MARK: - UITableViewDelegate
    extension MemoViewController: UITableViewDelegate {
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            print("선택된 메모: \(memos[indexPath.row].title)")
            tableView.deselectRow(at: indexPath, animated: true)
        }
    }

    애플에서는 위와 같이 Delegate 패턴을 이용해
    컴포넌트가 할 수 있는 기능해당 기능이 언제 실행되는지를 미리 정의해 두는 대신 각 기능이 실제로 어떻게 동작을 하는지는 정해두지 않음으로써
    개발자가 애플이 먼저 만들어 놓은 컴포넌트를 이용하여 쉽게 개발을 할 수 있는 동시에 실제 기능에 대한 동작은 개발자가 직접 구현하게 함으로써 자유롭게 자신들의 앱에 맞는 기능을 넣을 수 있도록 하였습니다.

    정리하면 UIKit에서는 Delegate 패턴을 통해서 애플이 만들어 놓은 컴포넌트를 이용하면서 버튼이나 리스트와 같이 일반적으로 많이 사용되는 것들을 일일이 만들지 않아도 되는 동시에 애플이 해당 컴포넌트에 정의해놓은 기능 중에 필요한 것들만 delegate를 채택해서 자유롭게 원하는 기능을 구현할 수 있습니다.

    댓글

Designed by Tistory.