I am trying to get into MVVM in Swift and I am wondering how to handle events in subviews in MVVM, and how these events can travel up the chain of views/viewmodels. I'm talking about pure Swift for now (no SwiftRx etc.).
Example
Say I have a TableViewController with a TableViewModel. The view model holds an array of objects and creates a TableCellViewModel for each one, since each cell represents one of these objects. The TableViewController gets the number of rows to display from its model and also the view model for each cell, so it can pass it along to the cell.
We then have a TableCell and each cell has a TableCellViewModel. The TableCell queries its model for things like user-facing strings etc.
Now let's say TableCell also has a delete button that delete's that row. I'm wondering how to handle that: Usually, the cell would forward the button press to its view model, but this is not where we need it - we eventually need to know about the button press in either TableViewController or TableViewModel, so we can remove the row from the table view.
So the question is:
How does the button event get from a TableCell upwards in the view chain in MVVM?
Code
As requested in the comments, code that goes with the example:
class TableViewController: UIViewController, UITableViewDataSource {
var viewModel: TableViewModel = TableViewModel()
// setup and such
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.viewModel.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableCell
cell.viewModel = self.viewModel.cellViewModel(at: indexPath.item)
return cell
}
}
class TableViewModel {
// setup, get data from somewhere, ...
var count: Int {
return self.modelObjects.count
}
func cellViewModel(at index: Int) -> TableCellViewModel {
let modelObject = self.modelObjects[index]
let cellViewModel = TableCellViewModel(modelObject: modelObject)
return cellViewModel
}
}
class TableCell {
var viewModel: TableCellViewModel!
// setup UI, do what a cell does
func viewModelChanged() {
self.titleLabel.text = self.viewModel.title()
}
func deleteButtonPressed(_ sender: UIButton) {
// Oh, what to do, what to do?
}
}
class TableCellViewModel {
private var modelObject: ModelObject
init(modelObject: ModelObject) {
self.modelObject = modelObject
}
func title() -> String {
return self.modelObject.title
}
}
TableViewModel is the source of truth, so all global operations should be performed in there. Pressing a button is completely UI operation and viewModel shouldn't handle this in direct way.
So, for now we know two facts:
TableViewModel should delete the cell from array and then viewController should handle the deletion animation process;
Button press shouldn't be handled in child viewModel.
According to this you can achieve it by:
Pass button pressed event up to viewController (use callback or delegate pattern);
Call TableViewModel method to delete specific cell:
viewModel.deleteCell(at: indexPath)
Properly handle deletion animation in viewController.
may be you can use nextResponder util nextResponder is VC, and VC responder to delegate (eg:CellEventDelegate) that handle delete data and cell
UIResponder *nextResponder = pressedCell.nextResponder;
while (nextResponder) {
if ([nextResponder conformsToProtocol:#protocol(CellEventDelegate)]) {
if ([nextResponder respondsToSelector:#selector(onCatchEvent:)]) {
[((id<CellEventDelegate>)nextResponder) onCatchEvent:event];
}
break;
}
nextResponder = nextResponder.nextResponder;
}
Related
I am facing a problem with creation of dynamic view in Swift. However, the problem is not directly related to Swift itself, it is rather a Object-Oriented programming problem.
The problem is that I need to be able to add additional view elements to a view dynamically. And I am not sure if I'm doing it correctly. My solutions seems as overkill to me.
To solve the problem I thought Decorator pattern would be a good candidate. Additionally to have more control of the flow, I have introduced Template Method pattern.
I have a number of classes that define default look and feel on certain view controls like Labels, TextFields and Buttons. Here below you can see an approximate idea of how it is.
Here is my code:
class ViewElement{
// this class inherits from default UIKit elemnts and provides default UI view
}
// default cell is the cell that implements default elements layout and margings, etc
class DefaultCell: UITableViewCell {
let mainStack: UIViewStack
func addElement(ViewElement)
}
class BlueCell: DefaultCell {
let textField1: TextField
let label : Label
let button: Button
init(){
textField = TextField()
label = Label()
button = Button()
addElement(textField)
addElement(label)
addElement(button)
}
}
Here is the tableViewDataSource implementation
class BlueTable: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
switch weather {
case good:
labelOne
labelTwo
buttonOne
cell.addElement(labelOne)
cell.addElement(labelTwo)
cell.addElement(buttonOne)
case bad:
// idem
cell.addView(badWeatherView)
}
return cell
}
}
As you can see, the greater the number of conditions, the bigger my switch statement.
Additional problem arises from the fact that I will need to access the additional elements that I assign in the condition, like callbacks, tap events etc. Also the fact that those elements in conditional are added via addElement method, means that those elements will be added at the bottom of the mainStack.
In order to have control over the way elements are added to the stack I decided to go with the following solution: Template Method pattern
protocol OrderableElements {
func top()
func middle()
func bottom()
}
extension OrderableElements {
func render() {
top()
middle()
bottom()
}
}
Now the BlueCell implements the protocol and looks like this
class BlueCell: DefaultCell, OrderableElements {
init(){
textField = TextField()
label = Label()
button = Button()
}
func top() {
addElement(textField)
}
func middle() {
addElement(label)
}
func bottom(){
addElement(button)
}
}
The tabledatasource class will then look as follows:
class BlueTable: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
switch weather {
case good:
labelOne
labelTwo
buttonOne
cell.addElement(labelOne)
cell.addElement(labelTwo)
cell.addElement(buttonOne)
case bad:
// idem
cell.addView(badWeatherView)
}
...
**cell.render()**
return cell
}
}
Now because I need to add the new view elements in certain location or better said, at certain moments in during the scope of BlueCell, I introduced Decorators for the cell, like this:
class CellDecorator: OrderableElements {
var cell: BlueCell
init(cell: BlueCell){
self.cell = cell
}
func top() {
self.cell.top()
}
func middle(){
self.cell.middle()
}
func bottom(){
self.cell.bottom()
}
func getCell() {
return self.cell
}
}
Here is the concrete implementation
class GoodWeatherDecorator: CellDecorator {
let goodLabel
let goodTextField
let goodButton
override top() {
super.top()
addElement(goodLabel)
}
override middle(){
super.middle()
addElement(goodTextfield)
}
override bottom(){
super.bottom()
addElement(goodButton)
}
}
The final implementation of the cellForRowAt method looks like below:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
var decoratedCell = CellDecorator(cell: cell)
switch weather {
case good:
decoratedCell = GoodWeatherDecorator(cell: cell)
case bad:
decoratedCell = BadWeatherDecorator(cell: cell)
}
decoratedCell.configure() // <------------ here is the configure call
cell = decoratedCell.getCell() // <------- here I get cell from the decorator
return cell
}
}
Now I do understand that my implementation of the decorator pattern is not 100% valid, because I don't inherit from the BlueCell class, for example. But that does not bother me that much. The things that bothers me is that I think that this solution to the problem is kind of overkill.
All works the right way, but I can help the feeling of having done too much to solve this trivial problem.
What do you think? How would you solve this kind of problem?
Thanks in advace
Given that you only show two types of cells and your solution doesn't actually get rid of the switch statement, I'd say that your solution counts as "overkill."
You don't show it, but it seems that you have a Weather enum. I'll assume that...
enum Weather: String {
case good
case bad
}
In the table view datasource, my goal would be to have something like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let weather = weathers[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: weather.rawValue, for: indexPath) as! ConfigurableCell
cell.configure(with: weather)
return cell as! UITableViewCell
}
In order to achieve the above, I would have several cells laid out in the storyboard file with different identifiers. I would have a subclass for each type of cell in my code where all of them conform to the ConfigurableCell protocol.
protocol ConfigurableCell {
func configure(with weather: Weather)
}
If you can't conform your Weather enum to the String type, you will need a switch statement to convert a weather object to a string identifier, but otherwise, no switch statements necessary.
You should follow Daniel T.'s answer.
But here's a suggested upgrade that I use on my own projects.
Instead of just using
protocol ConfigurableCell {
func configure(with weather: Weather)
}
I use this for reusability purposes in many different scenarios.
protocol Configurable {
associatedtype Initializables
func configure(_ model: Initializables) -> Self
}
Example use cases:
UIViewController
class SomeViewController: UIViewController {
var someIntProperty: Int?
...
}
extension SomeViewController: Configurable {
struct Initializables {
let someIntProperty: Int?
}
func configure(_ model: SomeViewController.Initializables) -> Self {
self.someIntProperty = model.someIntProperty
return self
}
}
// on some other part of the code.
let someViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! SomeViewController
_ = someViewController.configure(SomeViewController.Initializables(someIntProperty: 100))
UITableViewCell
class SomeTableViewCell: UITableViewCell {
var someIntProperty: Int?
var someStringProperty: Int?
...
}
extension SomeTableViewCell: Configurable {
struct Initializables {
let someIntProperty: Int?
let someStringProperty: Int?
}
func configure(_ model: SomeTableViewCell.Initializables) -> Self {
self.someIntProperty = model.someIntProperty
self.someStringProperty = model.someStringProperty
return self
}
}
// on cellForRow
let cell = tableView.dequeueReusableCell(withIdentifier: "SomeTableViewCell", for: indexPath) as! SomeTableViewCell
return cell.configure(SomeTableViewCell.Initializables(someIntProperty: 100, someStringProperty: "SomeString"))
Notes:
As you can see it's very reusable and easy to use and implement. Downside is the code generated could be long when using configure
Have a right view controller that slides in and out over the main view controller. This right view controller has a table in it to contain the passed information from the main.
I can access and pass the data to the controller from the main without issue but in the right view I need to then bind the data passed to it from the main.
The problem is that even though I try binding the data after the view comes into focus it gives nil on the tableView.reloadData().
RightViewController has 2 functions that are used by the main
func loadAlerts(){
self.tableView.reloadData()
}
func setAlerts(alerts: Alerts){
self.alerts = alerts
}
Alerts is just a custom object. It does contain values. self.alerts is a class variable.
MainViewController calls these 2 functions this way
self.rightViewController = self.storyboard?.instantiateViewController(withIdentifier: "RightViewController") as! RightViewController
Set the data after getting it from the api call
if let count = self.alerts?.Alerts.count {
if count == 0 {
return
}
//set on controller
rightViewController.setAlerts(alerts: self.alerts!)
}
This is defined at class level like
private var rightViewController: RightViewController!
Then I have a delegate defined for when the right controller is opened from a gesture and it calls like this
func rightDidOpen() {
rightViewController.loadAlerts()
}
This works fine for everything but the a tableView. Even by telling the tableView to load on the main thread like so
DispatchQueue.main.async{
self.tableView.reloadData()
}
Didn't change anything. At this point the alerts has values.
I don't mind refactoring the entire thing if need be so any ideas, thoughts or info of how I can get this to work is appreciated. If more info is needed just let me know.
--
Here the table delegate and source defined
class RightViewController : UIViewController, UITableViewDataSource, UITableViewDelegate
and from front end assigned to the uicontroller (its calle Alerts Scene). Forgot to mention that if I do the api call directly in the right controller it works fine but I'm trying to reduce api calls so am refactoring this.
Here are the methods. Sorry for the misunderstanding.
//MARK: Tableview delegates
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let count = alerts?.Alerts.count{
return count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let alertD = alerts?.Alerts[indexPath.row] {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "AlertTableViewCell") as! AlertTableViewCell
cell.name.text = alertD.Summary
cell.icon.image = Helpers.listImage24dp(id: alertD.TOA)
cell.selectionStyle = .none
cell.name.textColor = UIColor.blue
return cell
}
return UITableViewCell()
}
I have one view controller named TableViewController and another customised cell called feed.swift
The cells are getting reused properly and I have put tags on various buttons as I wan't to know what button of what feed is pressed on.
In my cellForRowAtIndexPath I'm populating my username with json that I have parsed. It looks like this
cell.username.text = username[indexPath.row]
output-> ["andre gomes", "renato sanchez", "renato sanchez"]
Then I have tagged my username button like this
cell.usernamePress.tag = indexPath.row
This is going on in my TableViewController
In my feed.swift I'm checking if a button is pressed and printing out the tag assigned to that button
#IBAction func usernameBut(sender: AnyObject) {
print(usernamePress.tag)
}
output-> 2
Now I need to access the username array of TableViewController in feed.swift and do something like username[usernamePress.tag]
I tried making a global.swift file but I'm not able to configure it for an array of strings.
import Foundation
class Main {
var name:String
init(name:String) {
self.name = name
}
}
var mainInstance = Main(name: "hello")
Even after doing this I tried printing mainInstance.name and it returned hello even after changing it. I want a solution where the array of strings holds the values I set in TableViewController and I can be able to use them in feed.swift
Any suggestions would be welcome! I'm sorry if there are any similar question regarding this but I'm not able to figure out how to use it for a mutable array of strings
I suggest you don't use the array directly in your FeedCell but instead return the press-event back to your TableViewController where you handle the event. According to the MVC Scheme, which is the one Apple requests you to use (checkout Apples documentation), all your data-manipulation should happen in the Controller, which then prepares the Views using this data. It is not the View that is in charge to display the right values.
To solve your problem I would choose to pass back the press-event via the delegation-pattern, e.g. you create a FeedCellDelegate protocol that defines a function to be called when the button is pressed:
protocol FeedCellDelegate {
func feedCell(didPressButton button: UIButton, inCell cell: FeedCell)
}
Inside your FeedCell you then add a delegate property, which is informed about the event by the View:
class FeedCell {
var delegate: FeedCellDelegate?
...
#IBAction func pressedUsernameButton(sender: UIButton) {
delegate?.feedCell(didPressButton: sender, inCell: self)
}
}
If your TableViewController then conforms to the just defined protocol (implements the method defined in there) and you assign the ViewController as the View's delegate, you can handle the logic in the Controller:
class TableViewController: UITableViewController, FeedCellDelegate {
...
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("FeedCell", forIndexPath: indexPath) as! FeedCell
cell.delegate = self
// Further setup
return cell
}
func feedCell(didPressButton button: UIButton, inCell cell: FeedCell) {
guard let indexPath = tableView.indexPathForCell(cell) else { return }
// Do your event-handling
switch (button.tag) {
case 2: print("Is username button")
default: print("Press not handled")
}
}
}
As you might recognize I changed your class name. A Feed sounds more like a Model-class whereas FeedCell implies its role to display data. It makes a programmer's life way easier if you choose self-explaining names for your classes and variables, so feel free to adapt that. :)
you should add a weak array property to the tableViewCell:
weak var userNameArray:[String]?
Then in your tableViewController pass the username array into the cell:
fun tableView(tableView:UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// create the cell, then...
if let array = self.username {
cell.userNameArray = array
}
}
Then you can use the array in the cell itself to populate its fields handle button taps, etc.
My table view allows multiple cell selection, where each cell sets itself as selected when a button inside the cell has been clicked (similar to what the gmail app does, see picture below). I am looking for a way to let the UITableViewController know that cells have been selected or deselected, in order to manually change the UINavigationItem. I was hoping there is a way to do this by using the delegate methods, but I cannot seem to find one. didSelectRowAtIndexPath is handling clicks on the cell itself, and should not affect the cell's selected state.
The most straight forward way to do this would be to create our own delegate protocol for your cell, that your UITableViewController would adopt. When you dequeue your cell, you would also set a delegate property on the cell to the UITableViewController instance. Then the cell can invoke the methods in your protocol to inform the UITableViewController of actions that are occurring and it can update other state as necessary. Here's some example code to give the idea (note that I did not run this by the compiler, so there may be typos):
protocol ArticleCellDelegate {
func articleCellDidBecomeSelected(articleCell: ArticleCell)
func articleCellDidBecomeUnselected(articleCell: ArticleCell)
}
class ArticleCell: UICollectionViewCell {
#IBAction private func select(sender: AnyObject) {
articleSelected = !articleSelected
// Other work
if articleSelected {
delegate?.articleCellDidBecomeSelected(self)
}
else {
delegate?.articleCellDidBecomeUnselected(self)
}
}
var articleSelected = false
weak var delegate: ArticleCellDelegate?
}
class ArticleTableViewController: UITableViewController, ArticleCellDelegate {
func articleCellDidBecomeSelected(articleCell: ArticleCell) {
// Update state as appropriate
}
func articleCellDidBecomeUnselected(articleCell: ArticleCell) {
// Update state as appropriate
}
// Other methods ...
override tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueCellWithIdentifier("ArticleCell", forIndexPath: indexPath) as! ArticleCell
cell.delegate = self
// Other configuration
return cell
}
}
I would have a function like 'cellButtomDidSelect' in the view controller and in 'cellForRowAtIndexPath', set target-action to the above mentioned function
I want to achieve something similar to the following example image
where I want to present a selected or default category on the first screen
and when it's clicked it moves to the next screen with other category options to choose.
In this example, where it says Entertainment and Project 01 represent different data entities (data store) and it looks like it's using UITableView with each UITableViewCell connected to different data store.
I first want to know if my analysis so far is correct.
In my case, I just need to do that Entertainment part and in the next screen, show all category options like the second screen in the example. And after selection is made, the first screen should reflect the selection from the second screen.
Is UITableView the right choice to show this single field (cell) and segue to another ViewController?
All the demo app examples I see, don't demonstrate the usage of UITableView for this purpose so I'm not sure what is the best option in my case.
Yes UITableView is a good option. Just create a delegate method in "choose category" class. Call the delegate on didSelectRowAtIndexPath method of the UITableView in Category class. Then assign Expense class as the delegate for the category class.
Basically you'll be performing a segue once you click on entertainment. User will be presented with "Choose Category controller" Once the user selects the table row. Delegate will be called. Implement that delegate in the expense class. And in the implementation just reload the table with the new value.
UPDATE:
Your choose category class will be something similar to :
import UIKit
protocol ChooseCategoryControllerDelegate: class {
func categoryController(controller: ChooseCategoryTableViewController, didSelectCategory category: String)
}
class ChooseCategoryTableViewController: UITableViewController {
let categories = ["category1","category2","category3","category4"]
var selectedcategory: String!
weak var delegate: ChooseCategoryControllerDelegate!
// MARK: - Table view data source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("TypeCell", forIndexPath: indexPath)
cell.textLabel?.text = categories[indexPath.row]
return cell
}
// MARK: - Table view delegate
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedcategory = categories[indexPath.row] as? String
delegate.categoryController(self, didSelectCategory: selectedcategory)
}
}
In your expense controller add the following code outside your class declaration:
extension ExpenseController: ChooseCategoryControllerDelegate {
func typesController(controller: ChooseCategoryTableViewController, didSelectCategory category: String) {
let selectedCategory = category
dismissViewControllerAnimated(true, completion: nil)
//update your table category with the new category!!
//reload your table here
}
}
Re-Update:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "CategorySegue" {
let controller = segue.destinationViewController as! ChooseCategoryTableViewController
controller.delegate = self
}
}
Also name your segue as "CategorySegue"
It depends, if the only option is the Entertainment then maybe UITableView is not the best option. Regardless, you can use a UITableView with static cells and a segue from the entertainment cell to the category.