Delegation to pass data back from modally presented view controller - ios

Let's say we have two view controllers, a parent with a label and a modally presented child with a table view. How would I pass the user's selection in the table view back to the parent using delegation?
ViewController1:
var delegate: vc2delegate?
override func viewDidLoad {
super.viewDidLoad()
let label.text = ""
}
ViewController2:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
let selections = ["1", "2", "3", "4", "5"]
cell.selections.text = selections[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? Cell {
cell.didSelect(indexPath: indexPath as NSIndexPath)
}
dismiss(animated: true, completion: nil)
}
//wherever end of class is
protocol vc2delegate {
// delegate functions here
}
Do I even have the right approach? I never really got down this pattern and I think it's crucial for me to learn for iOS. Another tricky caveat may be that viewDidLoad() doesn't get called when you dismiss a modal view controller.

Take a look at the UIViewController life cycle docs: ViewDidLoad only gets called once.
There are plenty of guides on how to do this, just do a quick search.
You'll need to update the dataSource logic as I added a quick string array, and you'll most likely have something a bit more complex, but the idea is still the same.
BTW, I used your naming convention of vc1/vc2, but I hope you have more meaningful names for your controllers.
In your code you have the delegate on the wrong VC. Here is a quick code sample of what it should look like:
class VC1: UIViewController {
let textLabel = UILabel()
// whenever you're presenting the vc2
func presentVC2() {
var vc2 = VC2()
vc2.delegate = self
self.present(vc2, animated: true, completion: nil)
}
}
extension VC1: VC2Delegate {
func updateLabel(withText text: String) {
self.textLabel.text = text
}
}
protocol VC2Delegate: class {
func updateLabel(withText text: String)
}
class VC2: UIViewController {
weak var delegate: VC2Delegate?
let dataSource = ["string 1", "tring 2"]
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let string = dataSource[indexPath.row]
self.delegate?.updateLabel(withText: string)
dismiss(animated: true, completion: nil)
}
}

You can use callback function also to update your label from tableview:
1) Declare callback function into your VC2:
var callback:((String) -> Void)?
2) Call this function in your tableview CellForRowAt method in VC2:
let dataSource = ["string 1", "tring 2"]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let string = dataSource[indexPath.row]
var cell = tableView.dequeueReusableCell(withIdentifier: "yourCell") as! YourCell
//here you can call callback function & pass string to VC1
cell.callback?(dataSource[indexPath.row])
}
3) Now you can call callback closure in VC1 in anywhere you call your VC2:
class VC1: UIViewController {
let textLabel = UILabel()
//I'm calling this(presentVC2()) function on ViewDidLoad you can call anywhere you want
func viewDidLoad() {
super.viewDidLoad()
presentVC2()
}
// whenever you're presenting the vc2
func presentVC2() {
var vc2 = VC2()
vc2.callback = { text in
self.textLabel.text = text
}
self.present(vc2, animated: true, completion: nil)
}
}

Related

Prevent tableview from being reused (MVVM )

