Get model from UICollectionView indexpath - ios

I'm using RxSwift to bind a model array to a collection view
How do I get the model object from a given indexPath?
I'm doing the binding like this:
vm.bikeIssueCatagories()
.drive(self.collectionView.rx.items(cellIdentifier: "BikeIssueCategoryCollectionViewCell", cellType: UICollectionViewCell.self)) { row, data, cell in
}.disposed(by: disposeBag)
The core of my issue is, that I need to get both the model object and the cell that a user selects. Using collectionView.rx.modelSelected(T.self) only gives me the model og type T. And calling collectionView.rx.itemSelected only gives me the selected IndexPath
collectionView.rx.itemSelected.asDriver()
.driveNext { [unowned self] indexPath in
guard let model = try? collectionView.rx.model(at: indexPath) else { return }
guard let cell = self.collectionView.cellForItem(at: indexPath) else { return }
}.disposed(by: disposeBag)
But this gives me an error when trying to the the model at indexPath:
Type 'inout UICollectionView' does not conform to protocol
'ReactiveCompatible'
Just trying:
let indexPath = IndexPath.init()
self.collectionView.rx.model(at: indexPath)
also gives me an error:
Ambiguous reference to member 'model(at:)'
SO... How to get both the model object and the cell that a user selects?

I could have done like RamwiseMatt proposed. Making a method on my ViewModel that takes the IndexPath and return the model. I did however end up using a zip:
let modelSelected = collectionView.rx.modelSelected(SelectableBikeIssueCategory.self).asObservable()
let cellSelected = collectionView.rx.itemSelected.asObservable()
.map { [weak self] indexPath -> UICollectionViewCell in
guard let cell = self?.collectionView.cellForItem(at: indexPath) else { fatalError("Expected cell at indexpath") }
return cell
}
Observable.zip(cellSelected, modelSelected)
.map { [weak self] cell, category -> SomeThing in
return SomeThing()
}

Your self-accepted solution is not optimal, because the modelSelected stream maps indexPath internally and needs to be used when there is no need to know about indexPath. In your case, it is better to use use itemSelected and convert to a tuple for example.
collectionView.rx.itemSelected
.map { [weak self] indexPath in
guard
let model = try? self?.collectionView.rx.model(at: indexPath) as YourModelName?,
let cell = self?.collectionView.cellForItem(at: indexPath)
else {
preconditionFailure("Describing of error")
}
return (cell, model)
}

Your ViewModel defines a method bikeIssueCatagories(), which is what your UIViewController binds the UICollectionView to. To get your model at the correct position, you can use the itemSelected property you mentioned, which gives you an Observable<IndexPath>, which you should feed into your ViewModel. There, you can use the IndexPath's item property to determine which model from your data array (given in bikeIssueCatagories()) is the one you are looking for. The modelSelected property makes this easier for you since it already knows the datasource, so all you have to provide is the type of your Model (replace T.self with YourModelName.self.)
I'm not sure why you want a reference to your cell as well, but if you must, you can just use cellForItem(at:) on your UICollectionView (feeding it the IndexPath you obtained via itemSelected.)

Related

How to clear datas on RxTableView

