简体   繁体   中英

How to handle SignalProducer with ReactiveSwift and Firebase asynchronous method calls?

I am working on an iOS App with Swift 3 using ReactiveSwift 1.1.1, the MVVM + Flow Coordinator pattern and Firebase as a backend. I only recently started to adapt to FRP and I am still trying to figure out how to integrate new functionalities into my existing code base.

For instance, my model uses a asynchronous method from Firebase to download thumbnails from the web and I want to provide a SignalProducer<Content, NoError> to subscribe from my ViewModel classes and observe, if thumbnails have been downloaded, which then updates the UI.

// field to be used from the view-models to observe
public let thumbnailContentSignalProducer = SignalProducer<Content, NoError> { (observer, disposable) in
    // TODO: send next content via completion below
} 

// thumbnail download method
public func findThumbnail(bucketId: String, contentId: String) {
    guard let userId = userService.getCurrentUserId() else {
        debugPring("Error id")
        return
    }

    let ref = self.storageThumbnail.reference()
    let contentRef = ref
        .child(userId)
        .child(bucketId)
        .child(FirebaseConstants.pathImages)
        .child("\(contentId).jpg")

    contentRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
        guard let data = data else {
            debugPrint("Error download")
            return
        }
        let content = Image(data: data)
        content.id = contentId
        content.userId = userId
        content.bucketId = bucketId

        // TODO: emit signal with content
        // How to send the content via the SignalProducer above?
    })
}

I have also tried something similar with Signal<Content, NoError> , whereas I used the Signal<Content, NoError>.pipe() method to receive a (observer, disposable) tuple and I saved the observer as a private global field to access it form the Firebase callback.

Questions:

Is this the right approach or am I missing something?

How do I emit the content object on completion?

UPDATE:

After some hours of pain, I found out how to design the SingalProducer to emit signals and to subscribe from the ViewModels.

Maybe the following code snippet will help also others:

// model protocol
import ReactiveSwift
import enum Result.NoError

public protocol ContentService {
    func findThumbnail(bucketId: String, contentId: String)
    var thumbnailContentProducer: SignalProducer<Content, NoError> { get }
}


// model implementation using firebase
import Firebase
import FirebaseStorage
import ReactiveSwift

public class FirebaseContentService: ContentService {

    // other fields, etc.
    // ...

    private var thumbnailContentObserver: Observer<Content, NoError>?
    private var thumbnailContentSignalProducer: SignalProducer<Content, NoError>?
    var thumbnailContentProducer: SignalProducer<Content, NoError> {
        return thumbnailContentSignalProducer!
    }

    init() {
        thumbnailContentSignalProducer = SignalProducer<Content, NoError> { (observer, disposable) in
            self.thumbnailContentObserver = observer
        }
    }

    func findThumbnail(bucketId: String, contentId: String) {
        guard let userId = userService.getCurrentUserId() else {
            // TODO handle error
            return
        }

        let ref = self.storageThumbnail.reference()
        let contentRef = ref
            .child(userId)
            .child(bucketId)
            .child(FirebaseConstants.pathImages)
            .child("\(contentId).jpg")

        contentRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
            guard let data = data else {
                // TODO handle error
                return
            }
            let content = Image(data: data)
            content.id = contentId
            content.userId = userId
            content.bucketId = bucketId
            // emit signal
            self.thumbnailContentObserver?.send(value: content)
        })
    }
}


// usage from a ViewModel
contentService.thumbnailContentProducer
    .startWithValues { content in
        self.contents.append(content)
    }

Maybe someone can verify the code above and say that this is the right way to do it.

I think you were on the right path when you were looking at using Signal with pipe . The key point is that you need to create a new SignalProducer for each thumbnail request, and you need a way to combine all of those requests into one resulting signal. I was thinking something like this (note this is untested code, but it should get the idea across):

class FirebaseContentService {
    // userService and storageThumbnail defined here
}

extension FirebaseContentService: ReactiveExtensionsProvider { }

extension Reactive where Base: FirebaseContentService {
    private func getThumbnailContentSignalProducer(bucketId: String, contentId: String) -> SignalProducer<Content, ContentError> {
        return SignalProducer<Content, ContentError> { (observer, disposable) in
            guard let userId = self.base.userService.getCurrentUserId() else {
                observer.send(error: ContentError.invalidUserLogin)
                return
            }

            let ref = self.base.storageThumbnail.reference()
            let contentRef = ref
                .child(userId)
                .child(bucketId)
                .child(FirebaseConstants.pathImages)
                .child("\(contentId).jpg")

                contentRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
                guard let data = data else {
                    observer.send(error: ContentError.contentNotFound)
                    return
                }
                let content = Image(data: data)
                content.id = contentId
                content.userId = userId
                content.bucketId = bucketId

                observer.send(value: content)
                observer.sendCompleted()
            })
        }
    }
}

class ThumbnailProvider {
    public let thumbnailSignal: Signal<Content, NoError>

    private let input: Observer<(bucketId: String, contentId: String), NoError>

    init(contentService: FirebaseContentService) {
        let (signal, observer) = Signal<(bucketId: String, contentId: String), NoError>.pipe()

        self.input = observer
        self.thumbnailSignal = signal
            .flatMap(.merge) { param in
                return contentService.reactive.getThumbnailContentSignalProducer(bucketId: param.bucketId, contentId: param.contentId)
                    .flatMapError { error in
                        debugPrint("Error download")
                        return SignalProducer.empty
                    }
            }
    }

    public func findThumbnail(bucketId: String, contentId: String) {
        input.send(value: (bucketId: bucketId, contentId: contentId))
    }
}

Using ReactiveExtensionsProvider like this is the idiomatic way of adding reactive APIs to existing functionality via a reactive property.

The actual requesting code is confined to getThumbnailContentSignalProducer which creates a SignalProducer for each request. Note that errors are passed along here, and the handling and conversion to NoError happens later.

findThumbnails just takes a bucketId and contentId and sends it through the input observable.

The construction of thumbnailSignal in init is where the magic happens. Each input, which is a tuple containing a bucketId and contentId , is converted into a request via flatMap . Note that the .merge strategy means the thumbnails are sent as soon as possible in whatever order the requests complete. You can use .concat if you want to ensure that the thumbnails are returned in the same order they were requested.

The flatMapError is where the potential errors get handled. In this case it's just printing "Error download" and doing nothing else.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM