RxSwift:需要使用flatMap和reduce的帮助

时间:2021-03-27 19:03:24

I am writing a simple Diceware password generator to experiment with RxSwift. I am struggling with using flatMap and reduce in separate steps.

我正在编写一个简单的Diceware密码生成器来试验RxSwift。我正在努力使用flatMap并在单独的步骤中减少。

Current code

目前的代码

I have an observable wordCount that is bound to the UIStepper value and generate a new password with a given number of words.

我有一个可观察的wordCount绑定到UIStepper值并生成一个具有给定数量的单词的新密码。

RxSwift:需要使用flatMap和reduce的帮助

    let rawPassword = wordCount
        .asObservable()
        .map { wordCount in
            self.rollDice(numberOfDice: wordCount)
                .map { numbers in wordMap[numbers]! }
        }

rollDice returns an Observable<String> (for example: ["62345", "23423", "14231", ...]) and is then mapped to words.

rollDice返回一个Observable (例如:[“62345”,“23423”,“14231”,...]),然后映射到单词。

rawPassword is an Observable<Observable<String>>

rawPassword是一个Observable >

In this example, it would be: [["spec", "breed", "plins", "wiry", "chile", "cecil"]].

在这个例子中,它将是:[[“spec”,“breed”,“plins”,“wiry”,“chile”,“cecil”]]。

I then have a reducedPassword which flatMap and reduce to a String:

然后我有一个reducePassword,其中flatMap和reduce减少为一个字符串:

    let reducedPassword = rawPassword
        .flatMap { raw in
            raw.reduce("") { prev, value in
                let separator = "-"
                return prev == "" ? value : "\(prev)\(separator)\(value)"
            }
    }

This works and I end up with the string: spec-breed-plins-wiry-chile-cecil.

这有效,我最终得到了字符串:spec-breed-plins-wiry-chile-cecil。

The problem

问题

Now I want to change the word separator from the UI. I just want to re-apply the reduce on my rawPassword when the text from the UITextField is updated.

现在我想从UI更改单词分隔符。我只想在更新UITextField的文本时重新应用我的rawPassword上的reduce。

I am trying to use combineLatest to combine the Observable<String> for the separator with my Observable<Observable<String>> rawPassword, like this:

我试图使用combineLatest将分隔符的Observable 与我的Observable > rawPassword结合起来,如下所示:

    let reducedPassword = Observable.combineLatest(rawPassword, separator.asObservable()) { raw, sep in
        raw.reduce("") { prev, value in
            return prev == "" ? value : "\(prev)\(sep)\(value)"
        }
    }

But the reduce never fires and clicking the stepper does nothing. I have tried to flatMap in a separate step but then, with combineLatest I only end up with the last word. Is combineLatest the right approach at all?

但是减少永远不会发生,点击步进器什么都不做。我试图在一个单独的步骤中进行flatMap但是然后,使用combineLatest,我最终只得到了最后一个单词。结合起来是最正确的方法吗?

1 个解决方案

#1


2  

The Problem

The problem is that reducedPassword (in your combineLatest case) is really Observable<Observable<String>>. It helps to type annotate, especially when learning RxSwift to ensure things are actually what you expect them to be.

问题是reducedPassword(在你的combineLatest案例中)实际上是Observable >。它有助于输入注释,特别是在学习RxSwift时,确保事物实际上与您期望的一样。

