How to pull one event from an observable when another emits












0














I can't work out how to pull one event from an observable when another fires (at least, I think that's the essence of the problem but I'm so confused I might be wrong).



I have a ViewModel which is passed an array of objects on initialisation. The corresponding ViewController shows one of those objects at a time for the user to accept or decline (two buttons), along with an option to apply the response to all remaining objects (a checkbox). A side-effect of accepting the object (and possibly all remaining ones) is a database insertion. When there are no more objects the ViewController will be dismissed.



How can I model this reactively (using RxSwift/Cocoa)?



I'd also like to be able to show the user how many objects are remaining but that seems like an additional complexity.










share|improve this question


















  • 2




    What have you tried so far? Any code you can share?
    – Gustavo Barbosa
    Nov 12 at 23:32
















0














I can't work out how to pull one event from an observable when another fires (at least, I think that's the essence of the problem but I'm so confused I might be wrong).



I have a ViewModel which is passed an array of objects on initialisation. The corresponding ViewController shows one of those objects at a time for the user to accept or decline (two buttons), along with an option to apply the response to all remaining objects (a checkbox). A side-effect of accepting the object (and possibly all remaining ones) is a database insertion. When there are no more objects the ViewController will be dismissed.



How can I model this reactively (using RxSwift/Cocoa)?



I'd also like to be able to show the user how many objects are remaining but that seems like an additional complexity.










share|improve this question


















  • 2




    What have you tried so far? Any code you can share?
    – Gustavo Barbosa
    Nov 12 at 23:32














0












0








0







I can't work out how to pull one event from an observable when another fires (at least, I think that's the essence of the problem but I'm so confused I might be wrong).



I have a ViewModel which is passed an array of objects on initialisation. The corresponding ViewController shows one of those objects at a time for the user to accept or decline (two buttons), along with an option to apply the response to all remaining objects (a checkbox). A side-effect of accepting the object (and possibly all remaining ones) is a database insertion. When there are no more objects the ViewController will be dismissed.



How can I model this reactively (using RxSwift/Cocoa)?



I'd also like to be able to show the user how many objects are remaining but that seems like an additional complexity.










share|improve this question













I can't work out how to pull one event from an observable when another fires (at least, I think that's the essence of the problem but I'm so confused I might be wrong).



I have a ViewModel which is passed an array of objects on initialisation. The corresponding ViewController shows one of those objects at a time for the user to accept or decline (two buttons), along with an option to apply the response to all remaining objects (a checkbox). A side-effect of accepting the object (and possibly all remaining ones) is a database insertion. When there are no more objects the ViewController will be dismissed.



How can I model this reactively (using RxSwift/Cocoa)?



I'd also like to be able to show the user how many objects are remaining but that seems like an additional complexity.







swift rx-swift reactivex






share|improve this question













share|improve this question











share|improve this question




share|improve this question










asked Nov 12 at 20:19









Rik

9811




9811








  • 2




    What have you tried so far? Any code you can share?
    – Gustavo Barbosa
    Nov 12 at 23:32














  • 2




    What have you tried so far? Any code you can share?
    – Gustavo Barbosa
    Nov 12 at 23:32








2




2




What have you tried so far? Any code you can share?
– Gustavo Barbosa
Nov 12 at 23:32




What have you tried so far? Any code you can share?
– Gustavo Barbosa
Nov 12 at 23:32












1 Answer
1






active

oldest

votes


















0














This is an example implementation for the described behaviour.



Remember though that stackoverflow is not meant to work like this. You should show the code that you wrote to try to solve your problem first.



// This encodes a database action. You can subscribe to view model's `dbAction` to perform your desired side effect.
enum DBAction<T> {
case insert(object: T)
case delete(object: T)
}

