Gradient at Top/Bot of Scrollview When There's Additional Content - ios

I have a scrollview>ContentView>TextLabel setup where the gradient is showing up but not working how I want it to. It's a clear background scrollView, so all I'm looking for is a clear gradient mask over the text. I found something similar to I'm looking for in Objective-C but that thread doesn't have a Swift solution.
My end goal seems like something most people might use, so I'm surprised there's not a Swift version yet. The functionality I'm trying to code is:
Sometimes the text will fit perfectly in the scrollView's fixed size so there should be no gradient.
When the text is longer than can fit and so some of it is below the scrollView's bottom cutoff, the bottom of the view should fade to clear.
Once the user scrolls down and the top should fade, indicating that there's more above.
I tried this code to handle bullet #2:
let gradient = CAGradientLayer()
gradient.frame = self.bio_ScrollView.superview!.bounds ?? CGRectNull
gradient.colors = [UIColor.whiteColor().CGColor, UIColor.clearColor().CGColor]
//gradient.locations = [0, 0.15, 0.25, 0.75, 0.85, 1.0]
gradient.locations = [0.6, 1.0]
self.bio_ScrollView.superview!.layer.mask = gradient
But it fades everything, including the button below it, which is clearly not in the scrollview:
If I remove the .superview and apply it directly to the scrollView, it just fades all the text below the initial part that was visible:
Does anyone know anything about how to implement this correctly?

Figured it out. First, make sure you've added the right view hierarchy. The scroll view needs to be embedded in a container view (which is what the gradient will be applied to):
Top/container view: Set this up however you want
Scrollview: Pin all edges to container (4 total constraints)
Scrollview content view: Pin edges + Center X (5 total constraints)
Label: Pin edges (4 total constraints)
Add "scrollViewDelegate" to the ViewController class:
class ViewController_WithScrollView: UIViewController, UIScrollViewDelegate {
....
Connect the four views listed above with IBOutlets. Then, set your scrollView delegate within the viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
yourScrollView.delegate = self
//+All your other viewDidLoad stuff
}
Then implement the scrollViewDidScroll func, which will run automatically thanks to the work you did above.
func scrollViewDidScroll (scrollView: UIScrollView) {
if scrollView.isAtTop {
self.applyGradient_To("Bot")
} else if scrollView.isAtBottom {
self.applyGradient_To("Top")
} else {
self.applyGradient_To("Both")
}
}
Then add this magical gradient code:
func applyGradient_To (state: String) {
let gradient = CAGradientLayer()
gradient.frame = self.yourScrollView.superview!.bounds ?? CGRectNull
switch state {
case "Top":
gradient.colors = [UIColor.clearColor().CGColor,UIColor.whiteColor().CGColor]
gradient.locations = [0.0,0.2]
case "Bot":
gradient.colors = [UIColor.whiteColor().CGColor, UIColor.clearColor().CGColor]
gradient.locations = [0.8,1.0]
default:
gradient.colors = [UIColor.clearColor().CGColor,UIColor.whiteColor().CGColor,UIColor.whiteColor().CGColor, UIColor.clearColor().CGColor]
gradient.locations = [0.0,0.2,0.8,1.0]
}
self.yourScrollView.superview!.layer.mask = nil
self.yourScrollView.superview!.layer.mask = gradient
}
That should do it!

Related

Applying CAGradientLayer on Dynamically Sized UITableViewCell

