How to convert Delegate to Observable RxSwift?
Asked Answered
A

2

6

I have delegate methods, which I need to wrap by Delegate Proxy in RxSwift. I have done it using Bond and Reactive, but here, in RxSwift, I am not able to find the proper way to convert it.

Follow is Protocols

    import UIKit

/**
 A protocol for the delegate of a `DetailInputTextField`.
 */

    @objc
    public protocol CardInfoTextFieldDelegate {

        /**
         Called whenever valid information was entered into `textField`.

         - parameter textField:         The text field whose information was updated and is valid.
         - parameter didEnterValidInfo: The valid information that was entered into `textField`.
         */
        func textField(_ textField: UITextField, didEnterValidInfo: String)

        /**
         Called whenever partially valid information was entered into `textField`.

         - parameter textField:                  The text field whose information was updated and is partially valid.
         - parameter didEnterPartiallyValidInfo: The partially valid information that was entered.
         */
        func textField(_ textField: UITextField, didEnterPartiallyValidInfo: String)

        /**
         Called whenever more text was entered into `textField` than necessary. This can be used to provide this overflow as text in the next text field in the responder chain.

         - parameter textField:      The text field which received more information than required.
         - parameter overFlowDigits: The overflow of text which does not fit into `textField` and might be entered into the next receiver in the responder chain.
         */
        func textField(_ textField: UITextField, didEnterOverflowInfo overFlowDigits: String)
    }

What I did earlier is

import Foundation
import Bond
import Caishen


extension DetailInputTextField {
    var bnd_cardInfoDelegate: ProtocolProxy {
        return protocolProxy(for: CardInfoTextFieldDelegate.self, setter: NSSelectorFromString("setCardInfoTextFieldDelegate:"))
    }

