Alpha gradient on view ios [duplicate] - ios

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

Related

Get rendered bounds of a view in viewDidLoad()

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 make a fade out border for uiview

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

`CAGradientLayer` Define Start and End Points in Terms of Local Coordinate Space

A CAGradientLayer has two properties startPoint and endPoint. These properties are defined in terms of the unit coordinate space. The result is that if I have two gradient layers with the same start and end points each with different bounds, the two gradients will be different.
How can the startPoint and endPoint of a CAGradientLayer layer be defined not in terms of the unit coordinate space but in standard point coordinates so that the angle/size of the gradient is not affected by the bounds of the layer?
The desired result is that a gradient layer can be resized to any size or shape and the gradient remain in place, although cropped differently.
Qualifications:
I know that this seems like an absolutely trivial transformation between coordinate spaces, but apparently either, yes I am in fact that dumb, or perhaps there's something either broken or extremely counter-intuitive about how CAGradientLayers work. I haven't included an example of what I expect should be the right way to do it, because (assuming I'm just dumb) it would only be misleading.
Edit:
Here is the implementation I have of a CALayer which adds a sub CAGradientLayer and configures it's start and end points. It does not produce the desired results.
#interface MyLayer ()
#property (nonatomic, strong) CAGradientLayer *gradientLayer;
#end
#implementation MyLayer
- (instancetype)init {
if (self = [super init]) {
self.gradientLayer = [CAGradientLayer new];
[self addSublayer:self.gradientLayer];
self.gradientLayer.colors = #[ (id)[UIColor redColor].CGColor, (id)[UIColor orangeColor].CGColor, (id)[UIColor greenColor].CGColor, (id)[UIColor blueColor].CGColor];;
self.gradientLayer.locations = #[ #0, #.333, #.666, #1 ];
}
return self;
}
- (void)layoutSublayers {
[super layoutSublayers];
self.gradientLayer.frame = self.bounds;
self.gradientLayer.startPoint = CGPointMake(0, 0);
self.gradientLayer.endPoint = CGPointMake(100 / self.bounds.size.width, 40 / self.bounds.size.height);
}
#end
I have a .xib file with a number of MyLayer's of different sizes. The gradients of the layers are all different.
You can't define the startPoint and endPoint otherwise. You have two options:
Calculate those based on the view height (50 pixels for a 100 pixels view height = 0.5, for a 100 pixels view height = 0.25)
Create a gradient at the largest fixed size required (ex: with a height of 568), and add it as a subview of another view that will be resized for your needs, with clipsToBounds enabled. That way, you could achieve what you want (have the gradient always start at the top and clip the bottom, keep the gradient centered and clip the top and the bottom, etc)
How can the startPoint and endPoint of a CAGradientLayer layer be defined not in terms of the unit coordinate space but in standard point coordinates so that the angle/size of the gradient is not effected by the bounds of the layer?
It can't. But you can easily think of a different strategy. For example:
Paint the gradient a different way (using Quartz instead relying on a CAGradientLayer to paint it for you).
Use a mask so that the layer appears to be a certain size and shape, and you can change that size and shape by changing the mask, but actually the layer itself is one big constantly sized layer with the same gradient all the time.
Detect that the gradient layer has changed bounds, and change the gradient startPoint and endPoint to match. Here's a working example of a view whose layer is a gradient layer and does this - but you have to remember to redraw the layer every time the bounds change!
override func drawLayer(layer: CALayer!, inContext ctx: CGContext!) {
let grad = layer as! CAGradientLayer
grad.colors = [UIColor.whiteColor().CGColor, UIColor.redColor().CGColor]
let maxx:CGFloat = 500.0 // or whatever
let maxy:CGFloat = 500.0 // or whatever
grad.startPoint = CGPointMake(0,0) // or whatever!
grad.endPoint = CGPointMake(maxx/self.bounds.width, maxy/self.bounds.height)
}

Circular UIView (with cornerRadius) without Blended Layer

I'm trying to get the circle below to have an opaque solid white color where the cornerRadius cuts out the UIView.
UIView *circle = [[UIView alloc] initWithFrame:CGRectMake(i * (todaySize + rightMargin), 0, smallSize, smallSize)];
circle.layer.cornerRadius = smallSize/2;
circle.layer.borderWidth = 0.5;
circle.layer.backgroundColor = [UIColor whiteColor].CGColor;
circle.backgroundColor = [UIColor whiteColor];
[self addSubview:circle];
I've tried a few things like setting the backgroundColor and opaque without any luck. Color Blended Layers still shows that the surrounding of the circle is transparent. Does anybody know how to solve this?
To avoid blending when using rounded corners, the rounding needs to be done in drawRect, rather than as a property on the layer. I needed UICollectionView cells with a rounded background in an app I'm working on. When I used layer.cornerRadius, the performance took a huge hit. Turning on color blended layers yielded the following:
Not what I was hoping for, I want those cells to be colored green indicating there is no blending occurring. To do this, I subclassed UIView into RoundedCornerView. My implementation is real short and sweet:
import UIKit
class RoundedCornerView: UIView {
static let cornerRadius = 40.0 as CGFloat
override func drawRect(rect: CGRect) {
let borderPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: RoundedCornerView.cornerRadius)
UIColor.whiteColor().set()
borderPath.fill()
}
}
Then I set the view I was rounding to be a RoundedCornerView in my nib. Running at that point yielded this:
Scrolling is buttery smooth and there is no longer any blending occurring. One odd side effect of this is that the view's backgroundColor property will color the excluded area of the corners, not the main body of the view. This means that the backgroundColor should be set to whatever is behind your view, not to the desired fill color.
Try using a mask to both avoid blending and dealing with the parent / child background color match.
override func layoutSubviews() {
super.layoutSubviews()
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 20).cgPath
layer.mask = maskLayer
}
Set clipsToBoundson the view or masksToBounds on the layer to YES

Fading out items in UICollectionView

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.

Resources