How to Transform Rx_Tap of Uibutton to a Network Request Directly Without Sending the Request in a Nested Subscribe

RxSwift: tap a button to do a request with Alamofire, how to make it work?

The main issue here is that when performing the network request, you are not waiting for the network call to respond. So observer.onComplete() is triggered before observer.onNext()

class NetManager: NSObject {

static let standard = NetManager()
var dataRequest: DataRequest?

func request() -> Observable<Bool> {
return Observable<Bool>.create { (observer) -> Disposable in
let data = // ...
// ...
self.dataRequest = AF.request(aURL, method: HTTPMethod.post, parameters: data, encoder: JSONParameterEncoder.prettyPrinted, headers: aHead).response { (response: DataResponse<Data?>) in
self.hud?.hide()
if let data = response.data {
observer.onNext(true)
// ...
}
else {
observer.onNext(false)
}
observer.onComplete() // Moving it here instead will wait for the response
}
return Disposables.create { }
}
}
}

At this point, your code should work as it is, but you're still overcomplicating a very simple thing.

Here is what to do:

btn.rx.tap
.flatMap { _ in
return NetManager.standard.request()
}
.subscribe(onNext: { (success) in
if success {
if let app = UIApplication.shared.delegate as? AppDelegate {
app.goHome()
}
}
})
.disposed(by: rx.disposeBag)

How to implement the logic without state variables

You need a state machine, but you can contain the state so you aren't leaving the monad... Something like this:

func example(activator: Observable<Bool>, signaler: Observable<Void>) -> Observable<Void> {
enum Action {
case signal
case active(Bool)
}
return Observable.merge(signaler.map(to: Action.signal), activator.map(Action.active))
.scan((isWaiting: false, isActive: false, fire: Void?.none)) { state, action in
switch action {
case .signal:
if state.isActive {
return (state.isWaiting, state.isActive, ())
}
else {
return (true, state.isActive, .none)
}
case .active(let active):
if active && state.isWaiting {
return (false, active, ())
}
else {
return (state.isWaiting, active, .none)
}
}
}
.compactMap { $0.fire }
}

Note how the logic inside the scan closure is the same as the external logic you already have. With the above, you can now do something like this:

let activator = PublishRelay<Bool>()
let signaler = PublishRelay<Void>()

example(
activator: activator.asObservable(),
signaler: signaler.asObservable()
)
.bind(onNext: f)

Lastly, as a bonus. Here's a unit test proving it works:

class RxSandboxTests: XCTestCase {
func test() {
let scheduler = TestScheduler(initialClock: 0)
let activator = scheduler.createColdObservable([.next(20, true), .next(40, false), .next(50, true), .next(70, false), .next(100, true)])
let signaler = scheduler.createColdObservable([.next(10, ()), .next(30, ()), .next(60, ()), .next(80, ()), .next(90, ()), .next(110, ())])

let result = scheduler.start {
example(activator: activator.asObservable(), signaler: signaler.asObservable())
}

XCTAssertEqual(result.events.map { $0.time }, [220, 230, 260, 300, 310])
}
}

trigger an api again once network goes off in RxSwift

Note that you didn't say how you want to deal with any other type of error so the code below just ignores them... Remove the filter if you want to show the alert for all errors.

let error = PublishSubject<Error>()

let retry = error
.filter { $0 is NoNetwork }
.map { $0.localizedDescription }
.flatMapFirst(presentScene(animated: true, scene: { message in
UIAlertController(title: "Error", message: message, preferredStyle: .alert)
.scene { $0.connectOK() }
}))
.share()

Observable.merge(buttonAction, retry)
.subscribe(onNext: { [activity] in
activity.startAnimating()
})
.disposed(by: bag)

Observable.merge(buttonAction, retry)
.flatMapFirst {
apiManager.callApi()
.catch { error.onNext($0); return .just(()) }
}
.subscribe(onNext: { [activity] in
activity.stopAnimating()
})
.disposed(by: bag)

The presentScene function comes from my CLE library. In this code activity is a standard UIActivityIndicatorView instead of whatever you are using.

How observer.onComplete() without onNext, works in chain of flatmap?. - It has to skip all the following flatmaps Right?

TL;DR - The onAction2() doesn't complete because it's waiting to see if any more button taps are going to occur.


In onAction1() you are initiating the Observable chain with a just call. The just operator emits a value and then emits a completed event.

In onAction2() you are initiating the Observable chain with a button which emits next events when tapped but doesn't emit a completed event until the button goes out of scope.

In both cases, you then route the event into a flatMapLatest call. One of the properties that flatMap Observables have is that they don't complete until all of the Observables they are subscribed to complete. In this case that's both the latest Observable that it created when it was triggered as well as the source observable.

In both cases, the Observable that the flatMap creates when triggered complete, but the source doesn't complete in the onAction2 case (as explained above) so the flatMap doesn't complete in that case. It's waiting to see if any more button taps are going to occur.

Not getting triggered on RxMoya

In the first example, you are creating and retaining the provider.

In the second example, you are creating the provider, then calling request but you aren't retaining the provider so it is deallocated immediately after the request is made and long before the server satisfies the request.

It takes a while for the network request to come back, and the provider needs to stay in existence until it does.

Swift: Sending network request one after another

A simple network layer utilizing Combine

This answer is heavily influenced by this post. I removed unnecessary code where possible and added code where needed to compose a working playground.

Api unrelated layer parts

import Foundation
import Combine

struct Agent {

func run<T: Decodable>(_ request: URLRequest, _ decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher<T, Error> {
return URLSession.shared
.dataTaskPublisher(for: request)
.tryMap { result -> T in
return try decoder.decode(T.self, from: result.data)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}

Api related layer parts

enum GithubAPI {
static let agent = Agent()
static let base = URL(string: "https://api.github.com")!
}

extension GithubAPI {

static func repos(username: String) -> AnyPublisher<[Repository], Error> {
let request = URLRequest(url: base.appendingPathComponent("users/\(username)/repos"))
return agent.run(request)
}

struct Repository: Codable {
let node_id: String
let name: String
}

static func issues(repo: String, owner: String) -> AnyPublisher<[Issue], Error> {
let request = URLRequest(url: base.appendingPathComponent("repos/\(owner)/\(repo)/issues"))
return agent.run(request)
}

struct Issue: Codable {
let url: URL
}

}

Actual clean and swifty code you are looking for

Note that issue request depends on repo request. Actual requests are triggered with sink call.

let user = "Alamofire"
let repos = GithubAPI.repos(username: user)
let firstRepo = repos.compactMap { $0.first }
let issues = firstRepo.flatMap { repo in
GithubAPI.issues(repo: repo.name, owner: user)
}
let token = issues.sink(receiveCompletion: { (_) in }) { (issues) in
print(issues)
}

RxAlmofire pass parameters to requestJSON [:]

You need to construct the URL from URLComponents.

var components = URLComponents(string: "https://www.example.com/path")!
components.queryItems = [
.init(name: "param1", value: "value1"),
.init(name: "param2", value: "value2"),
]

let url = components.url!

The url above will be

https://www.example.com/path?param1=value1¶m2=value2

The use of ! should not be problematic above as the data you are passing should be valid. Please refer to the documentation of these methods though to assess the requirement for proper nil handling.



Related Topics



Leave a reply



Submit