Adding subviews to only one UICollectionViewCell on button tap - ios

Each UICollectionViewCell has its own button hooked up to the following action:
#IBAction func dropDown(sender:UIButton){
var pt = sender.bounds.origin
var ptCoords : CGPoint = sender.convertPoint(pt, toView:sender.superview);
var ptCoords2 : CGPoint = sender.convertPoint( ptCoords, toView: collectionView!);
var cellIndex: NSIndexPath = self.collectionView!.indexPathForItemAtPoint(ptCoords2)!
//var i : NSInteger = cellIndex.row;
//var i2 : NSInteger = cellIndex.section;
var selectedCell = collectionView?.cellForItemAtIndexPath(cellIndex) as CollectionViewCell!
selectedCell.button.backgroundColor = UIColor.blackColor()
for (var i = 0; i < 3; i++){
var textView : UITextView! = UITextView(frame: CGRectMake(self.view.frame.size.width - self.view.frame.size.width/1.3, CGFloat(50 + (30*(i+1))), CGRectGetWidth(self.view.frame), CGFloat(25)))
textView.backgroundColor = UIColor.whiteColor()
selectedCell.contentView.addSubview(textView)
}
}
What I want to do is add 3 subviews to only the cell that's been tapped. The subviews are added successfully, but as soon as I scroll, cells that come into view & correspond to the previously set indexPath are loaded with 3 subviews. I figure this is due to the dequeueReusableCellWithReuseIdentifier method, but I can't figure out a way around it. I considered removing the subviews on scrollViewDidScroll, but ideally I would like to keep the views present on their parent cell until the button is tapped again.
EDIT:
Okay, I ditched the whole convertPoint approach and now get the cell index based on button tags:
var selectedCellIndex : NSIndexPath = NSIndexPath(forRow: cell.button.tag, inSection: 0)
var selectedCell = collectionView?.cellForItemAtIndexPath(selectedCellIndex) as CollectionViewCell!
Regardless, when I try to add subviews to only the cell at the selected index, the subviews are duplicated.
EDIT:
I've created a dictionary with key values to track the state of each cell like so:
var cellStates = [NSIndexPath: Bool]()
for(var i = 0; i < cellImages.count; i++){
cellStates[NSIndexPath(forRow: i, inSection: 0)] = false
}
which are set by cellStates[selectedCellIndex] = true within the dropDown function. Then in the cellForItemAtIndexPath function, I do the following check:
if(selectedIndex == indexPath && cellStates[indexPath] == true){
for (var i = 0; i < 3; i++){
var textView : UITextView! = UITextView(frame: CGRectMake(cell.frame.size.width - cell.frame.size.width/1.3, CGFloat(50 + (30 * (i+1))), CGRectGetWidth(cell.frame), CGFloat(25)))
textView.backgroundColor = UIColor.whiteColor()
cell.contentView.addSubview(textView)
println("display subviews")
println(indexPath)
}
} else {
println("do not display subviews")
println(indexPath)
}
return cell
where selectedIndex, the NSIndexPath of the active cell set via the dropDown function, is compared to the indexPath of the cell being created & the cellState is checked for true.
Still no luck - the subviews are still displayed on the recycled cell. I should mention that "display subviews" and "do not display subviews" are being logged correctly while scrolling, so the conditional statement is being evaluated successfully.
MY (...hack of a...) SOLUTION!
Probably breaking a bunch of best coding practices, but I assigned tags to all the created subviews, remove them at the beginning of the cellForItemAtIndexPath method, and create them again if the cellState condition returns true.

No problem. Basically, you need to store program state OUTSIDE your UI components in what is commonly called a "model". Not sure what your app is so I am going to make up an example. Assume you want to show a grid where each cell is initially green and they toggle to red when the user taps it. You would need to store the state (I.e., whether a cell has been tapped or not) in some two dimensional array, which is going to contain a Boolean for ALL cells, and not just the ones that are currently showing (assuming you have enough cells to make the grid scroll). When the user taps a cell you set the flag in corresponding array element. Then, when the iOS calls you back to provide a cell (in the dequeue method) you check the state in the array, apply the appropriate color to the UIView of the cell, then return it. That way, iOS can reuse the cell view objects for efficiency, while at the same time you apply your model state to corresponding cells dynamically. Let me know if this clear.

