카테고리 없음

델리게이트 패턴으로 화면 전환하기

밤새는 탐험가89 2024. 11. 17. 08:08

https://explorer89.tistory.com/226

 

UISheetPresentation을 통해 수정, 삭제, 닫기 버튼 기능 구현

UISheetPresentationController를 사용하면 하단에서 올라오는 시트 스타일의 모달을 손쉽게 구현할 수 있습니다. UISheetPresentationController는 iOS 15 이상에서 사용할 수 있는 API로, detents를 통해 시트의 높이

explorer89.tistory.com

 

 

전체 흐름 요약

  1. ProfileViewController에서 UserFeedViewController로 이동
    pushViewController로 화면 전환.
  2. UserFeedViewController에서 ProfileFeedEditViewController 띄우기
    present로 모달 팝업 표시.
  3. ProfileFeedEditViewController에서 삭제 버튼을 눌렀을 때
    Delegate를 통해 UserFeedViewController에 삭제 완료를 알림.
  4. UserFeedViewController에서 RootViewController로 이동
    popToRootViewController 또는 popToViewController를 호출하여 ProfileViewController로 돌아감.

 

1. delegate 프로토콜 정의 

// 삭제 작업을 완료한 후 ProfileViewController로 돌아가도록 알리는 Delegate를 정의
protocol ProfileFeedEditDelegate: AnyObject {
    func didDeleteFeed()
}

 

 

2. ProfileFeedEditViewController에서 Delegate 호출 Delegate를 사용하여 삭제 작업을 알리고, 두 화면을 모두 닫아 ProfileViewController로 돌아갑니다.

class ProfileFeedEditViewController: UIViewController {
    
    // MARK: - Variables    
    weak var delegate: ProfileFeedEditDelegate?
    
    // MARK: - UI Components
    ...
    
