Danny의 iOS 컨닝페이퍼
article thumbnail
반응형

Custom Delegate

오늘은 Custom delegate 생성 방법에 대하여 알아봅시다.

기본 Delegate 관한 내용은 [iOS/Swift] 델리게이트 패턴 (Delegate)를 참고해 주세요.

 

보통 Delegate 패턴은

두 컨트롤러 간의 데이터를 전달을 위한 쌍방향 커뮤니케이션 방법이다.

 

예를 들어 우리가 인터넷 쇼핑을 한다고 생각해 보자.

원하는 물건을 선택하고 수량 입력 후 장바구니 추가 버튼을 누르게 되면

누르는 즉시 장바구니에 물건이 업데이트된다.

 

개발자의 입장에서 보자면

사용자의 입력을 받아 추가 버튼을 누르는 순간

상품 컨트롤러에서 얻은 정보를 장바구니 컨트롤러로 전달시켜 작업을 하게 되는 거죠.

상품 컨트롤러가 장바구니 컨트롤러에게 일을 시킨다!

 

상품 컨트롤러에서 Delegate 프로토콜은 채택하고 사용하게 되면

장바구니 컨트롤러 말고도 결제 컨트롤러 또는 위시리스트 컨트롤러 등 

여러 방면으로 재사용을 가능하게 되죠.

 

이렇게 delegate 패턴은 서로 다른 두 컨트롤러를 사용함에 있어 재사용이 가능하고 

효과적으로 데이터 전달 및 업데이트를 도와주는 패턴입니다.

 

 

구성

Delegate Protocol

해야 할 일의 목록

발신자가 수신자에게 전달할 내용의 정의하는 곳입니다.

 

Sender(발신자)

일을 시키는 객체

유져와 커뮤니케이션을 통해 각종 이벤트를 받아온다.

 

Receiver(수신자) - 대리자

일을 하는 객체

수신자를 대신하여 작업을 처리할 대리자입니다

 

Sender에서 일어나는 이벤트에 관한 코드를 Receiver에서 작성합니다.

즉, Sender의 일을 Receiver에게 위임하며 이때 할 일들은 protocol을 통해 전달된다.

 

 

Custom Delegate 만들기

ViewController들 간의 데이터 전달의 위해 Custom Delegate를 만들어 봅시다.

 

 

현재 기분을 나타내는 앱을 만들어 보려고 합니다.

SecondVC에서 현재 기분클릭하면 선택한 이미지에 따라서

FirstVC에서 이미지가 업데이트되도록 만들어 보겠습니다.

 

SecondVC (Sender) -> FirstVC (Receiver)

데이터의 흐름은 이런 식으로 가겠죠?

 

SecondVC가 일을 시키고

FirstVC가 받은 일을 대신 처리합니다.

 

 

순서는 이와 같습니다.

 

1. 프로토콜을 구현하기

2. 일을 시킬 컨트롤러에서 delegate 선언

3. 일을 할 컨트롤러를 대리자로 설정

4. 할 일을 지시하기

6. 지시받은 동작을 대리자에서 대신 처리하기

 

큰 순서는 이렇습니다.

1. 프로토콜 구현하기 2. 동작을 하도록 메서드를 정의 3. 객체의 대리자 설정하기

 

지금 하려는 순서는 앱의 흐름대로 만들어 순서가 뒤죽박죽인데 편한 방법으로 만드시면 됩니다.

대략적인 흐름을 알면 도움이 될 거 같아서 이런 순으로 작성했습니다.

전체 코드도 첨부하겠습니다.

 

 

프로토콜을 구현하기

이름을 지을 때 사용목적에 맞게 지어주면 됩니다.

// ⭐️ AnyObject 오직 클래스타입 인스턴스를 나타냅니다.
protocol SecondVCImageDelegate: AnyObject {
    // ⭐️ 델리게이트 만들 때 규칙 (안 써도 무방하지만 애플이 만든 모든 델리게이트에 이와같이 적용돼 있어요.)
    // 보통 파라미터의 첫번째 항목은 이 대리자(delegate)를 발생시킨 개체를 사용한다. 
    // 여기선 SecondViewController
    func updateImage(_ viewController: SecondViewController, emotion: String)
}

 

 

일을 시킬 컨트롤러에서 delegate 선언

을 시킬 컨트롤러(SecondVC)에서 delegate선언해 줍시다.

