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()
Related
I have a function which returns a list of Items using elastic search and falls back on realm cache. I'm wondering how can I use Combine to achieve the same.
I am trying to do something like this where I have a publisher for each store but I am getting stuck on the sorting them by score.
func search(for text: String) -> AnyPublisher<[Item], Error> {
return store.search(with: text)
// Invalid syntax *
.map { searchResults in
let sorted = cacheStore.search(with: text)
.map { items in
items
.map { item in (item, searchResults.first { $0.id == item.id }?.score ?? 0) }
.sorted { $0.1 > $1.1 } // by score
.map { $0.0 } // to item
}
return sorted.eraseToAnyPublisher()
}
// *
.catch { _ in cacheStore.search(with: text) }
.eraseToAnyPublisher()
}
This is the original function.
func search(for text: String, completion: #escaping (Result<[Item], Error>) -> Void) {
store.search(with: text) {
// Search network via elastic search or fall back to cache search
// searchResults is of type [(id: Int, score: Double)] where id is item.id
guard let searchResult = $0.value, $0.isSuccess else {
return self.cacheStore.search(with: text, completion: completion)
}
self.cacheStore.fetch(ids: searchResult.map { $0.id }) {
guard let items = $0.value, $0.isSuccess else {
return self.cacheStore.search(with: text, completion: completion)
}
let scoredItems = items
.map { item in (item, searchResult.first { $0.id == item.id }?.score ?? 0) }
.sorted { $0.1 > $1.1 } // by score
.map { $0.0 } // to item
completion(.success(scoredItems))
}
}
}
I think what you are aiming for is something like the Playground below.
Most of the playground is code is code that mocks up searches using Futures. The particularly relevant section is:
return searchNetwork(key: key)
.map { key,value in cache[key] = value; return value }
.catch {_ in searchCache(key: key) }
.eraseToAnyPublisher()
If the network request from searchNetwork succeeds then the value passes through the map which adds it to the cache and returns the value from the network. If searchNetwork fails then catch will substitute the publisher that searches the cache.
import Foundation
import Combine
var cache = [
"one" : "for the money",
"two" : "for the show"
]
enum SearchError: Error {
case cacheMiss
case networkFailure
}
func searchCache(key : String) -> AnyPublisher<String, SearchError>
{
return Future<String, SearchError> { fulfill in
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
if let value = cache[key] {
fulfill(.success(value))
} else {
fulfill(.failure(.cacheMiss))
}
}
}.eraseToAnyPublisher()
}
func searchNetwork(key: String) -> AnyPublisher<(String, String), SearchError> {
return Future<(String, String), SearchError> { fulfill in
fulfill(.failure(.networkFailure))
}.eraseToAnyPublisher()
}
func search(for key: String) -> AnyPublisher<String, SearchError> {
return searchNetwork(key: key)
.map { key,value in cache[key] = value; return value }
.catch {_ in searchCache(key: key) }
.eraseToAnyPublisher()
}
let searchForOne = search(for: "one").sink(
receiveCompletion: { debugPrint($0) },
receiveValue: { print("Search for one : \($0)") }
)
let searchForThree = search(for: "three").sink(
receiveCompletion: { debugPrint($0) },
receiveValue: { print("Search for three : \($0)") }
)
I figured out the solution by doing something like this:
let cachedPublisher = cacheStore.search(with: text)
let createPublisher: (Item) -> AnyPublisher<Item, Error> = {
return Just($0).eraseToAnyPublisher()
}
return store.search(with: request)
.flatMap { Item -> AnyPublisher<[Item], Error> in
let ids = searchResults.map { $0.id }
let results = self.cacheStore.fetch(ids: ids, filterActive: true)
.flatMap { items -> AnyPublisher<[Item], Error> in
let sorted = items
.map { item in (item, searchResults.first { $0.id == item.id }?.score ?? 0) }
.sorted { $0.1 > $1.1 } // by score
.map{ $0.0 } // to item
return Publishers.mergeMappedRetainingOrder(sorted, mapTransform: createPublisher) // Helper function that calls Publishers.MergeMany
}
return results.eraseToAnyPublisher()
}
.catch { _ in cachedPublisher }
.eraseToAnyPublisher()
Whenever paging in tableview, the view model is running fetchDataRx. It works well, but I don't think it's right to sort the entire data every time you paginate and call the addSnapShotListener. If my code is not correct, how can I correct it?
// MARK: ViewController.swift
timeLineTableView.rx.didScroll
.withLatestFrom(viewModel.activated)
.subscribe(onNext: { [weak self] isActivated in
if !isActivated {
guard let self = self else { return }
let position = self.timeLineTableView.contentOffset.y
if position > self.timeLineTableView.contentSize.height - 100 - self.timeLineTableView.frame.size.height {
self.viewModel.fetchPosts.onNext(())
}
}
})
.disposed(by: disposeBag)
//MARK: ViewModel.swift
let fetchPosts: AnyObserver<Void>
let fetching = PublishSubject<Void>()
fetchPosts = fetching.asObserver()
fetching
.do(onNext: { _ in activating.onNext(true) })
.withLatestFrom(posts)
.map { $0.count }
.flatMap{ (count) -> Observable<[post]> in
fireBaseService.fetchDataRx(startIdx: count) }
.map { $0.map { ViewPost(post: $0) } }
.do(onNext: { _ in activating.onNext(false) })
.do(onError: { err in error.onNext(err) })
.subscribe(onNext: { newPosts in
let oldData = posts.value
posts.accept(oldData + newPosts)
})
.disposed(by: disposeBag)
//MARK: FirebaseService.swift
protocol FirebaseServiceProtocol {
func fetchDataRx(startIdx: Int) -> Observable<[post]>
func fetchData(startIdx: Int, completion: #escaping (Result<[post], Error>) -> Void)
}
class FireBaseService: FirebaseServiceProtocol {
func fetchDataRx(startIdx: Int) -> Observable<[post]> {
return Observable.create { (observer) -> Disposable in
self.fetchData(startIdx: startIdx) { result in
switch result {
case .success(let data):
observer.onNext(data)
case .failure(let error):
observer.onError(error)
}
observer.onCompleted()
}
return Disposables.create()
}
}
func fetchData(startIdx: Int, completion: #escaping (Result<[post], Error>) -> Void) {
let db = Firestore.firestore()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
if startIdx == 0 {
DispatchQueue.global().async {
let first = db.collection("lolCourt")
.order(by: "date")
.limit(to: 8)
var nextPosts = [post]()
first.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in querySnapshot!.documents {
guard let url = document.data()["url"] as? String else {
continue
}
guard let champion1 = document.data()["champion1"] as? String else {
continue
}
guard let champion1Votes = document.data()["champion1Votes"] as? Double else {
continue
}
guard let champion2 = document.data()["champion2"] as? String else {
continue
}
guard let champion2Votes = document.data()["champion2Votes"] as? Double else {
continue
}
guard let text = document.data()["text"] as? String else {
continue
}
guard let date = document.data()["date"] as? Double else {
continue
}
nextPosts.append(post(url: url,
champion1: champion1,
champion1Votes: champion1Votes,
champion2: champion2,
champion2Votes: champion2Votes,
text: text,
date: formatter.string(from: Date(timeIntervalSince1970: date))))
}
}
completion(.success(nextPosts))
}
}
}
else {
DispatchQueue.global().async {
let first = db.collection("lolCourt")
.order(by: "date")
.limit(to: startIdx)
first.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error retrieving : \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
return
}
let next = db.collection("lolCourt")
.order(by: "date")
.start(afterDocument: lastSnapshot)
.limit(to: 8)
var nextPosts = [post]()
next.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in querySnapshot!.documents {
guard let url = document.data()["url"] as? String else {
continue
}
guard let champion1 = document.data()["champion1"] as? String else {
continue
}
guard let champion1Votes = document.data()["champion1Votes"] as? Double else {
continue
}
guard let champion2 = document.data()["champion2"] as? String else {
continue
}
guard let champion2Votes = document.data()["champion2Votes"] as? Double else {
continue
}
guard let text = document.data()["text"] as? String else {
continue
}
guard let date = document.data()["date"] as? Double else {
continue
}
nextPosts.append(post(url: url,
champion1: champion1,
champion1Votes: champion1Votes,
champion2: champion2,
champion2Votes: champion2Votes,
text: text,
date: formatter.string(from: Date(timeIntervalSince1970: date))))
}
}
completion(.success(nextPosts))
}
}
}
}
}
}
I think you are doing it wrong... I would expect to see something more like this:
class FireBaseService {
func getPage<T>(query: Query? = nil, build: #escaping ([String: Any]) -> T) -> Observable<([T], nextPage: Query?)> {
Observable.create { observer in
let db = Firestore.firestore()
let page = query ?? db.collection("lolCourt")
.order(by: "date")
.limit(to: 8)
let listener = page
.addSnapshotListener { snapshot, error in
guard let snapshot = snapshot else { observer.onError(error ?? RxError.unknown); return }
let items = snapshot.documents.map { build($0.data()) }
if let lastSnapshot = snapshot.documents.last {
let next = page
.start(afterDocument: lastSnapshot)
observer.onSuccess((items, nextPage: next))
}
else {
observer.onSuccess((items, nextPage: nil))
}
}
return Disposables.create { listener.remove() }
}
}
}
Use the above in your favorite state machine system. Here is an example using my CLE library.
// in view controller
let fireBaseService = FireBaseService()
let activityIndicator = ActivityIndicator()
let errorRouter = ErrorRouter()
func getPage(nextPage: Query?) -> Observable<([Post?], nextPage: Query?)> {
fireBaseService.getPage(query: nextPage, build: Post.init(dict:))
.rerouteError(errorRouter)
.trackActivity(activityIndicator)
}
let posts = cycle(
inputs: [
getPage(nextPage: nil).map(ViewModel.Input.response),
timeLineTableView.rx.reachedBottom(offset: 20).map(to: ViewModel.Input.next)
],
initialState: ([Post?](), nextPage: Query?.none),
environment: getPage(nextPage:),
reduce: ViewModel.reduce(state:input:getPage:)
)
.map { $0.0.compactMap { $0 } }
and the view model:
enum ViewModel {
enum Input {
case response([Post?], nextPage: Query?)
case next
}
static func reduce(state: inout ([Post?], nextPage: Query?), input: Input, getPage: #escaping (Query) -> Observable<([Post?], nextPage: Query?)>) -> Observable<Input> {
switch input {
case let .response(posts, nextPage):
state.0 += posts
state.nextPage = nextPage
case .next:
guard let nextPage = state.nextPage else { break }
return getPage(nextPage)
.map(Input.response)
}
return .empty()
}
}
ViewModel:
searchButtonInDidTapSubject.withLatestFrom(retailIdSubject.map { text in return text}).flatMapLatest { [unowned self] retailId in
return service.searchRetailPayCashBack(retailId: retailId, createdAt: self.createdDates, sig: self.sig).materialize();
}.subscribe(onNext: { [weak self] event in
switch (event) {
case .next(_):
self?.checkResultSubject.onNext(true)
break;
case .error(let error):
self?.errorSubject.onNext(error as! ErrorResponse);
break;
default:
break;
}
}).disposed(by: disposeBag);
// here i need to convert retailId , currentTime and accessToken
after that i send request api
private var sig: String{
get {
let accessToken = self.keychain.get(Constants.accessToken)
// ???
let newAccessToken = String((accessToken?.substring(with: 11..<21))!)
let retailid = ???
let newSig = "\(newAccessToken)\(self.createdDates)\(retailid)"
let md5Base64 = newSig.base64Encoded()
let md5Data = self.MD5(md5Base64!)
return String(md5Data!)
}
}
retailId needs to be equal to retailid
Not sure what are you trying to achieve, but it sounds like the sig should be generated each time you tap the button, because the retailId can be different.
searchButtonInDidTapSubject
.withLatestFrom(retailIdSubject.map { text in return text })
.flatMapLatest { [unowned self] retailId in
return service
.searchRetailPayCashBack(
retailId: retailId,
createdAt: self.createdDates,
sig: self.sig(withRetailId: retailId, createdDate: self.createdDates)
)
.materialize()
}
.subscribe(onNext: { [weak self] event in
switch (event) {
case .next(_):
self?.checkResultSubject.onNext(true)
break;
case .error(let error):
self?.errorSubject.onNext(error as! ErrorResponse);
break;
default:
break;
}
})
.disposed(by: disposeBag)
func sig(withRetailId retailId: String, createdDate: String) -> String {
let accessToken = self.keychain.get(Constants.accessToken)
// ???
let newAccessToken = String((accessToken?.substring(with: 11..<21))!)
let newSig = "\(newAccessToken)\(createdDate)\(retailId)"
let md5Base64 = newSig.base64Encoded()
let md5Data = self.MD5(md5Base64!)
return String(md5Data!)
}
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)
}
}
}
}
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)
}