2 way binding in UITableView using RxSwift - uitableview

I am using MVVM pattern with RxSwift, RxCocoa, RxDataSources.
I have successfully populated the UITableView with array of PaletteViewModel present in ListViewModel by using RxDataSource but it's one way binding.
I want to achieve what I have shown in the picture i.e. I want to bind the UITextField from UITableViewCell to the Observable which is present at some index in the array in ListViewModel
I want to do 2 way binding with the UITextField and answer property of the PaletteViewModel. If the user changes the text in the textField it should change the value in the answer property present at particular index and vice versa.
How Can I achieve something complex like this using MVVM pattern using ReactiveX frameworks?
What if the UITableViewCell at some IndexPath is removed from the memory as it's not visible and the observable's value is changed will it result in crash as the UITextField at that IndexPath will return nil?

A UITextField is an input element. You don't need a two way binding to it because you shouldn't be dynamically changing it. The most you should do is initialize it and you don't need a binding for that.
You don't mention what the final output will be for this input so the answer may be different than the below. This particular solution assumes that you need to push all the answers as a group to a server or database. Maybe when a button is tapped.
There is a lot of code below, but it compiles as it stands (with the proper imports.) You can subscribe to ListViewModel.answers to see all the answers collected together.
class ViewController: UIViewController {
#IBOutlet weak var myTableView: UITableView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let answersSubject = PublishSubject<(PaletteID, String)>()
let viewModel = ListViewModel(answersIn: answersSubject.asObservable())
viewModel.paletteViewModels
.bind(to: myTableView.rx.items(cellIdentifier: "Cell", cellType: MyCell.self)) { index, element, cell in
cell.answerTextField.text = element.initialAnswer
cell.answerTextField.rx.text.orEmpty
.map { (element.id, $0) }
.bind(to: answersSubject)
.disposed(by: cell.bag)
}
.disposed(by: bag)
}
}
class MyCell: UITableViewCell {
#IBOutlet weak var answerTextField: UITextField!
let bag = DisposeBag()
}
struct ListViewModel {
let paletteViewModels: Observable<[PaletteViewModel]>
let answers: Observable<[PaletteID: String]>
init(answersIn: Observable<(PaletteID, String)>) {
paletteViewModels = Observable.just([])
answers = answersIn
.scan(into: [PaletteID: String]()) { current, new in
current[new.0] = new.1
}
}
}
struct PaletteViewModel {
let id: PaletteID
let initialAnswer: String
}
struct PaletteID: RawRepresentable, Hashable {
let rawValue: String
}

Related

Is it correct to store a reference of the Model object in a UITableViewCell that represents in it?

I would like to know if it is "correct" to store a reference to the Model that a UITableViewCell represents in it.
The reason I ask is due to the necessity of knowing the Model in case of a click action in a button inside it.
Is there a better (a.k.a: desirable) way of doing this?
Example:
class Person {
var name: String
var lastName: String
var age: Int
}
protocol PersonCellDelegate: NSObjectProtocol {
// should the second parameter be the model that the cell represents?
func songCell(_ cell: PersonCell, didClickAtEditButtonOfPerson person: Person)
}
class PersonCell: UITableViewCell {
#IBOutlet private weak var nameLabel: UILabel!
#IBOutlet private weak var lastNameLabel: UILabel!
#IBOutlet private weak var ageLabel: UILabel!
#IBOutlet private weak var editButton: UIButton!
// does the cell need to store its reference?
var person: Person! {
didSet {
nameLabel.text = person.name
// ...
}
}
weak var delegate: PersonCellDelegate?
// ...
}
A table view cell is a view. Less it knows about the application logic, better it is.
You could retrieve the entity used using the indexPath(for:) method :
protocol MyTableViewCellDelegate: AnyObject {
func myTableViewCellDidSomething(_ cell: MyTableViewCell)
}
class MyTableViewCell: UITableViewCell {
weak var delegate: MyTableViewCellDelegate?
}
class ViewController: UITableViewController, MyTableViewCellDelegate {
var personList: [Person] = []
func myTableViewCellDidSomething(_ cell: MyTableViewCell) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
let person = personList[indexPath.row]
// ...
}
}
You ask:
does the cell need to store its reference?
No. In fact, that locks you into reference semantics and you might consider value semantics for the Person object. I also think it muddies the ownership model. Who now owns this Person object?
And even if you were committed to reference semantics, and wanted to use this pattern to detect Person changes, be wary that your didSet pattern is only half of the solution. The Person type is mutable and you’re detecting when the object is replaced with a new Person object, but not when the individual properties of Person change. If you’re going to go down this didSet road with mutable reference types, you might also want to add KVO for the relevant properties, too.
This pattern entails a fairly tight coupling of view objects and model objects. As others have suggested, you might consider other patterns for addressing this and/or reducing the burden on the view controller.
If you’re looking for automatic updating of the cell when the Person object mutates (and potentially vice versa), you can consider binding patterns, such as offered by libraries like RxSwift, Bond, etc.
I’d also refer you to Dave Delong’s presentation A Better MVC, which walks you through considerations if you don’t want to give up on MVC, but figure out ways to work with it, or Medium’s iOS Architecture Patterns, which is an introduction to other options.
In strict MVC, view should not access model directly.
When the user click the button in the cell, call delegate method. Let the delegate (usually is view controller) handle the click event (like modify model).
After updating the model, controller will update the view if needed.
I do use it in some cases, especially as you do here, in a custom cell.
I don't rely on the UItableViewCell to hold the data for me, that I have in the model, as the cell can be reused, when it is off screen
It depends:
If you can move cells in the table view (manually or by pressing a button) using insertRows and deleteRows then it's almost the only way (along with protocol/delegate) to be able to get the index path of a cell efficiently without reloading the entire table view.
In a straight table view where no cells are moved don't pass the model to the cell. You can use callback closures which capture the index path and even the model item.
So, there isn't one right way, so I can just tell you would I would do.
If I didn't move cells, I would keep having model property. In cell class you shouldn't set properties of outlets, since cells are reusable. You just let controller know that data source is changed and you should reload rows/certain row.
var person: Person! // instead, tell controller, that person has been changed
Next, I would left delegate pattern and I would use closure variables. It makes code more Swifty (in future you can search for RxSwift).
class PersonCell: UITableViewCell {
var personChanged: (Person) -> Void = { _ in }
var person: Person!
func foo() {
// change person's properties
personChanged(person)
}
func setCell() {
nameLabel.text = person.name
}
}
Then set all the things such as label's text in cellForRowAt UITableViewDelegate's method. Also don't forget to set cell's closure and declare what should happen after person is changed
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = // ...
cell.person = people[indexPath.row]
cell.setCell()
cell.personChanged = { [weak self] person in
guard let self = self else { return }
self.people[indexPath.row] = person
self.tableView.reloadRows(at: [indexPath], with: .automatic)
}
return cell
}