class SomeViewModel<Object> {
struct Output {
let currentObject: Observable<Object>
let remainingObjectCount: Observable<Int>
let dbAction: Observable<DBAction<Object>>
}

struct Input {
let acceptOrDecline: Observable<(keepOrDrop: Bool, applyToAll: Bool)>
}

let totalObjectCount: Int
let objects: Observable<Object>

init(objects: [Object]) {
self.totalObjectCount = objects.count
self.objects = .from(objects) // 1
}

func transform(input: Input) -> Output {
let applyToAll: Observable<Void> = input.acceptOrDecline.map { $0.applyToAll }.filter { $0 == true }.map { _ in }
let acceptOrDecline = input.acceptOrDecline.map { $0.keepOrDrop }

let currentObject = Observable.zip( // 2
objects,
acceptOrDecline.map { _ in }.startWith() // 3
) { object, _ in object }
.takeUntil(applyToAll) // 4
.share()

// 5
let actionForCurrent = input.acceptOrDecline.flatMap { tuple in
tuple.applyToAll ? Observable.repeatElement(tuple.keepOrDrop, scheduler: MainScheduler.instance) : .just(tuple.keepOrDrop)
}

let dbAction = Observable.zip(
objects,
actionForCurrent
) { (object: Object, shouldKeep: Bool) -> DBAction<Object> in
if shouldKeep {
return DBAction.insert(object: object)
} else {
return DBAction.delete(object: object)
}
}

let remainingObjectCount = currentObject.scan(totalObjectCount) { acc, _ in
acc - 1
}.concat(.just(0))

return Output(
currentObject: currentObject,
remainingObjectCount: remainingObjectCount,
dbAction: dbAction
)
}
}



  1. Here I create an obversable that will emit each elements in the source array, on after the other.


  2. zip combines elements from two observables. The good thing with zip is that it will wait for a distinct element from each source. When subscribing to the result of zip, a new element will be emitted after input.acceptOrDecline emits. Hence, we'll receive a new object after each decision.


  3. startWith() will force a first emission, so that we receive the first object we want the user to take a decision on.


  4. takeUntil will make our observable complete when applyToAll emits. So that we do not receive a new element when the applyToAll checkbox is checked.


  5. repeatElement will indefinitely repeat an element. So when applyToAll is true, we'll repeat the decision indefinitely. Because we zip the result of flatMap to objects, it will repeat the decision for the number of remaning object in objects.




To build the source observable for the view model, assuming you are using two UIButtons and a UISwitch



let acceptButton: UIButton
let dropButton: UIButton
let applyToAll: UISwitch

let accept = acceptButton.rx.tap.map { true }
let drop = dropButton.rx.tap.map { false }
let input = Input(
acceptOrDecline: Observable.combineLatest(
Observable.merge(accept, drop),
applyToAll.rx.value
) { (keepOrDrop: $0, applyToAll: $1) }
)


Note that this is a proposed implementation that compiles but that I did not test. You'll find here leads to implement your desired behavior, but I cannot guaranty this is 100% correct.






