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
How to Set Cmutablepointer<Objcbool> to False in Swift
Ios10, Swift 3, and Fcm Delegate Error
Swiftui Tabbedview Only Shows First Tab's Content
Xcode 6 Project Crashing After Segue on iOS 7.1
Apple Watch Table - First 4 Rows Not Appearing
Skspritenode Does Not Let Touches Pass Through When Isuserinteractionenabled False
Uiview Rounded Corner - Swift 2.0
Nsnotificationcenter Addobserver in Swift While Call a Private Method
How to Get the Today's and Tomorrow's Date in Swift 4
Spritekit/Swift - How to Check Contact of Two Nodes When They Are Already in Contact
How to Store Array in Nsuserdefault in Swift
How Do View Types Like Text or Image Conform to the View Protocol in Swiftui
Storyboard Reference to Cocoapods Storyboard Seems Broken
Removing a View Controller from Memory When Instantiating a New View Controller
How to Read Heart Rate from iOS Healthkit App Using Swift
Receipt Validation on iOS In-App-Purchase Returns Multiple Transaction