One of two things:
- Disallow pooling of cells.
- Maintain sufficient info in your mode to be able to draw cells depending on the model rather than on their location on screen. That is, store a bit in your model that determines whether or not to show the three views for each "logical" cell. Then, when asked to dequeue a cell, check its model and add/remove the backgrounds dynamically.

Related

How to equally space labels in a UIStackView?

As a technical assessment for a job I'm interviewing for, I'm making a basic word search game, where the user looks for translations of a given word in a given language. I've got a fair amount of iOS experience, but I've never done dynamically-generated views with run-time-determined text labels, etc. before.
To be clear, I know this is a job assessment, but regardless of whether I get the job, or whether I'm even able to finish the assessment in time, I think this is an interesting problem and I'd like to learn how to do this, so I'll be finishing this as an app to run in the simulator or on my own phone.
So. I have a view embedded in/controlled by a UINavigationController. I have a couple of informational labels at the top, a set of buttons to perform actions across the bottom of the view, and the main view area needs to contain a 2D grid of characters. I'd like to be able to take an action when a character is tapped, such as highlight the character if it's part of a valid word. I'd like to be able to support grids of different sizes, so I can't just create an autolayout-constrained grid of labels or buttons in Interface Builder.
I've tried various methods for displaying the 2D grid of characters, but the one I thought had the most promise was as follows:
Use a UITableView to represent the rows of characters. Inside each table view cell, use a UIStackView with a dynamically generated collection of UILabels.
To fill my UITableView, I have the following:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = gameGrid.dequeueReusableCell(withIdentifier: "GameGridCell") as! TWSGameGridCell
if indexPath.row < word.grid.count {
cell.characters = word.grid[indexPath.row]
cell.configureCell()
return cell
}
return cell
}
The function in the custom table cell class that configures the cell (ideally to create and display the row of characters) is this:
func configureCell()
{
self.stackView = UIStackView(arrangedSubviews: [UILabel]())
self.stackView.backgroundColor = (UIApplication.shared.delegate as! TWSAppDelegate).appBackgroundColor()
self.stackView.distribution = .fillEqually
let myRect = self.frame
self.stackView.frame = myRect
let characterGridWidth = myRect.width / CGFloat(characters.count)
for cIndex in 0..<characters.count {
let labelRect = CGRect(x: myRect.origin.x + (CGFloat(cIndex) * characterGridWidth), y: myRect.origin.y, width: CGFloat(characterGridWidth), height: myRect.height)
let currentLabel = UILabel(frame: labelRect)
currentLabel.backgroundColor = (UIApplication.shared.delegate as! TWSAppDelegate).appBackgroundColor()
currentLabel.textColor = (UIApplication.shared.delegate as! TWSAppDelegate).appTextColor()
currentLabel.font = UIFont(name: "Palatino", size: 24)
currentLabel.text = characters[cIndex]
self.stackView.addArrangedSubview(currentLabel)
}
}
My guess is that it's running into trouble in that the labels have no visible rect when they're created, so they don't display anywhere.
The resulting view when run in the simulator is a white box covering as many rows of the table view as should be filled with rows of characters, and the rest of the table view shows with the custom background color I'm using), as per this image:
What I'm trying for is for that white box to have the same background color as everything else, and be filled with rows of characters. What am I missing?
Problem might be about adding 'labelRect', you already specify the rect of your stackView. I think regardless of the frame of labels, stackView should naturally be able to create it's inner labels and distribute them inside of itself.
Also can you add the following, after you initialize your stackView, inside your configureCell method:
self.stackView.axis = .horizontal
Edit: from the comment below, the solution was to create the labels (though I switched to buttons for further functionality), add them all to a [UIButton], then use that to create the UIStackView (self.stackView = UIStackView(arrangedSubviews: buttons)).

UICollectionView cellForItemAt: won't be called while layoutAttributesForElements are returning values

