I use a form for a logging page in my app and I have a bind on the footer to display any error, as you can see below :
ContentView.Swift :
Form { Section(footer: Text(self.viewModel.errorMessage))
ViewModel.swift
init() {
self.isCurrentNameValid
.receive(on: RunLoop.main)
.map { $0 ? "" : "username must at least have 5 characters" }
.assign(to: \.errorMessage, on: self)
.store(in: &cancelSet)
}
The problem is the assign in the viewModel is perform in the init so when I launch my app it will display the message even though the user didn't try to write anything yet.
Is there a way to skip first event like in RxSwift where you would just .skip(1) in combine framework?
Insert the .dropFirst() operator.
self.isCurrentNameValid
.dropFirst()
// ... the rest is as before ...
Related
I am trying organize and design a Combine-based API framework for communicating and reacting to OBS Studio (live-streaming software), using obs-websocket. I've written my own Combine Subject-wrapped Publisher for WebSocket communication, and am now using that to talk with OBS. I've created Publishers for sending request messages to OBS, as well as listening for event messages emitted by OBS. I've also been developing this alongside an actual SwiftUI app that uses the new framework.
While developing the app, I found that there are some complex combinations of requests and event listeners that I would need to use around the app. So, as part of the framework, I built out a few Publishers that merge results from an initial request and event listeners for changes to those properties. Testing these out, they worked great, so I committed to git and attempted to implement them in my app code. Instead, they didn't work consistently. I realized that by using the same Publishers in multiple places of the app, it was creating duplicate Publishers (because almost all Publishers are structs/value-types).
So, I found an article about implementing the .share() operator (https://www.swiftbysundell.com/articles/using-combines-share-operator-to-avoid-duplicate-work/) and I tried it out. Specifically, I set up a system for storing the different publishers that could be recalled while still active. Some of them are keyed by relevant values (like keying by the URL in the article), but others are just single values, as there wouldn't be more than one of that publisher at a time. That worked fine.
class PublisherStore {
typealias ResponsePublisher = AnyPublisher<OBSRequestResponse, Error>
var responsePublishers = [String: ResponsePublisher]()
typealias BatchResponsePublisher = AnyPublisher<OpDataTypes.RequestBatchResponse, Error>
var batchResponsePublishers = [String: BatchResponsePublisher]()
typealias EventPublisher = AnyPublisher<OBSEvent, Error>
var eventPublishers = [OBSEvents.AllTypes: EventPublisher]()
var eventGroupPublishers = [String: EventPublisher]()
var anyOpCode: AnyPublisher<UntypedMessage, Error>? = nil
var anyOpCodeData: AnyPublisher<OBSOpData, Error>? = nil
var allMessagesOfType = [OBSEnums.OpCode: AnyPublisher<OBSOpData, Error>]()
var studioModeState: AnyPublisher<Bool, Error>? = nil
var currentSceneNamePair: AnyPublisher<SceneNamePair, Error>? = nil
var sceneList: AnyPublisher<[OBSRequests.Subtypes.Scene], Error>? = nil
var sceneItemList = [String: AnyPublisher<[OBSRequests.Subtypes.SceneItem], Error>]()
var activeSceneItemList: AnyPublisher<[OBSRequests.Subtypes.SceneItem], Error>? = nil
var sceneItemState = [String: AnyPublisher<SceneItemStatePair, Error>]()
}
Where I started running into issues is attempting to implement the final part of the article: adding a custom DispatchQueue. What's been confusing me is the placement of subscribe(on:)/receive(on:) operators, and which ones should be to DispatchQueue.main vs. my internal custom queue. Here's what I have in my primary chain that calls one of the custom merged Publishers:
try connectToOBS()
.handleEvents(receiveOutput: { _ in print("Main thread outside before?:", Thread.isMainThread) })
// <1>
.tryFlatMap { _ in try studioModeStatePublisher() } // <- custom Publisher
// <2>
.handleEvents(receiveOutput: { _ in print("Main thread outside after?:", Thread.isMainThread) })
.output(in: 0..<4)
.sink(receiveCompletion: { print("Sink completion:", $0); expectation1.fulfill() },
receiveValue: { _ in })
.store(in: &observers)
I have .receive(on: DispatchQueue.main)
Should I be placing .receive(on: DispatchQueue.main) at <1> or <2>? When I put it at <2> or leave it out, I don't get any print outs past the custom publisher. If I put it at <1>, it works, but is that the right way to do it? Here is the code for the custom publisher (sorry for it being a bit messy):
public func getStudioModeStateOnce() throws -> AnyPublisher<Bool, Error> {
return try sendRequest(OBSRequests.GetStudioModeEnabled())
.map(\.studioModeEnabled)
// If error is thrown because studio mode is not active, replace that error with false
.catch { error -> AnyPublisher<Bool, Error> in
guard case Errors.requestResponseNotSuccess(let status) = error,
status.code == .studioModeNotActive else { return Fail(error: error).eraseToAnyPublisher() }
return Just(false)
.setFailureType(to: Failure.self)
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
public func studioModeStatePublisher() throws -> AnyPublisher<Bool, Error> {
// <3>
if let pub = publishers.studioModeState {
return pub
}
// Get initial value
let pub = try getStudioModeStateOnce()
// Merge with listener for future values
.merge(with: try listenForEvent(OBSEvents.StudioModeStateChanged.self, firstOnly: false)
.map(\.studioModeEnabled))
.removeDuplicates()
.receive(on: sessionQueue) // <- this being what Sundell's article suggested.
.handleEvents(receiveCompletion: { [weak self] _ in
self?.publishers.studioModeState = nil
})
.share()
.eraseToAnyPublisher()
// <4>
publishers.studioModeState = pub
return pub
}
Calls at <3> and <4> probably need to be done on the sessionQueue. What would be the best practice for that? The deepest level of publisher that this relies on looks like this:
try self.wsPublisher.send(msg, encodingMode: self.encodingProtocol)
// <5>
.eraseToAnyPublisher()
Should I put .receive(on: sessionQueue) at <5>? Even when the tests work, I'm not sure if I'm doing it right. Sorry for such a long thing, but I tried to add as much detail as possible and tried to bold my questions. Any and all help would be welcomed, and I'd be happy to provide any extra details if needed. Thanks!
Edit: I've realized that it was actually subscribing to the stored Publisher and would receive values if the value changed in OBS post-subscription. But the issue of receiving the most recent value is the issue. I replaced .share() with the .shareReplay idea from this article: (https://www.onswiftwings.com/posts/share-replay-operator/). Again, it works in my testing (including delays in subscription), but still doesn't receive the most recent value when used in my SwiftUI app. Anyone have any ideas?
I have a login use case which involves a remote service call and a pin.
In my view model I have a behaviour relay for pin like so
let pin = BehaviorRelay(value: "")
Then I have this service:
protocol LoginService {
func login(pin: String) -> Single<User>
}
Also in the view model I have a publish relay (to back a submit button) and also a state stream. State must initially set to .inactive and once the submit relay fires I need the state to go .loading and eventually .active.
var state: Observable<State> {
return Observable.merge(
.just(.inactive),
submit.flatMap { [service, pin] in
service.login(pin: pin.value).asObservable().flatMap { user -> Observable<State> in
.just(.active)
}.catch { error in
return .just(.inactive)
}.startWith(.loading)
})
}
The problem is that should the pin change after submit (and my use case involves clearing the pin once submit button clicked), the service is called a second time with the new pin value (in this case empty string).
I want this stream to just take the value for pin and run the service once only and ignore any new value for pin unless the submit was fired again.
Hmm... The code shown only triggers when submit emits a next event, not when pin emits so either you have other code that you aren't showing that is causing the problem, or you are sending a .next event into your publish relay inappropriately.
In short, only send a .next event when the user taps the submit button and the code you posted will work fine. Also, clearing out the pin text field will not change the pin behavior relay unless you are doing something odd elsewhere so that shouldn't be an issue.
This is essentially the same as what you have, but uses the withLatestFrom operator:
class ViewModel {
let submit = PublishRelay<Void>()
let pin = BehaviorRelay(value: "")
let state: Observable<State>
init(service: LoginService) {
self.state = submit
.withLatestFrom(pin)
.flatMapLatest { [service] in
service.login(pin: $0)
.map { _ in State.active }
.catch { _ in .just(.inactive) }
.asObservable()
.startWith(.loading)
}
.startWith(.inactive)
}
}
I'm not a fan of all the relays though and I don't like that you are throwing away the User object. I would likely do something more like this:
class ViewModel {
let service: LoginService
init(service: LoginService) {
self.service = service
}
func bind(pin: Observable<String>, submit: Observable<Void>) -> (state: Observable<State>, user: Observable<User?>) {
let user = submit
.withLatestFrom(pin)
.flatMapLatest { [service] in
service.login(pin: $0)
.map(Optional.some)
.catchAndReturn(nil)
}
.share()
let state = Observable.merge(
submit.map { .loading },
user.map { user in user == nil ? .inactive : .active }
)
.startWith(State.inactive)
return (state: state, user: user)
}
}
I think you are trying too hard to chain things together :-).
Let's take your problem apart and see if that helps.
The important event for you is the button push. When the user pushes the "submit" button you want to make an attempt to log in.
So attach a subject to your pin entry field and let it capture the result of the user's typing. You want this to be a stream that holds the latest value of the pin:
// bound to a text input field. Has the latest pin entered
var pin = BehaviorSubject(value: "")
Then you can have an infinite stream that just gets sent a value when the button is pushed. The actual value sent is not as important as the fact that it emits a value when the user pushes the button.
var buttonPushes = PublishSubject<Bool>()
From that, we're going to create a stream that emits a value each time the button is pushed. We'll represent a login attempt as a struct, LoginInfo that contains all the stuff you need to try and log in.
struct LoginInfo {
let pin : String
/* Maybe other stuff like a username is needed here */
}
var loginAttempts = buttonPushes.map { _ in
LoginInfo(pin: try pin.value())
}
loginAttempts sees a button push and maps in into an attempt to log in.
As part of that mapping, it captures the latest value from the pin stream, but loginAttempts is not directly tied to the pin stream. The pin stream can go on changing forever and loginAttempts won't care until the user pushes the submit button.
Context
I am following the example from WWDC 2019 722 https://developer.apple.com/videos/play/wwdc2019/722/ and WWDC 2019 721 https://developer.apple.com/videos/play/wwdc2019/721/ and making a field with validation the runs an asynchronous network check on a field.
What should happen, as mentioned in the talk, is that the username field should:
Debounce
Show a loading indicator
Perform the network request
End with the network result
Hide the loading indicator
And show or hide a validation message as a result of the network response
I have a prototype that has the debounce, and mocks the network request by using the delay operator. All of this is working well for the most part.
let a = $firstName
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.flatMap { name -> AnyPublisher<String, Never> in
if name == "" {
return Just(name)
.eraseToAnyPublisher()
} else {
return Just(name)
.handleEvents(receiveOutput: { _ in self.isFirstNameLoading = true})
.delay(for: .seconds(2), scheduler: DispatchQueue.main)
.handleEvents(receiveOutput: { _ in self.isFirstNameLoading = false})
.eraseToAnyPublisher()
}
}
.map { name -> Bool in name != "Q" }
.assign(to: \.isFirstNameValid, on: self)
The debounce waits until the input has paused. The flatMap acts as a conditional branching in the Combine flow of operators: if the value is empty, do not bother with the network request; else, if the value has value after the debounce, perform the network request. Lastly, my example is that "Q" is always an error, for mock purposes.
However, the slight problem is that the debounce happens before the branching. I would like to move the debounce to the else branch of the conditional, like so.
let a = $firstName
.flatMap { name -> AnyPublisher<String, Never> in
if name == "" {
return Just(name)
.eraseToAnyPublisher()
} else {
return Just(name)
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.handleEvents(receiveOutput: { _ in self.isFirstNameLoading = true})
.delay(for: .seconds(2), scheduler: DispatchQueue.main)
.handleEvents(receiveOutput: { _ in self.isFirstNameLoading = false})
.eraseToAnyPublisher()
}
}
.map { name -> Bool in name != "Q" }
.assign(to: \.isFirstNameValid, on: self)
When this happens, the true branch of the conditional (empty input) is run correctly, and the map+assign after the flatMap run correctly. However, when the input has value, and the else branch of the conditional runs, nothing after the debounce is run at all.
I have tried switching the DispatchQueue to OperationQueue.main and RunLoop.main to no avail.
Keeping the debounce to before the conditional works okay for now, but I'm wondering if I'm doing anything wrong with my attempt to put it in the branch. I'm also wondering if this would be the correct way to do "branching" in operators with Combine, particularly with my use of flatMap and Just().
Any help would be appreciated!
A Just only ever produces one output. Attaching a debounce to it is not going to debounce anything. At best, it will just delay the output of the Just by the debounce interval. At worst, there's a bug preventing it from working at all, which is what it sounds like based on your description.
I have a network request called login that returns an Observable<UserInfo>. I need to make another API call from that result based on whether the data returned from login has a count > 1, otherwise, I just need to go to a different view controller. I’m trying to use flatMapLatest to do the check for the first request login and make the next network call jobStates (which returns an Observable<JobState>, but I don’t think I’m arranging them correctly. Any ideas? Is there a better / easier way to do this?
Here's what it looks like:
I would expect to see something like this:
func login() {
let loginResult = networkService
.login(login: usernameTextField.text!, password: passwordTextField.text!)
.share()
loginResult
.filter { $0.count > 1 }
.subscribe(onNext: { userInfo in
// stop here and go to a different view with userInfo data
})
.disposed(by: disposeBag)
let networkService = self.networkService // so you don't have to capture self below
loginResult
.filter { $0.count <= 1 }
.flatMapLatest { networkService.jobStates(locationId: $0.locationId) }
.subscribe(
onNext: { data in
// do whatever with data from second request
},
onError: { error in
// if either request errors, you will end up here.
})
.disposed(by: disposeBag)
}
When you have two different possible outcomes, you need two different subscribes.
I am trying to develop a pagination system in an iOS app using RxSwift. The use case is simple: the user can enter text in a search field and the app performs a request that is paginated. When he changes the value, a new request is performed on the first page (that means the value of the observable must be reseted to 1). If the user clears the search field (or enters a text with less than 2 characters), the list of results is cleared and the current page is reset. The next page is fetched when the user scrolls to the bottom of the list.
This is not a swift or iOS specific case and I suppose it could be written in an identical way using RxKotlin or RxJs or any other reactive extension.
My current attempt is to set up an observable for the text and a observable for the current page and to combine them in order to perform the request with both parameters.
I already succeeded in doing exactly what I am looking for but using global attributes for storing the current query and the current page. I would like to find a way to use only the values emitted by the observables without having to maintain them (I guess the code will be more concise and easy to read and understand).
Here is my current code :
// self.nextPage is a Variable<Int>
let moreObs: Observable<Int> = self.nextPage.asObservable()
.distinctUntilChanged() // Emit only if page has changed.
// self.searchTextObservable is a PublishedSubject<String> that receives the values from the textfield
let searchObs: Observable<String> = self.searchTextObservable
.throttle(0.4, scheduler: MainScheduler.instance) // Wait 400ms when the user stops writing.
.distinctUntilChanged() // Emit only if query has changed.
self.resultsObservable = Observable
.combineLatest(searchObs, moreObs) { query, page in
return ["q": query, "p": "\(page)"]
}
.subscribeOn(MainScheduler.instance) // Emit on main thread.
.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background)) // Perform on background thread.
.map { params in
if params["q"]!.characters.count > 2 {
return params
}
return [:]
}
.flatMap { params in
return params.isEmpty ?
Observable.of([]) :
self.search(params)
}
.map { results in
if results.count > 0 {
self.results.appendContentsOf(results)
} else {
self.results = []
}
return self.results
}
So far, the only feature that does not work is the reset operation on the nextPage's value. If I force it to 1 when the searchObs emits:
let searchObs: Observable<String> = self.searchTextObservable
.throttle(0.4, scheduler: MainScheduler.instance) // Wait 400ms when the user stops writing.
.distinctUntilChanged() // Emit only if query has changed.
.map {query in
self.nextPage.value = 1
return query
}
Then I have 2 requests that are performed.
Am I misusing Rx?
I wouldn't use combineLatest. Your page number is dependent on your current search text, so you should chain it with flatMapLatest. That way, you aren't responsible to maintain its state yourself, rather, let operator chaining reset that for you.
let disposeBag = DisposeBag()
let searchText = PublishSubject<String>() // search field text
let newPageNeeded = PublishSubject<Void>() // fires when a new page is needed
struct RequestPage {
let query: String
let page: Int
}
let requestNeeded = searchText.asObservable()
.flatMapLatest { text in
newPageNeeded.asObservable()
.startWith(())
.scan(RequestPage(query: text, page: 0)) { request, _ in
return RequestPage(query: text, page: request.page + 1)
}
}
requestNeeded
.subscribeNext { print($0) }
.addDisposableTo(disposeBag)
searchText.onNext("A")
searchText.onNext("B")
newPageNeeded.onNext(())
searchText.onNext("C")
newPageNeeded.onNext(())
newPageNeeded.onNext(())
This will output:
(RequestPage #1)(query: "A", page: 1)
(RequestPage #1)(query: "B", page: 1)
(RequestPage #1)(query: "B", page: 2)
(RequestPage #1)(query: "C", page: 1)
(RequestPage #1)(query: "C", page: 2)
(RequestPage #1)(query: "C", page: 3)