How to pull one event from an observable when another emits
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
add a comment |
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
2
What have you tried so far? Any code you can share?
– Gustavo Barbosa
Nov 12 at 23:32
add a comment |
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
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
swift rx-swift reactivex
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
add a comment |
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
add a comment |
1 Answer
1
active
oldest
votes
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
)
}
}
- Here I create an obversable that will emit each elements in the source array, on after the other.
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 afterinput.acceptOrDecline
emits. Hence, we'll receive a new object after each decision.
startWith()
will force a first emission, so that we receive the first object we want the user to take a decision on.
takeUntil
will make our observable complete whenapplyToAll
emits. So that we do not receive a new element when theapplyToAll
checkbox is checked.
repeatElement
will indefinitely repeat an element. So whenapplyToAll
is true, we'll repeat the decision indefinitely. Because we zip the result offlatMap
toobjects
, it will repeat the decision for the number of remaning object inobjects
.
To build the source observable for the view model, assuming you are using two UIButton
s 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.
add a comment |
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
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
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
)
}
}
- Here I create an obversable that will emit each elements in the source array, on after the other.
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 afterinput.acceptOrDecline
emits. Hence, we'll receive a new object after each decision.
startWith()
will force a first emission, so that we receive the first object we want the user to take a decision on.
takeUntil
will make our observable complete whenapplyToAll
emits. So that we do not receive a new element when theapplyToAll
checkbox is checked.
repeatElement
will indefinitely repeat an element. So whenapplyToAll
is true, we'll repeat the decision indefinitely. Because we zip the result offlatMap
toobjects
, it will repeat the decision for the number of remaning object inobjects
.
To build the source observable for the view model, assuming you are using two UIButton
s 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.
add a comment |
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
)
}
}
- Here I create an obversable that will emit each elements in the source array, on after the other.
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 afterinput.acceptOrDecline
emits. Hence, we'll receive a new object after each decision.
startWith()
will force a first emission, so that we receive the first object we want the user to take a decision on.
takeUntil
will make our observable complete whenapplyToAll
emits. So that we do not receive a new element when theapplyToAll
checkbox is checked.
repeatElement
will indefinitely repeat an element. So whenapplyToAll
is true, we'll repeat the decision indefinitely. Because we zip the result offlatMap
toobjects
, it will repeat the decision for the number of remaning object inobjects
.
To build the source observable for the view model, assuming you are using two UIButton
s 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.
add a comment |
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
)
}
}
- Here I create an obversable that will emit each elements in the source array, on after the other.
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 afterinput.acceptOrDecline
emits. Hence, we'll receive a new object after each decision.
startWith()
will force a first emission, so that we receive the first object we want the user to take a decision on.
takeUntil
will make our observable complete whenapplyToAll
emits. So that we do not receive a new element when theapplyToAll
checkbox is checked.
repeatElement
will indefinitely repeat an element. So whenapplyToAll
is true, we'll repeat the decision indefinitely. Because we zip the result offlatMap
toobjects
, it will repeat the decision for the number of remaning object inobjects
.
To build the source observable for the view model, assuming you are using two UIButton
s 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.
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
)
}
}
- Here I create an obversable that will emit each elements in the source array, on after the other.
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 afterinput.acceptOrDecline
emits. Hence, we'll receive a new object after each decision.
startWith()
will force a first emission, so that we receive the first object we want the user to take a decision on.
takeUntil
will make our observable complete whenapplyToAll
emits. So that we do not receive a new element when theapplyToAll
checkbox is checked.
repeatElement
will indefinitely repeat an element. So whenapplyToAll
is true, we'll repeat the decision indefinitely. Because we zip the result offlatMap
toobjects
, it will repeat the decision for the number of remaning object inobjects
.
To build the source observable for the view model, assuming you are using two UIButton
s 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.
edited Nov 13 at 9:58
answered Nov 13 at 9:52
tomahh
9,50522858
9,50522858
add a comment |
add a comment |
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.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
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
2
What have you tried so far? Any code you can share?
– Gustavo Barbosa
Nov 12 at 23:32