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!)
}
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()
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()
}
}
I'm trying to implement an async call to a function loadUserFromFirebase and populate and return an array based on user attributes that I then use to write to a firebase collection.
I'm trying to use the Combine framework from Swift using future and promises but for some reason, the receiveCompletion and receiveValue don't get called and thus I get an empty array from my async function call.
Here is my code:
var cancellable = Set<AnyCancellable>()
func loadUserFromFirebase(groupUserIds: [String], handler: #escaping ([groupMate]) -> Void) {
var groupmateID = 0
var groupMates : [groupMate] = []
print("groupUserIds: \(groupUserIds)" )
for groupUserUID in groupUserIds {
print("groupUserUID: \(groupUserUID)" )
let future = Future<groupMate, Never> { promise in
self.ref.collection("Users").document(groupUserUID).getDocument(){
(friendDocument, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
// print("friendDocument: \(String(describing: friendDocument?.data()))" )
let groupUsername = (friendDocument?.data()?["username"]) as! String
let groupUID = (friendDocument?.data()?["uid"]) as! String
let groupName = (friendDocument?.data()?["name"]) as! String
let groupPic = (friendDocument?.data()?["imageurl"]) as! String
promise(.success(groupMate(id: groupmateID, uid: groupUID , name: groupName , username: groupUsername, pic: groupPic)))
}
groupmateID += 1
}
}
print("in receiveCompletion")
future.sink(receiveCompletion: { completion in
print("in receiveCompletion")
print(1, completion)
switch completion {
case .failure(let error):
print(3, error)
handler([])
return
case .finished:
break
}
},
receiveValue: {
print("in receiveValue")
groupMates.append($0)
print(groupMates)
handler(groupMates)
}).store(in: &cancellable)
}
}
func creategroup(groupName: String){
addedTogroupUsers.append(self.uid)
print("here111")
loadUserFromFirebase(groupUserIds: addedTogroupUsers) { groupMates in
print("here222")
let groupData: [String: Any] = [
"groupName": "\(groupName)",
"groupmates": groupMates
]
print("here333 \(groupData)")
print("groupMates are \(self.groupMates)")
var groupref: DocumentReference? = nil
groupref = self.ref.collection("groups").addDocument(data: groupData) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID: \(groupref!.documentID)")
for addedgroupUser in self.addedTogroupUsers {
self.ref.collection("Users").document(addedgroupUser).updateData([
"groups": FieldValue.arrayUnion([groupref!.documentID])
])
}
}
}
print("groupName is \(groupName) and addedTogroup are \(self.addedTogroupUsers)")
}
}
I'm trying to see if AnyCancellable is the way to go but since I'm using a chained array of future promises, I'm not sure how to implement it. Please let me know how you'd solve this problem so that the array does get populated since the documents do exist and the print inside the method call work but the groupMates array in the createGroup function prints an empty array afterwards. Thanks!
Edit: Added AnyCancallable to Code along with completion handler as suggested
dealing with async functions can be tricky. You are getting an empty array, because you are returning too early, in loadUserFromFirebase. Try this approach (untested) using the old style closure:
func loadUserFromFirebase(groupUserIds: [String], handler: #escaping ([groupMate]) -> Void) { // <-- here
var groupmateID = 0
var groupMates : [String] = []
print("groupUserIds: \(groupUserIds)" )
for groupUserUID in groupUserIds {
print("groupUserUID: \(groupUserUID)" )
let future = Future<groupMate, Never> { promise in
self.ref.collection("Users").document(groupUserUID).getDocument(){ (friendDocument, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
print("friendDocument: \(String(describing: friendDocument?.data()))" )
let groupUsername = (friendDocument?.data()?["username"]) as! String
let groupUID = (friendDocument?.data()?["uid"]) as! String
let groupName = (friendDocument?.data()?["name"]) as! String
let groupPic = (friendDocument?.data()?["imageurl"]) as! String
promise(.success(groupMate(id: groupmateID, uid: groupUID , name: groupName , username: groupUsername, pic: groupPic)))
}
groupmateID += 1
}
}
future.sink(receiveCompletion: { completion in
print("in receiveCompletion")
print(1, completion)
switch completion {
case .failure(let error):
print(3, error)
handler([]) // <-- here
return // <-- here
case .finished:
break
}
},
receiveValue: {
print("in receiveValue")
groupMates.append($0)
print(groupMates)
handler(groupMates) // <-- here
})
}
// <-- do not return here
}
func creategroup(groupName: String) {
addedTogroupUsers.append(self.uid)
// -- here wait until you get the data
loadUserFromFirebase(groupUserIds: addedTogroupUsers) { groupMates in
let groupData: [String: Any] = [
"groupName": "\(groupName)",
"groupmates": groupMates // <-- here
]
print("groupMates are \(self.groupMates)")
var groupref: DocumentReference? = nil
groupref = ref.collection("groups").addDocument(data: groupData) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID: \(groupref!.documentID)")
for addedgroupUser in self.addedTogroupUsers {
self.ref.collection("Users").document(addedgroupUser).updateData([
"groups": FieldValue.arrayUnion([groupref!.documentID])
])
}
}
}
print("groupName is \(groupName) and addedTogroup are \(addedTogroupUsers)")
}
}
Note, if you are targeting ios 15, macos 12, you are far better served if you use the swift 5.5 async/await/task features. They really work well.
EDIT: trying to return all results
func loadUserFromFirebase(groupUserIds: [String], handler: #escaping ([groupMate]) -> Void) {
var groupmateID = 0
var groupMates : [String] = []
print("groupUserIds: \(groupUserIds)" )
var arr: [Future<groupMate, Never>] = [Future<groupMate, Never>]()
var cancellable = Set<AnyCancellable>()
for groupUserUID in groupUserIds {
print("groupUserUID: \(groupUserUID)" )
let future = Future<groupMate, Never> { promise in
self.ref.collection("Users").document(groupUserUID).getDocument(){ (friendDocument, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
print("friendDocument: \(String(describing: friendDocument?.data()))" )
let groupUsername = (friendDocument?.data()?["username"]) as! String
let groupUID = (friendDocument?.data()?["uid"]) as! String
let groupName = (friendDocument?.data()?["name"]) as! String
let groupPic = (friendDocument?.data()?["imageurl"]) as! String
promise(.success(groupMate(id: groupmateID, uid: groupUID , name: groupName , username: groupUsername, pic: groupPic)))
}
groupmateID += 1
}
}
arr.append(future)
}
Publishers.MergeMany(arr)
.collect()
.sink { _ in
print("-----> merging ")
} receiveValue: { value in
print("-----> value: \(value)")
groupMates = value // <--- maybe append?
print(groupMates)
handler(groupMates)
}
.store(in: &cancellable)
}
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
I'm new to asking questions.
I need help in creating a feature of the app which is to unlock the tabbar when a purchase is made.
I used this code to lock my tabbar which is in the override func viewDidLoad()
I would really appreciate if someone could sort this out for me! I have been researching all day with no answers. Thanks!
if let arrayOfTabBarItems = tabBarController?.tabBar.items as AnyObject as? NSArray,let tabBarItem1 = arrayOfTabBarItems[0] as? UITabBarItem {
tabBarItem1.isEnabled = true
}
if let arrayOfTabBarItems = tabBarController?.tabBar.items as AnyObject as? NSArray,let tabBarItem2 = arrayOfTabBarItems[1] as? UITabBarItem {
if nonConsumablePurchaseMade == true {
tabBarItem2.isEnabled = true
print("done")
} else
{
tabBarItem2.isEnabled = false
print("failed")
}
}
if let arrayOfTabBarItems = tabBarController?.tabBar.items as AnyObject as? NSArray,let tabBarItem3 = arrayOfTabBarItems[2] as? UITabBarItem {
tabBarItem3.isEnabled = false
}
if let arrayOfTabBarItems = tabBarController?.tabBar.items as AnyObject as? NSArray,let tabBarItem4 = arrayOfTabBarItems[3] as? UITabBarItem {
tabBarItem4.isEnabled = false
}
and this might help. They are the functions for the purchases.
func purchaseProduct(with id: String){
SwiftyStoreKit.retrieveProductsInfo([id]) { result in
if let product = result.retrievedProducts.first {
SwiftyStoreKit.purchaseProduct(product, quantity: 1, atomically: true) { result in
switch result {
case .success(let product):
// fetch content from your server, then:
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
case .error(let error):
switch error.code {
case .unknown: print("Unknown error. Please contact support")
case .clientInvalid: print("Not allowed to make the payment")
case .paymentCancelled: break
case .paymentInvalid: print("The purchase identifier was invalid")
case .paymentNotAllowed: print("The device is not allowed to make the payment")
case .storeProductNotAvailable: print("The product is not available in the current storefront")
case .cloudServicePermissionDenied: print("Access to cloud service information is not allowed")
case .cloudServiceNetworkConnectionFailed: print("Could not connect to the network")
case .cloudServiceRevoked: print("User has revoked permission to use this cloud service")
}
}
}
}
}
}
func verifyPurchase(with id: String , sharedSecret: String) {
let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: sharedSecret)
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
switch result {
case .success(let receipt):
let productId = "myID"
// Verify the purchase of Consumable or NonConsumable
let purchaseResult = SwiftyStoreKit.verifyPurchase(
productId: id,
inReceipt: receipt)
switch purchaseResult {
case .purchased(let receiptItem):
print("\(productId) is purchased: \(receiptItem)")
if self.product_ID == self.productId {
// Save your purchase locally (needed only for Non-Consumable IAP)
self.nonConsumablePurchaseMade = true
UserDefaults.standard.set(self.nonConsumablePurchaseMade, forKey: "nonConsumablePurchaseMade")
}
case .notPurchased:
print("The user has never purchased \(productId)")
}
case .error(let error):
print("Receipt verification failed: \(error)")
}
}
}
#objc func returnTextView(gesture: UIGestureRecognizer) {
guard activeField != nil else {
return
}
activeField?.resignFirstResponder()
activeField = nil
}