Background: My app allows users to select a gradient border to apply to UITableViewCells that are dynamically sized based on the content within them. I am currently creating this border by inserting a CAGradientLayer sublayer to a UIView that sits within the cell.
Issue: Because each cell is sized differently, I am resizing the CAGradientLayer by overriding layoutIfNeeded in my custom cell class. This works, but seems suboptimal because the border is being redrawn over and over again and flickers as the cell is resizing.
Link to Screen Capture:
https://drive.google.com/file/d/1SiuNozyUM7LCdYImZoGCWeoeBKu2Ulcw/view?usp=sharing
Question: Do I need to take a different approach to creating this border? Or am I missing something regarding the UITableViewCell lifecycle? I have come across similar issues on SO, but none that seem to address this redraw issue. Thank you for your help.
CAGradientLayer Extension to Create Border
extension CAGradientLayer {
func createBorder(view: UIView, colors: [CGColor]) {
self.frame = CGRect(origin: CGPoint.zero, size: view.bounds.size)
self.colors = colors
let shape = CAShapeLayer()
shape.lineWidth = 14
shape.path = UIBezierPath(roundedRect: view.bounds, cornerRadius: 12).cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
self.mask = shape
}
}
TableViewCell Class - Insert CAGradientLayer
override func awakeFromNib() {
super.awakeFromNib()
reportCard.layer.insertSublayer(gradientLayer, at: 0)
...
}
TableViewCell Class - Resize the Border and Apply User Selected Design
override func layoutIfNeeded() {
super.layoutIfNeeded()
switch currentReport?.frameId {
case "sj_0099_nc_frame_001":
gradientLayer.createBorder(view: reportCard, colors: [App.BorderColors.lavender, App.BorderColors.white])
case "sj_0099_nc_frame_002":
gradientLayer.createBorder(view: reportCard, colors: [App.BorderColors.red, App.BorderColors.white])
case "sj_0099_nc_frame_003":
gradientLayer.createBorder(view: reportCard, colors: [App.BorderColors.yellow, App.BorderColors.white])
default:
gradientLayer.createBorder(view: reportCard, colors: [App.BorderColors.white, App.BorderColors.white])
}
}
Turns out I was looking in the wrong place all along. The code in my original post is functional, and updating the gradientLayer frame in layoutIfNeeded() or setNeedsLayout() rather than layoutSubviews() accurately draws the gradientLayer. Per Apple documentation, layoutSubviews() should not be called directly.
The source of the bug was not in my custom cell, but in my tableViewController. I had an extraneous call to reloadData().
Instead of inside awakeFromNib() use this
override func layoutSubviews() {
super.layoutSubviews()
reportCard.layer.insertSublayer(gradientLayer, at: 0)
reportCard.clipsToBounds = true
}

Why is CALayer mask not centered and off by a few pixels?

I have the following code:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
var gradient = new CAGradientLayer();
gradient.Frame = view.Bounds;
gradient.Colors = colors;
var mask = new CALayer();
mask.Frame = view.Bounds;
mask.Contents = view.Image.CGImage;
mask.ContentsGravity = CALayer.GravityCenter;
gradient.Mask = mask;
view.Layer.AddSublayer(gradient);
}
which produces the following result:
"view" is a UIImageView which is centered perfectly.
The number "1" is exactly in the middle. Here you can see that the mask is not centered perfectly.
The image here is the heart icon from the standard resources, it's not a custom image. (If e.g. used in a button, then it's centered perfectly).
Before posting I've tried all possible combinations when it comes to setting Frame and Bounds, the mask is always off-center by a few pixels.
Someone else reported a similar issue here, but there was no solution: Gradient Color over Template Image in Swift 4
This makes me believe that there's a bug with how masks are applied. Could anyone confirm?
There’s no bug with masking. This is crucial stuff, basic to all drawing. If there were a bug the whole interface would be broken. The bug is in your code.
It's all basically just a matter of timing. Do not talk about any frames or bounds until you know what they actually are.
To demonstrate, I'm going to work backwards: first I'll show you the result of my code, then I'll show you the code. Here's the result. I start with a storyboard that has a square centered by autolayout, so you know I'm not cheating:
Now I run the app and superimpose over the entire screen a gradient layer with a centered heart image mask:
As you can see, the heart is in exactly the right place. It is centered in precisely the same sense as the yellow square, and that is why it appears centered in the square.
Now here's the code. Notice that the assembly work is done just once, in viewDidLoad, but all the measurement work is done when we have actual measurements to work with, namely, in viewDidLayoutSubviews. This is Swift but you should have no difficulty translating (C# or whatever):
class ViewController: UIViewController {
let gradient : CAGradientLayer = {
let g = CAGradientLayer()
g.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
return g
}()
let mask = CALayer()
let image : UIImage = {
let im = UIImage(systemName: "heart.fill")!
return im
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.layer.addSublayer(gradient)
mask.contents = image.cgImage
mask.contentsGravity = .center
gradient.mask = mask
}
override func viewDidLayoutSubviews() {
gradient.frame = view.bounds
mask.frame = gradient.bounds
}
}
Go ye and do likewise.

Side of TableViewCell gets cut off when applying a gradient mask

My tableView sits within a blank content view with constraints holding it in place. The table will function properly in the container by itself, but as soon as I apply a gradient using the code below, the gradient works perfectly, except the right hand side of my tableViewCells get cut off. It only happens when the gradient is applied. Any recommendations would be appreciated.
Before Gradient is added, table view looks like this:
TableView with full TableViewCells
After the gradient is added, the table view looks like this:
TableView with cut TableViewCells
This is the code I am using to apply the gradient.
let gradientLayerMask = CAGradientLayer()
gradientLayerMask.frame = gradientMask.bounds
gradientLayerMask.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
gradientLayerMask.locations = [0.0,0.1,0.9,1.0]
gradientMask.layer.mask = gradientLayerMask
exerciseTable.contentInset = UIEdgeInsets(top: 15, left: 0, bottom: 0, right: 0)
Put some delay for gradient code
func delay(time: Double, closure:
#escaping ()->()) {
DispatchQueue.main.asyncAfter(deadline: .now() + time) {
closure()
}
}
Use this way.
delay(time: 0.2) {
//gradient code
}
I hope it's work for you.

