Setting Image To Nil Causes ImageView to Disappear in Stack View - ios

I have recently moved a bunch of UIImageViews into nested Stack Views (Horizontal and Vertical). Whenever the user presses the play button it animates, then starts a timer. Once the timer reaches 0 it is supposed to flip each card consecutively and hide the image itself. This was working until I added them to a Stack View. I've read that the stack views are buggy when it comes to this sort of thing, but any attempt at fixing it hasn't worked thus far. I'm at an impasse, can anyone point me in the right direction?
Issue Occurs In Here I Think
//INFO: Disable cards while viewing.
for card in cardsPhone
{
card.isUserInteractionEnabled = false
flipCard(sender: card)
}
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + 5) {
for card in self.cardsPhone
{
DispatchQueue.main.async {
//THIS IS THE ANIMATION CALL THAT MAKES VIEWS DISAPPEAR.
self.flipCard(sender: card)
}
usleep(20000)
}
DispatchQueue.main.async{
//INFO: Enable cards after hidden.
for card in self.cardsPhone
{
card.isUserInteractionEnabled = true
}
//INFO: Enable play button after cards are hidden to prevent crashing layout.
self.playButtonView.isUserInteractionEnabled = true
}
}
Flip Animation Controller
func flipCard(sender: UIImageView)
{
playButtonView.isUserInteractionEnabled = false
//INFO: If there is no UIImage, assign the appropriate image from the array.
if sender.image == nil
{
sender.image = cardsImages[sender.tag]
UIView.transition(with: sender, duration: 0.3, options: .transitionFlipFromLeft, animations: nil, completion: nil)
}
//INFO: If there is an image, remove it and replace with no Image.
else
{
sender.image = nil
UIView.transition(with: sender, duration: 0.3, options: .transitionFlipFromRight, animations: nil, completion: nil)
}
}
Update
I've discovered that if I replace the image on the UIImageView then that is actually what is causing it to disappear. So the issue is related to the code above in the else statement where sender.image = UIImage(). It doesn't matter if I replace with a different image or not, the moment the image is changed it disappears.

I've read that the stack views are buggy when it comes to this sort of thing
Well, that's not so. They are in fact quite sophisticated and consistent, and their behavior is well defined.
What is a stack view? It's a builder of autolayout constraints. And that is all it is. Putting views inside a stack view means "Please configure these views with equal spacing" (or whatever your setting is for the stack view) "using autolayout constraints that you impose."
Okay, but how does the stack view do that? Autolayout requires four pieces of information, x and y position, and width and height. The stack view will supply the position, but what about the size (width and height) of its views? Well, one way to tell it is to give your views internal constraints (if they don't conflict with what else the stack view will do). But in the absence of that, it has to use the intrinsic size of its views. And the intrinsic size of an image view under autolayout is the size of the image it contains.
So everything was fine when you had image views with images that were the same size. They all had the same intrinsic size and now the stack view could space them out. But then you took away an image view's image. Now it has no image. So it has no intrinsic size. So the stack view just removes it from the row (or column or whatever it is).
Your workaround is actually quite a good one, namely, to make sure that the image view always has an image; if it is to look blank, you just give it a blank image. The image looks like the background, but it is still an image with an actual size, and so it gives the image view a size and the image view continues to hold its place.
However another solution would have been simply to give the image view a width constraint the same size as the card image's width.

Solution
I've discovered that changing an image on a UIImageView while it's inside of a stack causes it to completely disappear. This is a weird Xcode but but the solution I found was to create a custom background on the fly to get rid of the image and only have the background color. This was done with the following code snippet. It's a bit messy, but it gets the job done.
func setColorImage(viewToModify: UIImageView, colorToSet: UIColor)
{
let rect = CGRect(origin: .zero, size: viewToModify.layer.bounds.size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
colorToSet.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard (image?.cgImage) != nil else { return }
viewToModify.image = image
viewToModify.backgroundColor = colorToSet
viewToModify.isUserInteractionEnabled = false
}
Essentially this code creates a new cgRect, sets the color, creates a new image from that color, then applies it back to the UIImageView.

Related

Gradient layer not in the right place

I have the following code as follows:
playView.layer.cornerRadius = 16
let gradient1 = CAGradientLayer()
gradient1.frame = playView.frame
gradient1.cornerRadius = 16
if #available(iOS 10.0, *) {
// set P3 colour
} else {
// set sRGB colour
}
gradient1.startPoint = CGPoint(x: 0, y: 0)
gradient1.endPoint = CGPoint(x: 1, y: 1)
playView.layer.insertSublayer(gradient1, at: 0)
On the 3rd line of the block of code above, I set the frame of the gradient equal to the frame I want it to fill.
When I run the app on different devices, the gradient layer will only fill the correct area if the device the app is being run on is the one selected in the Interface Builder.
I currently have the code in viewDidLoad(), and so the issue can be solved by moving the code to viewDidAppear(), but then when the app is loaded, there will be a slight delay before the gradient appears, not giving a smooth look and feel.
Is there another method I can put the code in, so that the gradient shows in the correct place, whilst at the same time being there as soon as the user sees the screen? Or alternatively, a way to make the gradient fill the view, whilst still keeping the code in viewDidLoad()?
EDIT: viewWillAppear() does not work, nor does viewWillLayoutSubviews(). Surely there must be away to solve this?
You can put inside this block:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
}
viewDidAppear() works. This screen is the root controller - I don't know if that makes a difference or not, but there is no visible delay on applying the gradient backgrounds.
I would be interested if anyone could explain this? I have a bar chart in another part of the app and in viewDidAppear() there is code to complete the bar chart, however there is a delay in it being filled in.
Change the layer.frame property inside the viewDidLayoutSubviews
method. This is to make sure that the subview (playView) has already a proper frame.

