Add value to label inside nested collection view in Swift 3 - ios

I need to be able to set the text label inside a nested collection view.
I can show each of the values inside a print statement but when I assign it to the UILabel I get an fatal error: unexpectedly found nil while unwrapping an Optional value
I'm loading in the data from the parent collection view and assigning it to the data source:
private var timeZone: [AlternativeTimeZone]!
var setTimeZones: [AlternativeTimeZone] {
get {
return self.timeZone
}
set {
self.timeZone = newValue
}
}
childcollectionviewService.setTimeZones = alternateTimesZonesData
The service is an external class which houses all reference to the parent collection view in order to keep the view controller small.
My nested collection view code looks standard.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AlternateTimeZoneCell", for: indexPath) as? AlternateTimeZoneCell else { return UICollectionViewCell() }
print("Time Zone:", timeZone[indexPath.row].time)
cell.alternateTime.text = timeZone[indexPath.row].time
return cell
}
AlternativeTimeZone is a simple struct with a text string property. I am inside a nested collection view so could that indexPath be overriding the childs?

It sounds to me like you have a broken outlet link in the cell template for the field alternateTime in your AlternateTimeZoneCell. Check to see if that's nil in the cell that you load. Also check to see if the dequeue is succeeding.

I managed to solve this by removing the data source and delegate declaration into the awakeFromNib method as it turns out the init was trying to set the alternateTime value before the data was given to it.

Related

How to copy/duplicate a custom collectionview cell in Swift?

I want to copy a custom collectionview cell from a viewcontroller to another, the problem is the collectionview cell disappears once I tap on it because of - apparently - adding it in the 2nd viewcontroller's as a subview.
I tried most of the methods here Create a copy of a UIView in Swift
One about Archiving returns nil.
Other about prototypes and structs, it doesn't return a new address for the view.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
struct ObjectToCopied {
var objToBeRetrieved: UIView? = nil
init(objToSaved : UIView?) {
objToBeRetrieved = objToSaved
}
}
let pickedView : UIView? = collectionView.cellForItem(at: indexPath)
let tempObj = ObjectToCopied(objToSaved: pickedView)
let copiedObj = tempObj
let copiedView = copiedObj.objToBeRetrieved as? UIView
// here the copiedview's address is THE SAME as tempObj's ObjToBeRetrieved.
}
Custom CollectionView cell image example:
Don't copy UIView object!
Your model isn't good.
You have to store the data – information somewhere outside the cell directly.
You have to call your 'database' using indexPath to retrieve the information. No more!
About your question.
This object doesn't copy your object.
struct ObjectToCopied {
var objToBeRetrieved: UIView? = nil
init(objToSaved : UIView?) {
objToBeRetrieved = objToSaved
}
}
ObjectToCopied is a value type. But UIView is a reference type. So, objToBeRetrieved contains the same address as pickedView. Solution? Use objToBeRetrieved = objToSaved.copy(). But, please, really don't do it. That's really bad. Futhermore, I think this will corrupt something. Use better application model – the first suggestion.
Why do you need to copy the collectionView cell? You can simply pass your data to next viewController and create new view or cell (whatever you need) and display data on it.
Any specific reason to copy collectionView cell?

How to communicate between Classes in a hierarchy in Swift

