Как реализовать правильный оператор switchMap в Combine?

Рассмотрим следующий пример:

    cancellable = Just(2).map { x in
        Just(x * x).delay(for: 2.0, scheduler: RunLoop.main)
    }
    .switchToLatest()
    .sink(receiveCompletion: {_ in
        print("completed")
    }, receiveValue: {result in
        print(result)
    })

Здесь я пытаюсь имитировать поведение известного оператора switchMap, используя операторы Combine. Я ожидаю получить результат через две секунды и завершение. На самом деле ни результата, ни завершения не наступает. Что очень плохо, потому что апстрим завершен!

Похоже, switchToLatest отменяет себя, как только восходящий поток завершается, и забывает завершить. С другой стороны, если я заменю его на flatMap, все будет работать, как ожидалось.

Есть ли хорошие примеры правильного оператора switchMap?

Отказ от ответственности: хорошо, я понимаю, что мой апстрим завершен. Хотя я хочу, чтобы мой switchMap работал независимо от того, завершился ли мой апстрим до внутреннего издателя или после.


person norekhov    schedule 25.11.2019    source источник
comment
Фу, похоже, еще одна ошибка в Combine. Я боролся с комбинацией .receive(on: queue).combineLatest(Just(2)). Просто молчит, как в вашем примере, и flatMap работает нормально.   -  person Ben Affleck    schedule 25.11.2019
comment
Спасибо, что задали этот вопрос; это меня тоже укусило, так что я рад узнать, что я не одинок.   -  person matt    schedule 07.03.2020
comment
Ошибка исправлена ​​в Xcode 11.4.   -  person matt    schedule 08.03.2020


Ответы (3)


Разве вы не имели в виду следующее (по крайней мере, насколько я понял, это приводит к тому, что вы ожидали)?

let cancellable = Just(2)
    .map { x in
        Just(x * x)
    }
    .delay(for: 2.0, scheduler: RunLoop.main)
    .switchToLatest()
    .sink(receiveCompletion: {_ in
        print("completed")
    }, receiveValue: {result in
        print(result)
    })
person Asperi    schedule 25.11.2019
comment
Извините, я не это имел в виду. Я хотел показать, что следующий пример не генерирует выходных данных и завершенных событий, что само по себе является ошибкой. Также в вашем примере вы задержали вывод карты, в то время как я отложил внутреннюю наблюдаемую карту, что вызвало рассматриваемое поведение. - person norekhov; 25.11.2019
comment
Чтобы сделать это еще проще: я думаю, что publisher.map(transform).switchToLatest() должен генерировать события, даже если издатель завершает работу раньше внутреннего издателя. - person norekhov; 25.11.2019

Я думаю, что это ошибка. Попробуйте вместо этого использовать flatMap, который работает должным образом.

let cancellable = Just(2)
.flatMap { (x) in
    Just(x * x)
        .delay(for: 2.0, scheduler: RunLoop.main)
}
.sink(receiveCompletion: {_ in
    print("completed")
}, receiveValue: {result in
    print(result)
})
person Ben Affleck    schedule 25.11.2019
comment
Правда, за исключением того, что flatMap - это не то, что я хочу :-) На самом деле я хочу switchMap. Представьте, что у меня есть поток вместо Just(), и я хочу отменить предыдущие подписки, когда он отправляется. Ну, я уже реализовал специальный оператор, который делает это, поэтому, вероятно, опубликую его здесь позже. - person norekhov; 25.11.2019
comment
@norekhov, пожалуйста, опубликуйте свое решение, хотел бы извлечь из него урок. - person Ben Affleck; 25.11.2019
comment
@norekhov кстати, доступных документов действительно не так много, я следую неофициальной: heckj.github.io/swiftui-notes/. Судя по тому, что он говорит, нет никакой разницы между .map().switchToLatest() и .flatMap(). Возможно, вы из ReactiveCocoa? - person Ben Affleck; 25.11.2019
comment
Вы можете проверить мое решение здесь: github.com/tcldr/Entwine/issues/16 . Может быть, он будет включен в Entwine или автор предложит другое решение. На самом деле это всего лишь switchToLatestWaitable, но его можно комбинировать с картой, чтобы сформировать правильный switchMap - person norekhov; 25.11.2019

В самом деле, switchToLatest - это, вероятно, то, что вы ищете. Проблема в том, что глючит. По крайней мере, так оно и было; ошибка исправлена ​​в Xcode 11.4 (в настоящее время в бета-версии), и теперь switchToLatest ведет себя так, как и следовало ожидать. Теперь ваш код работает правильно:

    Just(2).map { x in
        Just(x * x).delay(for: 2.0, scheduler: RunLoop.main)
    }
    .switchToLatest()
    .sink(receiveCompletion: {_ in
        print("completed")
    }, receiveValue: {result in
        print(result)
    }).store(in:&storage)  // 4, completed
person matt    schedule 07.03.2020
comment
Когда вы говорите, что это исправлено в Xcode 11.4, означает ли это, что любые устройства под управлением iOS 13.3 или ниже будут затронуты этой ошибкой? Я предполагаю, что версия Combine, поставляемая с этими ОС, содержит ошибки, поэтому независимо от того, был ли двоичный файл собран с Xcode 11.4 или нет, вы согласны? - person Rog; 07.09.2020
comment
@Rog Я думаю, это значит, да. По сути, эту функцию Combine нельзя было использовать до iOS 13.4. Жалко, но, думаю, так и произошло. К счастью, крайне маловероятно, чтобы какой-либо пользователь с iOS 13 не обновился до iOS 13.4 или новее. - Но вы можете проверить это на себе, не нужно строить догадки. - person matt; 07.09.2020