DelegateProxy를 사용해서 델리게이트 패턴을 사용하는 라이브러리를 Rx로 둔갑시켜보자

김하민·2025년 2월 13일
0
post-thumbnail

네 안녕하십니까.

참으로 굉장히 오랜만이네요.

제가 뭘 하고 있었냐면,

틴더식 카드 스와이프

를 구현한 오픈소스 라이브러리를 갖다가 프로젝트에 적용시키고 있었단 말이죠.

뭐 대충 이런 겁니다.

Shuffle이라는 오픈소스 라이브러리인데요, 델리게이트 패턴을 사용한 녀석이라, 처음에 적용하는 데에는 큰 무리가 없었습니다.

UICollectionView와 상당히 흡사한 Delegate와 Datasource 패턴을 채용했기 때문이죠.

근데... RxSwift를 사용해서 리팩터링을 진행하던 과정 중에 이 친구를 어떻게 Rx로 사용할 것인가?

라는 의문이 들었고, 잘 찾아보니 답이 나왔습니다.

답은 DelegateProxy다.

그럼 작성해줍시다.

extension SwipeCardStack: HasDelegate {
    public typealias Delegate = SwipeCardStackDelegate
}

일단 위의 코드로 Shuffle의 SwipeCardStack의 스와이프 액션 등을 처리해주는 Delegate가 있다고 명시해주고,


class RxSwipeCardStackDelegateProxy
: DelegateProxy<SwipeCardStack, SwipeCardStackDelegate>
, DelegateProxyType
, SwipeCardStackDelegate {
    
    weak private(set) var cardStack: SwipeCardStack?
    
    init(cardStack: SwipeCardStack) {
        self.cardStack = cardStack
        super.init(parentObject: cardStack, delegateProxy: RxSwipeCardStackDelegateProxy.self)
    }
    
    static func registerKnownImplementations() {
        self.register { RxSwipeCardStackDelegateProxy(cardStack: $0) }
    }
}

위 코드처럼 DelegateProxy를 작성해줍니다.

클래스명은 RxCocoa 내부에서 Rx+(원래 클래스 이름)+DelegateProxy로 다 지어놨길래 마음의 평안을 위해 통일해줍니다.

extension Reactive where Base: SwipeCardStack {
    var delegate: DelegateProxy<SwipeCardStack, SwipeCardStackDelegate> {
        return RxSwipeCardStackDelegateProxy.proxy(for: base)
    }
    
    // (기존 delegate의 메서드들을 넣을 곳)
    
}

그 담엔 이렇게 Reactive의 extension으로 작성을 해주고요,
아래에 기존 delegate의 메서드들을 넣어줍시다.

// 기존 delegate의 메서드들
@objc
optional func cardStack(_ cardStack: SwipeCardStack, didSelectCardAt index: Int)

@objc
optional func cardStack(_ cardStack: SwipeCardStack, didSwipeCardAt index: Int, with direction: SwipeDirection)

@objc
optional func cardStack(_ cardStack: SwipeCardStack, didUndoCardAt index: Int, from direction: SwipeDirection)

그러니까 요놈들을

// 넣어줌
extension Reactive where Base: SwipeCardStack {
    var delegate: DelegateProxy<SwipeCardStack, SwipeCardStackDelegate> {
        return RxSwipeCardStackDelegateProxy.proxy(for: base)
    }
    
    var didSelectCardAt: Observable<Int> {
        return delegate
            .methodInvoked(#selector(SwipeCardStackDelegate.cardStack(_:didSelectCardAt:)))
            .map { num in
                return num[1] as? Int ?? 0
            }
    }
    
    var didSwipeAllCards: Observable<Void> {
        return delegate
            .methodInvoked(#selector(SwipeCardStackDelegate.didSwipeAllCards(_:)))
            .map { _ in () }
    }
    
    var didSwipeCardAt: Observable<(Int, SwipeDirection)> {
        return delegate
            .methodInvoked(#selector(SwipeCardStackDelegate.cardStack(_:didSwipeCardAt:with:)))
            .map { args in
                return (
                    index: args[1] as? Int ?? 0,
                    direction: args[2] as! SwipeDirection
                )
            }
    }
}

이렇게 넣으면 된다...!

그럼 기존의 델리게이트에서 메서드가 호출되는 타이밍에 .methodInvoked가 호출되어 Observable을 반환하게 됩니다.

그럼 View와 ViewModel에 적용해볼까요?

아니 왜 또 안되는거야

근데 안되네요. 기존의 Delegate 패턴을 사용했을때 잘 뜨던 카드 스택들이 래핑하고 나니 안됩니다...

왜 안되는지... 뭐가 잘못되었는지 한참을 생각하고 코드를 읽어보다가...

설마... 하는 생각이 들어 다른 뷰의 프로퍼티로 분리해뒀던 것 때문에↓

프로젝트/
├─ class ItemSearchView/
│  ├─ 이것저것 뷰 컴포넌트들
├─ class ItemCardsView/
│  ├─ cardStack = SwipeCardStack()

그런 건가 싶어 합쳐주었고↓


프로젝트/
├─ class ItemSearchView/
│  ├─ 이것저것 뷰 컴포넌트들
│  ├─ cardStack = SwipeCardStack()
├─ class ItemCardsView -> delete/

그러니까 됩니다 또...?

사소한 소란이 있긴 했지만

여튼 그렇게 해결이 되었습니다.

예에에에에소리질러~

(실제로 몇시간동안 끙끙댄 후에 해결하고 소리지름 리얼참트루스토리)

어 근데 그럼 Datasource는요?

그게 말입니다....

Shuffle이 가지고있는 Delegate와 Datasource가 UICollectionView와 유사하게 되어 있어,

RxCocoa의 UICollectionView+Rx와 동일한 구조로 작업하면 되지 않을까... 하고 코드를 읽고 적용해보려 했거든요?

근데 안돼요.

계속 에러가 뜨더라고요...

정확히는:

'subscribeProxyDataSource(ofObject:dataSource:retainDataSource:binding:)' requires that 'any RxSwipeCardStackDataSourceProxy.Delegate' (aka 'any SwipeCardStackDataSource') be a class type

이렇게 뜨는데,
대충 해석하자면:

응 니가 준거 클래스 타입 아니야 돌아가~

라네요? 그래서 RxSwipeCardStackDataSourceProxy와 관련된 클래스들을 찾아봤는데 죄다 클래스였고,

SwipeCardStackDataSource도 확인해봤는데 AnyObject였단 말입니다..

public protocol SwipeCardStackDataSource: AnyObject {
  func cardStack(_ cardStack: SwipeCardStack, cardForIndexAt index: Int) -> SwipeCard
  func numberOfCards(in cardStack: SwipeCardStack) -> Int
}

그래서 일단은 데이터소스는 기존 형식을 사용하는 중인데...
이거 왜 이러는지 아시는 분은 댓글로 알려주시면 감사하겠습니다...

0개의 댓글