I'm pretty new in RX, so i'm kind confused how to do it correctly.
My view model:
let feedItems: BehaviorSubject<[FeedItem]> = BehaviorSubject(value: [FeedItem]())
let isLoadingMore: PublishSubject<Bool> = PublishSubject()
let loadPageTrigger: PublishSubject<Void> = PublishSubject()
let isRefreshing: PublishSubject<Bool> = PublishSubject()
func fetchFeed(showLoading: Bool = false, loadMore: Bool = false) {
//api call and set self.feedItems.onNext(response) after completion
}
In my VC i bind feedItems to collectionView, check when collectionView in bottom and bind it to isLoadingMore, and also bind UIRefresherControl to isRefreshing.
In my ViewModel i want to get value of isRefreshing and isLoadingMore in loadPageTrigger and call fetchFeed with some parameters:
ViewModel:
override init() {
super.init()
loadPageTrigger.subscribe(onNext: {
// need to get values of isRefreshing and isLoadingMore to call fetchFeed with params
self.fetchFeed()
}).disposed(by: disposeBag)
}
Subjects provide a convenient way to poke around Rx, however they are not recommended for day to day use.
-- Intro to Rx
If you’re loading up your view model full of Subjects then you haven't quite gotten the grasp of Rx yet. That's okay though, everybody has to start somewhere.
The most direct solution based on what you already have is something like this:
loadPageTrigger
.withLatestFrom(Observable.combineLatest(isRefreshing, isLoadingMore))
.subscribe(onNext: { [unowned self] isRefreshing, isLoadingMore in
// here you have isRefreshing and isLoadingMore as Bools
self.fetchFeed()
})
.disposed(by: disposeBag)
Note the unowned self. It is very important to make sure you aren't capturing self in closures that are being retained by the disposeBag which is held in self.
The optimum solution would involve only Observables and no Subjects at all.
The IObservable<T> interface is the dominant type that you will be exposed to for representing a sequence of data in motion, and therefore will comprise the core concern for most of your work with Rx...
-- Intro to Rx
Related
Could someone explain why I get this warning: Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
I'm know that if I wrap the changes in DispatchQueue.main.async the problem goes away. Why does it happen with some view modals and not others? I thought that since the variable has #Published it's automatically a publisher on main thread?
class VM: ObservableObject {
private let contactsRepo = ContactsCollection()
#Published var mutuals: [String]?
func fetch() {
contactsRepo.findMutuals(uid: uid, otherUid: other_uid, limit: 4) { [weak self] mutuals in
guard let self = self else { return }
if mutuals != nil {
self.mutualsWithHost = mutuals // warning...
} else {
self.mutualsWithHost = []
}
}
}
}
Evidently, contactsRepo.findMutuals can call its completion handler on a background thread. You need to ward that off by getting back onto the main thread.
The #Published property wrapper creates a publisher of the declared type, nothing more. The documentation may be able to provide further clarity.
As for it happening on some viewModels and not others, we wouldn't be able to tell here as we don't have the code. However it's always best practice to use DispatchQueue.main.async block or .receive(on: DispatchQueue.main) modifier for combine as you've already figured out when updating your UI.
The chances are your other viewModel is already using the main thread or the properties on the viewModel aren't being used to update the UI, again without the code we'll never be sure.
I have an ObservableObject that publishes some values using #Published property wrappers. This object also holds a timer.
The question is, how can I fire an event as soon as the timer is executed and handle that event in a view in SwiftUI (I'd prefer using something like onReceive)?
Using the Combine framework for publishing changing values already, I'd like to implement this event triggering / handling properly. But all that I've read so far about Combine is always about handling value changes. But in my case it's rather a single simple event (without any values).
I know that I could simply use a closure and call that when the timer expires, and I will do that if there's no better, combine-like solution.
This is a conceptual question for a very simple problem so I think it's self explaining without me coming up with a code example?
The way SwiftUI works with Combine is via .onReceive, which expects a publisher. An object can expose a publisher - whether Timer or something else - as a property.
Combine publishers work by emitting values, and if you just need to signal that an event has happened, you can emit () aka Void values.
This object, by the way, need not be an ObservableObject, but it could be.
class Foo: ObservableObject {
let timer = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.map { _ in } // map to Void
.eraseToAnyPublisher() // optional, but a good practice
}
Now, you can use .onReceive to subscribe to the timer event:
struct ContentView: View {
#StateObject var foo = Foo()
#State var int: Int = 0
var body: some View {
Text("\(int)")
.onReceive(timer) {
self.int += 1
}
}
}
Of course, you're not restricted to a TimerPublisher. For example, if some random event happens, you can use a PassthroughSubject to publish a value:
class Foo {
let eventA: AnyPublisher<Void, Never>
private let subject = PassthroughSubject<Void, Never>()
init() {
eventA = subject.eraseToAnyPublisher()
let delay = Double.random(in: 10...100)
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
// something random happened, notify
self?.subject.send()
}
}
}
Just an idea, but you could dedicate a specific thread to that Object? And then listen and fire things on that Queue specifically. I've never done this before but it seems like the right sort of idea.
https://developer.apple.com/documentation/dispatch/dispatchqueue
https://www.freecodecamp.org/news/ios-concurrency/
I'm trying to solve an async sequential problem, for example:
There are one 'OriginalData' in myclass, and I want to do some sequential operations to it: operationA, operationB and operationC:
operationA accepts OriginalData and return outputA, after it finishes, then operationB should take the outputA as input and return outputB, and move to operationC..
What I've done was using the callbacks:
// pseudocode
class Myclass {
func operationA(inputA, callback: operationB)
func operationB(inputB, callback: operationC)
..
}
As a result, if using callbacks, it will result in a callback hell and lots trouble. I turned into RxSwift, but not sure how to use RxSwift to solve it.
(P.S I've already read the RxSwift's official document, but still cannot make my idea clear. Best Appreciated for your helps!)
I think you can solve this problem by using PublishSubject as follows:
let operationA = PublishSubject<String>()
let operationB = PublishSubject<String>()
let operationC = PublishSubject<String>()
let disposeBag = DisposeBag()
operationA.asObserver()
.subscribe(onNext:{element in
operationB.onNext(element)
})
.disposed(by: disposeBag)
operationB.asObserver()
.subscribe(onNext:{element in
operationC.onNext(element)
})
.disposed(by: disposeBag)
operationC.asObserver()
.subscribe(onNext:{element in
print(element)
})
.disposed(by: disposeBag)
operationA.onNext("A")
I came across this problem when testing my View:
In my ViewModel I call to an asynchronous operation and when the response arrives, I use a PublishSubject to produce a change in my View. In my View, I call DispatchQueue.main.async in order to hide or show a button.
ViewModel
let refreshButtons = PublishSubject<Bool>(true)
refreshButtons.onNext(true)
View
model.refreshButtons.asObservable()
.subscribe(onNext: {
[unowned self] success in
self.updateButtons(success)
})
.addDisposableTo(disposable)
private func updateButtons(_ show:Bool) {
DispatchQueue.main.async{
button.isHidden = !show
}
}
Now I don't know how to unit test that refreshButtons.onNext(true) will hide or show my button.
The solutions I can think of are:
Overriding the method and having an async expectation, but for that I need to make the method public, what I don't want, or
Dispatching the main queue in my ViewModel and not in the view, what it sounds odd to me, but might me ok.
How can I solve this?
Thank you in advance.
You could use an async expectation based on a predicate in your unit test to wait an see if the button is not hidden anymore.
func testButtonIsHidden() {
// Setup your objects
let view = ...
let viewModel = ...
// Define an NSPredicate to test your expectation
let predicate = NSPredicate(block: { input, _ in
guard let _view = input as? MyView else { return false }
return _view.button.isHidden == true
})
// Create an expectation that will periodically evaluate the predicate
// to decided whether it's fulfilled or not
_ = expectation(for: predicate, evaluatedWith: view, handler: .none)
// Call the method that should generate the behaviour you are expecting.
viewModel.methodThatShouldResultInButtonBeingHidden()
// Wait for the
waitForExpectationsWithTimeout(1) { error in
if let error = error {
XCTFail("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
Something worth noting is that the value you pass to the NSPredicate should be a class. That is because classes are passed by reference, so value inside the predicate block will be the same as the one touched by your view model. If you were to pass a struct or enum though, which are passed by copy, the predicate block would receive a copy of the value as it is at the time of running the setup code, and it will always fail.
If instead you prefer to use UI tests as suggested by #Randall Wang in his answer, then this post might be useful for you: "How to test UI changes in Xcode 7". Full disclosure, I wrote that post.
First of all, You don't need test private method
If you want to test if the button is hidden or not,try UI testing
here is the WWDC of UI testing.
https://developer.apple.com/videos/play/wwdc2015/406/
I have a MVVM test project to experiment RxSwift. I have a UItextfield a button. User write a food name, click on the button and a get from an API is triggered to get all recipes with that food.
View model
struct FoodViewModel
var foodIdentifier: Variable<String> = Variable<String>("")
init() {
foodIdentifier.asObservable().subscribe(onNext: { (identifier) in
self.getRecipes() // Get from API
})
}
}
ViewController
class FoodViewController: UIViewController {
#IBOutlet weak var foodTextField: UITextField!
#IBAction func setCurrentRace(_ sender: Any) {
viewModel.foodIdentifier.value = foodTextField.text!
}
}
After compile I got an error
Closure cannot implicitly capture a mutating self parameter
What I'm doing wrong ? I think it's because of struct of FoodViewModel. If yes, how can I achieve that using struct ?
-- EDIT
I wrote all of the below but forgot to answer your explicit question... The reason you are getting the error is because you are trying to capture self in a closure where self is a struct. If this were allowed, you would be capturing a copy of the view model that you haven't even finished constructing. Switching your view model to a class alleviates the problem because you are no longer capturing a copy, but the object itself for later use.
Here is a better way to set up a view model. You didn't give all the necessary information so I took some liberties...
First we need a model. I don't know exactly what should be in a Recipe so you will have to fill it in.
struct Recipe { }
Next we have our view model. Note that it doesn't directly connect with anything in the UI or the server. This makes testing very easy.
protocol API {
func getRecipies(withFood: String) -> Observable<[Recipe]>
}
protocol FoodSource {
var foodText: Observable<String> { get }
}
struct FoodViewModel {
let recipes: Observable<[Recipe]>
init(api: API, source: FoodSource) {
recipes = source.foodText
.flatMapLatest({ api.getRecipies(withFood: $0) })
}
}
In real code, you aren't going to want to make a new server call every time the user types a letter. There are a lot of examples on the web that explain how to build in a delay that waits until the user stops typing before making the call.
Then you have the actual view controller. You didn't mention what you wanted to do with the results of the server call. Maybe you want to bind the result to a table view? I'm just printing the results here.
class FoodViewController: UIViewController, FoodSource {
#IBOutlet weak var foodTextField: UITextField!
var api: API!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = FoodViewModel(api: api, source: self)
viewModel.recipes.subscribe(onNext: {
print($0)
}).disposed(by: bag)
}
var foodText: Observable<String> {
return foodTextField.rx.text.map { $0 ?? "" }.asObservable()
}
let bag = DisposeBag()
}
Notice how we avoid having to make an IBAction. when you are coding up a view controller with Rx, you will find that almost all the code ends up in the viewDidLoad method. This is because with Rx, you are mainly just worried about wiring everything up. Once the observables are wired up, user action will cause things to happen. It's more like programming a spreadsheet. You just put in the formulas and link the observables together. User's data entry takes care of the actual action.
The above is just one way of setting everything up. This method matches closely with Srdan Rasic's model: http://rasic.info/a-different-take-on-mvvm-with-swift/
You could also turn the food view model into a pure function like this:
struct FoodSink {
let recipes: Observable<[Recipe]>
}
func foodViewModel(api: API, source: FoodSource) -> FoodSink {
let recipes = source.foodText
.flatMapLatest({ api.getRecipies(withFood: $0) })
return FoodSink(recipes: recipes)
}
One takeaway from this... Try to avoid using Subjects or Variables. Here's a great article that helps determine when using a Subject or Variable is appropriate: http://davesexton.com/blog/post/To-Use-Subject-Or-Not-To-Use-Subject.aspx