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.
Related
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
}
How to implement a one-side and fade out border for UIView. I'm sorry that I don't have enough reputation to post a screenshot. But there is an example in iOS native Contact app. If you click on "+" then "add phone", there will be a vertically line, which is exactly what I want.
I have already implemented one-side border for an UIView but can't find out a way to make it fade out. I tried CIFilter, which didn't work. Then I noticed that this fade out effect is simliar to shadow. I tried to simulate the fade out by setting shadow but failed again.
Thanks for any answer and suggestion in advance.
You could use a CAGradientLayer:
let layer = CAGradientLayer()
layer.colors = [UIColor.white.cgColor, UIColor.gray.cgColor]
// play around with layer.locations and layer.{startPoint|endPoint} to suit your needs
You can then subclass UIView and override its layerClass to return the gradient layer, and then
size and position that view as you please:
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
// configure the layer with color, end/start and locations in init()
This allows you to drop that view in anywhere and use auto layout to size and position it as you please.
You can create a CAGradientLayer, but instead of subclassing the UIView, just set it to the mask property of the layer:
let gradientMaskLayer = CAGradientLayer()
gradientMaskLayer.frame = maskedView.bounds
gradientMaskLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor]
gradientMaskLayer.locations = [0.8, 1]
maskedView.layer.mask = gradientMaskLayer
I am applying shadow to a UITableViewCell using CALayer.
Here's my code:
- (void)addShadowToView:(UIView *)view
{
// shadow
view.layer.shadowColor = [[UIColor colorWithWhite:0.0f alpha:0.1f] CGColor];
view.layer.shadowOpacity = 1.0f;
view.layer.shadowOffset = CGSizeMake(0.0f, 3.0f);
view.layer.shadowRadius = 6.0f;
CGRect shadowFrame = view.layer.bounds;
CGPathRef shadowPath = [UIBezierPath bezierPathWithRect:shadowFrame].CGPath;
view.layer.shadowPath = shadowPath;
}
The issue is that for some tableviewcells, the shadow does not span the entire width of the cell. For some cells it would be correct, for others it would be faulty. I do notice that the rotation of the device also affects it, and reloading of the tableview data sometimes solves it.
What is the best way to mitigate this issue (and with that I don't mean to reload the whole tableview on each rotation etc.)?
Example bottom of cell where shadow is correctly applied:
Bottom of cell in same tableview after scrolling down (shadow only applied for first 75% of width):
Edit: I have noticed the issue is caused from these lines of code:
CGRect shadowFrame = view.layer.bounds;
CGPathRef shadowPath = [UIBezierPath bezierPathWithRect:shadowFrame].CGPath;
view.layer.shadowPath = shadowPath;
If I leave them out, everything is fine. But I've been told there is certain performance benefit when using this. Somehow the shadow is not correctly applied to new dimensions after rotating..
You can override the setter for you're cell's frame and call addShadowToView:. You can optimize this more by storing your cell's size and only updating the shadow path when the size changes for example:
#property (nonatomic, assign) CGSize size;
And
- (void) setFrame:(CGRect)frame
{
[super setFrame:frame];
// Need to check make sure this subview has been initialized
if(self.subviewThatNeedsShadow != nil && !CGSizeEqualToSize(self.size,_frame.size)
{
[self addShadowToView: self.subviewThatNeedsShadow];
}
}
The easiest solution is to add the shadow to the UITableViewCell's contentView (vs the layer for the cell's backing view). Since the cell's bounds change on scroll, if you add the shadow to the root view then you would have to update the shadow's path on each scroll event which would be costly and not necessary.
You're definitely correct re: the performance hit by not explicitly setting the shadowPath though. If you don't have any animated content within the cell, I'd also recommend rasterizing it to further improve performance.
EDIT: You must also ensure that when you set the shadow path that the contentView's bounds are in their 'final' position. If the size of the cell is later modified, this will result in the contentView's bounds changing and thus an incorrect shadowPath. The solution to this is to update the path in the UITableViewCell's -layoutSubviews method.
Here the concern is not the parent view frame where your working here concern is its sublayer and its size which should be changed when layout changes. You can override the below method which will help you to setup correct frame on layout changing.
public override void LayoutSublayersOfLayer(CALayer layer)
{
base.LayoutSublayersOfLayer(layer);
if (layer.Name == "gradient")
{
layer.Frame = view.Layer.Frame;
}
}
In above code view is the where you added sublayer. If you are playing with multiple layers in same view than you can use the identifier name property to work on particular layer.
Thanks for #beyowulf's answer gave me clues in override UIView frame get and set
In my case, I would like to make shadow stick with the other subview in subclass tableView cell.
Swift 5
// TargetView old size
var lastSize: CGSize = .zero
// Override frame in subclass tableView cell
override var frame: CGRect {
get {
super.frame
}
set {
super.frame = newValue
if targetView != nil {
// Compared targetView size with old one.
if lastSize != targetView.frame.size {
/* Update the other subview's shadow path or layer frame here */
lastSize = targetView.frame.size
}
}
}
}
It works for me.
Given an arbitrary UIView on iOS, is there a way using Core Graphics (CAGradientLayer comes to mind) to apply a "foreground-transparent" gradient to it?
I can't use a standard CAGradientLayer because the background is more complex than a UIColor. I also can't overlay a PNG because the background will change as my subview is scrolled along its parent vertical scrollview (see image).
I have a non-elegant fallback: have my uiview clip its subviews and move a pre-rendered gradient png of the background as the parent scrollview is scrolled.
This was an embarrassingly easy fix: apply a CAGradientLayer as my subview's mask.
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = _fileTypeScrollView.bounds;
gradientLayer.colors = [NSArray arrayWithObjects:(id)[UIColor whiteColor].CGColor, (id)[UIColor clearColor].CGColor, nil];
gradientLayer.startPoint = CGPointMake(0.8f, 1.0f);
gradientLayer.endPoint = CGPointMake(1.0f, 1.0f);
_fileTypeScrollView.layer.mask = gradientLayer;
Thanks to Cocoanetics for pointing me in the right direction!
This is how I'll do.
Step 1 Define a custom gradient view (Swift 4):
import UIKit
class GradientView: UIView {
override open class var layerClass: AnyClass {
return CAGradientLayer.classForCoder()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let gradientLayer = self.layer as! CAGradientLayer
gradientLayer.colors = [
UIColor.white.cgColor,
UIColor.init(white: 1, alpha: 0).cgColor
]
backgroundColor = UIColor.clear
}
}
Step 2 - Drag and drop a UIView in your storyboard and set its custom class to GradientView
As an example, this is how the above gradient view looks like:
https://github.com/yzhong52/GradientViewDemo
I used the accepted (OP's) answer above and ran into the same issue noted in an upvoted comment - when the view scrolls, everything that started offscreen is now transparent, covered by the mask.
The solution was to add the gradient layer as the superview's mask, not the scroll view's mask. In my case, I'm using a text view, which is contained inside a view called contentView.
I added a third color and used locations instead of startPoint and endPoint, so that items below the text view are still visible.
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.contentView!.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.clear.cgColor, UIColor.white.cgColor]
// choose position for gradient, aligned to bottom of text view
let bottomOffset = (self.textView!.frame.size.height + self.textView!.frame.origin.y + 5)/self.contentView!.bounds.size.height
let topOffset = bottomOffset - 0.1
let bottomCoordinate = NSNumber(value: Double(bottomOffset))
let topCoordinate = NSNumber(value: Double(topOffset))
gradientLayer.locations = [topCoordinate, bottomCoordinate, bottomCoordinate]
self.contentView!.layer.mask = gradientLayer
Before, the text that started offscreen was permanently invisible. With my modifications, scrolling works as expected, and the "Close" button is not covered by the mask.
I just ran into the same issue and wound up writing my own class. It seems like serious overkill, but it was the only way I could find to do gradients with transparency. You can see my writeup and code example here
It basically comes down to a custom UIView that creates two images. One is a solid color, the other is a gradient that is used as an image mask. From there I applied the resulting image to the uiview.layer.content.
I hope it helps,
Joe
I hate to say it, but I think that you are into the CUSTOM UIView land. I think that I would try to implement this in a custom UIView overiding the drawRect routine.
With this, you could have that view, place on top of your actual scrollview, and have your gradient view (if you will) "pass-on" all touch events (i.e. relinquish first responder).
I'm creating table view with custom cells, which looks like news feed - on gray background there are rectangles with rounded corners and shadow.
While cellForRowAtIndexPath method is calling I'm setting shadow like this:
cell.postCard.layer.cornerRadius = 3
cell.postCard.layer.shadowColor = UIColor.darkGrayColor().CGColor
cell.postCard.layer.shadowOffset = CGSizeMake(0,1)
cell.postCard.layer.shadowRadius = 3
cell.postCard.layer.shadowOpacity = 0.5
cell.layer.shadowPath = UIBezierPath(rect: cell.postCard.layer.bounds).CGPath
Where postCard is UIView - it's container for all context of cell.
I've read that I need to add shadowPath to get good performance but I think it's not working because when I'm slowly moving table view is stuttering. Can be reason of using simulator than real device?
You are setting shadow path to cell's layer, while modifying shadow settings for cell.postCard
cell.postCard.layer.shadowOpacity = 0.5
cell.layer.shadowPath = UIBezierPath(rect: cell.postCard.layer.bounds).CGPath
Is this really what you need?
Also, you should set 'cell.clipsToBounds = false' for your cell.
Try to read about the shouldRasterize and how it is working with shadows and corner radius of the UI elements. You can boost performance by manipulation of the shouldRasterize value.