I've been searching for help on this all day but I can't find an answer.
I have a subview in which I am drawing a rectangle, the rectangle is framed by the subview so I need to know the size of the subview as adjusted by autolayout to correctly frame it. I can't find a way of doing this from ViewDidLoad(), so that the rectangle is correctly rendered at start-up. I have tried the following:
Using dayView.setNeedsLayout() followed by dayView.layoutIfNeeded() before I draw the rectangle in viewDidLoad() but a check either side of these statements shows the dayView.bounds unchanged.
Drawing the view from viewDidLayoutSubviews(), which works, but results in my rectangle being drawn 5 times as viewDidLayoutSubviews() is called for every subview (I have 5 of them) that is redrawn (the relevant subview containing the rectangle is redrawn on call 4 of 5) - this seems wasteful of resources, surely there is a better way?
Drawing the view twice within ViewDidLoad(), hoping the first forced draw will cause the view to be resized, so the second draw will have access to the new bounds after the first draw (desperate I know, but it still doesn't work).
I hope someone can help.
func drawGradient(object: UIView, rect: CGRect, slackX: Int, gradWidth: Int, yPos: Int) -> Void {
// the rectangle width and height set to fit within view with x & y border.
let gradientView = UIView(frame: rect)
let gradient = CAGradientLayer()
gradient.frame = gradientView.frame
gradient.colors = getDayGradientLocations().gradientCol
gradient.locations = getDayGradientLocations().gradientLoc
dayView.layer.addSublayer(gradient)
let civilDawn = getTimeAsProportionOfDay(time: tides.civilDawn) * Double(rect.height) + Double(yPos)
let path = UIBezierPath()
path.move(to: CGPoint(x: slackX, y: Int(civilDawn)))
path.addLine(to: CGPoint(x: slackX + Int(rect.width), y: Int(civilDawn)))
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = CGFloat(2)
let civilDusk = getTimeAsProportionOfDay(time: tides.civilDusk) * Double(rect.height) + Double(yPos)
path.move(to: CGPoint(x: slackX, y: Int(civilDusk)))
path.addLine(to: CGPoint(x: slackX + Int(rect.width), y: Int(civilDusk)))
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = CGFloat(2)
object.layer.addSublayer(shapeLayer)
drawLabel(object: object, rect: rect, slackX: slackX, offset: 15, time: tides.civilDawn)
drawLabel(object: object, rect: rect, slackX: slackX, offset: -15, time: tides.civilDusk)
}
A couple of observations:
If you are adjusting a view’s frame in your view controller, the right place to do this is in viewDidLayoutSubviews. Yes, this is called a number of times, but it generally doesn’t have any observable impact on performance.
I wouldn't advise any of those extremely brittle techniques of setNeedsLayout, layoutIfNeeded, or DispatchQueue.main.async inside viewDidLoad. The viewDidLayoutSubviews is the right place if you’re going to do this in the view controller.
Generally, if doing custom subview drawing and layout of subviews, you do this in the layoutSubviews of the view, rather than in any of the view controller methods.
Likewise, if you're doing any manual drawing, you’d put that in the UIView subclass (or any of the relevant CALayer subclasses), not the view controller.
Even better, rather than adjusting frames manually, it’s better to let the auto layout system handle this for you if you can. If you find yourself manually adjusting a frame, there are generally better patterns.
FWIW, you can define a container view’s constraints to be based upon the size (or intrinsic size) of its subviews (and set the content-hugging and compression-resistance of the relevant views). We often think of auto-layout as a top-down engine, but it works both ways.
If you show us your “drawing” code and/or a screen snapshot or two, we can probably offer more concrete counsel.
Got to main thread in viewDidLoad and Perform action will resolve your issue
DispatchQueue.main.async {
// here you can add anything it will have proper frame
}
Related
I have a UISlider that I need to draw on top of on at arbitrary points along the slider.
I can draw the tick marks in viewDidLoad, but since I can't yet get the correct bounds of the UISlider at this point they are drawn in the wrong places. If I draw them in viewDidLayoutSubviews I do get the correct bounds, but the tick marks don't get displayed.
I'm trying to draw these marks as follows:
override func viewDidLayoutSubviews(){
super.viewDidLayoutSubviews()
writeTickMarks()
}
func writeTickMarks(){
// create a vertical line
let path = UIBezierPath()
path.move(to: CGPoint(x: calculatedXValue, y: calculatedYValue))
path.addLine(to: CGPoint(x: calculatedXValue, y: calculatedYValue + 5))
// draw the vertical line in blue with a thickness of 2
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.opacity = 0.5
shapeLayer.lineWidth = 2
// add the shape to the slider's view
self.slider.superview?.layer.addSublayer(shapeLayer)
}
I've tried calling setNeedsDisplay and layoutIfNeeded on self.view as well as self.slider.superview immediately following the call to writeTickMarks, but these calls don't seem to have any affect. :-/
How can I programmatically draw these new layers to my UISlider?
Disclaimer - I can't answer why the ticks aren't displayed at all when you call your method from viewDidLayoutSubviews, that's pretty odd. But...
I have had numerous headaches with the same concept - The view controller lifecycle and when it actually understands the view's true geometry. The trouble with viewDidLayoutSubviews is that it can often get called multiple times when a UI loads, and that can cause CALayers to be added multiple times.
I also have a CAShapeLayer which I use with a UIBezierPath. I have a hacky solution that works for me.
I do call the CAShapeLayer init method in viewDidLayoutSubviews (equivalent to your writeTickMarks() method).
I made my CAShapeLayer a class-level property, and check whether it's non-nil on every method call:
if self.shapeLayer != nil {
self.shapeLayer!.removeFromSuperView()
self.shapeLayer = nil
}
I remove and de-allocate it if it is non-nil to avoid it being added on every invocation of viewDidLayoutSubviews.
It's not very efficient, but gets around the multiple calls of viewDidLayoutSubviews and the unknown geometry in viewDidLoad.
The code should work as it is (and without the setNeedsDisplay/layoutIfNeeded calls). Check to make sure you aren't just drawing things off the screen. I've done something similar myself where my calculated y values were putting my points 900px below the bottom edge of the screen.
I have a view that has a pan gesture recognizer on it to grow the overall height of the view as you swipe it up or down. This works well enough for now but I also wanted to add a drop shadow that I had designed and make sure that it kept with the height of the changing draggable view. However, this is producing some strange effects and I want to see if there is a way to fix this
The relevant code to handle changing the view height and the shadow height is as follows
// SET SHADOW FOR DRAGABLE VIEW PATH IN VIEW DID LOAD
draggableView.layer.shadowColor = UIColor.black.cgColor
draggableView.layer.shadowOffset = CGSize(width: 0, height: 12)
draggableView.layer.shadowOpacity = 0.33
draggableView.layer.shadowRadius = 8
draggableView.layer.shadowPath = UIBezierPath(roundedRect:
draggableView.bounds, cornerRadius: 12).cgPath
// INSIDE THE RECOGNIZER HANDLER
if recognizer.state == .began || recognizer.state == .changed {
translation = recognizer.translation(in: self.view)
recognizer.setTranslation(CGPoint(x: 0.0, y: 0.0), in: self.view)
let endPosition = recognizer.location(in: draggableView) // the posiion at which PanGesture Ended
difference = endPosition.y - startPosition.y
var newFrame = draggableView.frame
newFrame.origin.x = draggableView.frame.origin.x
newFrame.origin.y = draggableView.frame.origin.y + difference
// HERE WE SET THE HEIGHT OF THE VIEW THAT IS BEING "DRAGGED"
newFrame.size.height = draggableView.frame.size.height - difference
// HERE WE UPDATE THE SHADOW PATH WITH THE NEW DRAGGABLE VIEW BOUNDS
draggableView.layer.shadowPath = UIBezierPath(roundedRect: draggableView.bounds, cornerRadius: 12).cgPath
draggableView.frame = newFrame
}
The effect I'm getting is the shadow not quite updating in time with the current pan gesture, here is a gif of what that looks like
I'm hoping there is a way I can get this to work without this weird behavior, I'm fairly new to swift though I understand the concepts it's more about not really knowing all the best ways to go about doing things
You need to invert the order of the following two lines of code
draggableView.layer.shadowPath = UIBezierPath(roundedRect: draggableView.bounds, cornerRadius: 12).cgPath
draggableView.frame = newFrame
The reason is that you are updating the potision (Frame) of your draggableView in the PanGesture delegate method. However, you are updating the shadow frame (line 1) BEFORE updating the position (frame) of the draggable view (line 2). So, inverting the two lines will work for you
Suggestions:
In your case, you are updating frames of two views (draggable and shadow). We can see that the CPU is struggling trying to updating both of them since it's lagging (especially in the shadow part since you are re-drawing it.)
My suggestions is that you can create a parent transparent view and put your current draggable view inside and draw the shadow. This will happen only once in your viewDidLoad. Then inside gesture deletege, you will only update frame of the parent view. In this case, you do not need to update the shadow every time and the dragging will be more smooth
I have two buttons in a stack view. I have used an extension of UIButton to round the outside corners. This works on the 7Plus which I designed for in storyboard but as soon as I run on a smaller device size in the simulator it stops working and I can only round corners on the left side of either button and not the right. Any ideas?
On a 7Plus
On a 7
These are the extensions I'm using
extension CGSize{
init(_ width:CGFloat,_ height:CGFloat) {
self.init(width:width,height:height)
}
}
extension UIButton{
func roundOneSide(topCorner: UIRectCorner, bottomCorner: UIRectCorner){
let maskPAth1 = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [topCorner , bottomCorner],
cornerRadii:CGSize(6.0, 6.0))
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = self.bounds
maskLayer1.path = maskPAth1.cgPath
self.layer.mask = maskLayer1
}
}
I have also tried the following code to no avail. It can only successfully round corners on the left when constraints come into play.
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: [.topLeft, .bottomRight], cornerRadii: CGSize(width: 6, height: 6)).cgPath
facebookBtn.layer.mask = maskLayer
facebookBtn.layer.masksToBounds = true
Use you use segment Control as jerky said or
Try this below code:
// For login button
UIBezierPath *cornersPathLeft = [UIBezierPath bezierPathWithRoundedRect:buttonLogin.bounds byRoundingCorners:(UIRectCornerBottomLeft|
UIRectCornerTopLeft) cornerRadii:CGSizeMake(5, 5)];
//Create a new layer to use as a mask
CAShapeLayer *maskLayerLeft = [CAShapeLayer layer];
// Set the path of the layer
maskLayerLeft.path = cornersPathLeft.CGPath;
buttonLogin.layer.mask = maskLayerLeft;
// For FB button
UIBezierPath *cornersPathRight = [UIBezierPath bezierPathWithRoundedRect:buttonFB.bounds byRoundingCorners:(UIRectCornerTopRight|
UIRectCornerBottomRight) cornerRadii:CGSizeMake(5, 5)];
CAShapeLayer *maskLayerRight = [CAShapeLayer layer];
maskLayerRight.path = cornersPathRight.CGPath;
buttonFB.layer.mask = maskLayerRight;
NOTE:
do maskToBounds = true it allows to give effect on the layers.
-
Difference Between MaskToBounds and ClipsToBounds
MaskToBounds
Any sublayers of the layer that extend outside its boundaries will be clipped to those boundaries. Think of the layer, in that case, as a window onto its sublayers; anything outside the edges of the window will not be visible. When masksToBounds = NO, no clipping occurs.
When the value of this property is true, Core Animation creates an implicit clipping mask that matches the bounds of the layer and includes any corner radius effects. If a value for the mask property is also specified, the two masks are multiplied to get the final mask value.
ClipsToBounds
The use case for clipsToBounds is more for subviews which are partially outside the main view.
For example, I have a (circular) subview on the edge of its parent (rectangular) UIView. If you set clipsToBounds to YES, only half the circle/subview will be shown. If set to NO, the whole circle will show up. Just encountered this so wanted to share
Conclusion
MaskToBounds are applied for the sublayer of any view. Like here OP added layer over button but it does not give effects. I mean layer is not bounds properly.
ClipToBounds are applied on the subVies of any view. Assume you have you have a view( says, viewBG ) and and now you added another view (says, upperView), now you dont wanted to see view upper to look outside the viewBG. ViewUpper always bounded inside its superview. so in this case you have to true the clipstobounds.
Practical experience
Try this below code in swift
let viewBG : UIView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
viewBG.backgroundColor = UIColor.lightGray
self.view.addSubview(viewBG)
let viewUpper : UIView = UIView(frame: CGRect(x: -50, y: -50, width: 100, height: 100))
viewUpper.backgroundColor = UIColor.blue
viewBG.addSubview(viewUpper)
Output
1. When i done viewBG.clipsToBounds = false
When i done viewBG.clipsToBounds = true
You need to enable clipToBound property of button in storyboard;
You could try a different, very simple method of curving the corners on a UIButton or Label.
Click on button
Click on identity inspector
Then add an attribute in the identity section
Key path is layer.cornerRadius
Set as Number
Finally give a value, the higher the value, the more rounded the corners are.
Well that was an easy fix. I just needed to round the corners in viewWillLayoutSubviews.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
loginBtn.roundOneSide(topCorner: .topLeft, bottomCorner: .bottomLeft)
facebookBtn.roundOneSide(topCorner: .topRight, bottomCorner: .bottomRight)
}
I built a demo app to check the relationship between layer's anchorpoint, position and frame.
The initial look of the view looks like following:
In the code, I change that red view's anchor point, it will looks like this, which I could understand since change of anchor point will affect that view's frame.
to maintain the view's frame as original one, I used the following code:
We could see from the console's printout the frame has already remained the same.
However the view's final look looks like following, which still changes its position, how could this happen?
All the code looks like this:
Code are as following:
// Reserve original frame
let oldFrame = self.controlledView.frame
// Here I changed the anchorPoint which will cause that view's frame change
self.controlledView.layer.anchorPoint = CGPoint(x: 0, y: 0)
// to avoid the change of frame, set it back
self.controlledView.frame = oldFrame
// From the console's log the frame doesn't change, the red view's final
// location should be the same with the first image. However it is aligned to the right,
// which I could not understand.
From the CALayer Class Reference on Apple Documentation
You specify the value for this property using the unit coordinate space. The default value of this property is (0.5, 0.5), which represents the center of the layer’s bounds rectangle. All geometric manipulations to the view occur about the specified point. For example, applying a rotation transform to a layer with the default anchor point causes the layer to rotate around its center. Changing the anchor point to a different location would cause the layer to rotate around that new point.
From the UIView Class Reference on Apple Documentation
This rectangle defines the size and position of the view in its superview’s coordinate system. You use this rectangle during layout operations to size and position the view. Setting this property changes the point specified by the center property and the size in the bounds rectangle accordingly. The coordinates of the frame rectangle are always specified in points
So from my point of view, once the view is inside another view, when you change the frame you are changing it's center and size relative to his superview and not with itself.
To test my theory, i perform a small example
import UIKit
class ViewController: UIViewController {
var insideView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Setup Inside view
self.insideView = UIView()
var frame: CGRect = CGRectZero
frame.size.height = 40.0
frame.size.width = 40.0
frame.origin.x = self.view.frame.size.width / 2 - 20.0
frame.origin.y = self.view.frame.size.height / 2 - 20.0
self.insideView.frame = frame
self.insideView.backgroundColor = UIColor.redColor()
self.view.addSubview(self.insideView)
NSLog("One layer -> InsideView size: %#", NSStringFromCGRect(self.insideView.frame))
// Output: 2015-05-22 20:13:11.342 test[42680:12030822] One layer -> InsideView size: {{140, 264}, {40, 40}}
// Setup Another layer
var insideLayer: CALayer = CALayer()
insideLayer.backgroundColor = UIColor.blueColor().CGColor
var insideLayerFrame: CGRect = self.insideView.layer.frame;
insideLayerFrame.origin.x = 0.0
insideLayerFrame.origin.y = 0.0
insideLayer.frame = insideLayerFrame
insideLayer.anchorPoint = CGPoint(x: 0.0, y: 0.0)
self.insideView.layer.addSublayer(insideLayer)
NSLog("Two layers -> InsideView size: %#", NSStringFromCGRect(self.insideView.frame))
// Output: 2015-05-22 20:13:11.342 test[42680:12030822] Two layers -> InsideView size: {{140, 264}, {40, 40}}
}
}
So i leave the layer of the view in it's position and add a new that i can manipulate.
And the result is:
Hope this can help :)
I have a UICollectionView and I'm implementing sticky headers as per this link: http://blog.radi.ws/post/32905838158/sticky-headers-for-uicollectionview-using#notes
It works fantastically however my window has a background image applied, and my header views have a transparent background. Consequentially, when my items scroll above the header view, you can still see them.
Ideally I would fade out the cells with a gradient, to the point it is invisible by the time it appears behind the header view.
Thanks.
You haven't posted any code, so here's a go at it without looking at code. Just setup a mask layer over your UICollectionView's superview and you're good to go:
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = self.collectionView.superview.bounds;
gradient.colors = #[(id)[UIColor clearColor].CGColor, (id)[UIColor blackColor].CGColor];
// Here, percentage would be the percentage of the collection view
// you wish to blur from the top. This depends on the relative sizes
// of your collection view and the header.
gradient.locations = #[#0.0, #(percentage)];
self.collectionView.superview.layer.mask = gradient;
For this solution to work properly, you'd have to embed your collection view in a super view of its own.
For more information on layer masks, check out the documentation.
I created a fade mask over a collectionview that has this kind of effect. Maybe you're looking for something similar.
// This is in the UICollectionView subclass
private func addGradientMask() {
let coverView = GradientView(frame: self.bounds)
let coverLayer = coverView.layer as! CAGradientLayer
coverLayer.colors = [UIColor.whiteColor().colorWithAlphaComponent(0).CGColor, UIColor.whiteColor().CGColor, UIColor.whiteColor().colorWithAlphaComponent(0).CGColor]
coverLayer.locations = [0.0, 0.5, 1.0]
coverLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
coverLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
self.maskView = coverView
}
// Declare this anywhere outside the sublcass
class GradientView: UIView {
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
}
Additionally, you can make it sticky (i.e. it will always fade out the cells on the edge, instead of scrolling with the collection) by adding this to the collectionview subclass.
func scrollViewDidScroll(scrollView: UIScrollView) {
self.maskView?.frame = self.bounds
}
would seem to me the code you are following/using has done heavy work for you. As far I can see (not in position to test right now) just pass the alpha attribute:
layoutAttributes.zIndex = 1024;
layoutAttributes.frame = (CGRect){
.origin = origin,
.size = layoutAttributes.frame.size
like such
layoutAttributes.zIndex = 1024;
layoutAttributes.alpha = 0.1; //add this
layoutAttributes.frame = (CGRect){
.origin = origin,
.size = layoutAttributes.frame.size
instead of having a transparent background on your header, I would create a gradient transparent png and use that instead. It'd be a lot more efficient and easier handling the gradient with an image than doing it with code.
You should use a UIScrollViewDelegate for the CollectionView and use the scrollviewdidscroll method to create the fade, or subclass UICollectionViewFlowLayout.
Here is how I achieved that effect. I created in photoshop a gradient image, fading to the color of the background, which is in my case black. Here's what it looks like:
I placed the ImageView on my ViewController. I stretched it to the correct size and location of where I wanted and used AutoLayout constraints to lock it in place. (I had to use the arrow keys on my keyboard to move it around at times because clicking and dragging the location of the image tended to drop it inside of the CollectionView)
Click the ImageView, go to Editor -> Arrange -> Send to Front to make sure it sits on top of the CollectionView.
Image mode is Scale to Fill, and I have deselected User Interaction Enabled.
This will take some tweaking to get everything perfect but it works very well and looks nice.
I'm not entirely sure how you mean by with your background image and whatnot, but maybe make the gradient image part of the actual background image you have, so it blends in.