Swift 3 - Access nested subviews properties in code - ios

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]
}

Related

How do you add a CAGradientLayer to a UIBackgroundConfiguration in the new collection views with compositional layouts?

I'd like to add a gradient to a collection view cell's background in the context of the new collection view with compositional layouts. Here's an example of how a cell's background is configured from Apple's sample code Implementing Modern Collection Views in line 180 of EmojiExplorerViewController:
func configuredGridCell() -> UICollectionView.CellRegistration<UICollectionViewCell, Emoji> {
return UICollectionView.CellRegistration<UICollectionViewCell, Emoji> { (cell, indexPath, emoji) in
var content = UIListContentConfiguration.cell()
content.text = emoji.text
content.textProperties.font = .boldSystemFont(ofSize: 38)
content.textProperties.alignment = .center
content.directionalLayoutMargins = .zero
cell.contentConfiguration = content
var background = UIBackgroundConfiguration.listPlainCell()
background.cornerRadius = 8
background.strokeColor = .systemGray3
background.strokeWidth = 1.0 / cell.traitCollection.displayScale
cell.backgroundConfiguration = background
}
}
Since the new UIBackgroundConfiguration is a structure rather than a layer-backed UIView subclass, I can't just add a CAGradientLayer instance as a sublayer.
What would be a good approach to adding a gradient to a cell background configuration?
Since the new UIBackgroundConfiguration is a structure rather than a layer-backed UIView subclass, I can't just add a CAGradientLayer instance as a sublayer.
Yes, you can. The fact that UIBackgroundConfiguration is a struct is irrelevant. It has a customView property that's a view, and that will be used as the background view (behind the content view) in the cell. So set that view to something (it is nil by default) and you're all set.
Here's an example. This is a toy table view for test purposes, but the test is exactly about configuration objects, so it is readily adaptable to demonstrate the technique. It doesn't matter whether you're using a table view, a collection view, or neither, as long as you are using something that has a UIBackgroundConfiguration property. As you can see, I've made a vertical gradient from black to red as the background to my cells.
Here's the relevant code. First, I have defined a gradient-carrier view type:
class MyGradientView : UIView {
override static var layerClass: AnyClass { CAGradientLayer.self }
}
Then, I use that view as the background view when I configure the cell:
var back = UIBackgroundConfiguration.listPlainCell()
let v = MyGradientView()
(v.layer as! CAGradientLayer).colors =
[UIColor.black.cgColor, UIColor.red.cgColor]
back.customView = v
cell.backgroundConfiguration = back
Whatever else you want to achieve is merely a variant of the above. For example, you could use an image view or a solid background view and combine them with the gradient view. The point is, the customView is the background view, and whatever view you set it to will be displayed in the background of the cell.
I should also point out that there is another way to do this, namely, to use a cell subclass and implement updateConfigurationUsingState:. The advantage of this approach is that once you've given the background configuration a customView, you can just modify that customView each time the method is called. You can use this technique to respond to selection, for example, as I have demonstrated in other answers here (such as https://stackoverflow.com/a/63064099/341994).

In Swift, how to reuse a property in a UIVIewController

The question is very simple and I bet lots of you guys may had this situation.
I just have a property name called "carrotImage" like a ">" symbol as a property and want to place 2 times in my UIViewController by using view.addSubview. I don't actually want to create another new property which has exactly the same image as ">", but I'm thinking that if I really do view.addSubview(carrotImage) 2 times but in different places, and apply SnapKit or built-in autolayouts on them respectively, it would affect the other one.
As you can see the image:
There are 2 '>' and I really don't want to create 2 exactly same thing. is there a good way to do it without repeating? Thanks.
Here is my carrotImage property:
var carrotImage: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(systemName: "arrowtriangle.right.fill")
imageView.tintColor = .lightGray
return imageView
}()
For your use case, you do have to have 2 separate UIImageViews. However, they have the same image and properties, so you can reuse the code. If you do not need to later reference carrotImage, you can just make it computed variable like this (remove = and ()):
var carrotImage: UIImageView {
let imageView = UIImageView()
imageView.image = UIImage(systemName: "arrowtriangle.right.fill")
imageView.tintColor = .lightGray
return imageView
}
This way you can reuse the same code to create new UIImageView with arrowtriangle.right.fill where you need.

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)).

How to change the color of pagination dots in UIPageControl with a different color per page

I am using UIPageControl in conjunction with UIScrollView to display 5 images. I would like to be able to set the current page indicator dot color to a different color for each page (when it is selected). I have tried setting the currentPageIndicatorTintColor in the Value Changed action of the UIPageControl, but this shows the previous color briefly before changing to the new color.
Does anyone know if its possible to use different colors for each dot, in such a way that the display is seemless?
Note: this is not a duplicate of How can i change the color of pagination dots of UIPageControl? because that question is about changing all the dots to have the same color, whereas I would like to change the individual page dots to have different colors when their page is currently showing.
This is how you can do this:
Step 1: Subclass UIPageControl and add a property dotImages to hold images for all dots.
class MyPageControl : UIPageControl {
var dotImages : [UIImage?] = []
Step 2: Initialise your dotImages
required init(coder: NSCoder) {
super.init(coder: coder)!
let firstDotImage = UIImage.init(named: "dot1.png")
let secondDotImage = UIImage.init(named: "dot2.png")
let thirdDotImage = UIImage.init(named: "dot3.png")
let fourthDotImage = UIImage.init(named: "dot4.png")
let fifthDotImage = UIImage.init(named: "dot5.png")
dotImages = [firstDotImage, secondDotImage, thirdDotImage, fourthDotImage, fifthDotImage]
}
Step 3: Implement updateDots function to set your own image
func updateDots () {
for var index = 0; index < self.subviews.count; index++ {
let dot = self.subviews[index] as! UIImageView
dot.image = self.dotImages[index]
}
}
Step 4: Call updateDots function based on you app need. You may call it initially while view is loading or may be while page is changing.
currentPageIndicatorTintColor WILL work.
You might have to actually do a bit of work here.
func scrollViewDidScroll(scrollView: UIScrollView) {
//If the page is over 50% scrolled change the dot to the new value.
//If it is less, change it back if the user is holding the page.
}
What I am suggesting is listen to the scrollview, and pre-empt the page, rather than waiting for the fact to happen. My Psuedo-code is kept abstract, but if this is not enough I can modify the answer more specifically depending on your code if you provide examples.
You must therefore be a UIScrollViewDelegate

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