How to change value of dictionary in View Controller from UiTableViewCell

So I have TableViewCell's that are being populated from 2 Dicitionaries in my view controller.
var categories : [Int : [String : Any]]!
var assignments : [Int: [String : Any]]!
I have a UiTextField in my cell that the user is supposed to be able to edit. I then want to be able to change the values of certain keys in that dictionary-based off what the user changes and re-display the table with those changes. My main problem is that I don't know how I will be able to access theese variables from within my cell. I have a method in my view controller that takes the row that the text field is in, along with the value of the textField, and updates the dictionaries. What I need is to be able to instantiate the view controller that the cell is in but I need the original instance that already has values loaded into the categories and assignments Dictionaries. If you have any other ideas on how I could accomplish this please post.
You can use delegate for sharing cell-data to your VC:
protocol YourCellDelegate() {
func pickedString(str: String)
}
class YourCell: UITableViewCell {
var delegate: YourCellDelegate! = nil
#IBOutlet weak var textField: UITextField!
.....//some set-method, where you can handle a text
func textHandle() {
guard let del = delegate else { return }
del.pickedString(textField.text)
}
.....
}
And usage in your VC: When you create cell, set its delegate self:
...
cell.delegate = self
...
and sure you VC supported your Delegate Protocol:
class YourVC: UIViewController, YourCellDelegate {
}
And now, you MUST implement protocol method:
class YourVC: UIViewController, YourCellDelegate {
....
func pickedString(str: String) {
}
....
}
All times, when you use textHandle() in your cell, pickedString(str: String) activates in yourVC with string from textField.
Enjoy!

RxSwift and static cells

I'd like to use RxSwift in my project, but because I'm newbie I maybe misunderstand some principles. Its clear for me how to catch button presses or use rxswift with UITableView with dynamic cells (there are a lot of tutorials for that), but I don't understand how to use it with UITableView with STATIC cells - I'd like to develop something like iOS Settings.app. Could you show me example? Is it a good practice to use rxswift for it? Or maybe I should use something else?
You can drag a #IBOutlet weak var button: UIButton! from static table view cell button, So it's something like this:
class TableViewController: UITableViewController {
#IBOutlet weak var button: UIButton!
...
override func viewDidLoad() {
super.viewDidLoad()
...
button.rx.tap
.subscribe()
.disposed(by: disposeBag)
}
...
}
Hope this may help.
To handle static cells, you have to use a UITableViewController (which I assume you know) but you can still use the rx operators on its tableView.
Of course you don't need to use the items operator on it because the cells are already built, but you can still use itemSelected to determine which cell was tapped on:
final class ViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.rx.itemSelected
.subscribe(onNext: { print("selected index path \($0)") })
.disposed(by: bag)
}
let bag = DisposeBag()
}

Moving data between custom cells in a dynamic table in Swift