I have a bit complex UICollectionView with my custom UICollectionViewLayout.
I added a pinch gesture behavior onto it and adjust the layout according to that result.
It's basically working pretty well, the cells are resized and repositioned properly in normal cases.
But when it comes to a particular condition the cells disappear and never appear again.
So far I'm unable to clarify the condition, but it happens often when you pinched the view to a smaller size.
The issue is, layoutAttributesForElements in my collection view layout is called (of course this also implies that numberOfCells:inSection is called as well) and it's returning reasonable cell geometry, but actual cell generation (cellForItemAt:) won't be called.
Before jumping into the code(as it's a bit too long and complicated) I want to ask you guys if any of you have had the same kind of situation.
Below is the summary of what's happening and what I see so far.
It's not that always happening. It happens only after pinching and reached to a certain condition. So this is not a basic kind of how-to-use-UICollectionView issue.
Even when it happens layoutAttributesForElements keeps being called (as you keep pinching)
The layout attributes don't have crazy values like zero size or position of far out of view range. They all have the position attributes that fit into the collection view's content size.
Collection view claims the proper content view size (at least as reported on the debugger.)
When it happens you cannot see any cells in the view hierarchy in the View Debugger. Meaning, it's not zero sized or clear colored but cells themselves are gone. This corresponds to the fact that cellForItemAt: is not called.
After it happened you cannot see the cells anymore even you pinch the screen back to the original scale.
Any information is appreciated. Thanks!
EDIT
My collection view layout is like this. This project is a musical sequencer and the collection view is showing your musical notes in piano roll style.
//
// YMPianoRollLayout.swift
//
import UIKit
class YMPianoRollLayout: UICollectionViewLayout {
let notes : Array<CGPoint> = []
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!;
}
override var collectionViewContentSize : CGSize {
let cv = self.collectionView as! YMPianoRollCollectionView
let screenInfo = cv.pianoRollViewController.screenInfo
let totalSize = screenInfo.collectionViewContentsSize();
print("contentSize = \(totalSize)")
return totalSize;
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// "screenInfo" keeps the user specified view parameters including the scale ratio by the pinch gesture
let cv = self.collectionView as! YMPianoRollCollectionView;
let pianoRoll = cv.pianoRollViewController;
// Check which musical note can be included in the view rect
let indexArray: Array<Int> = pianoRoll!.getNoteIndexes(inRect:rect, useOnlyStartTime: false);
var retArray : [UICollectionViewLayoutAttributes] = []
for i in indexArray {
if let _ = pianoRoll?.pattern.eventSequence[i] as? YMPatternEventNoteOn {
retArray.append( self.layoutAttributesForPatternEventInfo(i) )
}
}
// This always reports non-zero count. Also checked the positions of each array members
// by breaking here and they all had proper size and positions
print("retArray count = \(retArray.count)");
return retArray
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let i = Int((indexPath as NSIndexPath).row)
return self.layoutAttributesForPatternEventInfo(i)
}
//
// This is my specific func to convert the musical time-pitch into the view geometory
//
func layoutAttributesForPatternEventInfo(_ index: Int) -> UICollectionViewLayoutAttributes!{
let cv = self.collectionView as! YMPianoRollCollectionView
// "screenInfo" keeps the user specified view parameters including the scale ratio by the pinch gesture
let screenInfo = cv.pianoRollViewController.screenInfo
// Retrieve musical event
let event = cv.pianoRollViewController.pattern.eventSequence[index]
let index = IndexPath(row:index, section: 0)
let newAttr = UICollectionViewLayoutAttributes(forCellWith:index)
var frame : CGRect!
if let e = event as? YMPatternEventNoteOn {
let noteNo = e.noteNo;
let origin = YMMusicalValuePoint(time:event.time, noteNumber:noteNo);
let size = YMMusicalValueSize(timeLength:e.duration, numberOfNotes: 1);
// Actual size calculation is done in my "screenInfo" class.
frame = screenInfo.getPhysicalRange(YMMusicalValueRange(origin:origin, size:size));
} else {
frame = CGRect(x: 0, y: 0, width: 0, height: 0);
}
newAttr.frame = frame;
newAttr.zIndex = 1;
return newAttr
}
//
// For checking the bounds
//
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
print("BBB newBounds = \(newBounds)")
return true
}
}
Sounds like an interesting problem...
More info on your custom UICollectionViewLayout subclass (i.e. the code) would be useful but I can offer an overview of how UICollectionView typically updates visible content in response to bounds changes (i.e. content offset, rotation) and invalidateLayout calls.
When invalidateLayout fires the internal attributes cache is cleared signaling the need to ask the layout on the next layoutSubviews pass what should be visible. Conversely, if the layout is not invalidated and the attributes are known in the cache the layout will not be asked.
The magic all happens when the current CA transaction commits and UICollectionView's layoutSubviews is invoked. At this point, a diff between what was last known to be on screen and what should now be on screen is computed according the current "visible bounds" (effectively the scroll view's bounds which includes the offset into the layout).
Cells no longer visible in the new visible bounds will be returned to the reuse queue and newly appearing cells (as per the layout) will be constructed and existing items (that have changed) will be updated with their new attributes.
Based on your description, it sounds like when the next layoutSubviews fires the queried attributes (possibly from the cache!) aren't returning anything for the new visible bounds therefore nothing "new" appears -- along with the existing items disappearing...
A few things to investigate might include:
Is your gesture causing your custom layout to invalidate? If not, it probably should so the UICollectionView knows to not trust it's internal attributes cache and always ask the layout for new attributes during the "diffing" process.
How is the bounds of the collection changing during the pinch gesture?
This directly affects the diff since it will use this visible bounds to determine what should be displayed next.
How does your layout respond to shouldInvalidateForBoundsChange:?
Most layouts only invalidate when the extents (e.g. rotation) change, so the UICollectionView will normally rely on it's cached attributes when performing the diff. In your case if you are tweaking the bounds during the gesture, you'll want to unconditionally return YES here.
When you get into this wonky state, you might try pausing into the debugger e.g.
po [[cv collectionViewLayout] invalidateLayout]
po [cv setNeedsLayout]
po [cv layoutIfNeeded]
...and resume. Has everything re-appeared?
If so, it sounds like the layout isn't being invalidated under certain circumstances and the attributes being returned are indeed reasonable.
If not, ping the layout and see what if it is reporting as visible is reasonable via:
po [[cv collectionViewLayout] layoutAttributesForElementsInRect:cv.bounds]

