I have a ViewController with a UICollectionView and its elements are bound and cells created via:
self.viewModel.profileItems.bind(to: self.collectionView.rx.items){ (cv, row, item) ...
I also react to the user taps via:
self.collectionView.rx.modelSelected(ProfileItem.self).subscribe(onNext: { (item) in
if(/*special item*/) {
let xVC = self.storyboard?.instantiateViewController(identifier: "x") as! XViewController
xVC.item = item
self.navigationController?.pushViewController(xVC, animated: true)
} else {
// other generic view controller
}
}).disposed(by: bag)
The property in the xViewController for item is of Type ProfileItem?. How can changes to item in the XViewController be bound to the collectionView cell?
Thanks in advance
Your XViewController needs an Observable that emits a new item at the appropriate times... Then realize that this observable can affect what profileItems, or at least your view model, emits.
let xResult = self.collectionView.rx.modelSelected(ProfileItem.self)
.filter { /*special item*/ }
.flatMapFirst { [unowned self] (specialItem) -> Observable<Item> in
let xVC = self.storyboard?.instantiateViewController(identifier: "x") as! XViewController
xVC.item = item
self.navigationController?.pushViewController(xVC, animated: true)
return xVC.observableX // this needs to complete at the appropriate time.
}
self.collectionView.rx.modelSelected(ProfileItem.self)
.filter { /*not special item*/ }
.bind(onNext: { [unowned self] item in
// other generic view controller
}
.disposed(by: bag)
Now you need to feed xResult into your view model.
Related
When I dismiss my customVC while inserting items inside collection view, the app crashes with index out of range error. It doesnt matter how much I remove all collectionView data sources, it still crashes. Heres what I do to insert items:
DispatchQueue.global(qos: .background).async { // [weak self] doesn't do much
for customs in Global.titles.prefix(8) {
autoreleasepool {
let data = self.getData(name: customs)
Global.customs.append(data)
DispatchQueue.main.async { [weak self] in
self?.insertItems()
}
}
}
}
func insertItems() { // this function helps me insert items after getting data and it works fine
let currentNumbers = collectionView.numberOfItems(inSection: 0)
let updatedNumber = Global.customs.count
let insertedNumber = updatedNumber - currentNumbers
if insertedNumber > 0 {
let array = Array(0...insertedNumber-1)
var indexPaths = [IndexPath]()
for item in array {
let indexPath = IndexPath(item: currentNumbers + item, section: 0)
indexPaths.append(indexPath)
}
collectionView.insertItems(at: indexPaths)
}
}
I tried to remove all items in customs array and reload collection view before dismissing but still getting error:
Global.customs.removeAll()
collectionView.reloadData()
dismiss(animated: true, completion: nil)
I suspect that since I load data using a background thread, the collection view inserts the items even when the view is unloaded (nil) but using DispatchQueue.main.async { [weak self] in self?.insertItems() } doesn't help either.
Hi have a tableview with sections and I am making API call to populate the tableView. I am also using the MVVm architecture. Now users are able to delete items but I try reloading the sections or tableView but nothing happens as the deleted item still remains in the tableView. Below is my code. Any help is appreciated
My ViewModel
Observable.zip(identiferElements, deviceElements).map {(identifers, devices, _) -> [MyInfoSection] in
var items: [MyInfoSection] = []
let identiferRepository = identifers.map({ (repository) -> MyInfoSectionItem in
let cellViewModel = IdentifiersCellViewModel(with: repository)
return MyInfoSectionItem.identifiersItem(viewModel: cellViewModel)
})
if identiferRepository.isNotEmpty {
items.append(MyInfoSection.setting(title: "Identifier", items: identiferRepository))
}
let deviceRepository = devices.map({ (repository) -> MyInfoSectionItem in
let cellViewModel = DevicesCellViewModel(with: repository)
return MyInfoSectionItem.devicesItem(viewModel: cellViewModel)
})
if deviceRepository.isNotEmpty {
items.append(MyInfoSection.setting(title: "Active Devices", items: deviceRepository))
}
return items
}.bind(to: elements).disposed(by: rx.disposeBag)
deletedEvent.drive(onNext: { (item) in
switch item {
case .identifiersItem(let viewModel):
identiferDeleted.onNext(viewModel.repository)
case .devicesItem(let viewModel):
deviceDeleted.onNext(viewModel.repository)
}
}).disposed(by: rx.disposeBag)
identiferDeleted.asObservable().flatMapLatest({ [weak self] (value) -> Observable<ResponseBase> in
log(value)
guard let self = self, let id = value.id else { return Observable.just(ResponseBase()) }
return self.provider.deleteAddress(id: id)
.trackActivity(self.loading)
.trackError(self.error)
}).subscribe(onNext: { (res) in
log(res)
}).disposed(by: rx.disposeBag)
ViewController
//viewDidLoad
let input = MyInfoViewModel.Input(trigger: refresh, segmentSelection: segmentSelected, selection: tableView.rx.modelSelected(MyInfoSectionItem.self).asDriver(), deleted: tableView.rx.modelDeleted(MyInfoSectionItem.self).asDriver())
let output = viewModel.transform(input: input)
More code would be added based on request. Thanks
Use combineLatest instead of zip. The user can only delete an item out of one section at a time and zip waits until both sections emit a new value before emitting. There might be other problems, but that is one for sure.
I would need to see compilable code for your view model to help further.
I've written module based on RxSwift with Viewcontroller and ViewModel. ViewModel contains gesture's observers and images observables. Everything works well, except situation when application didBecameActive directly to mentioned module. Subscriptions of gestures don't work and imageView become blank.
They are set inside subscription to observable based on BehaviorSubjects, inside view:
func subscribePhotos(observerable: Observable<[(Int, UIImage?)]>) {
disposeBag = DisposeBag()
observerable.subscribeOnNext { [weak self] array in
array.forEach { identifier, image in
if let pictureView = self?.subviews.first(where: { view -> Bool in
guard let view = view as? PictureView else {
return false
}
return view.identifier == identifier
}) as? PictureView {
pictureView.set(image)
}
}
}.disposed(by: disposeBag)
}
In viewModel I set Observable:
var imagesObservable: Observable<[(Int, UIImage?)]> {
do {
let collection = try photosSubject.value()
if let photosObservables = collectionCreator?.getPhotosDetailsObservables(identifiers: collection.photoIdentifiers) {
let photosObservable = Observable.combineLatest(photosObservables)
return Observable.combineLatest(photosSubject, photosObservable,
resultSelector: { collection, currentArray -> [(Int, UIImage?)] in
var newArray = [(Int, UIImage?)]()
currentArray.forEach { stringIdentifier, image in
if let picture = grid.pictures.first(where: { $0. stringIdentifier == stringIdentifier }) {
newArray.append((picture.identifier, image))
}
}
return newArray
})
}
} catch { }
return Observable<[(Int, UIImage?)]>.never()
}
}
photosSubject is initialized in viewModel's init
photosSubject = BehaviorSubject<PictureCollection>(value: collection)
photosObservale
func createImageObservableForAsset(asset: PHAsset, size: CGSize) -> Observable<UIImage?> {
return Observable.create { obs in
PHImageManager.default().requestImage(for: asset,
targetSize: size,
contentMode: .aspectFit,
options: nil,
resultHandler: { image, _ in
obs.onNext(image)
})
return Disposables.create()
}
}
And in ViewController I connect them by calling method of view:
myView.pictureView.subscribePhotos(observerable: viewModel.imagesObservable)
After didBecameActive pictureView's property image of type UIImage isn't nil, but they disappear. I could listen notification didBecameActive and invoke onNext on observer, but I’m not sure is it correct way to figure out problem. Any idea what's reason of that?
Finally, I solved out this issue. Reason wasn't connected with Rx. Method drawing pictures draw(_:CGRect) was called after didBecomeActive and cleared myView. I changed method's body and now everything works well :)
I'm facing a problem when selecting the table view row on RxSwift. For details, the code on the do(onNext:) function is called twice, thus lead to the navigation pushed twice too. Here is my code in the viewModel, please help me resolve it. Thanks so much.
struct Input {
let loadTrigger: Driver<String>
let searchTrigger: Driver<String>
let selectMealTrigger: Driver<IndexPath>
}
struct Output {
let mealList: Driver<[Meal]>
let selectedMeal: Driver<Meal>
}
func transform(_ input: HomeViewModel.Input) -> HomeViewModel.Output {
let popularMeals = input.loadTrigger
.flatMap { _ in
return self.useCase.getMealList()
.asDriver(onErrorJustReturn: [])
}
let mealSearchList = input.searchTrigger
.flatMap { text in
return self.useCase.getMealSearchList(mealName: text)
.asDriver(onErrorJustReturn: [])
}
let mealList = Observable.of(mealSearchList.asObservable(), popularMeals.asObservable()).merge().asDriver(onErrorJustReturn: [])
let selectedMeal = input.selectMealTrigger
.withLatestFrom(mealList) { $1[$0.row] }
.do(onNext: { meal in
self.navigator.toMealDetail(meal: meal)
})
return Output(mealList: mealList, selectedMeal: selectedMeal)
}
Edit: Here's the implemetation on the ViewController:
func bindViewModel() {
self.tableView.delegate = nil
self.tableView.dataSource = nil
let emptyTrigger = searchBar
.rx.text.orEmpty
.filter { $0.isEmpty }
.throttle(0.1, scheduler: MainScheduler.instance)
.asDriver(onErrorJustReturn: "")
let loadMealTrigger = Observable
.of(emptyTrigger.asObservable(), Observable.just(("")))
.merge()
.asDriver(onErrorJustReturn: "")
let searchTrigger = searchBar.rx.text.orEmpty.asDriver()
.distinctUntilChanged()
.filter {!$0.isEmpty }
.throttle(0.1)
let selectMealTrigger = tableView.rx.itemSelected.asDriver()
let input = HomeViewModel.Input(
loadTrigger: loadMealTrigger,
searchTrigger: searchTrigger,
selectMealTrigger: selectMealTrigger
)
let output = viewModel.transform(input)
output.mealList
.drive(tableView.rx.items(cellIdentifier: MealCell.cellIdentifier)) { index, meal, cell in
let mealCell = cell as! MealCell
mealCell.meal = meal
}
.disposed(by: bag)
output.selectedMeal
.drive()
.disposed(by: bag)
}
Firstly, is this RxSwift?
If so, the .do(onNext:) operator provides side effects when you receive a new event via a subscription; Therefore, two "reactions" will happen when a table row is tapped: 1. subscription method and 2. .do(onNext:) event. Unfortunately, I do not have any further insight into your code, so there may be other stuff creating that error aswell.
Good luck!
I have two screens.
1.listing the food list
2.cart list
So in the foodlist i have cart button .So while clicking the cart button the name of the food should display in the cart list.
I have done in mvvm.
So in foodlistviewcontroller:-
cell.cartaddCell = {[weak self] in
if let i = self?.tableView.indexPath(for: $0) {
let cartmodel:CartModel = CartModel(withoffermodel:self!.offerViewModel.datafordisplay(atindex: indexPath))
let cartDataSource:ChartDataSourceModel = ChartDataSourceModel(array: nil)
let cartViewModel:ChartViewModel = ChartViewModel(withdatasource: cartDataSource)
cartViewModel.insertedArray = cartmodel
print(cartViewModel.insertedArray)
cartViewModel.add()
let cartViewController:ChartViewController = ChartViewController(nibName: "ChartViewController", bundle: nil, withViewModel: cartViewModel)
self?.navigationController?.pushViewController(cartViewController, animated: true)
// self?.present(cartViewController, animated: true, completion: nil)
// print(cartViewModel.insertedArray )
print(cartmodel.offerdetailAddName)
print(cartmodel)
print(i)
// self?.chartViewModel.delete(atIndex: i)
}
}
IN cartviewmodel:
func add() {
datasourceModel.dataListArray?.append(insertedArray!)
print(datasourceModel.dataListArray)
print(insertedArray?.offerdetailAddName)
}
So the name will display on the cartlist.
But when we directly click on the eventlist The name which added is not display in this screen.
So how to insert the row in this screen .
You'll need a reloadData on the UITableView.
Perhaps a delegate from the model to the UIViewController will be a good mechanism for this.