I am programming my first iOS application and am in the process of learning about protocols and delegation. The application is a basic random response generator. When the user shakes the device or taps the screen, a random response should display in a label.
Currently, I have three view controllers embedded in a navigation controller: MainViewController.swift, SettingsViewController.swift, and ResponsesViewController.swift.
I have an array in MainViewController.swift that holds the responses. I have successfully been able to pass the data to the third view controller, which is ResponsesViewController.swift, which is a table view for displaying the stored responses. I have set my first view controller, MainViewController as the delegate for my third view controller, ResponsesViewController, and implemented the method where I would like to remove the selected response from the data model (responses array).
My problem is when I try deleting a response I get fatal error: Cannot index empty buffer in the console. I used println(responses.count) to make sure that I was passing the array to the second and third view controllers successfully. I placed the println(responses.count) statement in all three view controllers within the methods viewDidLoad(), viewWillAppear(), and viewDidDisappear(). This showed that the data was passing successfully as there were 3 objects in the array with each println() statement. However, in my delegate, MainViewController.swift, I keep getting the error when I try to remove the selected response from the data model. I placed println(responses.count) into this method, but it keeps returning 0 and crashing with the error. It is only happening when func responsesViewController(controller: ResponsesViewController, didDeleteResponseAtIndexPath indexPath: NSIndexPath) is being called in the delegate (MainViewController.swift)
Here is my code:
MainViewController.swift
var responses: [Response] = []
let response1 = Response(text: "String 1")
let response2 = Response(text: "String 2")
let response3 = Response(text: "String 3")
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBarHidden = true
responses += [response1, response2, response3]
}
* delegate *
func responsesViewController(controller: ResponsesViewController, didDeleteResponseAtIndexPath indexPath: NSIndexPath) {
responses.removeAtIndex(indexPath.row)
}
ResponsesViewController.swift
protocol DeleteResponseDelegate {
func responsesViewController(controller: ResponsesViewController, didDeleteResponseAtIndexPath indexPath: NSIndexPath)
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
delegate?.responsesViewController(self, didDeleteResponseAtIndexPath: indexPath)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
tableView.reloadData()
}
}
-EDIT-
It turns out that I was able to fix my issue by simply passing the array from my third view controller back to my first view controller. I was obviously confused about delegation. Here is my updated code where I deleted the item from the array and then passed that array back to the first view controller:
ResponsesViewController.swift
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
responses.removeAtIndex(indexPath.row)
let mainViewController = navigationController?.viewControllers.first as MainViewController
mainViewController.responses = self.responses
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
tableView.reloadData()
}
}
From your comment -
I had assumed that the responses array in MainViewController still
held the objects that it was created with. Is that incorrect? Does the
data travel across view controllers and only have one instance of the
array at all times?
This would be true of an NSArray, which is an object. Swift arrays are structures, and structures are value types. Value types are copied when they are assigned.
In fact, all of the basic types in Swift—integers, floating-point
numbers, Booleans, strings, arrays and dictionaries—are value types,
and are implemented as structures behind the scenes.
All structures and enumerations are value types in Swift. This means
that any structure and enumeration instances you create—and any value
types they have as properties—are always copied when they are passed
around in your code.”
Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks. https://itun.es/au/jEUH0.l
Related
so here's the problem I had today. In one method of mine on a project I've been working on I fetch entities using core data, and it seems to work. Here's my method:
func getVehicles() {
let moc = DataController().managedObjectContext
let vehicleFetch = NSFetchRequest(entityName: "VehicleEntity")
do {
allVehicles = try moc.executeFetchRequest(vehicleFetch) as! [VehicleEntity]
} catch {
fatalError("Failed to fetch vehicles: \(error)")
}
}
I call that in my viewDidLoad method right after calling super.viewDidLoad.
I'm loading a property of those entities into a tableview and I do so like this:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("vehicleCell")
cell!.textLabel!.text = allVehicles[indexPath.item].vehicleType!
return cell!
}
That works great, and it returns the .vehicleType property from all the entities and and puts it in the table. Here's where my problem occurs, in the method that gets called when a row is selected:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedVehicle = allVehicles[indexPath.item]
if let del = delegate {
del.updateCurrentVehicle(selectedVehicle)
}
self.navigationController?.popViewControllerAnimated(true)
}
Selected vehicle comes back with a bunch of weird data. Interacting with it in the console at a breakpoint has the .vehicleType property coming back as nil and it gives me an exception when I try to access other properties, all of which are of type NSNumber. If I go and fetch the data again like this:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedVehicle: VehicleEntity
let moc = DataController().managedObjectContext
let vehicleFetch = NSFetchRequest(entityName: "VehicleEntity")
do {
selectedVehicle = try moc.executeFetchRequest(vehicleFetch)[indexPath.item] as! VehicleEntity
// allVehicles = fetchedVehicles
} catch {
fatalError("Failed to fetch vehicles: \(error)")
}
if let del = delegate {
del.updateCurrentVehicle(selectedVehicle)
}
self.navigationController?.popViewControllerAnimated(true)
}
Why am I not able to access the properties of entities in that array in my first way of selecting the row, but I am by reselecting it? Is this some weird bug, or some nuance of Swift I was unaware of?
If anyone could help explain this, I'd really appreciate it. I'm glad I made it work, but I'd really like to understand why one way works and the other way doesnt.
Edit in response to the comments:
VehicleEntity is in fact a subclass of NSManagedObject. Here is the code for updateCurrentVehicle:
protocol SavedVehicleDelegate {
func updateCurrentVehicle(newVehicle: VehicleEntity)
}
and
func updateCurrentVehicle(newVehicle: VehicleEntity) {
setVehicleData(newVehicle)
}
Although the selectedVehicle has bad data before it gets passed into that method. I set a breakpoint in both the cellForRowAtIndexPath and didSelectRowAtIndexPath methods, and in the former the data is fine and correct, but in the second the properties of the vehicle object are as I've described above.
The updateCurrentVehicle method is what I use to pass the selected VehicleEntity object back to the parent view (this is in a navigation controller)
I use the indexPath.item only because the tableview isn't divided up into sections, so the item just returns the overall index in the tableview as a whole, or that's my understanding of it anyways.
I have been learning swift through the last few days and I have come across an error that I have been stuck on for quite a while now.
I am attempting to get the selected indexPath so that I can then push data according to which item he selected. I have searched through and tried many different solutions I have found on stack overflow as well as different websites but I am not able to get this figured out still.
The code is below:
#IBOutlet var selectGroceryTable: UITableView!
/* Get size of table */
func tableView(tableView: UITableView, numberOfRowsInSection: Int) ->Int
{
return grocery.count;
}
/* Fill the rows with data */
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let myCell:UITableViewCell = selectGroceryTable.dequeueReusableCellWithIdentifier("groceryListRow", forIndexPath:indexPath) as! UITableViewCell
myCell.textLabel?.text = grocery[indexPath.row];
myCell.imageView?.image = UIImage(named: groceryImage[indexPath.row]);
return myCell;
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
print("Row Selected");
NSLog("Row Selected");
}
Nothing ever prints acting like the function is not being called. However, I do not understand why this would not be called?
Any help would be greatly appreciated!
UPDATE:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
selectGroceryTable.data = self;
selectGroceryTable.delegate = self; //gives error states you can not do this
}
There are a couple of things to check in cases like this:
First, what kind of method is didSelectRowAtIndexPath?
Answer: It's a UITableViewDelegate method. Did you set your view controller up as the delegate of the table view? If not, this method won't get called.
Second, have you made absolutely certain that the method signature is a perfect match for the method from the protocol? A single letter out of place, the wrong upper/lower case, a wrong parameter, and it is a different method, and won't be called. it pays to copy the method signature right out of the protocol header file and then fill in the body to avoid minor typos with delegate methods.
It looks to me like your method signature is correct, so my money is on forgetting to set your view controller up as the table view's delegate.
I am deleting a row from a UITableView that receives its information from CoreData. Currently, I have it figured out to where it can remove the CoreData info, but it's not wiping out the row in the UITableView. Most methods I've employed have resulted in crashes. Here is the raw code (credit: blog.revivalx.com)
var peopleArray: NSMutableArray = [People]() //People is CoreData entity
#IBOutlet weak var contactTable: UITableView!
let appDel: AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
let context: NSManagedObjectContext = appDel.managedObjectContext!
context.deleteObject(peopleArray[indexPath.row] as! NSManagedObject)
peopleArray.removeAtIndex(indexPath.row)
context.save(nil)
I've tried inserting:
contactTable.deleteRowsAtIndexPaths([indexPath.row
], withRowAnimation: UITableViewRowAnimation.Automatic)
into this code and have been met with errors. A plethora of sources have not turned up an answer for me on how to perform both removals simultaneously.
How do I remove the row from the UITableView and it's corresponding entry in CoreData at the same time in Swift?
Thanks
Edit:
Here is the function in its entirety:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == UITableViewCellEditingStyle.Delete {
let appDel: AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
let context: NSManagedObjectContext = appDel.managedObjectContext!
context.deleteObject(peopleArray[indexPath.row] as NSManagedObject)
peopleArray.removeAtIndex(indexPath.row)
contactTable.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
context.save(nil)
contactTable.reloadData()
}
}
This line
contactTable.deleteRowsAtIndexPaths([indexPath.row], withRowAnimation: UITableViewRowAnimation.Automatic)
should be change to this method due to the fact that it supposed to get an array of indexPath not array indexPath.row
contactTable.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
You are doing too much. This should (roughly) work according to the MVC (Model View Controller) design pattern:
Core Data takes care of the data, that is, the model.
UITableView is responsible for displaying the data.
Your code is the controller which reacts to user actions and tells the model what to do.
The user taps on delete, you tell Core Data to delete the object and the view to reload the data.
So you should remove deleteRowsAtIndexPaths from your delegate function. Telling the table view to reloadData should result in UITableView calling your data source delegate functions like tableView(_ tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath). In that function you ask Core Data for the right data and hand it over to the view.
Or better yet create a new Master-Detail Application with Swift and Core Data options and you get all the correct source code for free. This code handles it differently from what I suggested above: It tells the table view to deleteRowsAtIndexPaths, but it doesn't do both, either.
I know how to send the user to a new cell after they select a cell but what if the order of my cells change because I am retrieving data from Parse so for each new cell, the row number changes.
How do I ensure the user is sent to the correct page when they select a certain cell? This is what I'm currently using but I know there's got to be a better solution than hardcoding every possible option..
Any advice?
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.section == 0 && indexPath.row == 1 {
self.performSegueWithIdentifier("toSettingsPage", sender: self)
}
}
For my understanding of your questions, I suggest you use a NSMutableDictionary to store all the user info data, and on the didSelectRowAtIndexPath function, you will use the indexPath to find the correct user info.
Your input:
A table view
An index path (section, row) ordered pair
Your output:
A string that identifies a segue
An ideal data structure for this is a dictionary.
First, notice that the table view input is always the same (you only seem to care about one table view - the protocol for data source is written to handle as many table views as you like, but most people use one for one).
Second, think about your keys and values: your key is the index path. And in fact, the index path breaks down into just an Integer because it is always the same section, which is analogous to the situation with table view described above.
So your dictionary is going to be of type: Dictionary<Integer, String>.
Now, instead of using the dictionary directly, let's make a function to wrap it and call the function segueForIndexPathInDefaultTableView:
private let seguesForIndexPaths:[Integer:String] = [0:"segue0",1:"segue1",2:"segue2"]
private func segueForIndexPathInDefaultTableView(indexPath: NSIndexPath) {
return self.seguesForIndexPaths[indexPath.row]
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.performSegueWithIdentifier(self.segueForIndexPathInDefaultTableView(indexPath: indexPath), sender:self)
}
When I try to delete a row from the table view using the code below I keep getting a Thread 1: signal SIGABRT error. Im using Swift (Not sure what to make of it) and am building a table view based application. I've been trying to delete a row all day and I'm just not getting anywhere with it. This should be simple task but Swift is making it impossible for me.
// Override to support editing the table view.
override func tableView(tableView: UITableView?, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath?) {
if editingStyle == .Delete {
if let tv = tableView {
if let ip = indexPath {
tv.deleteRowsAtIndexPaths([ip], withRowAnimation: UITableViewRowAnimation.Fade)
}
}
// Delete the row from the data source
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
The error seems to be related to the AppDelegate file:
Are you also updating the data source that backs the table view? It can cause problems if the tableView(numberOfRowsInSection) method doesn't change its return value.