Self-sizing UICollectionView cells with async image loading

I have a horizontal UICollectionView in which I display images that are loaded asynchronously. The cells are supposed to have their width fit the image.
In viewDidLoad of my view controller, I set the estimated cell size:
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.estimatedItemSize = CGSize(width: 400, height: 400)
In cellForItem, I start the download task using Kingfisher:
cell.imageView.kf.setImage(with: url) { (_, _, _, _) in
cell.layoutSubviews()
}
Inside my cell, I have the following code inside the layoutSubviews method:
// 326 is the image view's height
// .aspectRatio is a custom extension that returns: size.width / size.height
imageViewWidthConstraint.constant = 326 * image.aspectRatio
layoutIfNeeded()
In the storyboard, I have properly setup the layout constraints so that imageViewWidthConstraint is respected for the cell's width.
The following is the result when running my app:
As you can see, the cells have a width of 400, although the image was loaded and the layout updated. And, as result, the images are stretched to fill the image view.
When I scroll to the right & then back, the cells are removed from the collection view and loaded back in, and are now properly laid out:
While scrolling, the cells adjust their width, sometimes to the correct width, sometimes to the wrong one.
What am I doing wrong here?
Since your images come in asynchronously it may take some time to be loaded. Only once they are loaded you can actually know the size or ratio of the image. That means for every image that is loaded you need to "reload" your layout. From a short search this looks promising.
At least this way you may get some animations, otherwise your cells will just keep jumping when images start to be loaded.
In any case I would advise you to avoid this. If I may assume; you are getting some data from server from which you use delivered URLs to download the images and show them on the collection view. The best approach (if possible) is to request that the API is extended so that you receive dimensions of images as well. So instead of
{
id: 1,
image: "https://..."
}
You could have
{
id: 1,
image: {
url: "https://...",
width: 100,
height: 100
}
}
You can now use these values to generate aspect ratio width/height before you download the images.
Next to that I don't really see any good solution for the whole thing to look nice (without jumping around).

Hide one UIView and show another

When I click on an a segment of my UISegmentedControl, I want one of two UIViews to be displayed. But how do I arrange it, that I only show the new view. Currently I am calling thisview.removeFromSuperview() on the old one, and then setup the new all from scratch. I also tried setting all the HeightConstants of the views subviews to zero and then set the heightConstants of the view itself to zero but I'd rather avoid that constraints-surgery..
What better approaches are there?
Agree with #rmaddy about using UIView's hidden property, a nice simple way to cause a view to not be drawn but still occupy its place in the view hierarchy and constraint system.
You can achieve a simple animation to make it a bit less jarring as follows:
UIView.animate(withDuration:0.4, animations: {
myView.alpha = 0
}) { (result: Bool) in
myView.isHidden = true
}
This will fade the alpha on the view "myView", then upon completion set it to hidden.
The same animation concept can be used also if you've views need to re-arrange themselves, animating layout changes will be a nice touch.
Based on #rmaddy and #CSmiths answer, I built the following function:
func changeView(newView: UIView, oldView: UIView) {
newView.isHidden = false
newView.alpha = 0
UIView.animate(withDuration:0.4, animations: {
oldView.alpha = 0
newView.alpha = 1
}) { (result: Bool) in
oldView.isHidden = true
}
}
I feel dumb now for all the hours I spent on that constraint-surgery. :|

Take snapshot of a UIView except some buttons [duplicate]

