Swift: Changing UIView frame but display doesn't update - ios

I'm trying to dynamically resize the frame of a UIView. I can update the frame in the code and see that the values have changed, but the actual display never changes, even though I run setNeedsDisplay().
I'm following this post, where I define a new frame via CGRectMake, and then set the UIView's frame to this new frame. i.e., I'm doing this...
let newFrame = CGRectMake(minX,minY, width, height)
VUBarCover.frame = newFrame
If I print out the value of the UIView's frame, I see that it's changing as I like. But the display (in the Simulator) never reflects these changes.
Any idea how to fix this?
Update: In greater detail: I'm trying to programmatically cover up one UIView with another. It's a horizontal "VU Meter", consisting of a base UIImageView showing a color gradient, that gets partially dynamically "covered" up by a UIView with a black background.
The UIImageView "VUBarImageView" is defined in StoryBoard and is subject to AutoLayout. The black UIView "VUBarCover" is defined purely programmatically, with no AutoLayout constraints.
Here's the relevant parts of the code for completeness... The trouble I'm having is in updateVUMeter()...
#IBOutlet weak var VUBarImageView: UIImageView!
var VUBarCover : UIView!
// this gets called after AutoLayout is finished
override func viewDidLayoutSubviews() {
// cover up VU Meter completely
let center = VUBarImageView.center
let width = VUBarImageView.frame.width
let height = VUBarImageView.frame.height
let frame = VUBarImageView.frame
VUBarCover = UIView(frame: frame)
MainView.addSubview(VUBarCover)
VUBarCover.backgroundColor = UIColor.blueColor() // use black eventually. blue lets us see it for testing
}
// partially cover up VUBarImage with black VUBarCover coming from the "right"
func updateVUMeter(inputValue:Float) { //inputValue is in dB
//let val = pow(10.0,inputValue/10) // convert from dB
let val = Float( Double(arc4random())/Double(UInt32.max) ) //for Simulator testing, just use random numbers between 0 and 1
print("val = ",val)
let fullWidth = VUBarImageView.frame.width
let maxX = VUBarImageView.frame.maxX
let width = fullWidth * CGFloat(1.0-val)
let minX = maxX - width
let minY = VUBarImageView.frame.minY
let height = VUBarImageView.frame.height
let newFrame = CGRectMake(minX,minY, width, height)
VUBarCover.frame = newFrame
VUBarCover.setNeedsDisplay()
print("fullWidth = ",fullWidth,"width = ",width)
print(" newFrame = ",newFrame,"VUBarCover.frame = ",VUBarCover.frame)
}
And sample results are:
val = 0.9177
fullWidth = 285.0 width = 23.4556210041046
newFrame = (278.544378995895, 93.5, 23.4556210041046, 10.0) VUBarCover.frame = (278.544378995895, 93.5, 23.4556210041046, 10.0)
val = 0.878985
fullWidth = 285.0 width = 34.4891595840454
newFrame = (267.510840415955, 93.5, 34.4891595840454, 10.0) VUBarCover.frame = (267.510840415955, 93.5, 34.4891595840454, 10.0)
val = 0.955011
fullWidth = 285.0 width = 12.8218790888786
newFrame = (289.178120911121, 93.5, 12.8218790888786, 10.0) VUBarCover.frame = (289.178120911121, 93.5, 12.8218790888786, 10.0)
...i.e., we see that VUBarCover.frame is changing 'internally', but on the screen it never changes. What we see instead is the full-size cover, completely covering the image below (ca. 300 pixels wide), even though its width should be only, about 12 pixels).
Even if I do a setNeedsDisplay() on the superview of VUBarCover, nothing changes.
Help? Thanks.

