How to merge String & Int to get in ONE flatMap - swift

enum Input { case text(String); case page(Int) }
I am managing pagination with keyword search to API method.
Now I either can pass keywords or page number, but not both at same time in Rx.
I have written following code with help of some existing available gist
let start = Observable.merge(reload, loadNext)
let stringObservable = keyword.asObservable().map { Input.text($0) }
let intObservable = start.asObservable().map { Input.page($0) }
let request_call = Observable.of(stringObservable, intObservable).merge()
let page = request_call
.flatMap { input in
Observable.combineLatest(Observable.just($0), api.loadData(page: $0, keyword: "breaking")) { (pageNumber: $0, items: $1) }
.materialize()
.filter { $0.isCompleted == false }
}
.share()
start keep Page Number, & keyword keeps search keywords.
I need to merge both, I did using ENUM & Merge,
Now I have to call API, but showing as Input,
So How can I get both values in one flatMap

Get rid of the Input enum and use combineLatest instead of merge.
Then request_call will be an Observable<(String, Int)> and you can use the two values in the loadData function.
let start = Observable.merge(reload, loadNext)
let stringObservable = keyword.asObservable()
let intObservable = start.asObservable()
let request_call = Observable.combineLatest(stringObservable, intObservable)
let page = request_call
.flatMap { text, page in
Observable.combineLatest(Observable.just(page), api.loadData(page: page, keyword: text)) { (pageNumber: $0, items: $1) }
.materialize()
.filter { $0.isCompleted == false }
}
.share()

Related

Subscription call 2 times, how to unsubscribe observable in RxSwift

Reset password call occurs 2 times, how can I remove the subscription in that block.
let digits = [firstDigit, secondDigit, thirdDigit, fourthDigit, fifthDigit, sixthDigit]
let digitsValid = digits.map { $0?.textField.rx.text.orEmpty.map({ $0.count == 1 }).share(replay: 1) }
let allDigitsFilled = Observable.combineLatest(digitsValid.map{ $0 ?? BehaviorRelay(value: false).asObservable()}).share(replay: 1)
allDigitsFilled.subscribe (onNext: { [weak self] (boolArray) in
guard let self = self else { return }
let isFilled = boolArray.allSatisfy({ $0 })
if isFilled {
self.viewModel.resetPassword()
}
}).disposed(by: disposeBag)
Your onNext block will be called every time any of the text fields changes its contents. Your first goal should be to make it so the block is only called when you want to reset the password.
So first, you want to put the .allSatisfy inside an Observable.filter call so your onNext block will only be called when all six text fields contain exactly 1 character. Once you do that, then you can simply use .take(1) which will complete the subscription once a next value is emitted.
Something like this should do it, and looks quite a bit cleaner:
let digits = [firstDigit, secondDigit, thirdDigit, fourthDigit, fifthDigit, sixthDigit]
let texts = digits.compactMap { $0?.textField.rx.text.orEmpty }
Observable.combineLatest(texts)
.filter { $0.allSatisfy { $0.count == 1 } }
.map { _ in }
.take(1)
.subscribe(onNext: { [viewModel] in
viewModel?.resetPassword()
})
.disposed(by: disposeBag)

RXSwift ObservableCollection with CombineLatest