    // MARK: - Initializations
    override func viewDidLoad() {
        ...
        didTappedButtons()
    }
    ...
  
    
    // MARK: - Functions
    func didTappedButtons() {
        closeButton.addTarget(self, action: #selector(didCalledCloseButton), for: .touchUpInside)
        deletButton.addTarget(self, action: #selector(didCalledDeleteButton), for: .touchUpInside)
    }
    
    
    // MARK: - Actions    
    @objc func didCalledDeleteButton() {
        print("데이터 삭제 완료")
        feedDataManager.deleteFeedItem(feedID: userFeed!.feedID)
        delegate?.didDeleteFeed()
        dismiss(animated: true)
    }
}

 

3. UserFeedViewController에서 Delegate 설정 ProfileFeedEditViewController가 닫힐 때 UserFeedViewController가 Delegate를 통해 상위 네비게이션 스택을 관리하도록 설정합니다.

extension UserFeedViewController: ProfileFeedEditDelegate {
    func didDeleteFeed() {
        // ProfileViewController로 돌아가기
        navigationController?.popViewController(animated: true)
    }
}

 

    @objc func didTappedSetupButton() {
        print("didTappedSetupButton() - called")
        
        let profileFeedEditVC = ProfileFeedEditViewController()
        profileFeedEditVC.userFeed = userFeed
        
        if let sheet = profileFeedEditVC.sheetPresentationController {
            // sheet.detents = [.medium()]
            
            // sheet 올라오는 높이 조절
            sheet.detents = [
                .custom { context in
                    return 200
                }
            ]
            sheet.prefersGrabberVisible = true
            sheet.prefersScrollingExpandsWhenScrolledToEdge = false
        }
        
        profileFeedEditVC.modalPresentationStyle = .pageSheet
        // 대리자 설정
        profileFeedEditVC.delegate = self
        present(profileFeedEditVC, animated: true)
    }

 

1. 주요 역할 비유

  • ProfileFeedEditViewController = 손님
    • 손님은 "요리사"에게 음식을 요청하는 역할입니다.
    • 즉, delegate.didDeleteFeed()를 호출하여 요청을 보냅니다.
  • UserFeedViewController = 요리사
    • 요리사는 손님의 요청(delegate.didDeleteFeed())을 받고, 손님이 원하는 대로 음식을 준비합니다.
    • 요청에 따라 요리사는 navigationController.popViewController를 호출하여 화면을 ProfileViewController로 이동하게 합니다.
  • delegate = 주문서
    • 손님(ProfileFeedEditViewController)이 요리사에게 요청을 전달하는 통신 수단입니다.
    • weak var delegate: ProfileFeedEditDelegate?는 손님이 요리사를 식별하기 위해 사용하는 "주문서"라고 볼 수 있습니다.

 

2. 과정 비유

이 관계를 요리사와 손님의 상호작용으로 풀어보면 다음과 같습니다:

 

손님이 레스토랑에 들어감:
UserFeedViewController가 ProfileFeedEditViewController를 present로 띄워 줍니다.

let profileFeedEditVC = ProfileFeedEditViewController()
profileFeedEditVC.delegate = self // 요리사(UserFeedViewController)가 주문서를 준비
present(profileFeedEditVC, animated: true)

 

손님이 음식을 주문:
ProfileFeedEditViewController에서 deleteAction 버튼을 눌러 delegate.didDeleteFeed()를 호출합니다.

@objc func deleteAction() {
    delegate?.didDeleteFeed() // 주문서를 통해 요청을 보냄
    dismiss(animated: true)   // 손님은 자리를 떠남
}

 

 

요리사가 주문서를 확인하고 요리를 준비:
UserFeedViewController는 ProfileFeedEditDelegate를 구현한 덕분에 요청(didDeleteFeed())을 받습니다. 요리사가 요청받은 대로 요리를 준비합니다.

extension UserFeedViewController: ProfileFeedEditDelegate {
    func didDeleteFeed() {
        navigationController?.popToRootViewController(animated: true) // 요리를 완성하고 ProfileViewController로 이동
    }
}

 

요리사가 음식을 준비하고 서빙:
요리사가 요청을 처리한 결과로 화면이 ProfileViewController로 전환됩니다.

 

 

비유에서 얻을 수 있는 델리게이트 패턴의 장점

  1. 유연성:
    요리사가 바뀌어도(즉, delegate의 구현 객체가 달라져도) 손님은 동일한 방식으로 요청을 전달할 수 있습니다.
  2. 재사용성:
    ProfileFeedEditViewController는 UserFeedViewController와 강하게 연결되지 않으므로, 다른 화면에서도 동일한 방식으로 동작할 수 있습니다.
  3. 명확한 역할 분리:
    손님(ProfileFeedEditViewController)은 요청만 하고, 요청에 대한 처리(요리)는 요리사(UserFeedViewController)가 담당합니다.

 

 

왜 주문서(Delegate)가 필요한가?

손님이 직접 요리사에게 요청하지 않고, 왜 "주문서"를 사용하는 걸까요?

  • 손님이 직접 요리사와 통신하면, 손님이 요리사에 강하게 의존하게 됩니다.
    • 예를 들어, ProfileFeedEditViewController가 UserFeedViewController를 직접 호출하면, 나중에 요리사가 바뀌면 손님의 코드도 수정해야 합니다.
  • 하지만 "주문서"(delegate)를 사용하면, 요리사가 바뀌더라도 손님의 코드는 변하지 않습니다.
    • 델리게이트 프로토콜을 구현하기만 하면, 새로운 요리사도 손님의 요청을 처리할 수 있습니다.

 

🔥 만약에 UserFeedViewController와 같이 직접적인 참조 관계가 없는 경우에는 위와 같은 기능을 델리게이트 패턴만으로는 안된다. 

 

왜 델리게이트 패턴만으로는 안 될까?

델리게이트 패턴은 1:1 관계에서 작동하며, 요청자(손님)와 응답자(요리사)가 서로 연결되어 있어야 합니다.

  • ProfileFeedEditViewController는 delegate를 통해 **현재 띄운 주체(UserFeedViewController)**에만 요청을 전달할 수 있습니다.
  • UserFeedViewController의 네비게이션 스택 위에 있지 않고, 독립적으로 존재하는 HomeViewController에는 직접 연결되지 않으므로 요청이 전달되지 않습니다.

 

이런 상황에서는 어떤 패턴을 써야 할까?

HomeViewController처럼 네비게이션 스택 상 위에 없거나 다른 뷰 컨트롤러에서도 요청을 받아야 한다면, 다음 중 하나를 사용해야 합니다:

  1. NotificationCenter
  2. Shared Data (싱글톤)
  3. Custom Closure (Callback)

 

🔥 NotificationCenter를 이용하여 화면 이동 (델리게아트 패턴 대체)

ProfileViewController에서 Notification 등록 먼저 ProfileViewController에서 특정 알림을 등록합니다. 이 알림을 받으면 현재 화면을 pop하여 이전 화면으로 돌아갈 수 있습니다.

import UIKit

class ProfileViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 삭제 완료 알림 등록
        NotificationCenter.default.addObserver(self, selector: #selector(handleDeleteNotification), name: NSNotification.Name("DeleteItemNotification"), object: nil)
    }

    @objc private func handleDeleteNotification() {
        // 삭제된 후 pop 동작 수행
        self.navigationController?.popViewController(animated: true)
    }

    deinit {
        // 메모리 누수를 방지하기 위해 Notification 해제
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name("DeleteItemNotification"), object: nil)
    }
}

 

 