With the inspiration coming from the idea that you can code anything, i tried my hand at a complicated CollectionView nested structure which goes as follows:
CustomCollectionViewController
--CustomCollectionViewCell
----CustomTableView
------CustomTableViewCell
--------CustomPickerView
In CustomCollectionViewController, the main data feed comes from property:
var cardFeed: [String: [Card]] = [:]
Card is my defined model and variable cardFeed is applied the usual way in UICollectionView Delegate & DataSource methods:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "card", for: indexPath) as! CustomCollectionViewCell
cell.contentView.clipsToBounds = true
cell.delegate = self
cell.card = self.cardFeed[string]![indexPath.row]
cell.cardIndex = indexPath.row
}
From the delegate methods, cardFeed above sets a main property in CustomCollectionViewCell used to update the interface:
var card: Card! {
didSet{
setupCard()
}
}
The property card is also the data feed for UITableView Delegate & Datasource.
Everything works perfectly, everything shows up as it should. Except for the fact that when a user picks values from the CustomPickerView, the main data feed, namely cardFeed defined in CustomCollectionViewController (shown above) must update!
My solution is this:
(1) Given that there are three components, define array that records changes in CustomPickerView selected rows and a call back method to pass down the variable:
var selectedRow: [Int] = [1, 0, 0] {
didSet {
if updateRow != nil {
updateRow!(self.selectedRow)
}
}
}
var updateRow: ( ([Int]) -> () )?
(2) In CustomCollectionViewCell define another call back with an extra argument, to keep track of what cell actually sent the selected row array :
var passSelectedRow: (([Int], Int) -> ())?
which is called in tableViews cellForRowAtIndexPath method:
cell.updateRow = { selectedRow in
self.passSelectedRow!(selectedRow, indexPath.row)
}
(3) finally update cardFeed in CustomCollectionViewController cellForItemAtIndexPath:
cell.passSelectedRow = { selectedRow, forIndex in
if self.cardFeed[string]![indexPath.row].chosenFood[forIndex].selectedRow != selectedRow {
self.cardFeed[string]![indexPath.row].chosenFood[forIndex].selectedRow = selectedRow
}
}
But here is the problem, if i now add a didSet to cardFeed, it will create an infinite loop because cellForRowAtIndexPath will be called indefinitely. If i get the CustomCollectionViewCell reference anywhere other than cellForItemAtIndexPath, self.collectionView?.reload() does not work! Is there a way i can update my variable cardFeed in CustomCollectionViewController from the selected rows in CustomPickerView?
When communicating between objects it is bad practice to make the child object have a strong reference to its owner, that is how you end up with retain cycles and bugs.
Let's take a look at the two most common ways of communicating between objects: delegation and notification.
with delegation:
Create a protocol for communicating what you want, in your example:
protocol PickerFoodSelectedDelegate : class {
func selected(row : Int, forIndex : Int)
}
Add weak var selectionDelegate : PickerFoodSelectedDelegate as a variable in the picker class
In the tableView class, during cellForItemAtIndexPath, you assign self to picker.selectionDelegate
You then create a similar structure for communicating between the table and the collection view.
The key part is that delegate references be declared as weak, to avoid retain cycles and bugs.
With notifications you can use NotificationCenter.default to post a notification with any object you want, in this case you would:
Subscribe to a notification name you choose in the table view.
Post a notification from the picker view when an option is chosen.
When the table receives the notification, extract the object.
Do the same from the table to the collection view.
Hope this helps!

Showing and hiding a view only on a specific cell of a table view

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.

Swift, CollectionView, fatal error: Index out of range