share|improve this answer























    Your Answer






    StackExchange.ifUsing("editor", function () {
    StackExchange.using("externalEditor", function () {
    StackExchange.using("snippets", function () {
    StackExchange.snippets.init();
    });
    });
    }, "code-snippets");

    StackExchange.ready(function() {
    var channelOptions = {
    tags: "".split(" "),
    id: "1"
    };
    initTagRenderer("".split(" "), "".split(" "), channelOptions);

    StackExchange.using("externalEditor", function() {
    // Have to fire editor after snippets, if snippets enabled
    if (StackExchange.settings.snippets.snippetsEnabled) {
    StackExchange.using("snippets", function() {
    createEditor();
    });
    }
    else {
    createEditor();
    }
    });

    function createEditor() {
    StackExchange.prepareEditor({
    heartbeatType: 'answer',
    autoActivateHeartbeat: false,
    convertImagesToLinks: true,
    noModals: true,
    showLowRepImageUploadWarning: true,
    reputationToPostImages: 10,
    bindNavPrevention: true,
    postfix: "",
    imageUploader: {
    brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
    contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
    allowUrls: true
    },
    onDemand: true,
    discardSelector: ".discard-answer"
    ,immediatelyShowMarkdownHelp:true
    });


    }
    });














    draft saved

    draft discarded


















    StackExchange.ready(
    function () {
    StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53269490%2fhow-to-pull-one-event-from-an-observable-when-another-emits%23new-answer', 'question_page');
    }
    );

    Post as a guest















    Required, but never shown

























    1 Answer
    1






    active

    oldest

    votes








    1 Answer
    1






    active

    oldest

    votes









    active

    oldest

    votes






    active

    oldest

    votes









    0














    This is an example implementation for the described behaviour.



    Remember though that stackoverflow is not meant to work like this. You should show the code that you wrote to try to solve your problem first.



    // This encodes a database action. You can subscribe to view model's `dbAction` to perform your desired side effect.
    enum DBAction<T> {
    case insert(object: T)
    case delete(object: T)
    }

    class SomeViewModel<Object> {
    struct Output {
    let currentObject: Observable<Object>
    let remainingObjectCount: Observable<Int>
    let dbAction: Observable<DBAction<Object>>
    }

    struct Input {
    let acceptOrDecline: Observable<(keepOrDrop: Bool, applyToAll: Bool)>
    }

    let totalObjectCount: Int
    let objects: Observable<Object>

    init(objects: [Object]) {
    self.totalObjectCount = objects.count
    self.objects = .from(objects) // 1
    }

    func transform(input: Input) -> Output {
    let applyToAll: Observable<Void> = input.acceptOrDecline.map { $0.applyToAll }.filter { $0 == true }.map { _ in }
    let acceptOrDecline = input.acceptOrDecline.map { $0.keepOrDrop }

    let currentObject = Observable.zip( // 2
    objects,
    acceptOrDecline.map { _ in }.startWith() // 3
    ) { object, _ in object }
    .takeUntil(applyToAll) // 4
    .share()

    // 5
    let actionForCurrent = input.acceptOrDecline.flatMap { tuple in
    tuple.applyToAll ? Observable.repeatElement(tuple.keepOrDrop, scheduler: MainScheduler.instance) : .just(tuple.keepOrDrop)
    }

    let dbAction = Observable.zip(
    objects,
    actionForCurrent
    ) { (object: Object, shouldKeep: Bool) -> DBAction<Object> in
    if shouldKeep {
    return DBAction.insert(object: object)
    } else {
    return DBAction.delete(object: object)
    }
    }

    let remainingObjectCount = currentObject.scan(totalObjectCount) { acc, _ in
    acc - 1
    }.concat(.just(0))

    return Output(
    currentObject: currentObject,
    remainingObjectCount: remainingObjectCount,
    dbAction: dbAction
    )
    }
    }



    1. Here I create an obversable that will emit each elements in the source array, on after the other.


    2. zip combines elements from two observables. The good thing with zip is that it will wait for a distinct element from each source. When subscribing to the result of zip, a new element will be emitted after input.acceptOrDecline emits. Hence, we'll receive a new object after each decision.


    3. startWith() will force a first emission, so that we receive the first object we want the user to take a decision on.


    4. takeUntil will make our observable complete when applyToAll emits. So that we do not receive a new element when the applyToAll checkbox is checked.


    5. repeatElement will indefinitely repeat an element. So when applyToAll is true, we'll repeat the decision indefinitely. Because we zip the result of flatMap to objects, it will repeat the decision for the number of remaning object in objects.




    To build the source observable for the view model, assuming you are using two UIButtons and a UISwitch



    let acceptButton: UIButton
    let dropButton: UIButton
    let applyToAll: UISwitch

    let accept = acceptButton.rx.tap.map { true }
    let drop = dropButton.rx.tap.map { false }
    let input = Input(
    acceptOrDecline: Observable.combineLatest(
    Observable.merge(accept, drop),
    applyToAll.rx.value
    ) { (keepOrDrop: $0, applyToAll: $1) }
    )


    Note that this is a proposed implementation that compiles but that I did not test. You'll find here leads to implement your desired behavior, but I cannot guaranty this is 100% correct.






    share|improve this answer




























      0














      This is an example implementation for the described behaviour.



      Remember though that stackoverflow is not meant to work like this. You should show the code that you wrote to try to solve your problem first.



      // This encodes a database action. You can subscribe to view model's `dbAction` to perform your desired side effect.
      enum DBAction<T> {
      case insert(object: T)
      case delete(object: T)
      }

      class SomeViewModel<Object> {
      struct Output {
      let currentObject: Observable<Object>
      let remainingObjectCount: Observable<Int>
      let dbAction: Observable<DBAction<Object>>
      }

      struct Input {
      let acceptOrDecline: Observable<(keepOrDrop: Bool, applyToAll: Bool)>
      }

      let totalObjectCount: Int
      let objects: Observable<Object>

      init(objects: [Object]) {
      self.totalObjectCount = objects.count
      self.objects = .from(objects) // 1
      }

      func transform(input: Input) -> Output {
      let applyToAll: Observable<Void> = input.acceptOrDecline.map { $0.applyToAll }.filter { $0 == true }.map { _ in }
      let acceptOrDecline = input.acceptOrDecline.map { $0.keepOrDrop }

      let currentObject = Observable.zip( // 2
      objects,
      acceptOrDecline.map { _ in }.startWith() // 3
      ) { object, _ in object }
      .takeUntil(applyToAll) // 4
      .share()

      // 5
      let actionForCurrent = input.acceptOrDecline.flatMap { tuple in
      tuple.applyToAll ? Observable.repeatElement(tuple.keepOrDrop, scheduler: MainScheduler.instance) : .just(tuple.keepOrDrop)
      }

      let dbAction = Observable.zip(
      objects,
      actionForCurrent
      ) { (object: Object, shouldKeep: Bool) -> DBAction<Object> in
      if shouldKeep {
      return DBAction.insert(object: object)
      } else {
      return DBAction.delete(object: object)
      }
      }

      let remainingObjectCount = currentObject.scan(totalObjectCount) { acc, _ in
      acc - 1
      }.concat(.just(0))

      return Output(
      currentObject: currentObject,
      remainingObjectCount: remainingObjectCount,
      dbAction: dbAction
      )
      }
      }



      1. Here I create an obversable that will emit each elements in the source array, on after the other.


      2. zip combines elements from two observables. The good thing with zip is that it will wait for a distinct element from each source. When subscribing to the result of zip, a new element will be emitted after input.acceptOrDecline emits. Hence, we'll receive a new object after each decision.


      3. startWith() will force a first emission, so that we receive the first object we want the user to take a decision on.


      4. takeUntil will make our observable complete when applyToAll emits. So that we do not receive a new element when the applyToAll checkbox is checked.


      5. repeatElement will indefinitely repeat an element. So when applyToAll is true, we'll repeat the decision indefinitely. Because we zip the result of flatMap to objects, it will repeat the decision for the number of remaning object in objects.




      To build the source observable for the view model, assuming you are using two UIButtons and a UISwitch



      let acceptButton: UIButton
      let dropButton: UIButton
      let applyToAll: UISwitch

      let accept = acceptButton.rx.tap.map { true }
      let drop = dropButton.rx.tap.map { false }
      let input = Input(
      acceptOrDecline: Observable.combineLatest(
      Observable.merge(accept, drop),
      applyToAll.rx.value
      ) { (keepOrDrop: $0, applyToAll: $1) }
      )


      Note that this is a proposed implementation that compiles but that I did not test. You'll find here leads to implement your desired behavior, but I cannot guaranty this is 100% correct.






      share|improve this answer


























        0












        0








        0






        This is an example implementation for the described behaviour.



        Remember though that stackoverflow is not meant to work like this. You should show the code that you wrote to try to solve your problem first.



        // This encodes a database action. You can subscribe to view model's `dbAction` to perform your desired side effect.
        enum DBAction<T> {
        case insert(object: T)
        case delete(object: T)
        }

        class SomeViewModel<Object> {
        struct Output {
        let currentObject: Observable<Object>
        let remainingObjectCount: Observable<Int>
        let dbAction: Observable<DBAction<Object>>
        }

        struct Input {
        let acceptOrDecline: Observable<(keepOrDrop: Bool, applyToAll: Bool)>
        }

        let totalObjectCount: Int
        let objects: Observable<Object>

        init(objects: [Object]) {
        self.totalObjectCount = objects.count
        self.objects = .from(objects) // 1
        }

        func transform(input: Input) -> Output {
        let applyToAll: Observable<Void> = input.acceptOrDecline.map { $0.applyToAll }.filter { $0 == true }.map { _ in }
        let acceptOrDecline = input.acceptOrDecline.map { $0.keepOrDrop }

        let currentObject = Observable.zip( // 2
        objects,
        acceptOrDecline.map { _ in }.startWith() // 3
        ) { object, _ in object }
        .takeUntil(applyToAll) // 4
        .share()

        // 5
        let actionForCurrent = input.acceptOrDecline.flatMap { tuple in
        tuple.applyToAll ? Observable.repeatElement(tuple.keepOrDrop, scheduler: MainScheduler.instance) : .just(tuple.keepOrDrop)
        }

        let dbAction = Observable.zip(
        objects,
        actionForCurrent
        ) { (object: Object, shouldKeep: Bool) -> DBAction<Object> in
        if shouldKeep {
        return DBAction.insert(object: object)
        } else {
        return DBAction.delete(object: object)
        }
        }

        let remainingObjectCount = currentObject.scan(totalObjectCount) { acc, _ in
        acc - 1
        }.concat(.just(0))

        return Output(
        currentObject: currentObject,
        remainingObjectCount: remainingObjectCount,
        dbAction: dbAction
        )
        }
        }



        1. Here I create an obversable that will emit each elements in the source array, on after the other.


        2. zip combines elements from two observables. The good thing with zip is that it will wait for a distinct element from each source. When subscribing to the result of zip, a new element will be emitted after input.acceptOrDecline emits. Hence, we'll receive a new object after each decision.


        3. startWith() will force a first emission, so that we receive the first object we want the user to take a decision on.


        4. takeUntil will make our observable complete when applyToAll emits. So that we do not receive a new element when the applyToAll checkbox is checked.


        5. repeatElement will indefinitely repeat an element. So when applyToAll is true, we'll repeat the decision indefinitely. Because we zip the result of flatMap to objects, it will repeat the decision for the number of remaning object in objects.




        To build the source observable for the view model, assuming you are using two UIButtons and a UISwitch



        let acceptButton: UIButton
        let dropButton: UIButton
        let applyToAll: UISwitch

        let accept = acceptButton.rx.tap.map { true }
        let drop = dropButton.rx.tap.map { false }
        let input = Input(
        acceptOrDecline: Observable.combineLatest(
        Observable.merge(accept, drop),
        applyToAll.rx.value
        ) { (keepOrDrop: $0, applyToAll: $1) }
        )


        Note that this is a proposed implementation that compiles but that I did not test. You'll find here leads to implement your desired behavior, but I cannot guaranty this is 100% correct.






        share|improve this answer














        This is an example implementation for the described behaviour.



        Remember though that stackoverflow is not meant to work like this. You should show the code that you wrote to try to solve your problem first.



        // This encodes a database action. You can subscribe to view model's `dbAction` to perform your desired side effect.
        enum DBAction<T> {
        case insert(object: T)
        case delete(object: T)
        }

        class SomeViewModel<Object> {
        struct Output {
        let currentObject: Observable<Object>
        let remainingObjectCount: Observable<Int>
        let dbAction: Observable<DBAction<Object>>
        }

        struct Input {
        let acceptOrDecline: Observable<(keepOrDrop: Bool, applyToAll: Bool)>
        }

        let totalObjectCount: Int
        let objects: Observable<Object>

        init(objects: [Object]) {
        self.totalObjectCount = objects.count
        self.objects = .from(objects) // 1
        }

        func transform(input: Input) -> Output {
        let applyToAll: Observable<Void> = input.acceptOrDecline.map { $0.applyToAll }.filter { $0 == true }.map { _ in }
        let acceptOrDecline = input.acceptOrDecline.map { $0.keepOrDrop }

        let currentObject = Observable.zip( // 2
        objects,
        acceptOrDecline.map { _ in }.startWith() // 3
        ) { object, _ in object }
        .takeUntil(applyToAll) // 4
        .share()

        // 5
        let actionForCurrent = input.acceptOrDecline.flatMap { tuple in
        tuple.applyToAll ? Observable.repeatElement(tuple.keepOrDrop, scheduler: MainScheduler.instance) : .just(tuple.keepOrDrop)
        }

        let dbAction = Observable.zip(
        objects,
        actionForCurrent
        ) { (object: Object, shouldKeep: Bool) -> DBAction<Object> in
        if shouldKeep {
        return DBAction.insert(object: object)
        } else {
        return DBAction.delete(object: object)
        }
        }

        let remainingObjectCount = currentObject.scan(totalObjectCount) { acc, _ in
        acc - 1
        }.concat(.just(0))

        return Output(
        currentObject: currentObject,
        remainingObjectCount: remainingObjectCount,
        dbAction: dbAction
        )
        }
        }



        1. Here I create an obversable that will emit each elements in the source array, on after the other.


        2. zip combines elements from two observables. The good thing with zip is that it will wait for a distinct element from each source. When subscribing to the result of zip, a new element will be emitted after input.acceptOrDecline emits. Hence, we'll receive a new object after each decision.


        3. startWith() will force a first emission, so that we receive the first object we want the user to take a decision on.


        4. takeUntil will make our observable complete when applyToAll emits. So that we do not receive a new element when the applyToAll checkbox is checked.


        5. repeatElement will indefinitely repeat an element. So when applyToAll is true, we'll repeat the decision indefinitely. Because we zip the result of flatMap to objects, it will repeat the decision for the number of remaning object in objects.




        To build the source observable for the view model, assuming you are using two UIButtons and a UISwitch



        let acceptButton: UIButton
        let dropButton: UIButton
        let applyToAll: UISwitch

        let accept = acceptButton.rx.tap.map { true }
        let drop = dropButton.rx.tap.map { false }
        let input = Input(
        acceptOrDecline: Observable.combineLatest(
        Observable.merge(accept, drop),
        applyToAll.rx.value
        ) { (keepOrDrop: $0, applyToAll: $1) }
        )


        Note that this is a proposed implementation that compiles but that I did not test. You'll find here leads to implement your desired behavior, but I cannot guaranty this is 100% correct.







        share|improve this answer














        share|improve this answer



        share|improve this answer








        edited Nov 13 at 9:58

























        answered Nov 13 at 9:52









        tomahh

        9,50522858




        9,50522858






























            draft saved

            draft discarded




















































            Thanks for contributing an answer to Stack Overflow!


            • Please be sure to answer the question. Provide details and share your research!

            But avoid



            • Asking for help, clarification, or responding to other answers.

            • Making statements based on opinion; back them up with references or personal experience.


            To learn more, see our tips on writing great answers.





            Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


            Please pay close attention to the following guidance:


            • Please be sure to answer the question. Provide details and share your research!

            But avoid



            • Asking for help, clarification, or responding to other answers.

            • Making statements based on opinion; back them up with references or personal experience.


            To learn more, see our tips on writing great answers.




            draft saved


            draft discarded














            StackExchange.ready(
            function () {
            StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53269490%2fhow-to-pull-one-event-from-an-observable-when-another-emits%23new-answer', 'question_page');
            }
            );

            Post as a guest















            Required, but never shown





















































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown

































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown







            Popular posts from this blog

            Bressuire

            Vorschmack

            Quarantine