I'm stuck on RxCocoa problem.
I'm gonna implement clear tableView with Rx.
The app using MVVM with RxCocoa needs clear data for initializing tableView with infinite scroll.
But with binding tableView, I dunno how to clear it.
Thanks.
ViewController
self.viewModel.requestData() // request data to Server
self.viewModel.output.hotDealList
.scan(into: [ItemModel]()) { firstPosts, afterPosts in // For Infinite Scroll
return firstPosts.append(contentsOf: afterPosts)
}
.bind(to: self.tableView.rx.items(cellIdentifier: "itemCell", cellType: HotDealTableViewCell.self)) { [unowned self] (index, item, cell) in
self.setCellUI(item: item, cell: cell)
}.disposed(by: self.bag)
ViewModel
struct Output {
let hotDealList = BehaviorSubject<[ItemModel]>(value: [])
}
func requestData(page: String = "0") {
let _ = self.service.requestItemList(["page":page])
.subscribe(
onNext:{ response in
guard let serverModels = response.posts, !serverModels.isEmpty else {
return
}
self.output.hotDealList.onNext(serverModels)
}
).disposed(by: self.bag)
}
The solution here is to expand the state machine that you already have started. A Moore Machine (which is the easiest state machine to understand) consists of a number of inputs, a state, a start state, and a number of outputs. It is expressed in Rx using the scan operator and an Input enum.
You already have the scan operator setup, but you only have one input, hotDealList. You need to include a second input for clearing.
Something like this:
enum Input {
case append([ItemModel])
case clear
}
let state = Observable.merge(
viewModel.output.hotDealList.map { Input.append($0) },
viewModel.output.clear.map { Input.clear }
)
.scan(into: [ItemModel]()) { state, input in
switch input {
case let .append(page):
state.append(page)
case .clear:
state = []
}
}
In Rx, the outputs of the state machine are expressed by bindings. You already have one:
state.bind(to: self.tableView.rx.items(cellIdentifier: "itemCell", cellType: HotDealTableViewCell.self)) { [unowned self] (index, item, cell) in
self.setCellUI(item: item, cell: cell)
}
.disposed(by: bag)
If you need more, be sure to share your state observable.
BTW, using self inside the binder like that is a memory leak. I suggest you move the setCellUI(item:cell:) method into the HotDealTableViewCell class so you don't need self.

How do I use multiple cells in a tableView using Rx?