또한 강한 순환 참조방지하기 위해 weak을 붙여서 변수를 만들어 줍시다.

class SecondViewController: UIViewController {
    // ⭐️ waek 키워드는 오직 클래스에서 사용가능합니다.(ARC 관련)
    // 힙영역 메모리는 클래스의 객체가 생성하기 때문이다.
    weak var delegate: SecondVCImageDelegate?
}

 

일을 할 컨트롤러를 대리자로 설정

secondVC 대신 일을 하기 위해 FirstVC대리자로 설정

이렇게까지 하면 Xcode에서 빨간불이 들어오고 난리도 아닐 텐데 일단 프로토콜만 채택해 줍시다.

흐름대로 만들고 있기 때문에 대신 동작을 할 프로토콜 메서드 정의는 밑에서 하겠습니다.

class FirstViewController: UIViewController, SecondVCLabelDelegate {

    @objc func startButtonTapped(_ sender: UIButton) {
        let secondVC = SecondViewController()
        // ⭐️ FirstVC를 대리자로 설정(SecondVC대신 일을 할꺼야!)
        secondVC.delegate = self
        self.navigationController?.pushViewController(secondVC, animated: true)
    }
}

 

 

할 일을 지시하기

일을 시킬 컨트롤러(SecondVC)로 다시 돌아와서

프로토콜에 정의해 둔 메서드를 통해 각각 일을 지시하기.

여기서는 버튼이 눌리면 작업을 지시함.

class SecondViewController: UIViewController {

    weak var delegate: SecondVCImageDelegate?

    @objc func happyButtonTapped(_ sender: UIButton) {
        // 버튼을 누르면 "데이터 happy를 갖고 updateImage 이미지를 동작해"라고 대리자에게 지시
        self.delegate?.updateImage(self, emotion: "happy")
        self.navigationController?.popViewController(animated: true)
    }
    
    @objc func sadButtonTapped(_ sender: UIButton) {
        // 버튼을 누르면 "데이터 sad를 갖고 updateImage 이미지를 동작해"라고 대리자에게 지시
        self.delegate?.updateImage(self, emotion: "sad")
        self.navigationController?.popViewController(animated: true)
    }
}

 

 

지시받은 동작을 대리자에서 대신 처리하기

만약 SecondVC에서 hppayButton을 눌렸다고 가정해 봅시다.

 

SecondVC에서 버튼이 눌리게 되면 데이터 "happy"를 갖고

updateImage를 실행하게 되겠죠?

 

이 updateImage는 대리자로 설정한 부분에서 정의해 줄 수 있는데요.

이것 때문에 FirstVC에서 데이터 "happy"를 갖고 작업을 정의할 수 있는 거죠.

 

즉, 이 메서드를 통해서

FirstVC에서 SecondVC데이터를 전달받고 대신 작업을 수행합니다.

class FirstViewController: SecondVCImageDelegate {

    @objc func startButtonTapped(_ sender: UIButton) {
        let secondVC = SecondViewController()
        secondVC.delegate = self
        self.navigationController?.pushViewController(secondVC, animated: true)
    }

    // 델리게이트 프로토콜 메서드 정의
    // 데이터를 SecondVC에서 받아와 FirstVC에서 작업
    func updateImage(_ viewController: SecondViewController, emotion: String) {
        startButton.isHidden = true
        emotionImageView.image = UIImage(named: emotion)
    }
}

 

 

전체 코드

FirstViewController

class FirstViewController: UIViewController {
    
    var emotionImageView: UIImageView = {
        let view = UIImageView(frame: CGRect(x: 50, y: 270, width: 300, height: 300))
        view.contentMode = .scaleAspectFill
        return view
    }()
    
