Iterate StackView, Swift - ios

I have a custom cell composed by 3 StackView. Each one of them has a title, a description and an image, horizontally.
I have to fill this cell with an Array, it could be made of max 3 elements, but it could have 2 or 1.
So in my viewModel I'm treating this array like this ...
let firstItem = myArray[0]
let secondItem = myArray[1]
let thirdItem = myArray[2]
And I fill the field with firstItem.name firstItem.description ... For each one of them (not the best approach I guess)
Then I made some check if index exist, if it doesn't I delete the StackView, I set manually some constraints and I fit the cell to the content ( If I have 2 elements the cell is shorter, If I have 3 elements the cell is bigger).
This is a piece of code after I check that index 3 doesn't exist:
self.stackView.removeFromSuperview()
self.ownConstraints.constant = value (20 for example)
My question is, what is the best approach to achieve this? With cell I usually append item with a for cycle one by one, but I'm not familiar with this approach on StackView inside a Cell.
This is what I have done with cell (a series of cell with 1 name and 1 description):
for (element) in myArray {
self.cellArray.append( elementName , elementDescription )
}

// hide all items in stackView
stackView.arrangedSubviews.forEach({ $0.isHidden = true })
// add or update arrangedSubviews
for (index, element) in myArray.enumerated() {
if index >= stackView.arrangedSubviews.count - 1 {
stackView.addArrangedSubview(UILabel())
}
(stackView.arrangedSubviews[index] as? UILabel)?.text = element
stackView.arrangedSubviews[index].isHidden = false
}
I would hide all subviews in stackView and only show subviews if content is available in your viewModel.

Related

Setting text for uilabel collection from array

I am having a UIScrollView with multiple UILabels inside, so I made a collection of IBOutlets and connected all the labels. Now the problem is am setting the label text from an array and the value in each label not in order as I have in the array.
let timeTables:[TimeTable] = TimeTable.TimeTableList(array:
respnse.responseArray)
print("From label count \(self.fromLabels.count)")
print("Time tabele count \(timeTables.count)")
for (index, item) in timeTables.enumerated() {//
print(index)
if index < self.fromLabels.count , index <
self.toLabels.count{
self.fromLabels[index].text = item.from
print(item.from as Any)
self.toLabels[index].text = item.to
print(item.to as Any)
}
}
How to make the label display the value in the array order?

Slide view together with UICollectionView cell

i have this structure. Scrollview -> UICollectionview + Label
This viewcontroller has array of items (BlockItem). On scrollViewDidScroll i change current test label (description)
func scrollViewDidScroll(_ scrollView:UIScrollView)
{
let midX:CGFloat = scrollView.bounds.midX
let midY:CGFloat = scrollView.bounds.midY
let point:CGPoint = CGPoint(x:midX, y:midY)
guard
let indexPath:IndexPath = collectionView.indexPathForItem(at:point)
else
{
return
}
let currentPage:Int = indexPath.item
if let found = items.first(where: {$0.id == String(block[currentPage])}) {
description.text = found.description
}
}
The main issue that i want my description label will be moving together when i move my collectionview cell and appear from the next cell. In other words, if I scroll to the left my description should move along with the cell, i.e. go to the left and appear on the right of the screen.
How can i do this? I know i could make a big collectionview but i need that only image part should be scrollable, not the entire block.
Here is what i want to achieve
Here is video example: scroll works only if i swipe on the image area, and doesn't work when i scroll down the page
https://drive.google.com/file/d/1kl1GYgXvK4bL3toTfOvpxF2WqS56pQO9/view?usp=sharing
You exactly said this:
"The main issue that I want my description label will be moving together when I move my collection view cell and appear from the next cell."
If you want your description label moving together, just include them into your collection cell. It's very clear from your point there
I don't understand why u need to separate it, but you want it to slide from new cell.
If you insisted that you want to archive the label, not in a scroll view, then use 2 label
One label for this cell and one label will come from scroll direction, u can archive this by creating manually. This is a simple code for a swipe left to scroll
UIView.animate(withDuration: 0.5) {
//insert your label animation
//current label moved with the scroll to left
//create a new label from the outer right of the current view, scroll it to the middle of your view
}
But it will be hard work for good animation or scroll support.

Swift 3 - Access nested subviews properties in code