First, you have a few errors worth noting:
Instance variables (like VUBarCover and MainView should always begin with a lowercase letter (like mainView).
You should call super.viewDidLayoutSubviews() at the beginning of your override implementation
Second, viewDidLayoutSubviews() can be called any number of times, so your implementation of this method should be idempotent, meaning it has an identical effect no matter how many times it's called. But when you call:
VUBarCover = UIView(frame: frame)
MainView.addSubview(VUBarCover)
You are creating a new UIView and adding it to the view hierarchy, along with the others you may have added in the past. So it looks like the frame isn't updating on the screen. In fact, you just can't tell because the old views are behind the one you're updating and they're not changing.
Instead, you probably want to create this once:
var vuBarCover = UIView()
Add it as a subview in viewDidLoad():
mainView.addSubview(vuBarCover)
And modify its frame in viewDidLayoutSubviews():
vuBarCover.frame = vuBarImageView.frame
You don't need to call setNeedsDisplay().

In my case, there were animations left on the layer, after removing them, the frame changes are updated:
myView.layer.removeAllAnimations()
iOS displays presentationLayer of a UIView on the screen, animations are played on that layer (frame updates goes there during animation).

I am making a custom UIView like you right now, maybe I can help.
Firstly, viewDidLayoutSubviews() like your comment: this gets called after AutoLayout is finished. And after every layout did set, you start to set new frame for your UIView and UIImage, maybe the bug is here. Please try to move your code in viewDidLayoutSubviews() to layoutSubviews(). And don't forget add super.layoutSubviews() in override function, which can help you away from hundreds of bugs :].
Secondly, only if you override the drawRect: method, then you should call setNeedsDisplay(), which is telling system that i need to redraw my view, and system will call drawRect: method appropriately.
Hope you can figure it out.

Related

circle collectionview image according to device size

Hello in my horizontal collectionview i want to circle image , if i set static height and width it works but it does not work if i set constraint
iam using this method to circle my image
public static func circularImageWhite(photoImageView: UIImageView?)
{
photoImageView!.layer.frame = photoImageView!.layer.frame.insetBy(dx: 0, dy: 0)
photoImageView!.layer.borderColor = UIColor.white.cgColor
photoImageView!.layer.cornerRadius = photoImageView!.frame.height/2
photoImageView!.layer.masksToBounds = false
photoImageView!.clipsToBounds = true
photoImageView!.layer.borderWidth = 1
photoImageView!.contentMode = UIViewContentMode.scaleAspectFill
}
i want to circle image on every device
Everything about your code is wrong.
photoImageView!.layer.frame = photoImageView!.layer.frame.insetBy(dx: 0, dy: 0)
That line is meaningless. If you inset the frame by zero you are not changing it. So that line does nothing at all.
photoImageView!.layer.masksToBounds = false
photoImageView!.clipsToBounds = true
A layer's masksToBounds and a view's clipsToBounds are actually the very same property. So you are setting the same property to false and then back to true in the very next line. Thus the first of those two lines does nothing at all.
photoImageView!.layer.cornerRadius = photoImageView!.frame.height/2
That is actually the heart of the matter. The problem is that you are setting the corner radius according to the frame height. But that assumes you know what the frame is. You don't. As you yourself said, this doesn't work if you set autolayout constraints on your view. Why? Because of the order in which things happen:
First, you use the current frame height to set the corner radius.
Then, the constraints kick in and change the frame. So now the corner radius that you set before doesn't "fit" the image view any more.
Moreover, setting the corner radius is a lousy way to clip a view to a circle. The correct way is to mask the view to an actual circle.
So, to sum up: You should use a UIImageView subclass that overrides its own layoutSubviews to set its own mask to a circle that fits the current size. As the size changes due to constraints, layoutSubviews will be called and your code will change the mask to fit properly.
(The white circular border can be yet another layer or subview that draws the circle.)
The matter comes up quite often, and I often see the cornerRadius misused in this same way, so here's an actual implementation:
class CircleImageView : UIImageView {
override func layoutSubviews() {
super.layoutSubviews()
self.layer.sublayers = nil
let radius = min(self.bounds.height, self.bounds.width)/2
let cen = CGPoint(x:self.bounds.width/2, y:self.bounds.height/2)
let r = UIGraphicsImageRenderer(size:self.bounds.size)
var im : UIImage?
var outline : UIImage?
r.image { ctx in
let con = ctx.cgContext
UIColor.black.setFill()
con.addArc(center: cen, radius: radius,
startAngle: 0, endAngle: .pi*2, clockwise: true)
let p = con.path
con.fillPath()
im = ctx.currentImage
con.clear(CGRect(origin:.zero, size:self.bounds.size))
con.addPath(p!)
UIColor.clear.setFill()
UIColor.white.setStroke() // border color, change as desired
con.setLineWidth(4) // border width, change as desired
con.strokePath()
outline = ctx.currentImage
}
// the circle mask
let iv = UIImageView(image:im)
iv.contentMode = .center
iv.frame = self.bounds
self.mask = iv
// the border
let iv2 = UIImageView(image:outline)
iv2.contentMode = .center
iv2.frame = self.bounds
self.addSubview(iv2)
}
}
Result:
Use CircleImageView as your image view and you'll get the right result. I repeat: the important thing is that this will continue to work no matter how the CircleImageView itself is subsequently resized.

UIImageview mask updating too slowly in response to gesture

I need to create a component that will let users choose from 2 choices in images. At first, you see 2 images side by side with a "handle" in the middle. If you move the handle to the left, you will see more of the image to right and less of the left image, as to reveal the right image, and vice versa.
Technically, I have 2 full size UIImageViews one on top of the other, and they are masked. I have a pan gesture and when the user slides the handle, the handle moves and the masks update themselves to adjust to "the new middle".
Here's the code responsible for adjusting the image mask. The constant is calculated in the method called by the gesture. I know my calculations of that constant are good because the "handle" and the masks are updated correctly.
BUT
the masks gets updated too late and when dragging, we see it being adjusted too late.
func adjustImagesMasks(to constant: CGFloat) {
choiceImageA.mask?.willChangeValue(forKey: "frame")
choiceImageB.mask?.willChangeValue(forKey: "frame")
let separationPoint: CGFloat = self.frame.width / 2.0 + constant
maskA.backgroundColor = UIColor.black.cgColor
maskA.frame = CGRect(origin: .zero, size: CGSize(width: separationPoint, height: self.frame.size.height))
maskB.backgroundColor = UIColor.black.cgColor
maskB.frame = CGRect(x: separationPoint, y: 0, width: self.frame.width - separationPoint, height: self.frame.size.height)
choiceImageA.mask?.didChangeValue(forKey: "frame")
choiceImageB.mask?.didChangeValue(forKey: "frame")
maskA.drawsAsynchronously = true
maskB.drawsAsynchronously = true
self.setNeedsDisplay()
maskA.setNeedsDisplay()
maskA.displayIfNeeded()
maskB.setNeedsDisplay()
maskB.displayIfNeeded()
}
The image views have their masks setup like this:
maskA = CALayer()
maskB = CALayer()
choiceImageA.layer.mask = maskA
choiceImageA.layer.masksToBounds = true
choiceImageB.layer.mask = maskB
choiceImageB.layer.masksToBounds = true
So to recap, my question is really about performance. The image views are being correctly adjusted, but too slowly. The "handle", which is positioned with constraints, get updated really quickly.
So apparently, CALayer tries to animate most of the changes to its properties. So the delay I was seeing was in fact due to an animation.
I resolved my issue by surrounding the call to adjustImagesMasks() with CATransaction.setValue(kCFBooleanTrue, forKey:kCATransactionDisableActions) and CATransaction.commit(). So for this transaction, I'm asking to not animate the changes. Because this is continuous (with the panning gesture), it is seemless.
Full code here:
CATransaction.setValue(kCFBooleanTrue, forKey:kCATransactionDisableActions)
adjustImagesMasks(to: newConstant)
CATransaction.commit()```.
This other post helped me a lot. There's a nice explanation too.
Hope this helps someone else.

Trying to make UIImageView a circle and it appears twice

I am trying to add an imageview as a circle in swift to a uiview. I call the following code in swift however it creates the image to appear briefly incorrectly before it is corrected to a circle
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
theirPicMaskFour.layer.cornerRadius = theirPicMaskFour.frame.height / 2
theirPicMaskFour.layer.masksToBounds = false
theirPicMaskFour.layer.borderWidth = 0;
theirPicMaskFour.clipsToBounds = true
}
The following is the way it looks initially.
The following is how it looks after.
Try
theirPicMaskFour.layer.cornerRadius = theirPicMaskFour.frame.size.width / 2
instead of
theirPicMaskFour.layer.cornerRadius = theirPicMaskFour.frame.height / 2
Try theirPicMaskFour.layer.masksToBounds = true and log out the height before u assign just to be sure that the height isn't something out of ordinary
[EDIT]
Alloc and init it in viewDidLoad but update the round rect in viewDidLayoutSubviews as it will be called multiple times, but changes on frames wont effect it as that will remain same.

SWIFT rotating an image in an UIImageview defined in the Main.storyboard

I'm new to SWIFT and I'm practicing to learn but I'm having some difficulty with something.
I have an image in an UIImageview defined at the Main.storyboard that I need to rotate.
I have an IBOutlet defined as:
#IBOutlet weak var imageToRotate: UIImageView!
I was trying to rotate it with the center of rotation on the rightmost side and middle of height and as per the apple documentation and some posts in here I used the anchorPoint:
imageToRotate.layer.anchorPoint = CGPointMake( 1.0, 0.5 )
Then I try to rotate it using the transform:
imageToRotate.transform = CGAffineTransformMakeRotation(CGFloat( myRadians ) )
The image is defined in the Main.storyboard and it looks good when it loads but when I issue the anchorPoint the image gets shifted to the left and the right edge goes to where the center of the image used to be and the rotation happens around that point.
I tried using:
imageToRotate.center = CGPointMake( 487, 160)
to move the image back to the place where it's supposed to be but it doesn't move so that also has me confused.
I think I'm missing something here but I fail to figure out what it is. Do I have to define a sublayer or something else?
Also the fact that just issuing the anchorpoint moves the image tells me there's something I'm missing.
In the class definition I used this and got it linked to the storyboard's UIImage:
#IBOutlet weak var imageToRotate: UIImageView!
inside the override of viewDidLoad() I used this:
imageToRotate.layer.anchorPoint = CGPointMake(1.0, 0.5)
imageToRotate.transform = CGAffineTransformMakeRotation(CGFloat( myRadians ) )
Thanks.
All you really need to do to rate around a point is combine a translation and a rotation. The translation is an x and y coordinate with the origin at the center of the image. The rotation is a CGFloat in radians. It may look something like this:
let square2 = UIView(frame: CGRectMake(200, 200, 50, 50))
view.addSubview(square2)
// Desired point from the center to rotate around
let x = CGFloat(25)
let y = CGFloat(0)
var transform = CGAffineTransformMakeTranslation(x, y)
transform = CGAffineTransformRotate(transform, CGFloat(M_PI_4))
transform = CGAffineTransformTranslate(transform,-x,-y)
square2.backgroundColor = UIColor.magentaColor()
square2.transform = transform
This creates the following rotation around the new axis of rotation:

Update position of UIImageView Swift

I'm trying to make a draggable image (monkey) takes the place of another draggable image (banana) when I'm pushing a button.
So far, I've been able to get the coordinates of the banana, and store them as the new x,y coordinates of the monkey. But, the view is not updating and the monkey does not moves to it's new place.
I'm sure that I need to update the view, but can't figure it out.
I've tried to add a new subview (self.view.addSubview(monkey)) but when I do that, it's the entire view that is reset and both monkey and banana are going back to their original coordinates.
Here's the code of the action button :
#IBAction func getCoordinates (sender : AnyObject) {
var bananax = banana.frame.origin.x
var bananay = banana.frame.origin.y
var monkeyx = monkey.frame.origin.x
var monkeyy = monkey.frame.origin.y
println("Banana : \(bananax) et \(bananay)")
println("Monkey's default coordinates : \(monkeyx) et \(monkeyy)")
monkeyx = bananax
monkeyy = bananay
println("Monkey's new coordinates : \(monkeyx) et \(monkeyy)")
}
Any idea on how to update only the monkey and not reseting the entire view ?
Thanks :)
These values are just thrown away when you set monkeyx and monkeyy below.
var monkeyx = monkey.frame.origin.x
var monkeyy = monkey.frame.origin.y
Assigning the values below just assigns the values from one local variable to another. It does not actually change the coordinates for monkey.
monkeyx = bananax
monkeyy = bananay
You must set the new coordinates on monkey by constructing a new CGRect and assigning monkey's frame:
monkey.frame = CGRect(x: bananax, y: bananay, width: monkey.frame.size.width, height: monkey.frame.size.height)
As you are just setting monkey's origin to banana's origin, you can remove all the var declarations and reduce this to one line by constructing monkey's new frame with monkey's size and banana's origin:
monkey.frame = CGRect(origin: banana.frame.origin, size: monkey.frame.size)
Hi create this extends if you want. For Swift
Create File Extends.Swift and add this code
/**
Extension UIView
by DaRk-_-D0G
*/
extension UIView {
/**
Set x Position
:param: x CGFloat
by DaRk-_-D0G
*/
func setX(#x:CGFloat) {
var frame:CGRect = self.frame
frame.origin.x = x
self.frame = frame
}
/**
Set y Position
:param: y CGFloat
by DaRk-_-D0G
*/
func setY(#y:CGFloat) {
var frame:CGRect = self.frame
frame.origin.y = y
self.frame = frame
}
/**
Set Width
:param: width CGFloat
by DaRk-_-D0G
*/
func setWidth(#width:CGFloat) {
var frame:CGRect = self.frame
frame.size.width = width
self.frame = frame
}
/**
Set Height
:param: height CGFloat
by DaRk-_-D0G
*/
func setHeight(#height:CGFloat) {
var frame:CGRect = self.frame
frame.size.height = height
self.frame = frame
}
}
For Use (inherits Of UIView)
inheritsOfUIView.setX(x: 100)
button.setX(x: 100)
view.setY(y: 100)
Now you can do
monkey.frame.origin = banana.frame.origin
If you are using a UIImageView for monkey and banana, you can simplify this by doing the following
monkey.center = banana.center
Understand that there is a difference between origin and center. See more here:
UIView's frame, bounds, center, origin, when to use what?
but based on question, I think center should work fine and it is simpler.

Resources