ProfileFeedEditViewController에서 Notification 전송 ProfileFeedEditViewController에서 삭제 작업이 완료된 후 NotificationCenter를 통해 "DeleteItemNotification" 알림을 전송합니다. 이 알림을 ProfileViewController에서 수신하여 pop 동작을 수행하게 됩니다.

@objc private func deleteAction() {
    // 삭제 작업 수행 (예: 데이터 삭제)
    print("삭제 작업 실행")
    
    // 첫 번째 dismiss: 팝업을 닫음
    dismiss(animated: true) {
        // NotificationCenter를 통해 삭제 알림 전송
        NotificationCenter.default.post(name: NSNotification.Name("DeleteItemNotification"), object: nil)
    }
}

 

 

  • Notification 등록: ProfileViewController에서 "DeleteItemNotification"이라는 이름의 알림을 등록합니다. 이 알림을 받으면 handleDeleteNotification 메서드가 호출되어 ProfileFeedEditViewController로 이전 화면으로 돌아가게 됩니다.
  • Notification 전송: ProfileFeedEditViewController의 deleteAction 메서드에서 알림을 전송하여 ProfileViewController가 이 알림을 받고 화면을 pop하도록 합니다.

 

델리게이트 패턴과 노티피케이션의 주요 차이점

델리게이트 패턴 NotificationCenter
1:1 관계에서 사용됩니다. 1:다 관계에서 사용됩니다.
특정 객체(delegate)를 명시적으로 설정해야 작동합니다. 메시지를 전역적으로 전달하므로 설정 없이 동작합니다.
강한 객체 참조를 방지하려면 weak로 설정해야 합니다. 특정 객체 참조 없이 메시지가 전달됩니다.
의존 관계가 명확합니다. 의존 관계가 느슨합니다.

 

 

 

NotificationCenter와 Delegate의 사용 결정

NotificationCenter를 사용해야 할 때

  • 다수의 구독자가 메시지를 받아야 하는 경우.
  • ViewController의 강한 참조 또는 상태 관리가 복잡한 경우.

Delegate 패턴을 사용해야 할 때

  • 명확한 1:1 의존 관계가 필요한 경우.
  • 특정 ViewController 간의 상호작용이 필요한 경우 (의존 관계를 명확히 관리 가능).