For a pure matter of training I'm developing a weather app coding the entire UI rather than using storyboard.
I have a nested structure of views as follows:
SuperView --> UIView (with 5 subviews of type UIView).
Each of the 5 UIViews contains: 1 UIImageView, 2 UILabels
Now, when I'm calling my delegate function to retrieve the weather I'm having trouble updating those values with weather icon, weather description, day.
I tried using Tags for each of the subviews but no joy.
To give you something to look at:
This is where I retrieve my forecast data (icons, description, day):
//MARK: Forecast Wheel elements
let forecastWeatherWheel = UIView()
var forecastDays = [String]()
var forecastDescriptions = [String]()
var forecastIcons = [String]()
func setForecastWeather(forecast: ForecastWeatherData) {
forecastDays = forecast.forecastDay
forecastDescriptions = forecast.weatherDescription
forecastIcons = forecast.icon
for (index,forecastContainerView) in (forecastWeatherWheel.subviews.filter{$0 is UIView}).enumerated(){
for (index,iconImageView) in (forecastContainerView.subviews.filter{$0 is UIImageView}).enumerated(){
let iconImage = iconImageView as! UIImageView
iconImage.image = UIImage(imageLiteralResourceName: forecastIcons[index])
}
}
}
With that nested for I've been - somehow - able to access the image property of my nested view but rather than looping through the array of icons it's using always the same Icon in all the 5 subviews...
Any help is highly appreciated as I'm struggling with this since more than 12 hrs :|
The real answer is of course to use a view subclass, with accessors for the image view and each label, instead of using the subview hierarchy like this. But here's what's wrong with what you're doing right now:
for (index,forecastContainerView) in (forecastWeatherWheel.subviews.filter{$0 is UIView}).enumerated(){
The filter here is pointless; everything in subviews is a UIView. You'll get 5 passes through here.
for (index,iconImageView) in (forecastContainerView.subviews.filter{$0 is UIImageView}).enumerated(){
Your filter here is only going to return a single view - the image view, since the others aren't image views. That means this loop is only going to execute once.
let iconImage = iconImageView as! UIImageView
iconImage.image = UIImage(imageLiteralResourceName: forecastIcons[index])
Which means that index here is your inner index, which is always 0.
Either use a different name for each index variable, or write it something like this (untested, typed in browser):
for (index, forecastContainerView) in forecastWeatherWheel.subviews.enumerated() {
let imageView = forecastContainerView.subviews.first(where: { $0 is UIImageView } ) as! UIImageView
imageView.image = UIImage(imageLiteralResourceName: forecastIcons[index]
}

How to maintain tableview scroll to stop reusing cell

I have multiple sections in tableview. I have multiple questions and multiple answers of each question. In multiple answers, I have one option and that is other (option). when I select the button of other, then it shows the text field for advice. Now i need to maintain the data of text field and that selected option's (Other) text when scrolling in tableview.I am using below code for all answer.
if (indexPath.section == 2)
{
let cellidentifier="cell3"
let cell=tableView.dequeueReusableCell(withIdentifier: cellidentifier,for:indexPath as IndexPath) as! TextfieldTableViewCell
let object_3:AnswerBaseClass = arrobject_answer[0][indexPath.row]
//print("arrobject is\(arrobject_answer[0][indexPath.row])")
if object_3.answer == "O"
{
// cell.lbl_answer.isHidden = true
cell.btn_selected.isHidden=true
//cell.lbl_answer_height.constant = 0
cell.Other_textfield.tag = 101
cell.Other_textfield.borderStyle = .line
cell.Other_textfield_top.constant = -30
cell.Height_2.constant = 30
}
else
{
cell.lbl_answer?.text = object_3.answer!
cell.Other_textfield_top.constant = 12
cell.Height_2.constant = 0
cell.lbl_answer.isHidden = false
cell.btn_selected.isHidden=false
if answer_main_data[0][indexPath.row] == true
{
cell.lbl_answer.tag = indexPath.row
cell.btn_selected.isSelected=true
if cell.lbl_answer.text == "Other"
{
for subview in cell.contentView.subviews
{
subview.removeFromSuperview()
}
if arrOtherTextfield_2.indices.contains(indexPath.row)
{
cell.addSubview(arrOtherTextfield_2[indexPath.row])
}
else
{
cell.Other_textfield.tag = 1100
cell.Other_textfield.borderStyle = .line
cell.Height_2.constant = 30
arrOtherTextfield_2.append(cell.Other_textfield)
}
}
else
{
cell.Height_2.constant = 0
}
}
else
{
cell.Other_textfield_top.constant = 12
cell.btn_selected.isSelected=false
cell.Height_2.constant = 0
}
}
cell.Other_textfield.borderStyle = .line
return cell
}
You will have to retain (store in some dictionary) the data entered in textfield, otherwise it will be lost when you scroll table and cell is reloaded. If you don't want to retain then you should use scroll view instead of table view. In scrollview it will not redraw UI even if you scroll up and down.
You need to separate UI and data. You embed data into your cell and when cells are reused, you lost data.
You can do 2 things:
Create a ViewModel class which contains data of cell: text, color, etc. Of course you need to update your ViewModel as you receive input You can google "MVVM pattern" for more information. Even if your cell is reused, your data is safe in ViewModel object.
You can keep your cells in an Array so that they won't be reused.

Adding subviews to only one UICollectionViewCell on button tap

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.

Resources