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()
}
Related
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)
}
}
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()
}
I tried to use this solution but is not working.
what I want i s a button in my customized cell that knows data from the array that is used from the tableview (later I'll apply it to CoreData), for example, print the value of the array that generated the tableview.
but I cannot understand how to do it
I have a customized cell class, where I tried to use both a button action or a outlet button (with tags):
import UIKit
class MyTableViewCell: UITableViewCell {
#IBOutlet weak var myCellImage: UIImageView!
#IBOutlet weak var myCellLabel: UILabel!
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
}
}
and a ViewController where is my tableview with an extension
extension ViewController {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return comicsArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = myTableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell
cell.myCellLabel.text = String(indexPath.row)
cell.myCellButton.tag = indexPath.row
cell.myCellButton.addTarget(self, action: Selector("logAction:"), for: .touchUpInside)
return cell
}
func logAction(sender: UIButton) {
let titleString = self.comicsArray[sender.tag]
let firstActivityItem = "\(titleString)"
let activityVC = UIActivityViewController(activityItems: [firstActivityItem], applicationActivities: nil)
self.present(activityVC, animated: true, completion: nil)
}
}
EDIT:
solved with help of abdullahselek by adding in subclasses cell:
public var dataFromTableView : String!
and implementing:
#IBAction func myCellButtonTapped(_ sender: UIButton) {
guard dataFromTableView != nil else {return}
print("pushed \(dataFromTableView!)")
}
and in cellForRowAt :
cell.data = comicsArray[indexPath.row]
Add a dictionary or object model property to your custom tableviewcell like
public var data: Dictionary!
And set this data property
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.data = comicsArray[indexPath.row]
}
Then you button can reach data with in your tableviewcell
I have a tableView with custom cell. in my custom cell I have a like button. for like Button I wrote a function to change state from .normal to .selected like this:
FeedViewCell
class FeedViewCell: UITableViewCell {
#IBOutlet weak var likeButton: UIButton!
var likes : Bool {
get {
return UserDefaults.standard.bool(forKey: "likes")
}
set {
UserDefaults.standard.set(newValue, forKey: "likes")
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.likeButton.setImage(UIImage(named: "like-btn-active"), for: .selected)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func likeBtnTouch(_ sender: AnyObject) {
print("press")
// toggle the likes state
self.likes = !self.likeButton.isSelected
// set the likes button accordingly
self.likeButton.isSelected = self.likes
}
}
FeedViewController :
class FeedViewController: UIViewController {
#IBOutlet var feedTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Register Cell Identifier
let feedNib = UINib(nibName: "FeedViewCell", bundle: nil)
self.feedTableView.register(feedNib, forCellReuseIdentifier: "FeedCell")
}
func numberOfSectionsInTableView(_ tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.feeds.count
}
func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FeedCell", for: indexPath) as! FeedViewCell
return cell
}
}
But my problem is when I tap like button in cell with indexPath.row 0 the state of button in cell with indexPath.row 3 change state too.
where is my mistake?
thanks
You didn't post all your code, but I can tell you that for this to work the #IBAction func likeBtnTouch(_ sender: AnyObject) { } definition must be inside the FeedViewCell class definition to make it unique to a particular instance of the cell.
As a rule of thumb, I normally ensure that all the UI elements inside my cell are populated in cellForRowAtIndexPath when using dequeued cells. Also it should be set from an external source. I.o.w not from a property inside the cell. Dequeuing cells reuse them, and if not setup properly, it might have some leftovers from another cell.
For example, inside cellForRowAtIndexPath:
self.likeButton.isSelected = likeData[indexPath.row]
I am trying to figure out from long time. Can someone tell me why my delegate method is never called. Its a tvOS project but i believe it should work as simple iOS app. On click of button i have a popup table view and on select i am trying to update button label with selected option.
protocol PopupSelectionHandlerProtocol{
func UpdateSelected(data:String)
}
class PopupViewController: UIViewController, UITableViewDataSource,UITableViewDelegate {
#IBOutlet weak var myTable: UITableView!
let months = [1,2,3,4,5,6,7,8,9,10,11,12]
let days = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]
let yearsRange = [2015,2016,2017,2018,2019,2020]
var popupType:String!
var delegate:PopupSelectionHandlerProtocol?
override func viewDidLoad() {
super.viewDidLoad()
popupType = "months"
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (popupType == "months"){
return 12
}else if (popupType == "days"){
return 31
}else if (popupType == "years")
{
return 6
}
return 10
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel?.text = String(months[indexPath.row])
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print(tableView.cellForRowAtIndexPath(indexPath)?.textLabel?.text)
delegate?.UpdateSelected((tableView.cellForRowAtIndexPath(indexPath)?.textLabel?.text)!)
self.dismissViewControllerAnimated(true, completion: nil)
}
}
And then This -
class VacationPlannerController: UIViewController,PopupSelectionHandlerProtocol {
#IBOutlet weak var fromMonth: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let popupDelegate = PopupViewController()
popupDelegate.delegate = self
}
func UpdateSelected(data:String){
print("Inside UpdateSelected VacationPlannerController \(data)")
fromMonth.titleLabel?.text = data
}
}
The problem is that, you are getting your delegate as nil, since there can be only one ViewController at a time presented. Since your popupViewController's view is not loaded. The viewDidLoad() method is not getting called, resulting in non-setting of popupDelegate.
If you want to check its nullity. Try this in your didSelect... Method
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.navigationController?.pushViewController(VacationPlannerController(), animated: true)
if(delegate==nil){
print("delegate is nil")
}
delegate?.UpdateSelected((tableView.cellForRowAtIndexPath(indexPath)?.textLabel?.text)!)
}
If you want the fromMonth button to be updated. First you will need to present/push VacationPlannerController in order to call its viewDidLoad(). Then only you will be able to update its property, that is, fromMonth label.
Two things to resolve this issue -
First in PopupViewController-
In didSelectRowAtIndexPath, replaced
delegate?.UpdateSelected((tableView.cellForRowAtIndexPath(indexPath)?.textLabel?.text)!)
with
self.delegate?.UpdateSelected((tableView.cellForRowAtIndexPath(indexPath)?.textLabel?.text)!)
And Second in VacationPlannerController-
Removed below code from viewDidLoad -
let popupDelegate = PopupViewController()
popupDelegate.delegate = self
And added prepareForSegue -
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let destinationVC = segue.destinationViewController as! PopupViewController
destinationVC.delegate = self
}
And issue resolved yeeee :)