layoutIfNeeded function behaviour

I am using a lot of gradient drawing using this function:
func drawGradient(colors: [CGColor], locations: [NSNumber]) {
let gradientLayer = CAGradientLayer()
gradientLayer.frame.size = self.frame.size
gradientLayer.frame.origin = CGPoint(x: 0.0,y: 0.0)
gradientLayer.colors = colors
gradientLayer.locations = locations
print(self.frame.size)
self.layer.insertSublayer(gradientLayer, at: 0)
}
The problem is that if I don't call self.view.layoutIfNeeded() in viewDidLoad() of UIViewController my gradient doesn't cover whole screen on iPhone X+. But if I call self.view.layoutIfNeeded() it makes my app crash on iOS 9.x and act weird on iPhone 5/5s. I really do not know any workaround and need help to understand how it all works.
You are calling drawGradient in viewDidLoad. That is too early. You need to wait until Auto Layout has sized the frame.
Move the call in viewDidLoad to an override of viewDidLayoutSubviews. Be careful though because viewDidLayoutSubviews is called more than once, so make sure you only call drawGradient once. You can add a property to your viewController called var appliedGradient = false and then check it before applying the gradient and flip it to true.
For your custom subclasses of UITableViewCell and UICollectionViewCell, override layoutSubviews and call drawGradient after super.layoutSubviews(). Again, make sure you only call it once.
Note: If your frame could resize (due to rotation of the phone) or differing cell sizes, you should keep track of the previous gradient layer and replace it with a new one in viewDidLayoutSubviews for your viewController and in layoutSubviews for your cells.
Here I've modified your drawGradient to make a global function called applyGradient that adds a gradient to a view. It replaces a previous gradient layer if there was one:
func applyGradient(colors: [CGColor], locations: [NSNumber], to view: UIView, replacing prior: CALayer?) -> CALayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame.size = view.frame.size
gradientLayer.frame.origin = CGPoint(x: 0.0,y: 0.0)
gradientLayer.colors = colors
gradientLayer.locations = locations
print(view.frame.size)
if let prior = prior {
view.layer.replaceSublayer(prior, with: gradientLayer)
} else {
view.layer.insertSublayer(gradientLayer, at: 0)
}
return gradientLayer
}
And it is used like this:
class ViewController: UIViewController {
// property to keep track of the gradient layer
var gradient: CALayer?
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
gradient = applyGradient(colors: [UIColor.red.cgColor, UIColor.yellow.cgColor],
locations: [0.0, 1.0], to: self.view, replacing: gradient)
}
}

Adding gradient to scrolling UITableView

I was able to add a gradient my UITableView, but I have the issue of when I have to scroll through my cells, the gradient background scrolls along also. I want the background to stay consistent as I scroll up or down. How can I achieve this? Do I have to create a custom UITableView in order to do this?
The pictures below show what it currently looks like.
Here is my code for adding the gradient to the UITableView:
func addGradientToBackground(){
var gradient : CAGradientLayer = CAGradientLayer()
gradient.frame = self.tableView.bounds
gradient.colors = [UIColor.blueColor().CGColor, UIColor.redColor().CGColor]
self.tableView.layer.insertSublayer(gradient, atIndex: 0)
}
Messing around will setting the gradient doesn't work either, like setting:
self.view.layer.insertSublayer(gradient, atIndex: 0)
or changing the bounds:
gradient.frame = self.tableView.frame
Also, in cellForRowAtIndexPath I set the UITableViewCells background color to clear:
cell.backgroundColor = UIColor.clearColor()
I can't add images, but here is the link if you wish to see them: http://imgur.com/Vtka1tO,6faLMkr#0
The gradient needs to be behind the table view if you don't want it to scroll. If you're using a UITableViewController, the only thing behind is the window, so you could give it the gradient, and make the cells and the table view have a clear background color. If you're using a UIViewController with a table view as a subview, then you could give the controller's main view a gradient background color.
Have you thought to add a background image as follows?
var imageView = UIImageView(frame: CGRectMake(10, 10, cell.frame.width - 10, cell.frame.height - 10))
let image = UIImage(named: ImageNames[indexPath.row])
imageView.image = image
cell.backgroundView = UIView()
cell.backgroundView.addSubview(imageView)

Resources