I know how to preserve the action we have done on UITableView, after scrolling back and forth.
Now Iam doing a simple UITableView on MVVM
which has a Follow button . like this.
Follow button changes to Unfollow after click and resets after scrolling.
Where and How to add the code to prevent this?
Here is the tableview Code
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Vm.personFollowingTableViewViewModel.count
}
var selectedIndexArray:[Int] = []
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: FollowList_MVVM.PersonFollowingTableViewCell.identifier , for: indexPath) as? PersonFollowingTableViewCell else{
return UITableViewCell()
}
cell.configure(with: Vm.personFollowingTableViewViewModel[indexPath.row])
cell.delegate = self
return cell
}
and configure(with: ) function
#objc public func didTapButton(){
let defaultPerson = Person(name: "default", username: "default", currentFollowing: true, image: nil)
let currentFollowing = !(person?.currentFollowing ?? false)
person?.currentFollowing = currentFollowing
delegate?.PersonFollowingTableViewCell(self, didTapWith: person ?? defaultPerson )
configure(with: person ?? defaultPerson)
}
func configure(with person1 : Person){
self.person = person1
nameLabel.text = person1.name
usernameLabel.text = person1.username
userImageview.image = person1.image
if person1.currentFollowing{
//Code to change button UI
}
custom delegate of type Person is used
I guess your main issue is with Button title getting changed on scroll, so i am posting a solution for that.
Note-: Below code doesn’t follow MVVM.
Controller-:
import UIKit
class TestController: UIViewController {
#IBOutlet weak var testTableView: UITableView!
var model:[Model] = []
override func viewDidLoad() {
for i in 0..<70{
let modelObject = Model(name: "A\(i)", "Follow")
model.append(modelObject)
}
}
}
extension TestController:UITableViewDelegate,UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TestTableCell
cell.dataModel = model[indexPath.row]
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
extension TestController:Actions{
func followButton(cell: UITableViewCell) {
let indexPath = testTableView.indexPath(for: cell)
model[indexPath!.row].buttonTitle = "Unfollow"
testTableView.reloadRows(at: [indexPath!], with: .automatic)
}
}
class Model{
var name: String?
var buttonTitle: String
init(name: String?,_ buttonTitle:String) {
self.name = name
self.buttonTitle = buttonTitle
}
}
Cell-:
import UIKit
protocol Actions:AnyObject{
func followButton(cell:UITableViewCell)
}
class TestTableCell: UITableViewCell {
#IBOutlet weak var followButtonLabel: UIButton!
#IBOutlet weak var eventLabel: UILabel!
var dataModel:Model?{
didSet{
guard let model = dataModel else{
return
}
followButtonLabel.setTitle(model.buttonTitle, for: .normal)
eventLabel.text = model.name
}
}
weak var delegate:Actions?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func followAction(_ sender: Any) {
delegate?.followButton(cell:self)
}
}
To convert this into MVVM approach, there are few things you need to change and move out.
The loop I have in viewDidLoad shouldn’t be there. That will be some API call, and should be handled by viewModel, and viewModel can delegate that to other repository to handle or handle itself. Upon receiving response viewModel update its state and communicate with View (in our case tableView) to re-render itself.
Code in extension where I am updating model object shouldn’t be in controller (model[indexPath!.row].buttonTitle = "Unfollow"), that has to be done by viewModel, and once the viewModel state changes it should communicate with view to re-render.
The interaction responder (Button action) in Cell class, should delegate action to viewModel and not controller.
Model class should be in its own separate file.
In short viewModel handles the State of your View and it should be the one watching your model for updates, and upon change it should ask View to re-render.
There are more things you could do to follow strict MVVM approach and make your code more loosely coupled and testable. Above points might not be 100% correct I have just shared some basic ideas i have. You can check article online for further follow up.
The above answer works . But I have gone through what suggested by #Joakim Danielson to find what exactly happens when you are updating the View and Why it is not updating on ViewModel
So I made an update to delegate function
ViewController delegate function
func PersonFollowingTableViewCell1( _ cell: PersonFollowingTableViewCell, array : Person, tag : Int)
Here, I called the array in the Viewmodel and assigned the values of array in func argument to it.
like ViewModel().Vmarray[tag].currentFollow = array[tag].currentFollow

Updated: How to checkmark select items from selection List by clicking Tableview cell in swift

I am trying to retrieve selected item in tableView from ViewController which has a list from sqlite like checkmark selection. My FirstViewController have tableView and on clicking selective cell it opens ViewController to select items.
Once selection has been made I have to reload FirstViewController tableview. Actually I am trying to do but not accurately doing. Anybody help me please to select item and set in tableview. I am confused and not able to do that passing selection via segue.
My github project link is below:
My Project: https://github.com/MasamMahmood/SqliteDataList/tree/master/SqliteDataList
Reference: https://blog.apoorvmote.com/how-to-pass-selection-via-segue/
Updated One FirstViewController:
class FirstViewController: UIViewControlller, ListDelegate {
var selectedIndex: Int?
var selectedSection: Int?
//Click event for navigation from FirstViewController to SecondViewController
#IBAction func BackButtonAction(_ sender: Any) {
let vc = self.storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
vc.delegate = self
self.navigationController?.pushViewController(vc, animated: true)
}
func listFunc(listValue: String) {
AppData?.sectionList?[selectedSection!].items?[selectedIndex!].textField = listValue
tableView.reloadData()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//Add these two line in your code
selectedIndex = indexPath.row
selectedSection = indexPath.section
}
}
Updated SecondViewController:
protocol ListDelegate {
func listFunc(listValue: String)
}
class SecondViewController: UIViewControlller {
var delegate: ListDelegate?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let indexPath = tableView.indexPathForSelectedRow
let currentCell = tableView.cellForRow(at: indexPath!)!
//print(currentCell.textLabel?.text as Any)
currentCell.accessoryType = .checkmark
delegate?.listFunc(listValue: currentCell.textLabel?.text ?? "")
self.navigationController?.popViewController(animated: true)
}
}
What you need is a delegate to communicate back to the FirstViewController. You can do that like so...
protocol ViewControllerDelegate: AnyObject {
func viewControllerDidMakeSelection(at index: Int)
}
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
weak var delegate: ViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.viewControllerDidMakeSelection(at: indexPath.row)
}
}
class FirstViewController: UIViewController, ViewControllerDelegate {
#IBOutlet weak var tableView: UITableView!
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc = self.storyboard?.instantiateViewController(withIdentifier: "ViewController") as! ViewController
vc.delegate = self
self.navigationController?.pushViewController(vc, animated: true)
}
func viewControllerDidMakeSelection(at index: Int) {
// This is where the magic happens
}
}

App crash If popViewController while scrolling UITableView