UIButton height not being set correctly on UITableView

I have an UITableView which holds a list of items. For each UITableViewcell in that table, depending on a property from the associated object to that cell, I toggle the visibility of an UIButton by changing the height constraint constant to 0, or to a value defined to make it visible. I've checked the Clip to Bounds option on the Xcode designer for that button.
If I feed the table view a list of items that set some of the buttons visible, and others hidden and scroll, the cells that had the button visible may have it hidden, and vice-versa. This is more noticeable when there's few cells with the button, and the rest without it.
The method that contains the logic to show or hide the UIButton is from within the UITableViewCell custom class for the cells, as it follows:
public partial class UITableViewCellCustom : UITableViewCell
{
public Object obj;
public void SetObject(Object obj)
{
// Do something with obj...
// Do something with the obj that determines if the buttons should be collapsed or not
Boolean collapseButton = ...;
ToggleButtonVisibility(collapseButton);
}
private void ToggleButtonVisibility(Boolean collapse)
{
NSLayoutConstraint uiButtonCancelHeightConstraint = UIButtonCancel.Constraints
.FirstOrDefault(query => query.FirstItem == UIButtonCancel
&& query.FirstAttribute == NSLayoutAttribute.Height);
NSLayoutConstraint uiButtonCancelTopConstraint = this.ContentView.Constraints
.FirstOrDefault(query => query.FirstItem == UIButtonCancel
&& query.FirstAttribute == NSLayoutAttribute.Top);
if (collapse)
{
uiButtonCancelHeightConstraint.Constant = 0;
uiButtonCancelTopConstraint.Constant = 0;
}
else
{
uiButtonCancelHeightConstraint.Constant = 30;
uiButtonCancelTopConstraint.Constant = 10;
}
}
}
The SetObject method is called from the UITableViewSource class that gets the object from the correct index and sets it to the cell ( No problem here ). Then, while some UILabels texts are changed with the values from the object, I check if the button is required or not ( No problem here ). When I call the ToggleButtonVisibility method, and attempt to change the two constraints -- height and top -- the values are applied, the top constraint is visibly changed, but the height constraint seems to be ignored when the cell is reused.
I've tried to force the ClipToBounds to true, force the method in the main thread, but none of them worked. What am I missing here?
Forgot to mention: When the button is pressed, the table view is cleared ( I feed the source an empty list, and reload the data ), an long task is performed, and then a new list is applied to the table, but the cell in question remains with the button bugged.
Notes:
Hiding the button by changing the Alpha to 0 or by set the Hidden to true is not an option, since it will leave an hole within the tableview.
Fixed this problem by wrapping the UIButton on a UIView and then resizing the view depending on my needs.

