I expected to see timer -> Event completed get outputted right after I saw running -> Event completed but it didn't happen.
Can someone explain why and give me some idea on how to complete the timer observable?
/// playground
import RxSwift
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
struct TimerCountdown {
let timer: Observable<Int>
init(running: Observable<Bool>) {
timer = running.flatMapLatest { $0 ? Observable<Int>.interval(1.0, scheduler: MainScheduler.instance) : Observable<Int>.never() }
}
}
let running = PublishSubject<Bool>()
let countdown = TimerCountdown(running: running)
_ = running.debug("running").subscribe()
_ = countdown.timer.debug("timer").subscribe()
running.onNext(true)
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
running.onNext(false)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 7.5) {
running.onNext(true)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 10.5) {
running.onCompleted()
}
This is the solution I found:
struct TimerCountdown {
let timer: Observable<Int>
init(running: Observable<Bool>) {
timer = running
.flatMapLatest { $0 ? Observable<Int>.interval(1.0, scheduler: MainScheduler.instance) : Observable<Int>.never() }
.takeUntil(running.materialize().filter { $0.isCompleted })
}
}
Here's another answer that doesn't require subscribing to running twice:
struct TimerCountdown {
let timer: Observable<Int>
init(running: Observable<Bool>) {
timer = running.materialize()
.flatMapLatest { (event) -> Observable<Int> in
switch event {
case let .next(value):
return value ? Observable<Int>.interval(1.0, scheduler: MainScheduler.instance) : Observable.never()
case .completed:
return Observable.empty()
case let .error(error):
return Observable.error(error)
}
}
}
}
Related
I have an input that accept string, and store it in local data. My use case is when I enter an email, the user must wait for 2 minute for requesting send email verification. But when I enter a different email name, I can't make the timer reset when the last email I enter is still countdown. I'm using RxSwift for timer, I don't know how to invalidate the timer in RxSwift.
This is what I came so far to reset the timer when user enters new email
// function that accept email from uitextfield.text
func resendEmailCountdown(with email: String) {
if email != getLoggedEmail() {
startCountdown(countdown: 0)
startCountdown(countdown: 120)
} else {
startCountdown(countdown: 120)
}
}
private func startCountdown(countdown: Int) {
let counter = countdown
if counter == 0 {
_ = Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
.take(0)
.subscribe(onNext: { [weak self] countdown in
guard let self = self else { return }
}).disposed(by: disposeBag)
} else {
_ = Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
.take(counter + 1)
.subscribe(onNext: { [weak self] countdown in
guard let self = self else { return }
let count = counter - countdown
if count != 0 {
self.eventResendEmailCountdown.onNext(count)
self.eventShowHideResendEmailButton.onNext(false)
} else {
self.eventShowHideResendEmailButton.onNext(true)
self.eventDismissCountdownBottomSheet.onNext(())
}
}).disposed(by: disposeBag)
}
}
The key is using flatMapLatest to cancel the previous timer and start up a new one.
Based on your description, here is what you need:
struct Output {
let eventResendEmailCountdown: Observable<Int>
let eventShowHideResendEmailButton: Observable<Bool>
let eventDismissCountdownBottomSheet: Observable<Void>
}
func example(text: Observable<String?>) -> Output {
let trigger = text.share()
let eventResendEmailCountdown = trigger
.flatMapLatest { _ in
Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
.map { 120 - $0 }
.take(until: { $0 == -1 })
}
let eventShowHideResendEmailButton = Observable.merge(
trigger.map { _ in false },
eventResendEmailCountdown.filter { $0 == 0 }.map { _ in true }
)
let eventDismissCountdownBottomSheet = eventResendEmailCountdown
.filter { $0 == 0 }
.map { _ in }
return Output(
eventResendEmailCountdown: eventResendEmailCountdown,
eventShowHideResendEmailButton: eventShowHideResendEmailButton,
eventDismissCountdownBottomSheet: eventDismissCountdownBottomSheet
)
}
Call it something like this:
let output = example(text: textField.rx.text.asObservable())
output.eventResendEmailCountdown
.debug("eventResendEmailCountdown")
.subscribe()
output.eventShowHideResendEmailButton
.debug("eventShowHideResendEmailButton")
.subscribe()
output.eventDismissCountdownBottomSheet
.debug("eventDismissCountdownBottomSheet")
.subscribe()
I'm going to search for hashtags. The current code is now complete with the creation of a search bar. I am using RxSwift and Mvvm pattern. Here's the code I've been working on so far.How should I search for hashtags?
func setUI() {
self.searchBar = UISearchBar(frame: CGRect(x:300, y:0, width:300, height:20))
searchBtn.rx.tap.subscribe(onNext: { _ in
self.navigationItem.searchController = self.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
UIView.animate(withDuration: 0.25) {
self.searchBar.frame = CGRect(x:0, y:0, width:300, height:20)
}
}).disposed(by: rx.disposeBag)
}
This is ViewController.
private let disposeBag = DisposeBag()
struct input {
let loadData: Signal<Void>
}
struct output {
let result: Signal<String>
let loadApplyList: PublishRelay<friends>
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let result = PublishSubject<String>()
let loadApplyList = PublishRelay<friends>()
input.loadData.asObservable().subscribe(onNext: { [weak self] in
guard let self = self else { return }
api.getFriend().subscribe(onNext: { (response, statuscode) in
switch statuscode {
case .ok:
if let response = response {
loadApplyList.accept(response)
}
default:
print("default")
}
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
return output(result: result.asSignal(onErrorJustReturn: ""), loadApplyList: loadApplyList)
}
}
And this is viewModel
Today again one combine problem I currently run in and I hope that someone of you can help. How can normal unit tests be written for ObservableObjects classes which contain #Published attributes? How can I subscribe in my test to them to get the result object which I can assert?
The injected mock for the web service works correctly, loadProducts() function set exactly the same elements from the mock in the fetchedProducts array.
But I don't know currently how to access this array in my test after it is filled by the function because it seems that I cannot work with expectations here, loadProducts() has no completion block.
The code looks like this:
class ProductsListViewModel: ObservableObject {
let getRequests: GetRequests
let urlService: ApiUrls
private let networkUtils: NetworkRequestUtils
let productsWillChange = ObservableObjectPublisher()
#Published var fetchedProducts = [ProductDTO]()
#Published var errorCodeLoadProducts: Int?
init(getRequestsHelper: GetRequests, urlServiceClass: ApiUrls = ApiUrls(), utilsNetwork: NetworkRequestUtils = NetworkRequestUtils()) {
getRequests = getRequestsHelper
urlService = urlServiceClass
networkUtils = utilsNetwork
}
// nor completion block in the function used
func loadProducts() {
let urlForRequest = urlService.loadProductsUrl()
getRequests.getJsonData(url: urlForRequest) { [weak self] (result: Result<[ProductDTO], Error>) in
self?.isLoading = false
switch result {
case .success(let productsArray):
// the products filled async here
self?.fetchedProducts = productsArray
self?.errorCodeLoadProducts = nil
case .failure(let error):
let errorCode = self?.networkUtils.errorCodeFrom(error: error)
self?.errorCodeLoadProducts = errorCode
print("error: \(error)")
}
}
}
}
The test I try to write looks like this at the moment:
import XCTest
#testable import MyProject
class ProductsListViewModelTest: XCTestCase {
var getRequestMock: GetRequests!
let requestManagerMock = RequestManagerMockLoadProducts()
var productListViewModel: ProductsListViewModel!
override func setUp() {
super.setUp()
getRequestMock = GetRequests(networkHelper: requestManagerMock)
productListViewModel = ProductsListViewModel(getRequestsHelper: getRequestMock)
}
func test_successLoadProducts() {
let loginDto = LoginResponseDTO(token: "token-token")
UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)
productListViewModel.loadProducts()
// TODO access the fetchedProducts here somehow and assert them
}
}
The Mock looks like this:
class RequestManagerMockLoadProducts: NetworkRequestManagerProtocol {
var isSuccess = true
func makeNetworkRequest<T>(urlRequestObject: URLRequest, completion: #escaping (Result<T, Error>) -> Void) where T : Decodable {
if isSuccess {
let successResultDto = returnedProductedArray() as! T
completion(.success(successResultDto))
} else {
let errorString = "Cannot create request object here"
let error = NSError(domain: ErrorDomainDescription.networkRequestDomain.rawValue, code: ErrorDomainCode.unexpectedResponseFromAPI.rawValue, userInfo: [NSLocalizedDescriptionKey: errorString])
completion(.failure(error))
}
}
func returnedProductedArray() -> [ProductDTO] {
let product1 = ProductDTO(idFromBackend: "product-1", name: "product-1", description: "product-description", price: 3.55, photo: nil)
let product2 = ProductDTO(idFromBackend: "product-2", name: "product-2", description: "product-description-2", price: 5.55, photo: nil)
let product3 = ProductDTO(idFromBackend: "product-3", name: "product-3", description: "product-description-3", price: 8.55, photo: nil)
return [product1, product2, product3]
}
}
Maybe this article can help you
Testing your Combine Publishers
To solve your issue I will use code from my article
typealias CompetionResult = (expectation: XCTestExpectation,
cancellable: AnyCancellable)
func expectValue<T: Publisher>(of publisher: T,
timeout: TimeInterval = 2,
file: StaticString = #file,
line: UInt = #line,
equals: [(T.Output) -> Bool])
-> CompetionResult {
let exp = expectation(description: "Correct values of " + String(describing: publisher))
var mutableEquals = equals
let cancellable = publisher
.sink(receiveCompletion: { _ in },
receiveValue: { value in
if mutableEquals.first?(value) ?? false {
_ = mutableEquals.remove(at: 0)
if mutableEquals.isEmpty {
exp.fulfill()
}
}
})
return (exp, cancellable)
}
your test needs to use this function
func test_successLoadProducts() {
let loginDto = LoginResponseDTO(token: "token-token")
UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)
/// The expectation here can be extended as needed
let exp = expectValue(of: productListViewModel .$fetchedProducts.eraseToAnyPublisher(), equals: [{ $0[0].idFromBackend == "product-1" }])
productListViewModel.loadProducts()
wait(for: [exp.expectation], timeout: 1)
}
The easy and clearest way for me is simply to test #published var after X seconds. An example bellow :
func test_successLoadProducts() {
let loginDto = LoginResponseDTO(token: "token-token")
UserDefaults.standard.save(loginDto, forKey: CommonConstants.persistedLoginObject)
productListViewModel.loadProducts()
// TODO access the fetchedProducts here somehow and assert them
let expectation = XCTestExpectation()
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
XCTAssertEqual(self.productListViewModel.fetchedProducts, ["Awaited values"])
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
}
I hope that helps !
I'm new to RxSwift and trying implement app that using MVVM architecture. I have view model:
class CategoriesViewModel {
fileprivate let api: APIService
fileprivate let database: DatabaseService
let categories: Results<Category>
// Input
let actionRequest = PublishSubject<Void>()
// Output
let changeset: Observable<(AnyRealmCollection<Category>, RealmChangeset?)>
let apiSuccess: Observable<Void>
let apiFailure: Observable<Error>
init(api: APIService, database: DatabaseService) {
self.api = api
self.database = database
categories = database.realm.objects(Category.self).sorted(byKeyPath: Category.KeyPath.name)
changeset = Observable.changeset(from: categories)
let requestResult = actionRequest
.flatMapLatest { [weak api] _ -> Observable<Event<[Category]>> in
guard let strongAPI = api else {
return Observable.empty()
}
let request = APIService.MappableRequest(Category.self, resource: .categories)
return strongAPI.mappedArrayObservable(from: request).materialize()
}
.shareReplayLatestWhileConnected()
apiSuccess = requestResult
.map { $0.element }
.filterNil()
.flatMapLatest { [weak database] newObjects -> Observable<Void> in
guard let strongDatabase = database else {
return Observable.empty()
}
return strongDatabase.updateObservable(with: newObjects)
}
apiFailure = requestResult
.map { $0.error }
.filterNil()
}
}
and I have following binginds in view controller:
viewModel.apiSuccess
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.apiFailure
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
But if I comment bindings, part with database updating stops executing. I need to make it execute anyway, without using dispose bag in the view model. Is it possible?
And little additional question: should I use weak-strong dance with api/database and return Observable.empty() like in my view model code or can I just use unowned api/unowned database safely?
Thanks.
UPD:
Function for return observable in APIService:
func mappedArrayObservable<T>(from request: MappableRequest<T>) -> Observable<[T]> {
let jsonArray = SessionManager.jsonArrayObservable(with: request.urlRequest, isSecured: request.isSecured)
return jsonArray.mapResponse(on: mappingSheduler, { Mapper<T>().mapArray(JSONArray: $0) })
}
Work doesn't get done unless there is a subscriber prepared to receive the results.
Your DatabaseService needs to have a dispose bag in it and subscribe to the Observable<[Category]>. Something like:
class ProductionDatabase: DatabaseService {
var categoriesUpdated: Observable<Void> { return _categories }
func updateObservable(with categories: Observable<[Category]>) {
categories
.subscribe(onNext: { [weak self] categories in
// store categories an then
self?._categories.onNext()
})
.disposed(by: bag)
}
private let _categories = PublishSubject<Void>()
private let bag = DisposeBag()
}
Then apiSuccess = database.categoriesUpdated and database.updateObservable(with: requestResult.map { $0.element }.filterNil())
I'm new in RXSwift, I just try to implement a simple async example
but subscribe will never be called.
What I miss ?
let disposeBag = DisposeBag()
Observable<Any>.create {
observer in
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 3) {
observer.onNext("done")
observer.onCompleted()
}
return Disposables.create()
}
.subscribe { print($0) }
.addDisposableTo(disposeBag)
================= UPDATE ======================
I'm trying to do something more advanced, a chain which allow pass value from previous, but the result was not expected. what I miss ?
output was
next(done 1 - done 2)
what I expected was
next(done 1)
next(done 1 - done 2)
completed
class AsyncObject {
func asyncTest1() -> Observable<String> {
return Observable<String>.create {
(o: AnyObserver<String>) -> Disposable in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
o.onNext("done 1")
o.onCompleted()
}
return Disposables.create()
}
}
func asyncTest2(value: String) -> Observable<String> {
return Observable<String>.create {
(o: AnyObserver<String>) -> Disposable in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
o.onNext("\(value) - done 2")
o.onCompleted()
}
return Disposables.create()
}
}
}
class ViewController: UIViewController {
let disposeBag = DisposeBag()
let observer = AsyncObject()
// MARK: Init Methods
override func viewDidLoad() {
super.viewDidLoad()
self.observer.asyncTest1()
.flatMap { self.observer.asyncTest2(value: $0) }
.subscribe { print($0) }
.addDisposableTo(disposeBag)
}
}
I think you can merge this with the original one, to achieve the expected result. So it should look something like this
let test1 = self.observer.asyncTest1()
let test2 = test1.flatMap { self.observer.asyncTest2() }
Observable
.of(test1, test2)
.merge()
.subscribe { print($0) }
.addDisposableTo(disposeBag)
got help from RxSwift Slack #sergdot,
let test1 = self.observer.asyncTest1().shareReplay(1)
let test2 = test1.flatMap { self.observer.asyncTest2(value: $0) }
Observable.of(test1, test2).merge().subscribe {
print($0)
}
or
let test1 = self.observer.asyncTest1().shareReplay(1)
let test2 = test1.flatMap { self.observer.asyncTest2(value: $0) }
test1.concat(test2).subscribe {
print($0)
}