I'm working on a "trading" application where I would like to have a static number of cells.
On load, users will see 5 cells, each displaying a label that says "Add."
When a "player" is added, that cell displays the players information, the other 4 cells still display the "Add" label. Another is added, 2 cells have player information, 3 have the "Add"
I'm having a hell of a time with this. Can anyone point my in the right direction? I have custom labels setup, I think my logic may just be off on how to perform this correctly.
You need to subclass the UICollectionViewDelegate and UICollectionViewDataSource protocols in your viewController, then you need to implement the numberOfItemsInSection and cellForItemAtIndexPath functions.
Additionally to that you need to create two type of cells in your storyboard and subclass them, in the following code i will suppose that you call AddedPlayerCell and DefaultCell your cells, i will suppose that each cell has a label called labelText too.
let players = ["Player1","Player2"] //players added till now
let numberOfCells = 5
//Here you set the number of cell in your collectionView
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return max(players.count,numberOfCells);
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if((indexPath.row + 1) < self.players.count){ //If index of cell is less than the number of players then display the player
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("yourIdentifierForAddedPlayerCell", forIndexPath: indexPath) as! AddedPlayerCell
cell.labelText.text = self.players[indexPath.row] //Display player
return cell;
}else{//Else display DefaultCell
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("yourIdentifierForDefaultCell", forIndexPath: indexPath) as! DefaultCell
cell.labelText.text = "Add"
return cell;
}
}
In order to manage two different cell types, you can:
Create 2 prototype cells for your collection view. Give one the identifier "Add" and the other "Info". The "Add" cell prototype will contain the label "Add", and the "Info" cell prototype will contain fields to display the player info.
Add an array property to your class which keeps track of which cells are displaying "Add".
var showingAdd = [true, true, true, true, true]
In cellForItemAtIndexPath, check the showingAdd array to determine which identifier to use when dequeueing the cell:
let identifier = showingAdd[indexPath.row] ? "Add" : "Info"
let cell = dequeueReusableCellWithIdentifer(identifier...)
if !showingAdd[indexPath.row] {
// configure the cell with the proper player info
// retrieve info from info property array item created in
// step 4.
let player = playerInfo[indexPath.row]
cell.playerName = player.name
...
}
When a cell is selected in didSelectItemAtIndexPath, check if it is showing add and then process it accordingly:
if showingAdd[indexPath.row] {
// query user to get player info
// store the info in a property array indexed by `indexPath.row`
playerInfo[indexPath.row] = PlayerInfo(name: name, ...)
showingAdd[indexPath.row] = false
// trigger a reload for this item
collectionView.reloadItemsAtIndexPaths([indexPath])
}
Related
I want to upload pictures for different user types. So for user type 1..I want to create a collection view displaying 3 cells containing 3 options to add and display pic and for other users, 2 cells each. Like in above pic, 2 cells were created for userType = 3. So far Im able to create required number of cells according to users but haven't been able to upload pictures in any of them. I already have separate imagePicker delegate method to upload pic somewhere else but that's a static pic. I want to upload pics in these dynamically created cells. So if user = 3, Return 2 cells containing image view and button (Document) to add pic. Here's my code so far:
let reuseIdentifier = "cell"
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let userType = typeOfUser //typeOfUser = 1,2,3
var cellNumber = 0
if userType == "2" {
cellNumber = 3 //to return 3 cells for userType 2
} else {
cellNumber = 2 //to return 2 cells for userType 1 and 3
}
return cellNumber
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! DocumentCollectionViewCell
if userType == "2" {
for index in 0 ..< 3 { // for user = "2", 3 cells required so I used forloop < 3
let imageData:NSData = cell.userPic.image!.jpegData(compressionQuality: 60)! as NSData //compress image
let base64 = imageData.base64EncodedString(options: .lineLength64Characters) //store image in base64
//code below to pass base64 to image view and give it some id(stuck at creating logic for that)
}
}
return cell
}
This code does return correct number of cells with 3 empty image views with add picture button below each of them for typeOfUser = "2" as required but how to make button below each imageView add 1 picture at a time to one cell? Make each button use that imagePicker delegate and open photo library to add pic in "n" cells for respective user types?
Like how can I add picture in above image by clicking Document button below 1st image and then second image and if some other user, add as much images I want to add by clicking button below each image view created in no matter how many cells are returned?
This is all I want in short:
For user = 3
return 2 cells like above image
add pics by clicking document button below each image
for other users do same for different number of cells.
You can achieve this through delegate.
Create Protocol:
protocol ButtonDelegate {
// Define expected delegate functions
func cellButtonClicked(tag: Int, indexPath: IndexPath)
}
Confirm this Protocol in class where you want to take action:
extension ViewController: ButtonDelegate {
func cellButtonClicked(tag: Int, indexPath: IndexPath) {
//implement Logic to open ImageLibrary, you have indexPath available here
}
}
Inside PhotoCell create delegate instance and info needed for delegate method:
class PhotoCell... {
var cellButtonDelegate: ButtonDelegate?
var indexPath: IndexPath = IndexPath()
}
In ViewController:func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell add these lines:
cell.indexPath = indexPath
cell.ImageButton.tag = 0 // tag to identify userNumber
cell.cellButtonDelegate = self
In button action inside PhotoCell, inform delegate function:
cellButtonDelegate.cellButtonClicked(tag: sender.tag, indexPath: self.indexPath)
I have a tableview, where each cell is a custom tableview cell. That custom tableview cell contains a tableview and a label.
Lets say, the outer tableview is called MainTableView. Each cell of MainTableView consists another tableview. The problem is , when I select
inner tableview cell one by one the previously selected cell is not get deselected.In first image I have selected cell contains text “Two”. Then In the second image I have selected cell contains text “Five” but perviously selected cell “Two” still in selection mode. I want to deselect the previous cell when select a new one.
I have tried
tableView.deselectRow(at: IndexPath, animated: Bool)
this method inside didSelectRowAt in custom tableviewcell class but it didn’t serve the purpose because the previous indexPath is from seperate tableview. So, how can I deselect the previous one?
to get the correct inner tableView,
Firstly , you should record the outside tableView's cell indexPath, which cell the inner tableView is in.
So your should record two indexPathes
var selectionRowInfo: (lastOutsideIP: IndexPath?, lastInnerIP: IndexPath?)
Secondly, you get the correct tableView, via the outsideTableView.
if the inner table is visible, you shall handle it immediately. through outTableView.indexPathsForVisibleRows
the else condition, you need not handle it. The tableView reuse mechanism will refresh its state.
// pseudo code.
if let lastOut = lastOutsideIP, let visibleRows = outTableView.indexPathsForVisibleRows, visibleRows.contains(lastOut){
let cell = tableView.cellForRow(at: lastOut) as! YourCell
// get the correct inner tableView via the cell
}
Because the inner tableViews are not related to each other. Select the cell of table one, will not affect the selection of cell of table two.
So your should build the connection manually.
Use a property to store the state var lastIndexPath: IndexPath?,
then every time select a indexPath,
// pseudo code.
if let last = lastIndexPath{
tableView.deselectRow(at: last, animated: true)
}
Please notice that, you should find the correct inner tableView, which has the lastIndexPath
The previous answer is along the right lines but has a flaw - it cannot distinguish between tableViews, which is the most important part. Also if the tableViews have different numbers of rows, it risks trying to access a row that doesn't exist and causing a crash.
To track the selected row in two tableViews (tv1 & tv2) you'll need to hold the selected row in each:
var tv1, tv2: UITableView!
var lastRowForTV1, lastRowForTV2: IndexPath?
and then respond to selections by identifying the tableView being used and adjusting the other (this assumes the two tableViews use the same datasource/delegate)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView === tv1 {
lastRowForTV1 = indexPath
if let last = lastRowForTV2 {
tv2.deselectRow(at: last, animated: true)
lastRowForTV2 = nil
}
} else if tableView === tv2 {
lastRowForTV2 = indexPath
if let last = lastRowForTV1 {
tv1.deselectRow(at: last, animated: true)
lastRowForTV1 = nil
}
}
}
I have solved the problem by using the idea of first answer given by dengApro . The idea was to find the correct inner table which contains the previously selected cell.
I have two files one is ViewController.swift that contains the outer tableview MainTableView. Another one is CustomTableViewCell.swift with CustomTableViewCell.xib that contains the custom cell with tableview.
fileprivate var lastSelectedInnerTableView : Int = -1
fileprivate var lastSelectedRow: Int = -1
fileprivate var tableViewList :[UITableView] = []
I have added these 3 variables in CustomTableViewCell.swift file outside the class CustomTableViewCell. lastSelectedSection , lastSelectedRow these 2 variable are used to keep
track of the last selected inner tableview (lastSelectedInnerTableView) and the last selected cell of that inner tableView (lastSelectedRow). tableViewList variable is used to keep the
Inner tableView. Inside awakeFromNib() I have append the created inner tableviews.
override func awakeFromNib() {
super.awakeFromNib()
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
tableViewList.append(self.tableView) // append the created inner tableviews
}
Then inside didSelectRowAt I have deselect the previous one:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if lastSelectedInnerTableView != -1 && lastSelectedRow != -1{
self.oldIndexPath.section = 0
self.oldIndexPath.row = lastSelectedRow
tableViewList[lastSelectedInnerTableView].deselectRow(at: self.oldIndexPath, animated: true)
}
lastSelectedInnerTableView = self.innerTableId
lastSelectedRow = indexPath.row
}
In ViewController.swift I have set innerTableId inside cellForRowAt
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let customCell: CustomTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "customCell") as! CustomTableViewCell
customCell.innerTableId = indexPath.row
customCell.customCellActionDelegate = self
return customCell
}
What I'm trying to do is when I tap the button in a cell, that button in that cell becomes invisible. The problem is when I tap the button, it becomes invisible, but when I scroll the collection view the hidden button goes from one to the other. For example, I tap the second one it hides but when I scroll I see that the 7th becomes hidden. Every time I scroll the hidden button change.
This is the code I wrote:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell : CollectionViewCellKharid3 = collectionView.dequeueReusableCell(withReuseIdentifier: "customcell3", for: indexPath) as! CollectionViewCellKharid3
cell.lblEsmeMahsul.text = mainCats[indexPath.row]
cell.imgMahsul.af_setImage(withURL: URL(string : (mainadress + "/Opitures/" + mainPicNumbers[indexPath.row]))!, placeholderImage: UIImage(named: "loadings" ))
cell.btnKharid.addTarget(self, action: #selector(btnColectionviewCellTapped), for : UIControlEvents.touchUpInside)
cell.btnKharid.tag = indexPath.row
cell.btnMosbat.addTarget(self, action: #selector(btnMosbatTapped), for : UIControlEvents.touchUpInside)
cell.btnMosbat.tag = indexPath.row
cell.configureCell()
return cell
}
#objc func btnColectionviewCellTapped(_ sender:UIButton){
// let indexPath : IndexPath = self.collectionview1.ind
print(sender.tag)
sender.isHidden = true
}
#objc func btnMosbatTapped(_ sender:UIButton){
let index = IndexPath(item: sender.tag , section: 0)
let cell = self.collectionviewForushVije.cellForItem(at: index) as? CollectionViewCellKharid3
cell?.lblTedad.text = "22"
print(sender.tag)
}
Cells get reused. You need to keep track of which cells have been tapped so you can set the proper button state in your cellForItemAt method.
Declare a property in your class:
var beenTapped: Set<Int> = []
Then in btnColectionviewCellTapped add:
beenTapped.insert(sender.tag)
And in cellForItemAt you need:
cell.btnKharid.isHidden = beenTapped.contains(indexPath.item)
You should also replace the use of indexPath.row with indexPath.item. row is for table views. item is for collection views.
It's a very common mis-use of UICollectionView(or UITableView). When deal with them, you should alway keep one thing in mind, re-use. The collection/tableview cell will be highly reuse by os when on need. The problem cause in your code is, you assume the one time set of one property in a cell will be persistence, which is wrong. The cell come from dequeue method, can always be a new cell or an existing cell, therefore, any configuration should be apply to a cell should be config again. Think in that way, all view in a cell is "dirty" when it get it from collection view, you should set the property you want before return it back(or have a mechanism to set it later). Therefore, in your case, just set the isHidden property every time you prepare the cell in cellForRow delegate.
I have a table view with custom cells. They are quite tall, so only one cell is completely visible on the screen and maybe, depending on the position of that cell, the top 25% of the second one. These cells represent dummy items, which have names. Inside of each cell there is a button. When tapped for the first time, it shows a small UIView inside the cell and adds the item to an array, and being tapped for the second time, hides it and removes the item. The part of adding and removing items works fine, however, there is a problem related to showing and hiding views because of the fact that cells are reused in a UITableView
When I add the view, for example, on the first cell, on the third or fourth cell (after the cell is reused) I can still see that view.
To prevent this I've tried to loop the array of items and check their names against each cell's name label's text. I know that this method is not very efficient (what if there are thousands of them?), but I've tried it anyway.
Here is the simple code for it (checkedItems is the array of items, for which the view should be visible):
if let cell = cell as? ItemTableViewCell {
if cell.itemNameLabel.text != nil {
for item in checkedItems {
if cell.itemNameLabel.text == item.name {
cell.checkedView.isHidden = false
} else {
cell.checkedView.isHidden = true
}
}
}
This code works fine at a first glance, but after digging a bit deeper some issues show up. When I tap on the first cell to show the view, and then I tap on the second one to show the view on it, too, it works fine. However, when I tap, for example, on the first one and the third one, the view on the first cell disappears, but the item is still in the array. I suspect, that the reason is still the fact of cells being reused because, again, cells are quite big in their height so the first cell is not visible when the third one is. I've tried to use the code above inside tableView(_:,cellForRow:) and tableView(_:,willDisplay:,forRowAt:) methods but the result is the same.
So, here is the problem: I need to find an EFFICIENT way to check cells and show the view ONLY inside of those which items are in the checkedItems array.
EDITED
Here is how the cell looks with and without the view (the purple circle is the button, and the view is the orange one)
And here is the code for the button:
protocol ItemTableViewCellDelegate: class {
func cellCheckButtonDidTapped(cell: ExampleTableViewCell)
}
Inside the cell:
#IBAction func checkButtonTapped(_ sender: UIButton) {
delegate?.cellCheckButtonDidTapped(cell: self)
}
Inside the view controller (NOTE: the code here just shows and hides the view. The purpose of the code is to show how the button interacts with the table view):
extension ItemCellsTableViewController: ItemTableViewCellDelegate {
func cellCheckButtonDidTapped(cell: ItemTableViewCell) {
UIView.transition(with: cell.checkedView, duration: 0.1, options: .transitionCrossDissolve, animations: {
cell.checkedView.isHidden = !cell.checkedView.isHidden
}, completion: nil)
}
EDITED 2
Here is the full code of tableView(_ cellForRowAt:) method (I've deleted the looping part from the question to make it clear what was the method initially doing). The item property on the cell just sets the name of the item (itemNameLabel's text).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
ItemTableViewCell.identifier, for: indexPath) as? ItemTableViewCell{
cell.item = items[indexPath.row]
cell.delegate = self
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
I've tried the solution, suggested here, but this doesn't work for me.
If you have faced with such a problem and know how to solve it, I would appreciate your help and suggestions very much.
Try this.
Define Globally : var arrIndexPaths = NSMutableArray()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = self.tblVW.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.textLabel?.text = String.init(format: "Row %d", indexPath.row)
cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: #selector(btnTapped), for: .touchUpInside)
if arrIndexPaths.contains(indexPath) {
cell.backgroundColor = UIColor.red.withAlphaComponent(0.2)
}
else {
cell.backgroundColor = UIColor.white
}
return cell;
}
#IBAction func btnTapped(_ sender: UIButton) {
let selectedIndexPath = NSIndexPath.init(row: sender.tag, section: 0)
// IF YOU WANT TO SHOW SINGLE SELECTED VIEW AT A TIME THAN TRY THIS
arrIndexPaths.removeAllObjects()
arrIndexPaths.add(selectedIndexPath)
self.tblVW.reloadData()
}
I would keep the state of your individual cells as part of the modeldata that lies behind every cell.
I assume that you have an array of model objects that you use when populating you tableview in tableView(_:,cellForRow:). That model is populated from some backend service that gives you some JSON, which you then map to model objects once the view is loaded the first time.
If you add a property to your model objects indicating whether the cell has been pressed or not, you can use that when you populate your cell.
You should probably create a "wrapper object" containing your original JSON data and then a variable containing the state, lets call it isHidden. You can either use a Bool value or you can use an enum if you're up for it. Here is an example using just a Bool
struct MyWrappedModel {
var yourJSONDataHere: YourModelType
var isHidden = true
init(yourJSONModel: YourModelType) {
self.yourJSONDataHere = yourJSONModel
}
}
In any case, when your cell is tapped (in didSelectRow) you would:
find the right MyWrappedModel object in your array of wrapped modeldata objects based on the indexpath
toggle the isHidden value on that
reload your affected row in the table view with reloadRows(at:with:)
In tableView(_:,cellForRow:) you can now check if isHidden and do some rendering based on that:
...//fetch the modelObject for the current IndexPath
cell.checkedView.isHidden = modelObject.isHidden
Futhermore, know that the method prepareForReuse exists on a UITableViewCell. This method is called when ever a cell is just about to be recycled. That means that you can use that as a last resort to "initialize" your table view cells before they are rendered. So in your case you could hide the checkedView as a default.
If you do this, you no longer have to use an array to keep track of which cells have been tapped. The modeldata it self knows what state it holds and is completely independent of cell positions and recycling.
Hope this helps.
I have a few UITableViewCells with the labels Test1, Test2, Test3, Test4 and Test5. How can I find out the indexPath of the cell which displays Test3? I don't wanna touch the cell or do something like this. Thanks for your answers!
The cells:
Core Data model:
Create a class variable reference to the cell, and then in cellForRow assign the cell with the "Test3" label to your class variable.
var test3Cell: UITableViewCell?
// ...
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = ... // Declaration here
// set up the cell
if cell.textLabel?.text == "Test3" {
self.test3Cell = cell
}
return cell
}
Doing it at the cell level seems backwards — your data model (the table’s UITableViewDataSource) would seem much easier to query.
If you have a reference to the label you can use indexPathForRowAtPoint.
let pointInTable = sender.convertPoint(theLabel.bounds.origin, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(pointInTable)
indexPath where indexPath is an optional.
How are you populating the table? If you are populating the label in the cell of tableview using the content from an NSArray.. you can find the indexpath from the index of the item in the Array obj..
If not, the way i can think of is to loop through the table cells in the tableView
The table view responds to the below methods
func numberOfSections() -> Int
func numberOfRowsInSection(section: Int) -> Int
you can create an NSIndexPath instance using the above information for each row .. NSIndexPath(forRow: , inSection: )
get the UITableviewCell instance using
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
then you can check the text of label in the cell to see if it matches what you need.
This method will check your current visible cells within the tableview and iterate through them looking for the text.
// Swift 1.2
// Store current visable cells in an array
let currentCells = tableView.visibleCells() as Array
// Iterate through cells looking for a match
for cell in currentCells {
println (cell.textLabel?!.text)
if cell.textLabel?!.text == "Test 3" {
println("Test 3 Found")
}
}
// Swift 2.0
// Store current visable cells in an array
let currentCells = tableView.visibleCells
// Iterate through cells looking for a match
for cell in currentCells where cell.textLabel?.text == "Test 3" {
print("Test 3 found")
}