I downloaded a collection of images and loaded them into a collectionView. I want to be able to add the individual item selected to an array I declared globally whenever I press on an individual cell so then I can loop through them to be deleted from core data later. This line prints fine in terms of the order of the item - print("You selected cell #(indexPath.item)!"), but the 2nd time I press another cell to add to the array, I get an fatal error: Index out of range error. I don't know what I'm getting this.
var selectedCell: [Int] = [] -> Declared globally
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
// handle tap events
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! MyCollectionViewCell
print("You selected cell #\(indexPath.item)!")
if self.selectedCell.contains(indexPath.item){
print("Item already added")
} else {
self.selectedCell.append(indexPath.item)
}
if selectedCell.count > 0 {
toolbarButton.title = "Remove Item"
}
// let selectCell:UICollectionViewCell = collectionView.cellForItemAtIndexPath(indexPath)!
// selectCell.contentView.backgroundColor = UIColor.whiteColor()
}
iOS 9.3, Xcode 7.3
I honestly think that "fatal error: Index out of range" does not apply to the simpler array of integer indexes that you declared, I think that it is associated to the index of the collection view itself.
Looks like you are conforming to the various protocols of UICollectionView, namely UICollectionViewDataSource, UICollectionViewDelegate, because you are at least receiving callbacks to the cell selected method.
First thing that jumps at me is...
Where is title property for toolbarButton declared, is it a custom subclass of UIButton, because title is not a set able property of standard UIButton class. This is how you'd typically set the title of a UIBUtton...
self.toolbarButton.setTitle("Remove Item", forState: .Normal)
Another thing is,
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! MyCollectionViewCell
The variable cell is never used in that function, it should give you a warning.
Also, check all of your uses of the array variable selectedCell, in the if statement where you check the count value you are not using self.selectedCell for example. Not sure what this would do, but I think this would cause your array to get re-synthesized, so count will be reset each time.
There are few other items that I don't understand, here is the code that I would use. Please check your code and syntax.
The following code works:
var selectedCell: [Int] = []
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath)
{
// handle tap events
print("You selected cell #\(indexPath.item)!")
if (self.selectedCell.contains(indexPath.item))
{
print("Item already added")
}
else
{
self.selectedCell.append(indexPath.item)
}
print("selectedCell.count: \(self.selectedCell.count)")
if (self.selectedCell.count > 0)
{
self.toolbarButton.setTitle("Remove Item", forState: .Normal)
print("selectedCell: \(self.selectedCell)")
}
else
{
//nil
}
}
The output would look something like this:
Note: When you click the same cell twice it is not added (in this case 8), once you have at least 1 item in the array, then the button title changes.
If you still can not figure it out, then see this post, it explains how to implement a collection view very well: https://stackoverflow.com/a/31735229/4018041
Hope this helps! Cheers.

How to set the title of a UIButton in a UICollectionViewCell with Swift

I have a ViewController with a UICollectionView where I'd like to display the first two letters of the players in the users friend list, like this:
func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
contactListCollection.registerClass(PlayerCell.self, forCellWithReuseIdentifier: "PlayerCell")
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("PlayerCell", forIndexPath: indexPath) as! PlayerCell
let contacts = contactList.getContactList() //Array of strings
for(var i = 0; i < contacts.count; i++){
var str = contacts[i]
// First two letters
let firstTwo = Range(start: str.startIndex,
end: str.startIndex.advancedBy(2))
str = str.substringWithRange(firstTwo)
cell.setButtonTitle(str);
}
return cell;
}
func collectionView(collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return contactList.getContactList().count;
}
My PlayerCell Class is as follows:
class PlayerCell : UICollectionViewCell {
#IBOutlet var button: UIButton?
func setButtonTitle(text: String){
button!.setTitle(text, forState: .Normal)
}
}
When run the code it gives:
Fatal error: unexpectedly found nil while unwrapping an Optional value
I found out that the button in my PlayerCell is nil
I have added the button inside my cell in the Storyboard and Connected those with referencing outlets
Am I missing something here?
Using xCode 7 with Swift 2.0
As part of the compilation process, Xcode converts the storyboard to a collection of XIB files. One of those XIB files contains your cell design.
When your app loads the collection view controller that you designed in the storyboard, the controller takes care of registering the cell's XIB file for the cell.
You are overwriting that registration by calling registerClass(_:forCellWithReuseIdentifier:), severing the connection between the “PlayerCell” reuse identifier and the XIB containing the design of PlayerCell.
Get rid of this line:
contactListCollection.registerClass(PlayerCell.self, forCellWithReuseIdentifier: "PlayerCell")
Also, make sure that in the storyboard you have set the cell's reuse identifier to "PlayerCell".
Try:
1. I would check that there isn't an old referencing outlet attached to the button. Right click on the button in the interface builder and ensure that only the appropriate outlet is still connected.
Ensure that in the storyboard you have set the class of the re-useable cell to PlayerCell
Ensure that you have Ctrl + dragged from the collection view to the view controller it is in and set the delegate and data source to itself.
Hopefully, one of these may help you.

Resources