I am trying to achieve something similar in rxswift example project from RxSwift repo. But in my case there are dependent observables. I couldn't find any solution without using binding in viewmodel
Here is the structure of my viewmodel:
First the definitions of input, output and viewmodel
typealias UserListViewModelInput = (
viewAppearAction: Observable<Void>,
deleteAction: Observable<Int>
)
typealias UserListViewModelOutput = Driver<[User]>
typealias UserListViewModel = (UserListViewModelInput, #escaping UserApi) -> UserListViewModelOutput
Then there is actual implementation which doesn't compile.
let userListViewModel: UserListViewModel = { input, loadUsers in
let loadedUserList = input.viewAppearAction
.flatMapLatest { loadUsers().materialize() }
.elements()
.asDriver(onErrorDriveWith: .never())
let userListAfterDelete = input.deleteAction
.withLatestFrom(userList) { index, users in
users.enumerated().compactMap { $0.offset != index ? $0.element : nil }
}
.asDriver(onErrorJustReturn: [])
let userList = Driver.merge([loadedUserList, userListAfterDelete])
return userList
}
Viewmodel has two job. First load the user list. Second is delete a user at index. The final output is the user list which is downloaded with UserApi minus deleted users.
The problem in here in order the define userList I need to define userListAfterDelete. And in order to define userListAfterDelete I need to define userList.
So is there a way to break this cycle without using binding inside view model? Like a placeholder observable or operator that keeps state?
This is a job for a state machine. What you will see in the code below is that there are two actions that can affect the User array. When the view appears, a new array is downloaded, when delete comes in, a particular user is removed.
This is likely the most common pattern seen in reactive code dealing with state. So common that there are whole libraries that implement some variation of it.
let userListViewModel: UserListViewModel = { input, loadUsers in
enum Action {
case reset([User])
case delete(at: Int)
}
let resetUsers = input.viewAppearAction
.flatMapLatest { loadUsers().materialize() }
.compactMap { $0.element }
.map { Action.reset($0) }
let delete = input.deleteAction.map { Action.delete(at: $0) }
return Observable.merge(resetUsers, delete)
.scan(into: [User](), accumulator: { users, action in
switch action {
case let .reset(newUsers):
users = newUsers
case let .delete(index):
users.remove(at: index)
}
})
.asDriver(onErrorJustReturn: [])
}
Related
In my ViewModel file I have an observable array created after applying map on it. Now before returning it I want to check if it has any content or not. If there is nothing in there I want to return it without applying map. Following is my code:
func retrieveDeals(location: CLLocation?) -> Observable<[SaleItem]> {
let specials = nearestFlightSpecials.retrieveNearestFlightSpecials(userLocation: location)
let happyHourDeals = specials.map {
$0.filter { $0.isHappyHour }
}
return happyHourDeals
}
Before I return happyHourDeals I want to check if it contains any element or not. The above array is subscribed in view but I don't want to apply the above logic there. I want to keep it here in ViewModel.
I suspect what you want to do is filter out empty output:
func retrieveDeals(location: CLLocation?) -> Observable<[SaleItem]> {
let specials = nearestFlightSpecials.retrieveNearestFlightSpecials(userLocation: location)
let happyHourDeals = specials.map {
$0.filter { $0.isHappyHour }
}
.filter { !$0.isEmpty } // this is the line you need.
return happyHourDeals
}
Terminology is important here. Observables don't "contain" values. Observables don't return values, they emit events.
Your happyHourDeals will still be returned but with the filter line, it will no longer emit empty arrays. What this means is that whatever is subscribed to the value returned will not be updated if specials.map { $0.filter { $0.isHappyHour } } emits an empty array.
I am trying to create a UICollectionView, so that I can add and delete items from it's data source as a Driver. I have a viewModel below
import Photos
import RxCocoa
import RxSwift
class GroupedAssetsViewModel {
enum ItemAction {
case add(item: PHAssetGroup)
case remove(indexPaths: [IndexPath])
}
let assets: Driver<[GroupedAssetSectionModel]>
let actions = PublishSubject<ItemAction>()
private let deletionService: AssetDeletionService = AssetDeletionServiceImpl()
init() {
assets = actions
.debug()
.scan(into: []) { [deletionService] items, action in
switch action {
case .add(let item):
let model = GroupedAssetSectionModel()
items.append(GroupedAssetSectionModel(original: model, items: item.assets))
case .remove(let indexPaths):
var assets = [PHAsset]()
for indexPath in indexPaths {
items[indexPath.section].items.remove(at: indexPath.item)
assets.append(items[indexPath.section].items[indexPath.row])
}
deletionService.delete(assets: assets)
}
}
.asDriver(onErrorJustReturn: [])
}
func setup(with assetArray: [PHAssetGroup] = [PHAssetGroup]()) {
for group in assetArray {
actions.onNext(.add(item: group))
}
}
}
but .scan closure is never being called, even though actions.onNext is being called in setup, therefore Driver's value is always empty.
It seems like I am getting some core concepts wrong, what might be the problem here?
Just because you have
actions.onNext(.add(item: group)) doesn't mean this sequence has started. You are publishing events to a subject that hasn't started. You must have a subscriber somewhere first for assets. Then only scan will get executed. Because observables are pull driven sequences. There must be a subscriber to even make them start.
I'm new to RxSwift and all example I found are handling simple cases.
I'm trying to do form validation for my textfields.
My custom TextField class has a method isValid() and a regexp. The isValid return is based on the regexp attribute.
So far, I have written the following :
let valids = [mLastName, mFirstName, mEmailField].map {
$0.rx.text.map {
text -> Bool in
// I want more complex logic here
// Like return field.isValid()
return text!.characters.count > 0
}
}
let _ = Observable.combineLatest(valids) { iterator -> Bool in
return iterator.reduce(true, { $0 && $1 })
}.subscribe(onNext: { allValid in
///update button according to AllValid
})
Does anyone know how to update the code to base the first Observable<Bool> be based on my isValid() method instead of text!.characters.count
There are probably many ways to do that.
You can use filter to transform rx.text Observable in your custom TextField class:
var isTextValid: Observable<Bool> {
return rx.text.filter { _ in
return self.isValid()
}
}
then you can combine isTextValid from all text fields with combineLatest.
You can also extract the validation logic from the custom text field (maybe you don't even need a custom text field at all).
Benefits:
validation could be easier to unit test
you can easily reuse validation in different places in your app (e.g. for UITextView if you ever use it).
The draft of a validator class:
class TextValidator {
var input: Observable<String>
var regex: NSRegularExpression
init(input: Observable<String>, regex: NSRegularExpression) {
self.input = input
self.regex = regex
}
func validate() -> Observable<Bool> {
return input.map { text in
//return true if regex matches the text
}
}
}
Then you can use it as follows:
let mailValidator = TextValidator(input: mLastName.rx.text, regex: /* actual regex*/)
let firstNameValidator = TextValidator(input: mFirstName.rx.text, regex: ...)
let _ = Observable.combineLatest(mailValidator.validate(), firstName.validate(), ...)
// and so on
Now if you want to write unit tests for the validators (which you probably should do), you can simply pass Observable.just("Some value") as input to TextValidator and verify what Observable returned by validate() does.
I found the answer myself. The problem was in the first map, I shouldn't use anonymous parameter.
See :
let valids = [mLastName, mFirstName, mEmailField].map { field in
field.rx.text.map({ _ in return field.isValid() })
}
_ = Observable.combineLatest(valids) { iterator -> Bool in
return iterator.reduce(true, { return $0 && $1 })
}.bindTo(self.mValidateButton.rx.isEnabled)
I would like to hide my Realm implementation and instead of working on RLMNotificationBlock I would like to use RXSwift. Below how my method looks like now (RLMNotificationBlock is a block that takes String and RLMRealm):
func addNotificationBlock(block: RLMNotificationBlock) -> RLMNotificationToken? {
let rlmObject = ...
return rlmObject.addNotificationBlock(block)
}
But I would like to switch to more reactive observer-pattern way. I looked at RxSwift docs and source code of rx_clickedButtonAtIndex, but I cannot figure out how I should put all these things together. I guess my code at the end would look like:
public var rx_realmContentChanged: ControlEvent<Int> {
let controlEvent = ControlEvent()
// My code go here
return controlEvent
}
I'm new with RXSwift and know only the basics. Any help will be appreciated.
There is an Rx Realm extension available on GitHub you can use: https://github.com/RxSwiftCommunity/RxRealm
It allows you to get an Observable out of a single Realm object or a Realm Collection. Here's an example from the README:
let realm = try! Realm()
let laps = realm.objects(Lap.self))
Observable.changesetFrom(laps)
.subscribe(onNext: { results, changes in
if let changes = changes {
// it's an update
print(results)
print("deleted: \(changes.deleted) inserted: \(changes.inserted) updated: \(changes.updated)")
} else {
// it's the initial data
print(results)
}
})
There is also an additional library especially built for binding table and collection views called RxRealmDataSources
If I understood you correctly, you just want to return Observable<RLMNotificationToken>
In this case you just need to do something like this
func addNotificationBlock(block: RLMNotificationBlock) -> Observable<RLMNotificationToken> {
return create { observer -> Disposable in
let rlmObject = ...
let token = rlmObject.addNotificationBlock(block)
// Some condition
observer.onNext(token)
// Some other condition
observer.onError(NSError(domain: "My domain", code: -1, userInfo: nil))
return AnonymousDisposable {
// Dispose resources here
}
// If you have nothing to dipose return `NopDisposable.instance`
}
}
In order to use it bind it to button rx_tap or other use flatMap operator
I am wanting to return a function which will in turn call back itself.
Is it possible to do through returning a closure calling itself?
My problem is that I'm unsure of the correct syntax to use here, as well as I'm not sure if it is even possible due to having a cyclic reference to itself (and swift being heavy on compiler type checking)
I am currying my functions so that the models and presenters do not need to know about the dataGateway further decoupling my code
Some background information about the problem, the API expects a page number to be passed into itself, I do not want to store this state. I want the function to pass something back so that the model can just call the next function when it needs to.
I know the curried function definition looks like this:
function (completion: ([Example.Product], UInt) -> Void) -> Example.Task?
look for __function_defined_here__ in my code samples
Original - example code
func fetch(dataGateway: DataGateway, category: String)(page: UInt)(completion: [Product] -> Void) -> Task? {
return dataGateway.productMap(category, page: page) { products in
completion(products.map { $0.build })
}
}
Idea 1 - return as tuple
func fetch(dataGateway: DataGateway, category: String)(page: UInt)(completion: [Product] -> Void) -> (Task?, __function_defined_here__) {
return (dataGateway.productMap(category, page: page) { products in
completion(products.map { $0.build })
}, fetch(dataGateway, category: category)(page: page + 1))
}
Idea 2 - pass back in the completion
func fetch(dataGateway: DataGateway, category: String)(page: UInt)(completion: ([Product], __function_defined_here__) -> Void) -> Task? {
return dataGateway.productMap(category, page: page) { products in
completion(products.map { $0.build }, fetch(dataGateway, category: category)(page: page + 1))
}
}
I ended up solving it with something like the following, what it does is create a class reference to store the next function in. I pass a reference to this object in the completion of the asynchronous operation.
extension Product {
class Next {
typealias FunctionType = (([Product], Next) -> Void) -> Task?
let fetch: FunctionType
init(_ fetch: FunctionType) {
self.fetch = fetch
}
}
func fetch(dataGateway: DataGateway, inCategory category: String)(page: UInt)(completion: ([Product], Next) -> Void) -> Task? {
return dataGateway.products(inCategory: category, page: page)() { products in
completion(products.map { $0.build }, Next(fetch(dataGateway, inCategory: category)(page: page + 1)))
}
}
}
let initial = Product.fetch(dataGateway, inCategory: "1")(page: 0)
pass the function in to a data model
data() { [weak self] products, next in
self?.data = products
self?.setNeedsUpdate()
self?.next = next
}
scrolling down to bottom of table view triggers the above again, using the next function instead of data