0

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.

  • To clarify: you ultimately want to have one signal that can be observed which will send a value each time `findThumbnail` is called? – jjoelson May 17 '17 at 18:32
  • @jjoelson Yes, so far I found a own solution and edited the post above. Is the solution ok? – Marius-Constantin Dinu May 17 '17 at 19:07
  • Your solution looks a bit fragile. The closure in `init` gets called each time a client calls `start`, meaning `thumbnailContentObserver` can get switched at any time, which means the `Content` values can be diverted to a different signal at any time. My solution ensures all `Content` values come on a single `Signal` which can be subscribed to multiple times if necessary. – jjoelson May 17 '17 at 19:35

1 Answers1

1

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.

jjoelson
  • 5,771
  • 5
  • 31
  • 51
  • Thank you very much! The code above saved my day. I have made some minor corrections and updated my original post. – Marius-Constantin Dinu May 17 '17 at 20:36
  • OK, I approved the edits, though I'm not sure why you want `thumbnailSignalRef` to be mutable and optional. It just needs to be set once in `init`, so imo it should be immutable and non-optional. – jjoelson May 17 '17 at 21:15
  • The problem is that I can't initialize the `thumbnailSignal` the way you described, because I receive a compile error due to the `self.getThumbnailContentSignalProducer`: Variable `self.thumbnailSignal` used before being initialized. I want to define a getter property in the protocol and define it via the implementing class in the init constructor, but I have tried bending that line all the way from weak to unowned etc. and can't define it better. Do you have any idea? – Marius-Constantin Dinu May 17 '17 at 22:20
  • 1
    Ah, I see. I think the best way to handle this is to separate out the code that provides the final `Signal` from the actual service code. Then you can pass the service class as a dependency. I will update the answer in a moment. – jjoelson May 18 '17 at 12:30
  • 1
    Super update!!! That was exactly, what I was looking for!!! Thank you for the great support @jjoelson. :) I would up-vote your answer, if I had a higher Stackoverflow rank. – Marius-Constantin Dinu May 18 '17 at 14:18