RxSwift: нужна помощь с использованием flatMap и уменьшить

Я пишу простой генератор паролей Diceware для экспериментов с RxSwift. Я борюсь с использованием flatMap и reduce на отдельных этапах.

Текущий код

У меня есть наблюдаемый wordCount, который привязан к значению UIStepper и генерирует новый пароль с заданным количеством слов.

введите здесь описание изображения

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

rollDice возвращает Observable<String> (например: ["62345", "23423", "14231", ...]) и затем преобразуется в слова.

rawPassword is an Observable<Observable<String>>

В этом примере это будет: [["spec", "breed", "plins", "wiry", "chile", "cecil"]].

Затем у меня есть reducedPassword, который flatMap и reduce до String:

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

Это работает, и я получаю строку: spec-breed-plins-wiry-chile-cecil.

Проблема

Теперь я хочу изменить разделитель слов в пользовательском интерфейсе. Я просто хочу повторно применить уменьшение к моему rawPassword, когда текст из UITextField будет обновлен.

Я пытаюсь использовать combineLatest, чтобы объединить Observable<String> для разделителя с моим Observable<Observable<String>> rawPassword, например:

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

Но reduce никогда не срабатывает, и нажатие на шаговый переключатель ничего не делает. Я пробовал flatMap на отдельном этапе, но потом, с combineLatest, я заканчиваю только последним словом. combineLatest правильный ли подход?


person Yann Bodson    schedule 24.08.2016    source источник
comment
почему бы не сделать разделитель наблюдаемым? возможно сохранить последние сгенерированные слова ...   -  person Olexiy Pyvovarov    schedule 25.08.2016
comment
Мой разделитель - Observable<String>, просто не могу понять, как его совместить с rawPassword.   -  person Yann Bodson    schedule 25.08.2016
comment
Мой ответ ниже не удовлетворил вопрос?   -  person solidcell    schedule 06.10.2016


Ответы (1)


Эта проблема

Проблема в том, что reducedPassword (в вашем combineLatest случае) действительно Observable<Observable<String>>. Это помогает набирать аннотации, особенно при изучении RxSwift, чтобы убедиться, что вещи действительно соответствуют вашим ожиданиям.

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

Исправление

Итак, чтобы ваш измененный reducedPassword заработал, вам нужно взять элементы, проходящие через (Observable<String>), и заменить им текущий Observable<Observable<String>>. Так что используйте 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 }

Все вместе

Я собрал весь ваш код вместе с исправлением во что-то, что можно запускать как есть, без пользовательского интерфейса:

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("___")

Выходы:

ноль-четыре
ноль |||| два
два |||| четыре |||| ноль |||| один
три___two___two___four

Примечания

  • Я не уверен, почему wordMap равно [String : String], а не просто [String]. Кажется странным индексировать карту чем-то вроде wordMap["123"] вместо wordMap[123].

  • Было бы легче реализовать и понять, если бы вы использовали не Observable<Observable<String>>, а скорее Observable<[String]>. Вот почему у вас возникла проблема, и даже когда вы ее понимаете, она все равно сложнее, чем должно быть.

  • Помните, набирайте аннотируйте свои переменные и замыкания, чтобы убедиться, что вы делаете то, что ожидаете. Вы поймете эти проблемы раньше.

Улучшенное решение

Вот еще один способ написать это, принимая во внимание примечания, которые я обрисовал выше.

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("___")
person solidcell    schedule 01.10.2016