I have a tableView with two cells: StaticSupportTableViewCell and SupportTableViewCell. As the name suggests the first cell is a single static cell on top of the tableView. SupportTableViewCell can be any number and should be displayed underneath the static cell.
I have code that binds and returns the correct cell:
viewModel.multiContent.bind(to: tableView.rx.items) { tableView, index, item in
if let cellViewModel = item as? StaticSupportTableViewCellViewModel {
let cell = tableView.dequeueReusableCell(withIdentifier: StaticSupportTableViewCell.identifier) as? StaticSupportTableViewCell
cell?.viewModel = cellViewModel
guard let guardedCell = cell else { return UITableViewCell()}
return guardedCell
}
if let cellViewModel = item as? SupportTableViewCellViewModel {
let cell = tableView.dequeueReusableCell(withIdentifier: SupportTableViewCell.identifier) as? SupportTableViewCell
cell?.viewModel = cellViewModel
guard let guardedCell = cell else { return UITableViewCell()}
return guardedCell
}
else { return UITableViewCell() }
}.disposed(by: disposeBag)
In the viewModel I have the multiContent variable:
var multiContent = BehaviorRelay<[Any]>(value: [])
Now if I accept the cell viewModels onto that relay one by one it works:
This works:
multiContent.accept([StaticSupportTableViewCellViewModel(myString: "TESTING")])
Or doing this instead:
multiContent.accept(mainService.serviceProviders.compactMap { SupportTableViewCellViewModel(serviceProvider: $0, emailRelay: emailRelay)})
But if I try both at the same time...
multiContent.accept([StaticSupportTableViewCellViewModel(myString: "TESTING")])
multiContent.accept(mainService.serviceProviders.compactMap { SupportTableViewCellViewModel(serviceProvider: $0, emailRelay: emailRelay)})
...only the last cell is shown. It's like the last one replaces the first one instead of being an addition to it.
So how do I accept both cell viewModels to the relay so that both are displayed in the tableView?
EDIT
I sort of got it right by adding the two cell viewModels into one array:
let contents: [Any] = [StaticSupportTableViewCellViewModel(brand: name, url: web.localized(), phoneNumber: phone.localized()), mainService.serviceProviders.compactMap { SupportTableViewCellViewModel(serviceProvider: $0, emailRelay: emailRelay)}]
And changed the binding:
if let cellViewModels = item as? [SupportTableViewCellViewModel] {...
This is problematic though as I'm stuck with and array of [SupportTableViewCellViewModel]. It doesn't work looping them and returning the cells as they overwrite one another.
The solution is to send in the cell viewModel SupportTableViewCellViewModel instead of [SupportTableViewCellViewModel], but how do I do that?
A BehaviorRelay only emits the most recent array accepted. When you call accept(_:) you aren't adding to the state of the relay, you are replacing its state. To emit both, you will need to concatenate the two arrays.
In general, you should avoid using Relays and Subjects in production code. They are good for learning the Rx system and samples but are hard to get right in real world situations. You only need them when converting non-Rx code into Rx or when setting up a feedback loop.
Also, a Subject, Relay or Observable shouldn't be a var. You don't want to replace one once setup. Make them lets instead.

RxSwift - optional issue on binding to tableView

I have an UITableView and a users variable whose signature is like:
let users: Variable<[User]?> = Variable<[User]?>(nil)
While I'm trying to bind this array of users in a UITableView, I'm getting the error Generic parameter 'Self' could not be inferred. This only occurs when I'm observing an Optional type. Why does this happen? What's the workaround for this situation?
Here is an example of how I'm doing:
private func bind() {
// Does not compile: Generic parameter 'Self' could not be inferred
users.asObservable().bind(to:
tableView.rx.items(cellIdentifier: "UserCell",
cellType: UITableViewCell.self)) { (index, user, cell) in
// Cell setup.
}.disposed(by: disposeBag)
}
You see this error because the compiler cannot infer the type of the Optional variable users passed into the following functions which work on generics and need to be able to infer the type.
An Optional in Swift is actually implemented as shown below. I guess in case of an Optional being potentially nil aka .none the compiler cannot infer the type for methods like e.g. items and bind(to:) which work on generics.
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
/// The absence of a value.
///
/// In code, the absence of a value is typically written using the `nil`
/// literal rather than the explicit `.none` enumeration case.
case none
/// The presence of a value, stored as `Wrapped`.
case some(Wrapped)
/// Creates an instance that stores the given value.
public init(_ some: Wrapped)
//...
}
Workaround 1.): You could just use filterNil() (RxOptionals lib) to avoid the problem.
private func bind() {
users.asObservable().filterNil().bind(to:
tableView.rx.items(cellIdentifier: "UserCell",
cellType: UITableViewCell.self)) { (index, user, cell) in
// Cell setup.
}.disposed(by: disposeBag)
}
Workaround 2.): Make users non-optional. If you have no users just set an empty array as value.
let users: Variable<[User]> = Variable<[User]>([])
Workaround 3.): Use Nil-Coalescing Operator ?? in map function like
private func bind() {
users.asObservable().map { optionalUsers -> [User] in
return optionalUsers ?? []
}
.bind(to:
tableView.rx.items(cellIdentifier: "UserCell",
cellType: UITableViewCell.self)) { (index, user, cell) in
// Cell setup.
}.disposed(by: disposeBag)
}
Sidenote: Variable is deprecated in the latest version of RxSwift
Just for reference:
Implementation of items
public func items<S: Sequence, Cell: UITableViewCell, O : ObservableType>
(cellIdentifier: String, cellType: Cell.Type = Cell.self)
-> (_ source: O)
-> (_ configureCell: #escaping (Int, S.Iterator.Element, Cell) -> Void)
-> Disposable
where O.E == S {
return { source in
return { configureCell in
let dataSource = RxTableViewReactiveArrayDataSourceSequenceWrapper<S> { (tv, i, item) in
let indexPath = IndexPath(item: i, section: 0)
let cell = tv.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! Cell
configureCell(i, item, cell)
return cell
}
return self.items(dataSource: dataSource)(source)
}
}
}
Implementation of bind(to:)
public func bind<O: ObserverType>(to observer: O) -> Disposable where O.E == E {
return self.subscribe(observer)
}
I had the same error without an optional type. It happened because I was using a UICollectionViewCell subclass in a UITableView.

Array of observable element in RxSwift

I am new to RxSwift and working on some sample to test. I have show some data on uitableview with the help of RxSwift. However when I try to delete any item from tableview and reload. Observable array didn't update and due to this scrolling at last item crashes. Below is the code, please help me know what I am doing wrong.
self.itemArray = NSMutableArray(objects: "First Item","Second Item","Third Item","Fourth Item","Fifth Item","Sixth Item","Seventh Item","Eight Item","Nineth Item","Tenth Item","Eleventh Item","Twelveth Item","Thirtheenth Item","Fourtheenth Item","Fifteenth Item","Sixteenth Item","Seventhenth Item","First Item","Second Item","Third Item","Fourth Item","Fifth Item","Sixth Item","Seventh Item","Eight Item","Nineth Item","Tenth Item","Eleventh Item","Twelveth Item","Thirtheenth Item","Fourtheenth Item","Fifteenth Item","Sixteenth Item","Seventhenth Item")
var seqValue = Observable.just(self.itemArray)
seqValue
.bind(to: rxtableView.rx.items) { (tableView, row, seqValue) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = self.itemArray.object(at: row) as? String
cell.backgroundColor = .clear
return cell
}.disposed(by: disposeBag)
self.rxtableView.rx.itemDeleted
.subscribe(onNext: { [unowned self]indexPath in
self.itemArray.removeObject(at: indexPath.row)
seqValue = Observable.just(self.itemArray)
self.rxtableView.reloadData()
}).addDisposableTo(disposeBag)
make it BehaviorSubject, i.e. -
var seqValue = BehaviorSuject.create(self.itemArray)
and instead of 'seqValue = Observable.just(self.itemArray)' do
seqValue.onNext(self.itemArray)
calling '.reloadData()' is not necessary
I think what you want is RxSwift's Variable:
let seqValue = Variable<String>(["First Item", ...])
so you in your .itemDeleted you can modify that variable:
seqValue.value.remove(at: indexPath.row)
Variable is an Observable, so changes are propagated automatically. No need to call reloadData().

Mapping between array of enums and a dictionary of enums

Here's what I'm trying to do:
Provide a tableView that allows the user to select a formula name (an enum) that will be set as their default
In this view I'll place a checkmark next to the formula that is the current default
Their chosen default formula is what will determine which formula is used to calculate a 1 rep maximum amount (the theoretical amount the user could lift based on actual lifts of lesser weight)
After calculating the value, the user will be able to save a record of the lifts they did, the formula used to calculate the maximum, and the calculated maximum weight.
That record will be saved in Core Data with one of the entities being Lift which will simply be the formula name
I've created a class that I want to handle the work of providing the current default formula to what ever part of the app needs it as well as set the default when a new one is selected.
Here is my enum of the formula names:
enum CalculationFormula: Int {
case Epley
case Brzychi
case Baechle
case Lander
case Lombardi
case MayhewEtAl
case OConnerEtAl
}
and with help from the SO community, I created this class to handle the management of userDefaults:
class UserDefaultsManager: NSUserDefaults {
let formulas = [CalculationFormula.Baechle, CalculationFormula.Brzychi, CalculationFormula.Epley, CalculationFormula.Lander, CalculationFormula.Lombardi, CalculationFormula.MayhewEtAl, CalculationFormula.OConnerEtAl]
let formulaNameDictionary: [CalculationFormula : String] =
[.Epley : "Epley", .Brzychi: "Brzychi", .Baechle: "Baechle", .Lander: "Lander", .Lombardi: "Lombardi", .MayhewEtAl: "Mayhew Et.Al.", .OConnerEtAl: "O'Conner Et.Al"]
let userDefaults = NSUserDefaults.standardUserDefaults()
func getPreferredFormula() -> CalculationFormula? {
guard NSUserDefaults.standardUserDefaults().dictionaryRepresentation().keys.contains("selectedFormula") else {
print("No value found")
return nil
}
guard let preferredFormula = CalculationFormula(rawValue: NSUserDefaults.standardUserDefaults().integerForKey("selectedFormula")) else {
print("Wrong value found")
return nil
}
return preferredFormula
}
func setPreferredFormula(formula: CalculationFormula) {
NSUserDefaults.standardUserDefaults().setInteger(formula.rawValue, forKey: "selectedFormula")
}
You can see I have an array of the enums in the order I want them displayed in the tableView and a dictionary of the enums so I can get each enum's string representation to display in each cell of the tableView. Here's how I populate the cell text label which works:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("formulasCell", forIndexPath: indexPath)
let currentFormula = formulas[indexPath.row].formulaName
cell.textLabel?.text = currentFormula
return cell
}
and here's where I'm setting the checkmark anytime a cell in the tableView is selected
func refresh() {
let preferredFormula = defaults.getPreferredFormula()
for index in 0 ... formulas.count {
let indexPath = NSIndexPath(forItem: index, inSection: 0)
if let cell = tableView.cellForRowAtIndexPath(indexPath) {
cell.accessoryType = preferredFormula == index ? .Checkmark : .None
}
}
}
As I mentioned at the beginning, I need to do many different things with this enum but to keep this question focused on one thing, I'll stay with this checkmark example which now doesn't work after creating my UserDefaultsManager class
The problem is obvious - preferredFormula is now an enum and I can't compare that to the index value which is an Int - but the solution is not. I could get the raw value of the enum but the rawValues aren't guaranteed to be in alignment with the cell indexPaths. Some ideas I've had are:
I could probably change the order of the enum cases so their raw values match the order I've put them in my formulas array, but that seems silly and unreliable
I could use the array index values but that seems equally silly and unreliable
If I just use the array, I don't have the string representations of the cases to display in the cells
It seems that using the array and dictionary together is a viable but the best I could come up with is maybe creating another dictionary that maps the enums to Ints but that would have the same issues I just listed.
Any guidance someone could provide would be greatly appreciated.
You seem to have made things a little more complicated than they need to be.
Firstly, you can use a String raw value for your enum and avoid the associated dictionary:
enum CalculationFormula: String {
case Epley = "Epley"
case Brzychi = "Brzychi"
case Baechle = "Baechle"
case Lander = "Lander"
case Lombardi = "Lombardi"
case MayhewEtAl = "Mayhew Et.Al."
case OConnerEtAl = "O'Conner Et.Al"
}
Second, Your UserDefaultsManager doesn't need to subclass NSUserDefaults, it is simply some utility functions. Also, you are doing a lot of checking in getPreferredFormula that you don't need to. I would suggest re-writing that class to use a computed property like so:
class UserDefaultsManager {
static let sharedInstance = UserDefaultsManager()
private var defaultsFormula: CalculationFormula?
var preferredFormula: CalculationFormula? {
get {
if self.defaultsFormula == nil {
let defaults = NSUserDefaults.standardUserDefaults()
if let defaultValue = defaults.objectForKey("selectedFormula") as? String {
self.defaultsFormula = CalculationFormula(rawValue: defaultValue)
}
}
return self.defaultsFormula
}
set {
self.defaultsFormula = newValue
let defaults = NSUserDefaults.standardUserDefaults()
if (self.defaultsFormula == nil) {
defaults.removeObjectForKey("selectedFormula")
} else {
defaults.setObject(self.defaultsFormula!.rawValue, forKey: "selectedFormula")
}
}
}
}
I have made the class a singleton; although this may have an impact on testability it simplifies issues that could arise if the default is changed in multiple places.
The appropriate place to set/clear the check mark is in cellForRowAtIndexPath so that cell reuse is handled appropriately. This code assumes that formulas is an array of CalculationFormula:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("formulasCell", forIndexPath: indexPath)
let currentFormula = formulas[indexPath.row]
cell.textLabel?.text = currentFormula.rawValue
let preferredFormula = UserDefaultsManager.sharedInstance.preferredFormula
cell.accessoryType = currentForumula == preferredFormula ? .Checkmark : .None
return cell
}

Resources