Pangu
Pangu

Reputation: 3819

How to replace protocol with DelegateProxy from RxSwift?

I'm starting to realize the benefits of using RxSwift and making an attempt to incorporate it into my code, but I'm not sure I'm understanding the usage of DelegateProxy.

Currently in my code I have the following:

struct SampleModel: Decodable
{
    var first_name: String
    var last_name: String

    init(first_name: String, last_name: String)
    {
        self.first_name = first_name
        self.last_name = last_name
    }
}

class ViewModel
{
    var model: SampleModel

    public var fullName: String {
        return model.first_name + model.last_name
    }

    init(model: SampleModel)
    {
        self.model = model
    }
}

I am using a protocol and delegate like so:

protocol NetworkDelegate
{
    func dataReceived(data: [Decodable]?, error: Error?)
}

class Network: NSObject
{
    var databaseLink = String()

    var networkDelegate: NetworkDelegate!

    init(databaseLink: String)
    {
        super.init()

        self.databaseLink = databaseLink
    }

    func getDatabaseData<T: Decodable>(model: T.Type)
    {
        guard let url = URL(string: databaseURL) else {
            return
        }

        request(url).responseJSON { (json) in
            switch json.result
            {
                case .success:
                    guard let data = json.data else {
                        return
                    }

                    do
                    {
                        let models = try JSONDecoder().decode([T].self,
                                                              from: data)

                        self.networkDelegate.dataReceived(data: models,
                                                          error: nil)
                    }
                    catch let jsonErr
                    {
                        print(jsonErr)
                    }

                case .failure(let error):
                    self.networkDelegate.dataReceived(data: nil,
                                                      error: error)
            }
        }
    }
}

extension ViewController: NetworkDelegate
{
    func dataReceived(data: [Decodable]?, error: Error?)
    {
        guard let models = data else {
            return
        }

        for model in models
        {
            guard let myModel = model as? SampleModel else {
                return
            }

            viewModels.append( ViewModel(model: myModel) )
        }
    }
}

var viewModels = [ViewModel]()
weak var networkDelegate: NetworkDelegate?

override func viewDidLoad()
{
    super.viewDidLoad()

    let network = Network(databaseLink: "some url")
    network.networkDelegate = self
    network.getDatabaseData(model: SampleModel.self)
}

On researching this topic RxSwift, I've found the following:

  1. Is this the best way to convert Swift protocol to RxDelegateProxy?
  2. Cannot receive event with custom DelegateProxy and Protocol

Using it as reference I created some skeleton code:

import RxSwift
import RxCocoa

protocol NetworkDelegate: class
{
    func dataReceived(data: [Decodable]?, error: Error?)
}

class NetworkDeleteProxy: DelegateProxy<ViewController, NetworkDelegate>, DelegateProxyType
{
    public static func registerKnownImplementations()
    {
        <#code#>
    }

    public static func currentDelegate(for object: ViewController) -> NetworkDelegate?
    {
        return object.networkDelegate
    }

    public static func setCurrentDelegate(_ delegate: NetworkDelegate?, to object: ViewController)
    {
        <#code#>
    }
}

However, I'm a bit confused afterwards on how to implement the rest of the functionality and use like the original way:

let network = Network(databaseLink: "some url")
network.networkDelegate = self
network.getDatabaseData(model: SampleModel.self)

How can I achieve this? Thanks.

Upvotes: 1

Views: 1657

Answers (1)

Val&#233;rian
Val&#233;rian

Reputation: 1118

DelegateProxy should rather be used when integrating with Cocoa APIs that rely on delegates.

If you are going to go the Rx way, think of everything as a stream of events.

Let's see how we could convert your code to an observable:

func getDatabaseData<T: Decodable>() -> Observable<[T]>
{
    guard let url = URL(string: databaseURL) else {
        return .error(NetworkServiceErrors.missingDatabaseURL)
    }

    return Observable.create { observer in
        self.request(url).responseJSON { (json) in
            switch json.result {
            case .success:
                guard let data = json.data else {
                    return observer.onError(NetworkServiceErrors.noData)
                }

                do
                {
                    let models = try JSONDecoder().decode([T].self,
                                                          from: data)

                    observer.onNext(models)
                }
                catch let jsonErr
                {
                    observer.onError(NetworkServiceErrors.parsingError(jsonErr))
                }

            case .failure(let error):
                observer.onError(NetworkServiceErrors.someError(error))
            }
            observer.onCompleted()

            return Disposables.create { /* usually some code to cancel the request */ }
        }
    }
}

Let's now build a ViewModel using your network layer:

class MyListViewModel {
    // inputs
    let loadData: AnyObserver<Void>

    // outputs
    let myModels: Driver<[SampleModel]>

    private let network = Network(databaseLink: "some url")

    init() {
        let loadData = PublishRelay<Void>()
        self.loadData = loadData.asObserver()

        self.myModels = loadData.flatMapLatest { _ -> Driver<[SampleModel]>
            return network.getDatabaseData().asDriver(onErrorJustReturn: [])
        }
    }
}

Your ViewController:

let viewModel = MyListViewModel()
let disposeBag = DisposeBag()

func viewDidLoad() {
    super.viewDidLoad()

    bindViewModel()
}

func viewWillAppear() {
    super.viewWillAppear()

    // trigger loading of your data
    viewModel.loadData.onNext(())
}

func bindViewModel() {
    // some button that reloads data
    button.rx.tap.subscribe(viewModel.loadData).disposed(by: disposeBag)

    // will log all events but you should actually use it to drive the content of a tableview for example
    viewModel.myModels.debug().drive().disposed(by: disposeBag)
}

This is just a basic example and I haven't tested the code but it should give you an idea of what you can do.

You can look at Moya (a network lib) and RxMoya to see a some more advanced implementation or for inspiration.

Upvotes: 2

Related Questions