apple/UIKit & ReactiveX

[iOS] RxDelegateProxy 1편 (feat. example)

lgvv 2022. 1. 12. 15:12

✅ 이번 시간에는 RxDelegateProxy에 대해서 알아볼 예정이야.

 

여.기.부.분.은!! 내가 엄청난 삽질(?)을 하면서 내부코드까지 다 뜯어보면서 알게된 부분이라 진짜 스스로...

근데 이론 부분은 확실히 알겠는데, "그래서 실제 프로젝트에 어떻게 적용할건데?" 라고 물으신다면 🥲 

MVVM 패턴을 고려해서 적용하는 일이 생각보다 쉬운 일은 아니야 ㅠㅠ

 

이번 포스팅에서는 정말 많은 글들을 참고했었는데, 제일 아래에 (참고)를 남겨 두도록 할게

 

✅ 목차

1️⃣ Protocol과 Delegate

2️⃣ RxDelegateProxy GuideLine

3️⃣ RxDelegateProxy example -> 하단에 참고링크 걸어둠!

 이 부분은 기존 파일에 조금 더 rx스럽게 개편해 보았음!

🟠 개편한 코드

RxSwiftDelegateExample.zip
0.69MB

✅ Protocol과 Delegate

우선 RxDelegateProxy를 잘 이해하기 위해서는 이 부분을 온전하게 이해하고 있을 필요가 있어.

 

우리 tableView를 예시로 생각해보자.

class VC1: UIViewController, UITableViewDelegate {
    var tableViewVC1: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableViewVC1.delegate = self // [CASE #1] self(VC1)에 있는 delegate 기능을 tableViewVC1이 사용하겠다.
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { }
}

class VC2: UIViewController {
    var tableViewVC2: UITableView!
    let vc1 = VC1()
    
    override func viewDidLoad() {
         super.viewDidLoad()
         tableViewVC2.delegate = vc1 // [CASE #2] vc1에 있는 delegate 기능(함수)를 tableViewVC2에서 사용하겠다.
         tableViewVC2.dataSource = vc1
    }
}

위에 코드를 보면 delegate에 대해서 직관적으로 이해할 수 있을거야.

VC2에서 Delegate와 DataSource가 구현되어 있지 않음에도 error이 발생하지 않는다는 사실!

 

🟠 그렇다면 protocol은 어떤 상관이 있는걸까?

UITableViewDataSource함수가 내부에 구현되어 있는 모습

자 이제 알겠지? DataSource가 결국에는 protocol이었다는 사실!! 

마찬가지로 Delegate도 내부를 열어보면 protocol로 되어있어

 

🟠  UITableViewDelegate에서는 필수로 구현해야하는 메소드가 없는데,

UITableViewDataSource에서는 필수로 구현해야 하는 메소드가 있는 이유는 뭐야?

 

그 이유는 protocol안에 어떻게 구현되어 있는지를 봐야하는데 다시 UITableViewDataSource의 내부를 보자면

UITableViewDataSource

자세히 관찰하면 optional이라는 키워드를 확인할 수 있는데, optional이라는 키워드가 붙으면 구현되지 않아도 되고 그렇지 않은 함수는 채택했을 때, 필수로 구현해주어야 해.

 

근데 여기서 하나 더 알아가야할 것이 있는데, Swift에서는 함수를 optional로 만들 수 없도록 되어있어. 

optional은 objc함수일 경우에만 가능한데, 근데 저게 어떻게 되는걸까?

그 이유는 바로 NSObjectProtocol를 상속받기 때문이지.

쉽게 말해서 objc로 사용할 수 있도록 만들어준다고 생각하면 좋아!

지금 내가 말한 NSObjectProtocol로 인해서.. Delegateproxy를 사용하는데 엄청 애를 먹게 되는데,,, 그건 포스팅을 쭉 따라가보면 나와

 

✅ RxDelegateProxy GuideLine

 

🟠 RxInputViewControllerProxy.swift

import UIKit
import RxSwift
import RxCocoa

// rx로 프로토콜을 만들때 obj-c를 사용하는 이유는 rx_sendString 부분에서 #selector를 사용하는데 이게 obj-c로만 사용할 수 있기 때문이다.
@objc protocol InputViewControllerDelegate: class {
    @objc optional func sendString(string: String)
}

// 이건 하나의 형식임으로 이에 맞게 메소드를 넣어줘
class RxInputViewControllerProxy: DelegateProxy<InputViewController, InputViewControllerDelegate>, DelegateProxyType, InputViewControllerDelegate, UITableViewDelegate {
    static func registerKnownImplementations() {
        self.register { (inputViewController) -> RxInputViewControllerProxy in
            RxInputViewControllerProxy(parentObject: inputViewController, delegateProxy: self)
        }
    }

    static func currentDelegate(for object: InputViewController) -> InputViewControllerDelegate? {
        return object.delegate
    }

    static func setCurrentDelegate(_ delegate: InputViewControllerDelegate?, to object: InputViewController) {
        object.delegate = delegate
    }
}

// 하하.. 이 부분을 하기 위해서는 Reactive Extension을 또 알아야하네 ㅠㅠ 하단에 포스팅 링크를 걸어둘게요!
extension Reactive where Base: InputViewController {
    var rx_deleagte: DelegateProxy<InputViewController, InputViewControllerDelegate> {
        return RxInputViewControllerProxy.proxy(for: self.base)
    }