let reducedPassword: Observable<Observable<String>> = Observable.combineLatest(rawPassword, separator.asObservable()) { // ...

The Fix

So, to get your modified reducedPassword to work, you need to take elements coming through (Observable<String>) and replace the current Observable<Observable<String>> with it. So use switchLatest() (which is like doing flatMap { $0 }):

因此,要使修改后的reducedPassword生效,您需要获取通过的元素(Observable )并用它替换当前的Observable >。所以使用switchLatest()(就像做flatMap {$ 0}):

let reducedPassword = Observable.combineLatest(rawPassword, separator.asObservable()) {
    raw, sep in

    raw.reduce("") { prev, value in
        prev == "" ? value : "\(prev)\(sep)\(value)"
    }
}
.switchLatest() // equivalent to: .flatMap { $0 }

All Together

I put all of your code together, along with the fix, into something that can be run as-is with no UI:

我把所有代码和修复程序放在一起,可以按原样运行,没有UI:

let disposeBag = DisposeBag()

let wordCount = BehaviorSubject<UInt>(value: 2)

let separator = BehaviorSubject<String>(value: "-")

let wordMap = ["0" : "zero", "1" : "one", "2" : "two", "3" : "three", "4" : "four"]

func rollDice(numberOfDice: UInt) -> Observable<String> {
    let maxNumber = wordMap.count
    return Observable.from(0..<numberOfDice)
        .map { _ in Int(arc4random()) % maxNumber }
        .map { String($0) }
}

let rawPassword = wordCount
    .asObservable()
    .map { wordCount in
        rollDice(numberOfDice: wordCount)
            .map { numbers in wordMap[numbers]! }
    }

let reducedPassword = Observable.combineLatest(rawPassword, separator.asObservable()) {
    raw, sep in

    raw.reduce("") { prev, value in
        prev == "" ? value : "\(prev)\(sep)\(value)"
    }
}
.switchLatest() // equivalent to: .flatMap { $0 }

reducedPassword
    .subscribe(onNext: { print($0) })

separator.onNext("||||")
wordCount.onNext(4)
separator.onNext("___")

Outputs:

输出:

zero-four
zero||||two
two||||four||||zero||||one
three___two___two___four

零四零||||二二||||四||||零||||一三___两个_two___四

Notes

  • I'm not sure why wordMap is [String : String] instead of just [String]. It seems strange to index a map with something like wordMap["123"] instead of wordMap[123].

    我不确定为什么wordMap是[String:String]而不仅仅是[String]。使用wordMap [“123”]而不是wordMap [123]来索引地图似乎很奇怪。

  • It would be easier to implement and understand if you were to not use Observable<Observable<String>>, but rather Observable<[String]>. This is why you had the problem, and even when you understand it, it's still more complex than it needs to be.

    如果你不使用Observable >,而是使用Observable <[String]>,那么实现和理解会更容易。这就是你遇到问题的原因,即使你理解它,它仍然比它需要的更复杂。

  • Remember, type annotate your variables and closures to make sure you're doing what you expect. You'll catch these issues earlier.

    请记住,键入注释您的变量和闭包,以确保您正在做您期望的。你会早点发现这些问题。

Improved Solution

Here's another way you could write it, taking into account the notes I've outlined above.

考虑到我上面概述的注释,这是你可以写的另一种方式。

let disposeBag = DisposeBag()

let wordCount = BehaviorSubject<UInt>(value: 2)

let separator = BehaviorSubject<String>(value: "-")

let wordMap = ["zero", "one", "two", "three", "four"]

func rollDice(numberOfDice: UInt) -> [Int] {
    let maxNumber = wordMap.count
    return (0..<numberOfDice).map { _ in
        Int(arc4random()) % maxNumber
    }
}

let words: Observable<[String]> = wordCount
    .asObservable()
    .map { (count: UInt) -> [String] in
        return rollDice(numberOfDice: count)
            .map { roll in wordMap[roll] }
    }

let password: Observable<String> = Observable.combineLatest(words, separator.asObservable()) {
    (words: [String], separator: String) -> String in

    words.reduce("") { prev, value in
        prev == "" ? value : "\(prev)\(separator)\(value)"
    }
}

password.subscribe(onNext: { print($0) })

separator.onNext("||||")
wordCount.onNext(4)
separator.onNext("___")

#1


2  

The Problem

The problem is that reducedPassword (in your combineLatest case) is really Observable<Observable<String>>. It helps to type annotate, especially when learning RxSwift to ensure things are actually what you expect them to be.

问题是reducedPassword(在你的combineLatest案例中)实际上是Observable >。它有助于输入注释,特别是在学习RxSwift时,确保事物实际上与您期望的一样。

let reducedPassword: Observable<Observable<String>> = Observable.combineLatest(rawPassword, separator.asObservable()) { // ...

The Fix

So, to get your modified reducedPassword to work, you need to take elements coming through (Observable<String>) and replace the current Observable<Observable<String>> with it. So use switchLatest() (which is like doing flatMap { $0 }):

因此,要使修改后的reducedPassword生效,您需要获取通过的元素(Observable )并用它替换当前的Observable >。所以使用switchLatest()(就像做flatMap {$ 0}):

let reducedPassword = Observable.combineLatest(rawPassword, separator.asObservable()) {
    raw, sep in

    raw.reduce("") { prev, value in
        prev == "" ? value : "\(prev)\(sep)\(value)"
    }
}
.switchLatest() // equivalent to: .flatMap { $0 }

All Together

I put all of your code together, along with the fix, into something that can be run as-is with no UI:

我把所有代码和修复程序放在一起,可以按原样运行,没有UI:

let disposeBag = DisposeBag()

let wordCount = BehaviorSubject<UInt>(value: 2)

let separator = BehaviorSubject<String>(value: "-")

let wordMap = ["0" : "zero", "1" : "one", "2" : "two", "3" : "three", "4" : "four"]

func rollDice(numberOfDice: UInt) -> Observable<String> {
    let maxNumber = wordMap.count
    return Observable.from(0..<numberOfDice)
        .map { _ in Int(arc4random()) % maxNumber }
        .map { String($0) }
}

let rawPassword = wordCount
    .asObservable()
    .map { wordCount in
        rollDice(numberOfDice: wordCount)
            .map { numbers in wordMap[numbers]! }
    }

let reducedPassword = Observable.combineLatest(rawPassword, separator.asObservable()) {
    raw, sep in

    raw.reduce("") { prev, value in
        prev == "" ? value : "\(prev)\(sep)\(value)"
    }
}
.switchLatest() // equivalent to: .flatMap { $0 }

reducedPassword
    .subscribe(onNext: { print($0) })

separator.onNext("||||")
wordCount.onNext(4)
separator.onNext("___")

Outputs:

输出:

zero-four
zero||||two
two||||four||||zero||||one
three___two___two___four

零四零||||二二||||四||||零||||一三___两个_two___四

Notes

  • I'm not sure why wordMap is [String : String] instead of just [String]. It seems strange to index a map with something like wordMap["123"] instead of wordMap[123].

    我不确定为什么wordMap是[String:String]而不仅仅是[String]。使用wordMap [“123”]而不是wordMap [123]来索引地图似乎很奇怪。

  • It would be easier to implement and understand if you were to not use Observable<Observable<String>>, but rather Observable<[String]>. This is why you had the problem, and even when you understand it, it's still more complex than it needs to be.

    如果你不使用Observable >,而是使用Observable <[String]>,那么实现和理解会更容易。这就是你遇到问题的原因,即使你理解它,它仍然比它需要的更复杂。

  • Remember, type annotate your variables and closures to make sure you're doing what you expect. You'll catch these issues earlier.

    请记住,键入注释您的变量和闭包,以确保您正在做您期望的。你会早点发现这些问题。

Improved Solution

Here's another way you could write it, taking into account the notes I've outlined above.

考虑到我上面概述的注释,这是你可以写的另一种方式。

let disposeBag = DisposeBag()

let wordCount = BehaviorSubject<UInt>(value: 2)

let separator = BehaviorSubject<String>(value: "-")

let wordMap = ["zero", "one", "two", "three", "four"]

func rollDice(numberOfDice: UInt) -> [Int] {
    let maxNumber = wordMap.count
    return (0..<numberOfDice).map { _ in
        Int(arc4random()) % maxNumber
    }
}

let words: Observable<[String]> = wordCount
    .asObservable()
    .map { (count: UInt) -> [String] in
        return rollDice(numberOfDice: count)
            .map { roll in wordMap[roll] }
    }

let password: Observable<String> = Observable.combineLatest(words, separator.asObservable()) {
    (words: [String], separator: String) -> String in

    words.reduce("") { prev, value in
        prev == "" ? value : "\(prev)\(separator)\(value)"
    }
}

password.subscribe(onNext: { print($0) })

separator.onNext("||||")
wordCount.onNext(4)
separator.onNext("___")