Swift UITableviewCell color alteration

I'm working on a UITableView where I need to change the background color of every second cell to help visibility of the data.
To do this, I tried making a simple variable called "cellAlteration" which I set to 0 before Loading the data (So the first cell should always be white)
var cellAlteration: Int = 0
Then where I setup my cell I do the following:
// #BackgroundColor
if cellAlteration == 0 {
self.backgroundColor = UIColor.whiteColor()
cellAlteration = 1
} else {
self.backgroundColor = UIColor.formulaWhiteColor()
cellAlteration = 0
}
(I know this should be changed to a BOOL datatype instead of Int, but that seems irrelevant for my issue at this moment)
Here is an image of the result:
As you can see it's not alternating correctly.
When I've been trying to test this, it looks like it gets the cells that is loaded first correctly (UITableView only loads and displays the cells you can see) - the problem with the coloring doesn't seem to happen until I scroll down and the UITableView has to load and display new cells.
So since my simple variable check approach isn't working, what's the correct way to do cell alternation in Swift?
Any help would be greatly appreciated :)!
You don't need a variable to do this. Base the color on whether the row is even or odd. Put this in cellForRowAtIndexPath after you dequeue a cell,
if indexPath.row % 2 == 0 {
cell.backgroundColor = UIColor.whiteColor()
}else{
cell.backgroundColor = UIColor.formulaWhiteColor()
}

UICollectionViewController Push Transitions

I'm trying to implement an interface where the user drills down through successive collection views of data, and would like to have each collection view animate to the next one as it's pushed. This has been very frustrating.
I've boiled it down to a very simple test case which I still can't get to work the way I think it ought to.
1- A UINavigationController has a UICollectionViewController which is hardwired to return a numberOfSections == 1, an itemsInSection = 8, and the backgroundColor of the cells is set to redColor in cellForItemAtIndexPath. The associated UICollectionViewFlowLayout.itemSize is set to 80, 80.
2 - When a cell is selected I push a new UICollectionViewController which is hardwired to return a numberOfSections == 1, an itemsInSection == 30, and the backgroundColor of the cells is set to blueColor in cellForItemAtIndexPath. The associated UICollectionViewFlowLayout.itemSize is set to 60, 60.
If I push the second controller with its useLayoutToLayoutNavigationTransitions property set to false, the controller slides in with the proper data display (30 blue cells). If I set useLayoutToLayoutNavigationTransitions to true before pushing, only the layout changes on push. The 8 red cells from the first controller are still displayed, but their size changes to 60, 60.
Apple's UICollectionViewController class reference says that when you set useLayoutToLayoutNavigationTransitions to true, "the navigation controller performs an animated layout change between the contents of the two collection view controllers instead of the traditional push animation", but I'm only getting the layout change.
My test code is stupid simple, so not sure what I might be doing wrong here. Does Apple's class reference misrepresent what the expected behavior should be? Is there any reason to just change layouts when you've pushed a new controller (and theoretically a new data source)? Is there an alternative way to animate to the next collection view?
For what it's worth, here is the only code in my test program other than returning appropriate values for numberOfSections and numberOfItems, and setting appropriate cell background colors.
override func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
let itemSize = (collectionViewLayout as! UICollectionViewFlowLayout).itemSize;
let theLayout = UICollectionViewFlowLayout()
theLayout.itemSize = CGSizeMake(itemSize.width - 20, itemSize.height - 20)
let toVC = SecondCollectionViewController(collectionViewLayout:theLayout)
toVC.useLayoutToLayoutNavigationTransitions = true
navigationController?.pushViewController(toVC, animated:true)
}

Resources