This is how I take a screenshot of my view:
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0)
view.drawViewHierarchyInRect(view.bounds, afterScreenUpdates: true)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
However, in the view, there is a UIVisualEffectsView which I'd like to exclude from the screenshot.
I tried hiding the UIVisualEffectsView just before taking the screenshot and un-hiding it afterwards but I don't want the user to see that process. (which he does if I simply hide the view because the iPad is too slow and it looks like the screen is flickering...)
Any ideas? Thanks in advance!
I would take advantage of the snapshotViewAfterScreenUpdates() method
This method very efficiently captures the current rendered appearance of a view and uses it to build a new snapshot view. You can use the returned view as a visual stand-in for the current view in your app.
Therefore you can use it in order to display an overlay UIView of the complete un-altered view hierarchy to the user while rendering a version of the hierarchy with your changes underneath it.
The only caveat is that if you're capturing a view controller's hierarchy, you'll have to create a 'content view' subview in order to prevent the overlay view from being rendered in your screenshot where you make the changes to the hierarchy. You'll then want to add your view hierarchy that you want to render to this 'content view'.
So your view hierarchy will want to look something like this:
UIView // <- Your view
overlayView // <- Only present when a screenshot is being taken
contentView // <- The view that gets rendered in the screenshot
view(s)ToHide // <- The view(s) that get hidden during the screenshot
Although, if you are able to add the overlayView to the view's superview - instead of to the view itself – you don't need to mess about with the hierarchy at all. For example:
overlayView // <- Only present when a screenshot is being taken
UIView // <- Your view – You can render this in the screenshot
view(s)ToHide // <- The view(s) that get hidden during the screenshot
otherViews // <- The rest of your hierarchy
Something like this should achieve the desired result:
// get a snapshot view of your content
let overlayView = contentView.snapshotViewAfterScreenUpdates(true)
// add it over your view
view.addSubview(overlayView)
// do changes to the view heirarchy
viewToHide.hidden = true
// begin image context
UIGraphicsBeginImageContextWithOptions(contentView.frame.size, false, 0.0)
// render heirarchy
contentView.drawViewHierarchyInRect(contentView.bounds, afterScreenUpdates: true)
// get image and end context
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// reverse changes to the view heirarchy
viewToHide.hidden = false
// remove the overlay view
overlayView.removeFromSuperview()
Maybe the simplest solution would be not to include your unwanted view into the hierarchy of the taken as screenshot view. You could simply put it on top of it.
Oh, actually, just following code (swift 4) can work. That is, create a context, and add the item you want in the snapshot. No need to add view in real screen.
Note: currentView is the view which snapshot based on, commonly, it will be view of whole screen. And addViews are the views you want to add.
func takeSnapShot(currentView: UIView , addViews: [UIView], hideViews: [UIView]) -> UIImage {
for hideView in hideViews {
hideView.isHidden = true
}
UIGraphicsBeginImageContextWithOptions(currentView.frame.size, false, 0.0)
currentView.drawHierarchy(in: currentView.bounds, afterScreenUpdates: true)
for addView in addViews{
addView.drawHierarchy(in: addView.frame, afterScreenUpdates: true)
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
for hideView in hideViews {
hideView.isHidden = false
}
return image!
}
Actually there is an easy way to do it.
For example we have a share button and View in storyboard and process with it in sample below code.
#IBAction func share(_ sender: Any) {
self.hideView.isHidden = true
let bounds = UIScreen.main.bounds
UIGraphicsBeginImageContextWithOptions(bounds.size, true, 0.0)
self.view.drawHierarchy(in: bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
//Add Activity view controller to use it
self.hideView.isHidden = false
}
I used it for hiding Ads if the screenshot taken with my share button and it works for me.
afterScreenUpdates: true works here for hide to view while you click the button. It hides for millisecond hideView and took screenshot then it appears back to your view like it was never hidden.
*This is my first answer and hope it helps someone.
Thanks.

Disable scrolling in a UITextView while avoiding both bouncing to top and invisibility of text at the present location for ios

I've tried a lot of things but couldn't come up with a solution. Any words of thought can help me evaluate on this. I made a full view DrawBoard class which is inherited from a UITextView class.When the switch on the view controller is on the user can type and scroll, when it is off the user can draw on the Drawboard.
#IBAction func changeSwitch(sender: UISwitch) {
if sender.on{
drawBoard.setNeedsDisplay()
drawBoard.scrollEnabled = true
drawBoard.editable = true
drawBoard.selectable = true
drawBoard.switchBool = false
}else if !sender.on {
drawBoard.switchBool=true
let a:CGPoint = drawBoard.contentOffset
drawBoard.scrollEnabled = false
drawBoard.setContentOffset(a, animated: false)
drawBoard.editable = false
drawBoard.selectable=false
}
}
It updates the scrolling but scrollEnabled= false just saves the text that is in the first page range of the textview so that it scrolls to the top automatically and disable the scrolling there.Then when I do the setContentOffset the drawable view of the background is visible and it draws on the right place of the textview. However the text that should be on top of it is not visible. This only happens when the switch button is set to off while I am out of the first page's range.How do I also make the text at that range visible?
Sorry if it's a really easy question I'm new to programming and got stuck for a considerable amount of time for this.Thank you.
You have to update the content size of the UIScrollView.
Like this:
scrollView.contentSize = CGSize(width: 100, height: 100)
"Content Size" determines the size of scrollable content, if it's not large enough to cover new views, you won't be able to see it in your scroll view.

Resources