    var rx_sendString: Observable<String?> {
        return rx_deleagte.sentMessage(#selector(InputViewControllerDelegate.sendString(string:))).map { arr in arr[0] as? String }
    }
}

위의 코드는 proxy로 만드는 방법에 대한 가이드라인이니까 보이는 대로 만들면 돼

 

 

✅  RxDelegateProxy example

(단, 위의 가이드라인에서 제시한 코드도 example안에 있어야 합니다.)

스토리보드

🟠 ViewController.swift

//
//  ViewController.swift
//  RxSwiftDelegateExample
//
//  Created by lgvv0908 on 2022/01/12
//

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController, InputViewControllerDelegate {
    
    @IBOutlet weak var inputLabel: UILabel!
    @IBOutlet weak var openButton: UIButton!
    @IBOutlet weak var openSubjectButton: UIButton!
    
    var disposeBag = DisposeBag()
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let storyBoard = UIStoryboard(name: "Main", bundle: .main)
        let inputVC = storyBoard.instantiateViewController(withIdentifier: "InputViewController") as? InputViewController
        
        // InputViewController에 rx로 delegate해서 사용하겠다
        inputVC?.rx.rx_deleagte.setForwardToDelegate(self, retainDelegate: false)
        
        inputVC?.rx.rx_sendString
            .bind(to: inputLabel.rx.text)
            .disposed(by: disposeBag)
        
        openButton.rx.tap // 파란색버튼 클릭시
            .debug("openButton")
            .throttle(.seconds(1), scheduler: MainScheduler.instance) // 버튼 클릭 후 1초동안 동일 이벤트 방출 안함
            .subscribe(onNext: { [weak self] _ in // tap하면 controlEvent가 발생되는데, 이를 막음
                if let inputVC = inputVC {
                    self?.present(inputVC, animated: true, completion: nil) // 화면 전환
                }
            })
            .disposed(by: disposeBag)
        
        
        openSubjectButton.rx.tap
            .bind { [weak self] in
                guard let inputVC = inputVC else { return } // 위에는 if let을 사용했는데 여기는 가드문 사용
                self?.present(inputVC, animated: true, completion: nil) // 화면 전환
            }
            .disposed(by: disposeBag)
        
        
        inputVC!.inputStringSubject
            .bind(to: inputLabel.rx.text)
            .disposed(by: disposeBag)
        
    }
}

 

🟠 InputViewController.swift

//
//  InputViewController.swift
//  RxSwiftDelegateExample
//
//  Created by lgvv0908 on 2022/01/12
//  Copyright © 2020 swieeft. All rights reserved.
//

import UIKit
import RxSwift
import RxCocoa

class InputViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var confirmButton: UIButton!
    @IBOutlet weak var confirmSubjectButton: UIButton!

    weak var delegate: InputViewControllerDelegate?
    
    var disposeBag = DisposeBag()

    var inputStringSubject = PublishSubject<String>() // 여기서 서브젝트를 갖고 있음
    var inputString: Observable<String> { // 프로퍼티인데, inputStringSubject값을 Observable<String>값으로 받고 있음
        return inputStringSubject.asObservable()
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        confirmButton.rx.tap
            .throttle(.seconds(1), scheduler: MainScheduler.asyncInstance)
            .subscribe(onNext: { [weak self] _ in
                guard let string = self?.textField.text else { return }

                self?.dismiss(animated: true, completion: {
                    self?.delegate?.sendString?(string: string) // 프로토콜로 데이터 전달함
                })
            })
            .disposed(by: disposeBag)

        confirmSubjectButton.rx.tap
            .debug("confirmSubjectButton")
            .throttle(.seconds(1), scheduler: MainScheduler.asyncInstance)
            .subscribe(onNext: { [weak self] _ in
                guard let string = self?.textField.text else { return }

              // self?.inputStringSubject.onNext(string) // 서브젝트로 방출함

                self?.dismiss(animated: true, completion: {
                    self?.inputStringSubject.onNext(string) // 서브젝트로 방출함
                    print("string -> \(string)")
//                    self?.inputStringSubject.onCompleted()
                })
            })
            .disposed(by: disposeBag)
        

        textField.rx.controlEvent(.editingDidEndOnExit)
            .debug("editingDidEndOnExit")
            .subscribe(onNext: { [weak self] _ in
                self?.view.endEditing(true)
            })
            .disposed(by: disposeBag)
    }
}

 

코드는 쓱 보면 이해할 수 있을거라고 믿어!

 

(참고)

https://danielt1263.medium.com/convert-a-swift-delegate-to-rxswift-observables-f52afe77f8d6

 

Convert a Swift Delegate to RxSwift Observables

RxSwift has a powerful delegate proxy system, but implementing your own proxy is easy. Just follow this simple recipe.

danielt1263.medium.com

 

https://swieeft.github.io/2020/02/20/RxSwiftDelegate1.html

 

RxSwift에서 Delegate 패턴 사용하기 (1/2) - 뀔뀔(swieeft)의 개발새발기

현재 다니고 있는 회사 프로젝트에서 RxSwift를 사용하고 있습니다. 근데 Delegate를 사용해도 될 것 같은 부분에 전임자분은 Delegate 대신에 Notification으로 코드를 작성해놓았더군요… 무엇 때문에?

swieeft.github.io