I have a program that will search a database and populate the collection view. That currently works. Every item has a "follow" button, and when that button is clicked I need it to perform an action, but I need that action to have access to the indexPath.item. The reason why the program is crashing is because the function for the button is hidden in the collection view method, but this is the only way I can gain access to the indexPath.item. How do I fix the crashing and still have access to the indexPath.item?
func followuser(sender: UIButton){
let pointInTable: CGPoint = sender.convertPoint(sender.bounds.origin, toView: self.collectionview)
if var indexPath :NSIndexPath = self.collectionview.indexPathForItemAtPoint(pointInTable){
print(pointInTable)
}
var relation: PFRelation = (PFUser.currentUser()?.relationForKey("Friendship"))!
//relation.addObject(users[indexPath.item])
print(relation)
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionview.dequeueReusableCellWithReuseIdentifier("searchcell", forIndexPath: indexPath) as! searchcustomcell
cell.follow.addTarget(self, action: "followuser:", forControlEvents: UIControlEvents.TouchUpInside)
I'm not sure what you mean by indexPath.item. NSIndexPath does not have a property item, so that code can't possibly work unless you've created a category of NSIndexPath.
And I'm not sure where you are getting the value for your indexPath variable.
You have a couple of challenges. The first challenge is to the get the indexPath of the cell that contains the button that was tapped.
One way you do that is to use the UICollectionView method indexPathForItemAtPoint. You'd take your button's bounds and use one of the UIView methods for converting points between coordinate systems to convert the button's origin from it's coordinate system to the collection view's coordinate system. You'd then take that converted coordinate and use it in a call to indexPathForItemAtPoint to get the indexPath that contains the button.
Next challenge: What is it you want to look up by indexPath?
Table views and collection views typically store their model data in either a 1 dimensional array (for a single row of items) or a 2 dimensional array (for a grid of cells.)
You need to take the index path and get either the row or the row and section and use that to index into your model to fetch whatever it is you need to fetch.
However we can't help you with that until you tell us how your collection view is organized and how the model that represents your cell data is stored.
Related
I've got a collection view with multiple cells created programically. I want to update a variable to a different value when the user taps a specific cell. So eg: Cell 1 is tapped -> var test = "cell1" , cell2 is tapped var test = "cell2". Usually I'd just create an IBAction by dragging from the storyboard but I'm not sure how to do it in this case.
I'm using Swift 3.1
To add interactivity to UITableViews, UICollectionViews, and other kinds of views which display collections of data, you can't use Storyboard actions, as the content is generated dynamically during runtime, and the Storyboard can only work for static content.
Instead, what you need to do is set your UICollectionView's delegate property to an object that implements the UICollectionViewDelegate protocol. One of the methods defined as part of the protocol is the collectionView(_:didSelectItemAt:) method. This method will get called whenever the user selects (taps) a collection view cell with the IndexPath to that cell as an argument. You can update your variable in that method. Just remember to deselect the cell after handling the tap by using the deselectItem(at:) method on your UICollectionView.
There are UICollectionView delegate that you need to implement. It goes like this
did select item at index path will give index path of the cell that was selected. Using indexpath or any other property of the data source array you are using, you can modify the variable value.
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if let cell = collectionView.cellForItemAtIndexPath(indexPath) {
//check your condition and modify the variable value depending on index path or any other property you are referring to.
}
}
I segued to a new View Controller. This view controller contains array data from the corresponding CollectionViewCell. How do I change the data to present the previous cell array? Without having to go back to the last view controller and selecting the cell from there? I'm using Swift 3 Below is an image that explains what I mean
The first image is in the first View Controller. The second and third images are what's displayed when either of the first two in the list are tapped. You can go to the next index using the arrow keys in the second View Controller. How do I achieve this functionality?
let dataSource = DataSource()
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if inSearchMode {
return filteredAnimal.count
}
return dataSource.anim.filter{ $0.isDefault! }.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AnimIndex", for: indexPath as IndexPath) as! AnimIndex
let animal: Animal
if inSearchMode {
animal = filteredAnimal[indexPath.row]
} else {
animal = dataSource.anim.filter{ $0.isDefault! }[indexPath.row]
}
cell.configureCell(animal)
return cell
}
This is a very common UI pattern. You should search on master/detail. You should be able to find lots of sample projects that give examples of this UI pattern.
Don't think in terms of cells. Think in terms of a model object - a store for your app data. You need a model object that represents the entire list of data you present in your first view controller.
That model object should be reachable from all view controllers that need to display information from it. You can either pass around a reference to the model object or make it a singleton. There are advantages to either approach.
For collection views and table views, which are organized by section and row, an array of arrays of data objects is often a good structure for your model. If your data is in a single section then you can use a single array.
When the user taps on a cell in your first view controller, you would invoke the second view controller (Either from a segue or by manually invoking it.) You'd pass the second view controller the indexPath of the selected object, and might also pass a reference to your model object (or, like I said, you could make the model object a globally accessable singleton, in which case you wouldn't need to pass it.)
Then, in the second (or 3rd) view controller, if the user taps the up/down button, you have access to the model object that stores ALL the data your app is presenting, so you can navigate to the next object from within any view controller. If your data is organzied in sections and rows or rows and columns you might want to add next/previous item methods to your data model so you only have to write that logic once.
I need an event, that changes a variable based on which TableViewCell I click. But unlike an action connected to a button, there is no action indicator for table view cells at all. So my question is:
I want to make a TableView that contains items of an array. Based on which item I click, I want to change my variable so that the result on the next ViewController depends on which button you click.
So to make things easier, here is an example what I want the app to look like:
On the first TableViewController I have a list based on an array and on the second ViewController I have a label that shows text based on the variable.
I have a nameArray = ["Mum", "Brother", "Me"] and a weightArray = [140, 160, 120] and a variable weight = 0. The label on the second ViewController tells the var weight. So when you click on "Mum" in the TableView I want the next ViewController to say 140, when I click on "Brother" then 160 and so on...
Until here everything works just fine and I have no problems with anything but changing the var based on what I click.
Long story, short sense:
I want an Action for the TableViewCell that changes the var like in an Action connected to a Button, but there is no Action outlet for Cells at all.
Use this method. Use indexPath.row to find what row number you selected
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var cell : UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
switch cell.labelSubView.text as! String {
case "Mum":
self.weight = weightArray[0]
case "Brother"
self.weight = weightArray[1]
and so on..
..
default:
statements
}
}
Note A better alternative
I also considered a case where you have too many entries in nameArray and switch statement might not be good. In that case you can get the text inside the selected row by cell.labelSubView.text as! String
next you can check if the nameArray contains the cell text and get the index of the name that matches the cell text. Next you can get the required weight at the same index in weightArray. And then do self.weight = weightArray[requiredIndex]
Hope this helps.
Update : My experienced friend #Duncan mentioned down below that switch statement in this case is a bad coding practice . I am not going to delete it because it is a lesson for me and also my fellow programmers who are relatively new to programming. So i have put it in a yellow box, stating that it is not a good code
A better option for this would be :
As Duncan mentions, creating an array of dictionary is a good option
Second option is the option in my answer after my Note
You need to maintain array of dictionaries , those dictionaries have keys like "person", and "weight", then you can easily get weight value after selecting the cell by using table view delegate method UITableViewDelegate's tableView:didSelectRowAtIndexPath:
Create an instance variable in your view controller (a var at the top level after the class definition) for the selected cell.
class MyTableViewController: UIViewController
var selectedRow: Int
override func tableView(tableView: UITableView,
didSelectRowAtIndexPath indexPath: NSIndexPath)
{
selectedRow = indexPath.row
//invoke a segue if desired
performSegueWithIdentifier("someSegue");
}
override func prepareForSegue(segue: UIStoryboardSegue,
sender: AnyObject?)
{
if segue.identifier == "someSegue"
{
//Cast the destination view controller to the appropriate class
let destVC = DestVCClass(segue.destinationViewController)
destVC.selectedRow = selectedRow
}
}
As Andey says in his answer, it's probably better to create a single array of data objects (dictionaries, structs, or custom data objects). Then when the user taps a cell, instead of passing the index of the selected row to the next view controller, you could pass the whole data object to the destination view controller. Then the destination view controller could extract whatever data it needed. (Weight, in your example.)
I noticed in that to disable the highlight feature of a table cell you would yse the following method:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
...
tableView.deselectRowAtIndexPath(indexPath, animated: false)
}
What I don't understand is what's going on in the first argument indexPath. In the instructional book I've been reading just about every other method has been using indexPath.row for selecting individual cells:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as CustomTableViewCell
cell.nameLabel.text = restaurantNames[indexPath.row]
cell.typeLabel.text = restaurantTypes[indexPath.row]
cell.locationLabel.text = restaurantLocations[indexPath.row]
}
Just out of curiosity I tried passing indexPath.row as an argument but got the error, 'NSNumber' is not a subtype of 'NSIndexPath'. In this particular case what is the main difference between using indexPath and indexPath.row.
From the NSIndexPath class reference:
The NSIndexPath class represents the path to a specific node in a tree
of nested array collections. This path is known as an index path.
Table views (and collection views) use NSIndexPath to index their items
because they are nested structures (sections and rows). The first index is the section of the tableview and the second index
the row within a section.
indexPath.section and indexPath.row are just a "convenience" properties, you always have
indexPath.section == indexPath.indexAtPosition(0)
indexPath.row == indexPath.indexAtPosition(1)
They are documented in NSIndexPath UIKit Additions.
So NSIndexPath is an Objective-C class and is used by all table view
functions. The section and row property are NSInteger numbers.
(Therefore you cannot pass indexPath.row if an NSIndexPath is expected.)
For a table view without sections (UITableViewStyle.Plain) the
section is always zero, and indexPath.row is the row number of
an item (in the one and only section). In that case you can use
indexPath.row to index an array acting as table view data source:
cell.nameLabel.text = restaurantNames[indexPath.row]
For a table view with sections (UITableViewStyle.Grouped) you have
to use both indexPath.section and indexPath.row (as in this
example: What would be a good data structure for UITableView in grouped mode).
A better way to disable the selection of particular table view rows is
to override the willSelectRowAtIndexPath delegate method:
override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
// return `indexPath` if selecting the row is permitted
// return `nil` otherwise
}
In addition, to disable the appearance of the cell highlight on touch-down, you can also set
cell.selectionStyle = .None
when creating the cell. All this is (for example) discussed in
UITableview: How to Disable Selection for Some Rows but Not Others.
Your question and confusion stems from the fact that Apple decided to use somewhat imprecise method name/signature
tableView.deselectRowAtIndexPath(indexPath, animated: false)
The "Row" word in the method signature implies that row is going to be deselected, therefore one would expect a row as a method parameter. But a table view can have more than one section. But, in case it doesn't have more sections, the whole table view is considered one section.
So to deselect a row (a cell really) in table view, we need to tell both which section and which row (even in case of having only one section). And this is bundled together in the indexPath object you pass as a parameter.
So the before-mentioned method might instead have a signature
tableView.deselectCellAtIndexPath(indexPath, animated: false)
which would be more consistent, or another option, albeit a bit less semantically solid (in terms of parameters matching the signature)
tableView.deselectRowInSectionAtIndexPath(indexPath, animated: false)
Now the other case - when you implement the delegate method
tableview:cellForRowAtIndexPath
here again (Apple, Apple...) the signature is not completely precise. They could have chose a more consistent method signature, for example
tableview:cellForIndexPath:
but Apple decided to stress the fact that we are dealing with rows. This is an 8 year old API that was never the most shining work of Apple in UIKit, so it's understandable. I don't have statistical data, but I believe having a single section (the whole tableview is one section) is the dominant approach in apps that use a table view, so you don't deal with the sections explicitly, and work only with the row value inside the delegate method. The row often serves as index to retrieve some model object or other data to be put into cell.
Nevertheless the section value is still there. You can check this anytime, the passed indexPath will show the section value for you when you inspect it.
NSIndexPath is an object comprised of two NSIntegers. It's original use was for paths to nodes in a tree, where the two integers represented indexes and length. But a different (and perhaps more frequent) use for this object is to refer to elements of a UITable. In that situation, there are two NSIntegers in each NSIndexPath: indexPath.row and indexPath.section. These conveniently identify the section of a UITableView and it's row. (Absent from the original definition of NSIndexPath, row and section are category extensions to it).
Check out tables with multiple sections and you'll see why an NSIndexPath (with two dimensions) is a more general way to reference a UITableView element than just a one-dimensional row number.
i see the perfect answer is from apple doc
https://developer.apple.com/documentation/uikit/uitableview/1614881-indexpath
if you print indexpath you may get [0,0] which is first item in the first section
I was confused between indexPath and indexPath.row until i understand that the concept is the same for the TableView and the CollectionView.
indexPath : [0, 2]
indexPath.section : 0
indexPath.row : 2
//
indexPath : the whole object (array)
indexPath.section : refers to the section (first item in the array)
indexPath.row : refers to the selected item in the section (second item in the array)
Sometimes, you may need to pass the whole object (indexPath).
I have a UITableViewController and when I click on it, it will show a DetailViewUIController.
I want to add a functionality which when I am in DetailViewUIController and I swipe to right, it will show the DetailViewUIController of next item and when left, it will show the previous item.
I find a link which kind of do that in swift. But it only has 3 static subviewcontroller.
https://medium.com/swift-programming/ios-swipe-view-with-swift-44fa83a2e078
The number of entries in my TableView can be pretty long, how can I do the same thing dynamically, i.e. without having static number of subviewcontroller created and add as 'addChildViewController'?
Update:
Thanks again #rdelmar for your help.
I am having trouble getting the ' set the contentOffset of the collection view to (collection view width) * selectedRow' to work.
In my prepareForSegue function, I have added:
x = Float(indexPath.row) * 375.0
var point = CGPointMake(CGFloat(x), 0)
println ("***x=")
println (x)
detailController.collectionView?.setContentOffset(point , animated: false)
where detailController is UICollectionViewController.
and in the
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as DetailViewCell
// Configure the cell
println("DetailViewController in cellForItemAtIndexPath()")
println(indexPath.row)
cell.myitem = allItems[indexPath.row]
return cell
}
And I always see cellForItemAtIndexPath trying to get 0th element of the allItems (that is the whole collections of all my objects.
So setContentOffset() does not change what I am displaying in the Detail View regardless which item I click in my TableView to launch the Detail View.
Any idea to solve this?
Thank you.
There are a lot of different ways you could do this. One way would be to make your detail view controller's view be a collection view (with paging enabled), whose cells are the same size as the screen. You would need to pass in the array that you use to populate your table so the collection view could populate its cells. This would be quite efficient, since the collection view would only ever need to instantiate two cells.