Hello I have UIViewController inside UINavigationController which has a UITableView in it. I'm reseting my data model when user tap back button (which triggers popViewController function of UINavigationController). If I popViewController , while scrolling UITableView, App crashes on cellForRowAt function. What can cause this problem?
cellForRowAt function:
class MyViewController: UIViewController, TableView... {
var myChecksModel: MyChecksModel!
.
.
.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: MyChecksListTableViewCell = tableView.dequeueReusableCell(withIdentifier: cellId) as? MyChecksListTableViewCell ??
MyChecksListTableViewCell(style: .default, reuseIdentifier: cellId)
let check = self.myChecksModel.chequeList[indexPath.row]
cell.myChecksData = check
return cell
}
Back Button
#objc func backButtonTapped(_ sender: UIButton) {
DispatchQueue.main.async {
self.myChecksModel.resetModel()
self.navigationController?.popViewController(animated: true)
}
}
Reset Model Function
class MyChecksModel: Codable {
var chequeList: [MyClass] = []
func resetModel() {
self.chequeList = []
}
Your app is crashing because of
index out of bound
You should call
self.myChecksModel.resetModel()
In
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.myChecksModel.resetModel()
}

How to pass indexPath value from performSegue() to prepareForSegue()

I have a tableView in a SubMenuViewController, when a user taps (using didSelectRowAt) on a cell and segues, I need to pass that cell to the next UserInputViewController,
Here is my code:
class SubMenuViewController: UIViewController {
//MARK: - Properties and outlets
var node: Node?
#IBOutlet weak var tableView: UITableView!
//MARK: - View controller methods
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.isNavigationBarHidden = false
self.navigationItem.title = node?.value.rawValue
let nib = UINib(nibName: "SubMenuTableViewCell", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "SubMenuCell")
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "userInput" {
let vc = segue.destination as! UserInputViewController
let indexPath = sender as! IndexPath
vc.node = node?.childenNode[indexPath.row]
}
}
}
//MARK: UITableViewDataSource methods
extension SubMenuViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return node!.childCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SubMenuCell", for: indexPath) as! SubMenuTableViewCell
let desciptionModule = node?.childenNode[indexPath.row].value
let description = Modules.description(module: desciptionModule!)
cell.title.text = description.main
cell.subtitle.text = description.sub
return cell
}
}
//MARK: - UITableViewDelegate methods
extension SubMenuViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 68
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let selectedNode = node?.childenNode[indexPath.row] else {
return
}
if selectedNode.isLeaveNode() {
performSegue(withIdentifier: "userInput", sender: indexPath)
} else {
let subMenuViewController = self.storyboard!.instantiateViewController(withIdentifier: "subMenu") as! SubMenuViewController
subMenuViewController.node = selectedNode
//let subMenuViewController = SubMenuViewController(node: selectedNode)
self.navigationController?.pushViewController(subMenuViewController, animated: true)
}
}
}
Right now, in my performSegue, I passed in my indexPath into the sender, and I should expect to get it back in prepareForSegue, but I can't. Any suggestions guys?
Thanks
In my opinion it isn't very good practice to pass the index path (or any other value that counts a s "data") as the sender argument; as its name suggests, it is intended for passing the object that sent the message (i.e., called the action method), in this case self (you could "relay" the original sender if your action method calls another action method instead, but that's off-topic here).
As #sCha kindly pointed out in the comments, the Apple documentation on this method in particular, though, seems to leave room for doubt nevertheless. The parameter name sender clearly comes from the homonimous argument in all control actions that follow Cocoa's target/action pattern.
My suggestion:
The best you can do I think is to store the index path in a property of your view controller:
var selectedIndexPath: IndexPath?
...set it on tableView(_:didSelectRowAt:):
if selectedNode.isLeaveNode() {
self.selectedIndexPath = indexPath
performSegue(withIdentifier: "userInput", sender: indexPath)
} else {
self.selectedIndexPath = nil
// ...
...and retrieve it (while resetting the property) in the prepareForSegue(_:sender:) implementation of the target view controller:
if let vc = segue.source as? SubmenuViewController {
if let indexPath = vc.selectedIndexPath {
vc.selectedIndexPath = nil // (reset it, just to be safe)
// Use indexPath...
}
}

Tableview.reloadData doesn't work

I have two controllers and I'd like to pass datas between them:
This is the first controller, a tableviewcontroller.
class BooksVC: UITableViewController {
var books: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Books"
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return books.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = books[indexPath.row]
return cell
}
}
And this is the second controller, a viewcontroller
class AddController: UIViewController {
#IBOutlet weak var inputField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func done(_ sender: Any) {
let myVC = storyboard?.instantiateViewController(withIdentifier: "BooksVC") as! BooksVC
myVC.books.append(inputField.text!)
myVC.tableView.reloadData()
dismiss(animated: true, completion: nil)
}
}
Reloaddata doesn't work, can you help me I would be very glad.
You need to have a reference to the first view controller to do what you need.
You can do something like this:
class AddController: UIViewController {
....
weak var booksVC: BooksVC?
#IBAction func done(_ sender: Any) {
booksVC.books.append(inputField.text!)
booksVC.tableView.reloadData()
dismiss(animated: true, completion: nil)
}
}
And when you instatiate an AddController, you need to pass a reference of your BooksVC. Something like this:
addController.booksVC = self
Alright for second ViewController when doneButton pressed you can
dissmisstheview
trasffer data
self.parentviewController. books.apped(inputField.text!)
self.dissmissviewcontroller
than in the first VC you can do something like this
in viewWillAppear func{
tableview.reloadData()
}

Resources