Firstly, apologies but I'm new to all of this (iOS Dev & Swift).
I have a number of custom cells that I dynamically load into a tableview. One of my cells is a data picker that, when the date is changed by the user, I want to send the updated data to one of the other cells but I'm stumped. Help please.
Assuming the cells are loaded and visible, you can pass a reference of one cell to another, but you'll need to create a couple of methods within your custom cells.
In my case I have two custom cells, a cell named CellBirthday that contains a label named birthDateLabel, and a cell that contains a DatePicker named CellDatePicker. I want to update birthDateLabel every time the DataPicker value changes.
I'll first load the cells and store a reference of CellBirthday inside CellDatePicker, then when the date picker changes, I'll update the label value inside CellBirthday. Here is the relevant code fragment to load the two cells. In this example, I use the same name for both the cell tag and class name, for example CellBirthday is both the cell tag and the class name specified in the storyboard:
let birthdayCell = tableView.dequeueReusableCell(withIdentifier: "CellBirthday") as! CellBirthday
let datePickerCell = tableView.dequeueReusableCell(withIdentifier: "CellDatePicker") as! CellDatePicker
datePickerCell.setBirthdayCell(BirthdayCell: birthdayCell)
And here are the custom classes:
class CellBirthday: UITableViewCell {
#IBOutlet fileprivate weak var birthDateLabel: UILabel!
var birthdayText: String? {
didSet {
birthDateLabel.text = birthdayText
}
}
}
class CellDatePicker: UITableViewCell {
#IBOutlet fileprivate weak var datePicker: UIDatePicker!
var birthdayCell: CellBirthday?
func setBirthdayCell(BirthdayCell birthdayCell: CellBirthday) {
self.birthdayCell = birthdayCell
}
func getDateString(FromPicker picker: UIDatePicker? = nil) -> String {
var dateText: String = ""
if picker != nil {
let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("MMMMdy")
dateText = dateFormatter.string(from: picker!.date)
}
return dateText
}
#IBAction func datePickerValueChange(_ sender: UIDatePicker) {
if birthdayCell != nil {
birthdayCell!.birthdayText = getDateString(FromPicker: sender)
}
}
}
Since your cells are dynamically loaded into the table, it is not possible to address a specific cell directly. You should trying changing the underlying data source when the user chooses a date, and call table.reloadData()

Saving CoreData in UITableViewController but UISlider data is only in the Prototype Cell - how do I pull this over in order to save to CoreData

I have created a tableviewcontroller, with a dynamic prototype cell. Within this view the user has an option to type in a review of a location from a button press as well as can rate the location on a scale of 1 to 10 with a UI Slider. The review is done with a UIAlertController within the tableviewcontroller - but the UISlider is within the cell itself. I am trying to save both pieces of data to core data within the tableviewcontroller. But the UISlider rating value is not available within the tableviewcontroller - is there a way to reference it in tableview from the cell or do I need to have two separate save functions? Here is some of my code thus far - within the tableview controller it doesn't recognize the variable assigned to the UISLider value in the prototype cell. Thanks in advance for any help!
In my tableviewcell:
class WineryInfoTableViewCell: UITableViewCell {
#IBOutlet weak var ratingLabel: UILabel!
#IBOutlet weak var sliderbutton: UISlider!
#IBAction func sliderValueChanged(sender: UISlider) {
var ratingValue = Float(sender.value)
var roundedRatingValue = roundf(ratingValue/0.5)*0.5
ratingLabel.text = "\(roundedRatingValue)"
}
in my tableviewcontroller
#IBAction func save() {
if let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext {
myRatingData = NSEntityDescription.insertNewObjectForEntityForName("WineryReview", inManagedObjectContext: managedObjectContext) as wineryReview
myRatingData.wineryName = wineryNames
//myRatingData.rating = ratingLabel.text how do I call this for saving?
myRatingData.review = myRatingEntry
var e:NSError?
if managedObjectContext.save(&e) != true {
println("insert error: \(e!.localizedDescription)")
return
}
}
// If all fields are correctly filled in, extract the field value
println("Winery: " + myRatingData.wineryName)
println("Review: " + myRatingData.review)
//println("Rating: " + ratingLabel.text!) how do I call this for saving?
}
I'm still new at Swift, but I think I may have an answer to a part of your dilemma. To reference the UISlider in your table cell, you can use 'tags' to get a reference to it.
For example:
let cell = tableView.dequeueReusableCellWithIdentifier(TableViewCellIdentifiers.someCell, forIndexPath: indexPath) as UITableViewCell
let slider = cell.viewWithTag(100) as UISlider
In the above example, I would have used assigned the UISlider with a tag of 100, so I am able to get a reference to it using the cell.viewWithTag(100).
Forgive me if this doesn't work!
You could pass a WineryReview object into the cell and set its rating directly from sliderValueChanged.
In cellForRowAtIndex:
cell.wineryReview = myReviewData
In TableViewCell:
#IBOutlet weak var ratingLabel: UILabel!
#IBOutlet weak var sliderbutton: UISlider!
var wineryReview: WineryReview?
#IBAction func sliderValueChanged(sender: UISlider) {
var ratingValue = Float(sender.value)
var roundedRatingValue = roundf(ratingValue/0.5)*0.5
wineryReview.rating = roundedRatingValue
ratingLabel.text = "\(roundedRatingValue)"
}
You could call save on the context from the TableViewController if you like. I did something very similar to this in a project.

Resources