    var bnd_didEnterValidInfo: StreamSignal<NSString> {
        return bnd_cardInfoDelegate.signal(for: #selector(CardInfoTextFieldDelegate.textField(_:didEnterValidInfo:)))
        { (s: PublishSignal<NSString>, _: UITextField, info: NSString) in
            s.next(info)
        }
    }

    var bnd_didEnterPartiallyValidInfo: StreamSignal<NSString> {
        return bnd_cardInfoDelegate.signal(for: #selector(CardInfoTextFieldDelegate.textField(_:didEnterPartiallyValidInfo:)))
        { (s: PublishSignal<NSString>, _: UITextField, info: NSString) in
            s.next(info)
        }
    }

    var bnd_didEnterOverflowInfo: StreamSignal<NSString> {
        return bnd_cardInfoDelegate.signal(for: #selector(CardInfoTextFieldDelegate.textField(_:didEnterOverflowInfo:)))
        { (s: PublishSignal<NSString>, _: UITextField, info: NSString) in
            s.next(info)
        }
    }
}

How can I do same exercise in RxSwift. I tried DelegateProxy but its unclear how it properly wrap it.

Agential answered 13/8, 2018 at 6:13 Comment(3)
If you're using RxSwift (observation) why do you want to keep on using delegation at all?Shelton
So, where delegate methods of some class with be called or assigned?Agential
if you observer Q, there are four delegates, and need them to be used, in main controller, by wrapping, but how we can do same here in Rx?Agential
T
19

Due to the popularity of this answer, I have written an article about it: Convert a Swift Delegate to RxSwift Observables

I believe this is the official way of converting a delegate into RxObservables:

class CardInfoTextField: NSObject {
    weak var delegate: CardInfoTextFieldDelegate? = nil
}

@objc
protocol CardInfoTextFieldDelegate {
    @objc optional func textField(_ textField: CardInfoTextField, didEnterValidInfo: String)
    @objc optional func textField(_ textField: CardInfoTextField, didEnterPartiallyValidInfo: String)
    @objc optional func textField(_ textField: CardInfoTextField, didEnterOverflowInfo overFlowDigits: String)
}

extension CardInfoTextField: HasDelegate {
    public typealias Delegate = CardInfoTextFieldDelegate
}

class CardInfoTextFieldDelegateProxy
    : DelegateProxy<CardInfoTextField, CardInfoTextFieldDelegate>
    , DelegateProxyType
, CardInfoTextFieldDelegate {

    //#MARK: DelegateProxy
    init(parentObject: CardInfoTextField) {
        super.init(parentObject: parentObject, delegateProxy: CardInfoTextFieldDelegateProxy.self)
    }

    public static func registerKnownImplementations() {
        self.register { CardInfoTextFieldDelegateProxy(parentObject: $0) }
    }
}

extension Reactive where Base: CardInfoTextField {
    var delegate: CardInfoTextFieldDelegateProxy {
        return CardInfoTextFieldDelegateProxy.proxy(for: base)
    }

    var didEnterValidInfo: Observable<String> {
        return delegate.methodInvoked(#selector(CardInfoTextFieldDelegate.textField(_:didEnterValidInfo:)))
            .map { $0[1] as! String }
    }

    var didEnterPartiallyValidInfo: Observable<String> {
        return delegate.methodInvoked(#selector(CardInfoTextFieldDelegate.textField(_:didEnterPartiallyValidInfo:)))
            .map { $0[1] as! String }
    }

    var didEnterOverflowInfo: Observable<String> {
        return delegate.methodInvoked(#selector(CardInfoTextFieldDelegate.textField(_:didEnterOverflowInfo:)))
            .map { $0[1] as! String }
    }
}

Once you have the above, you should be able to:

let validInfo: Observable<String> = myCardInfoTextField.rx.didEnterValidInfo
Tragedy answered 13/8, 2018 at 13:58 Comment(6)
Bloody hell. The key to this is to define the delegate method as optional, it also relies on objective-c run time so that means you can't use enums in your method.Ebon
Daniel, any idea how to implement this without the delegate protocol being exposed to Objective-C? I'm trying to do this to a third-party library that didn't add @objc to the protocolTroutman
Sure. If the delegate protocol isn't exposed to Objective-C then it can't be Optional. In that case, you have to implement all the functions in your delegate proxy and connect each of them to their own Subject. In the Reactive extension, you return the Subject as an Observable. I can provide an example if you post it as a question but maybe the above is enough for you to go on.Tragedy
@Troutman I have written an article detailing exactly what to do: Convert a Swift Delegate to RxSwift ObservablesTragedy
That isn't the problem, because the delegate protocol isn't exposed to Objective-C I can't use Selector (or #selector), which means I can't use delegate.methodInvoked either. But I made it work by subclassing the lib class and creating the reactive fields in there. Thanks anywayTroutman
As I point out in the article. If the delegate protocol isn't exposed to Objective-C then you need to implement the functions in the delegate proxy and forward them to the Reactive function through a Subject.Tragedy
S
0

EDIT

I've removed my previous code and adjusted it to your desired solution. In order to "wrap" delegates of various classes (mostly UI) into observables you can use DelegateProxy class which is a part of RxCocoa framework.

Assuming your DetailInputTextField class has a property delegate of type DetailInputTextFieldDelegate here is the example:

First the custom proxy:

import RxSwift
import RxCocoa

extension DetailInputTextField: HasDelegate {
    public typealias Delegate = DetailInputTextFieldDelegate
}

open class DetailInputTextFieldDelegateProxy
    : DelegateProxy<DetailInputTextField, DetailInputTextFieldDelegate>
    , DelegateProxyType
    , DetailInputTextFieldDelegate {

    /// Typed parent object.
    public weak private(set) var textField: DetailInputTextField?

    /// - parameter webView: Parent object for delegate proxy.
    public init(textField: ParentObject) {
        self.textField = textField
        super.init(parentObject: textField, delegateProxy: DetailInputTextFieldDelegateProxy.self)
    }

    // Register known implementations
    public static func registerKnownImplementations() {
        self.register { DetailInputTextFieldDelegateProxy(textField: $0) }
    }
}

Then you need to extend Reactive where you can add all the observables corresponding to delegate methods:

extension Reactive where Base: DetailInputTextField {
    public var delegate: DelegateProxy<DetailInputTextField, DetailInputTextFieldDelegate> {
        return DetailInputTextFieldDelegateProxy.proxy(for: base)
    }

    public var didEnterValidInfo: Observable<(UITextField,String)> {
    return delegate
        .methodInvoked(#selector(DetailInputTextFieldDelegate.textField(_:didEnterValidInfo:)))
        .map { params in
            // Parameters is an array, be sure you cast them correctly
            return (params[0] as! UITextField, params[1] as! String)
        }
    }
}

When you have the "wrapper" implemented, you can call it like this:

let textField = DetailInputTextField()
textField.rx.didEnterValidInfo
    .asObservable()
    .subscribe(onNext: { (textField: UITextField, string: String) in
        print("Test \(string)")
    })
    .disposed(by: disposeBag)

I hope it answers your question.

Shelton answered 13/8, 2018 at 9:44 Comment(4)
This DetailInputTextField is not mine class. Its declared already somewhere in library, so won't it effect? secondly what is delegate, its not identifying itAgential
If DetailInputTextField is not your class then you should not add code to it. I will update my answer.Shelton
Sure, I am waiting, I made something like extension Reactive where Base: DetailInputTextField { public var dataSource: DelegateProxy<DetailInputTextField, CardInfoTextFieldDelegate> { return RxDetailInputTextFieldProxy.proxy(for: base) } } /// For more information take a look at DelegateProxyType. open class RxDetailInputTextFieldProxy : DelegateProxy<DetailInputTextField, CardInfoTextFieldDelegate> , DelegateProxyType , CardInfoTextFieldDelegate { But it seems unclear yet.Agential
Please check my updated answer. Initially I had a another way in mind, but this was an interesting exercise for me too.Shelton

© 2022 - 2024 — McMap. All rights reserved.