I am trying to implement something like this,
let api1 = Observable.of(["documents"]) //Replace with observable to download docs
let api2 = Observable.of(["applications"]) //Replace with observable to download apps
let api3 = Observable.of(["videos"]) //Replace with observable to download videos
Observable.combineLatest(api1, api2, api3){(docs, apps, videos) in
return (docs, apps, videos)
}.skipWhile{ (docs, apps, videos) in
return docs.count == 0 && apps.count == 0 && videos.count == 0
}.subscribe(onNext:{(docs, apps, videos) in
})
.disposed(by:disposeBag)
In my case, I am trying to create observables dynamically and add it to an array like this,
private var discoverObservables = [Observable<Any>]()
func loadDiscoverFeeds(){
self.feeds.forEach({
feed in
switch feed.feedType{
case "a":
let observable = self.aObservable(url: feed.feedURL ?? "")
self.discoverObservables.append(observable)
break
case "b":
let observable = self.bObservable(url: feed.feedURL ?? "")
self.discoverObservables.append(observable)
break
case "c":
let observable = self.cObservable(url: feed.feedURL ?? "")
self.discoverObservables.append(observable)
break
case "d" :
let observable = self.dObservable(url: feed.feedURL ?? "")
self.discoverObservables.append(observable)
break
default:
break
}
})
}
private func aObservable(url : String) -> Observable<A?>{
return APIManager.shared.getA(url: url)
}
private func bObservable(url : String) -> Observable<B?>{
return APIManager.shared.getB(url: url)
}
private func cObservable(url : String) -> Observable<C?>{
return APIManager.shared.getC(url: url)
}
But this is not working because discoverObservables array is expecting the value of Type Observable<Any> and I am trying to add Observable<A?>
How can I do this correctly, I want to make sure all the observables return data before I start processing the data.
Edit
I am trying to load data from different sources before that is added to the view, basically, I have a collectionview, each section loads data from different API, I am trying to get all the required data from all sources before that is added to collection view.
Add the same protocol to A, B and C.
protocol YourProtocol {...}
class A: YourProtocol {...}
class B: YourProtocol {...}
class C: YourProtocol {...}
Then you can make :
private var discoverObservables = [Observable<YourProtocol>]()
The first code block seems to be doing the job with one exception, the condition checks if all of the (docs, apps, videos) are empty, perhaps you wanted to use || instead of &&.
As for the second code block with an array, I did something that could help.
struct A {}
let observable1 = Observable.just(A())
let observable2 = Observable.just(A())
let observable3 = Observable.just(A())
let observables: [Observable<A>] = [observable1, observable2, observable3]
Observable.combineLatest(observables).skipWhile { (streams) -> Bool in
streams.forEach {
if $0.count == 0 { return true }
}
return false
}.subscribe(...
This subscription will result with Observable<[A]>.
I'm going to specifically address this from your question: "I want to make sure all the observables return data before I start processing the data."
Strictly speaking, you probably don't want an Any structure. Better would be a protocol or enum. I see that other answers have addressed the protocol idea so I will use the enum idea:
enum EndpointResponse {
case a(A?)
case b(B?)
// etc...
}
let responses = Observable.zip(
feeds.map { (feed) -> Observable<EndpointResponse> in
switch feed.feedType {
case "a":
return aObservable(url: feed.feedURL ?? "").map { EndpointResponse.a($0) }
case "b":
return bObservable(url: feed.feedURL ?? "").map { EndpointResponse.b($0) }
default:
fatalError()
}
}
)
The above responses observable will contain an array of all the responses once they have all emitted values. In other words, the zip operator will gather up all the responses from all the network calls and emit a single array containing all of them.
My Previous answer:
There really isn't a lot of information to go on in the question, but something like this answers the direct question you ask about converting an Observable<X> to an Observable<Any>...
let discoverObservables = Observable.zip(
feeds.map { (feed) -> Observable<Any> in
switch feed.feedType {
case "a":
return aObservable(url: feed.feedURL ?? "").map { $0 as Any }
case "b":
return bObservable(url: feed.feedURL ?? "").map { $0 as Any }
case "c":
return cObservable(url: feed.feedURL ?? "").map { $0 as Any }
case "d":
return dObservable(url: feed.feedURL ?? "").map { $0 as Any }
default:
break
}
}
)

return and exit from compactMap

I am checking some dynamic texts with Regex Rules, using switch for each index of regex pattern array. everything is working fine but in the last case i want the map to stop mapping and do an early return! but seems like i can't return inside a map function! any better approach or solution ?
fileprivate func regexMatch(pattern regex: [String], in text: String) -> Dictionary<String, Any>
{
do
{
for (index, string) in regex.enumerated()
{
let regex = try NSRegularExpression(pattern: string, options: .caseInsensitive)
let results: [NSTextCheckingResult] = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
_ = results.compactMap
{
/* If i put a guard check to see if result is filled
* then return, works fine but iterates again for the next index and keeps returning till it’s over!
*/
switch index
{
case 0:
// Does something
case 1:
// Does something else
case 2:
// Does the last thing
// If succeed finding the match! Just return or exit the map!
let carNames = String(Range($0.range, in: text).map { String(text[$0]) }!).lowercased()
for case let car as Car in carsList!
{
if spottedCarNamesInReceipt.contains(car.name!.lowercased())
{
result["car"] = car
return // This does work though, but the map starts iterating again over the next index!
}
}
default:
break;
}
}
}
return result
} catch let error
{
print("Invalid regex: \(error.localizedDescription)")
return [:]
}
You don't need to use compactMap if you don't use results. And there is no way to exit from compactMap. Use for in cycle.
Using return statement forEach loop or any kind of Maps, exits only for the current call in the closure while for...in loop exits all the next subsequent calls as well. so for...in solves the problem if an early exit is needed.

Swift Get Next Page from header of NSHTTPURLResponse

I am consuming an API that gives me the next page in the Header inside a field called Link. (For example Github does the same, so it isn't weird.Github Doc)
The service that I am consuming retrieve me the pagination data in the following way:
As we can see in the "Link" gives me the next page,
With $0.response?.allHeaderFields["Link"]: I get </api/games?page=1&size=20>; rel="next",</api/games?page=25&size=20>; rel="last",</api/games?page=0&size=20>; rel="first".
I have found the following code to read the page, but it is very dirty... And I would like if anyone has dealt with the same problem or if there is a standard way of face with it. (I have also searched if alamofire supports any kind of feature for this but I haven't found it)
// MARK: - Pagination
private func getNextPageFromHeaders(response: NSHTTPURLResponse?) -> String? {
if let linkHeader = response?.allHeaderFields["Link"] as? String {
/* looks like:
<https://api.github.com/user/20267/gists?page=2>; rel="next", <https://api.github.com/user/20267/gists?page=6>; rel="last"
*/
// so split on "," the on ";"
let components = linkHeader.characters.split {$0 == ","}.map { String($0) }
// now we have 2 lines like '<https://api.github.com/user/20267/gists?page=2>; rel="next"'
// So let's get the URL out of there:
for item in components {
// see if it's "next"
let rangeOfNext = item.rangeOfString("rel=\"next\"", options: [])
if rangeOfNext != nil {
let rangeOfPaddedURL = item.rangeOfString("<(.*)>;", options: .RegularExpressionSearch)
if let range = rangeOfPaddedURL {
let nextURL = item.substringWithRange(range)
// strip off the < and >;
let startIndex = nextURL.startIndex.advancedBy(1) //advance as much as you like
let endIndex = nextURL.endIndex.advancedBy(-2)
let urlRange = startIndex..<endIndex
return nextURL.substringWithRange(urlRange)
}
}
}
}
return nil
}
I think that the forEach() could have a better solution, but here is what I got:
let linkHeader = "</api/games?page=1&size=20>; rel=\"next\",</api/games?page=25&size=20>; rel=\"last\",</api/games?page=0&size=20>; rel=\"first\""
let links = linkHeader.components(separatedBy: ",")
var dictionary: [String: String] = [:]
links.forEach({
let components = $0.components(separatedBy:"; ")
let cleanPath = components[0].trimmingCharacters(in: CharacterSet(charactersIn: "<>"))
dictionary[components[1]] = cleanPath
})
if let nextPagePath = dictionary["rel=\"next\""] {
print("nextPagePath: \(nextPagePath)")
}
//Bonus
if let lastPagePath = dictionary["rel=\"last\""] {
print("lastPagePath: \(lastPagePath)")
}
if let firstPagePath = dictionary["rel=\"first\""] {
print("firstPagePath: \(firstPagePath)")
}
Console output:
$> nextPagePath: /api/games?page=1&size=20
$> lastPagePath: /api/games?page=25&size=20
$> firstPagePath: /api/games?page=0&size=20
I used components(separatedBy:) instead of split() to avoid the String() conversion at the end.
I created a Dictionary for the values to hold and removed the < and > with a trim.

What are some ways I can optimize this special cased closure?

I want to use the sorted method on an array.
public func sorted(by areInIncreasingOrder: (Self.Iterator.Element, Self.Iterator.Element) -> Bool) -> [Self.Iterator.Element]
And I can see that it takes a function (which takes 2 Elements and returns a Bool), and then returns an an array of the Element (sorted)
Where I am getting stuck, is by trying to pass an anonymous function (closure), using $0 and $1 as arguments
I want to add a special case, for a specific key ("module")
And then I want to access a property on $0 and $1
But this means, ostensibly, that I have to cast those arguments, so that I can then access the properties on those arguments
And to make things worse, because the arguments could either be String or Node, I currently have repeated code (the switch statement)
So anyways, do any of you Swift veterans see things that I could do to make this better?
return self.sorted(by: {
for descriptor in sortDescriptors {
if descriptor.key == "module" {
if let firstNode = $0 as? Node {
if let secondNode = $1 as? Node {
let firstModule = firstNode.module?.name ?? ""
let secondModule = secondNode.module?.name ?? ""
let newDescriptor = NSSortDescriptor(key: nil, ascending: descriptor.ascending, selector: descriptor.selector)
switch newDescriptor.compare(firstModule, to: secondModule) {
case .orderedAscending:
return true
case .orderedDescending:
return false
case .orderedSame:
continue
}
}
}
}
switch descriptor.compare($0, to: $1) {
case .orderedAscending:
return true
case .orderedDescending:
return false
case .orderedSame:
continue
}
}
return false
})
}