    lazy var startButton: UIButton = {
        let button = UIButton(frame: CGRect(x: 125, y: 400, width: 150, height: 50))
        button.setTitle("Start", for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 30)
        button.addTarget(self, action: #selector(startButtonTapped), for: .touchUpInside)
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        self.view.addSubview(emotionImageView)
        self.view.addSubview(startButton)
        self.view.backgroundColor = .systemGreen
        navigationController?.navigationBar.prefersLargeTitles = true
        navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
        self.title = "FisrtVC"
    }

    @objc func startButtonTapped(_ sender: UIButton) {
        let secondVC = SecondViewController()
        // ⭐️ FirstVC를 대리자로 설정(SecondVC대신 일을 할꺼야!)
        secondVC.delegate = self
        self.navigationController?.pushViewController(secondVC, animated: true)
    }
}

// MARK: - 델리게이트 프로토콜 메서드 정의
extension FirstViewController: SecondVCImageDelegate {
    // 데이터를 SecondVC에서 받아와 FirstVC에서 작업
    func updateImage(_ viewController: SecondViewController, emotion: String) {
        startButton.isHidden = true
        emotionImageView.image = UIImage(named: emotion)
    }
}

 

SecondViewController & Delegate Protocol

// AnyObject 오직 클래스타입 인스턴스를 나타냅니다.
protocol SecondVCImageDelegate: AnyObject {
    // 델리게이트 만들 때 규칙이 존재한다. (안 써도 무방하지만 애플이 만든 모든 델리게이트에 적용돼 있어요.)
    // 보통 파라미터의 첫번째 항목은 이 대리자(delegate)를 발생시킨 개체를 사용한다.
    // 여기선(SecondViewController)를 채택
    func updateImage(_ viewController: SecondViewController, emotion: String)
}

class SecondViewController: UIViewController {
    
    private let lable: UILabel = {
        let lable = UILabel(frame: CGRect(x: 50, y: 250, width: 300, height: 40))
        lable.font = .systemFont(ofSize: 24, weight: .bold)
        lable.textAlignment = .center
        lable.text = "How are you feeling now?"
        return lable
    }()
    
    private lazy var happyButton: UIButton = {
        let button = UIButton(frame: CGRect(x: 80, y: 350, width: 80, height: 80))
        button.setImage(UIImage(named: "happy"), for: .normal)
        button.addTarget(self, action: #selector(happyButtonTapped), for: .touchUpInside)
        return button
    }()
    
    private lazy var sadButton: UIButton = {
        let button = UIButton(frame: CGRect(x: 240, y: 350, width: 80, height: 80))
        button.setImage(UIImage(named: "sad"), for: .normal)
        button.addTarget(self, action: #selector(sadButtonTapped), for: .touchUpInside)
        return button
    }()
    
    // waek키워드는 오직 클래스에서 사용한다. 
    // 힙영역 메모리는 클래스의 인스턴스가 생성하기 때문이다.
    weak var delegate: SecondVCImageDelegate?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        self.view.addSubview(lable)
        self.view.addSubview(happyButton)
        self.view.addSubview(sadButton)
        self.view.backgroundColor = .systemBlue
        self.title = "SecondVC"
    }
    

    @objc func happyButtonTapped(_ sender: UIButton) {
        // 버튼을 누르면 "데이터 happy를 갖고 updateImage 이미지를 동작해"라고 대리자에게 지시
        self.delegate?.updateImage(self, emotion: "happy")
        self.navigationController?.popViewController(animated: true)
    }
    
    @objc func sadButtonTapped(_ sender: UIButton) {
        // 버튼을 누르면 "데이터 sad를 갖고 updateImage 이미지를 동작해"라고 대리자에게 지시
        self.delegate?.updateImage(self, emotion: "sad")
        self.navigationController?.popViewController(animated: true)
    }
}

 

 

부록 (데이터전달 시)

보통 되돌아오는 방향(SecondVC -> FirstVC) 일 경우

Delegate pattern을 사용합니다.

 

세그 방향(FirstVC -> SecondVC) 일 경우

모듈화에는 좋지 않지만 프로퍼티를 통한 직접 접근이 훨씬 간결하고 쉽다고 합니다.

MVVM패턴으로 전달이 가능하다고 하긴 하네요.

 

 

부족한 설명이지만, 조금은 이해 가셨나요?

틀린 내용이 있다면 언제든지 지적해 주시면 감사히 받겠습니다. 🫠
읽어주셔서 감사합니다 😃

 

 

참고

 

Delegate Pattern In Swift

A design pattern that enables a class or structure to hand off (or delegate) some of its responsibilities to an instance of another type

medium.com

 

[iOS] Delegate 패턴을 이해해보자

Delegate v.(권한업무 등을) 위임하다

velog.io

 

[Swift] ViewController간 데이터 주고받기 - Delegate pattern

ViewController간 데이터를 주고받는 방법은 6가지가 있다. 1. 직접 프로퍼티에 접근 2. 함수를 통한 접근 3. Segue 4. Delegate 5. Closure 6. NotificationCenter 그 중 1, 4번을 간단한 예제를 통해 정리해보려고 한

weekoding.tistory.com

 

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!