Swift: Gradient splits on rotation - swift

In short, I have a gradient that is a mix of Dark Blue and Black. The gradient looks beautiful, however when I rotate the screen and put it in landscape, the two colors split and half of the screen has a blue background and the other half is black. Figuring I didn't do it right, I copied codes from these two sources:
YouTube video https://www.youtube.com/watch?v=pabNgxzEaRk
Website http://blog.apoorvmote.com/gradient-background-uiview-ios-swift/
This is my code:
let topColor = UIColor(red: 28/255.0, green: 25/255.0, blue: 127/255.0, alpha: 1)
let bottomColor = UIColor(red: 0/255.0, green: 0/255.0, blue: 25/255.0, alpha: 1)
let gradientColors: [CGColor] = [topColor.CGColor, bottomColor.CGColor]
let gradientLocations: [Float] = [0.0, 1.0]
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = gradientColors
gradientLayer.locations = gradientLocations
gradientLayer.frame = self.view.bounds
self.view.layer.insertSublayer(gradientLayer, atIndex: 0)
Can someone point me in the right direction to stop my gradient from splitting in two?

Keep in mind that when changing screen orientation all view's bounds change as well (assuming you are using auto layout) but the same thing is not applied on programmatically added layers. So in your case gradient layer still has the frame which is set based on old view bounds before rotation. To solve this I suggest subclassing your gradient view and update gradient layer frame in layoutSubviews function which is called on each view bounds change.
class GradientView: UIView {
let gradientLayer = CAGradientLayer()
override func awakeFromNib() {
gradientLayer.frame = self.bounds
gradientLayer.colors = yourColors
gradientLayer.locations = yourLocations
self.layer.addSublayer(gradientLayer)
}
override func layoutSubviews() {
gradientLayer.frame = self.bounds
}
}

You only need to override layoutSubview method to Autoresize your gradient view with respect to your device's view
Write only,
override func viewDidLayoutSubviews() {
gradientLayer.frame = self.view.bounds
}
I tried this and its working fine. Hope this help you.

Related

Setting toValue for CAShapeLayer animation from UICollectionView cellForItemAt indexPath

I have the following parts:
- My main view is a UIViewController with a UICollectionView
- The cell for the UICollectionView
- A subclass of the UIView to build a CAShapeLayer with an CABasicAnimation
In my main view I have a UICollectionView which renders a bunch of cells with labels etc. It also is showing a progress graph.
In my subclass ProgressCirclePath() I am drawing a CAShapeLayer which is acting as the progress graph rendered in each cell of my UICollectionView.
I have been able to pass data to each cell, e.g. the labels as well as the CAShapeLayer strokeEnd values.
Everything is fine until I try to add a CABasicAnimation to my path. In this case I am not able to set the value for the animations toValue. Testing using the print console reveals that the value is available in my UICollectionView but not in the animation block in my subClass (which is where it simply returns nil). I have tried simply setting the toValue as well as creating a variable within my ProgressCirlePath and setting it from the UICollectionView. Neither worked.
I'd appreciate any hints on why this is happening and how to solve this. Thanks!!
Within my UICollectionView:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = decksCollectionView.dequeueReusableCell(withReuseIdentifier: "DeckCell", for: indexPath) as! DeckCell
cell.creatorLabel.text = deckCellCreator[indexPath.item]
cell.titleLabel.text = deckCellTitle[indexPath.item]
cell.progressLabel.text = "\(deckCellCompletionPercentage[indexPath.item])%"
cell.progressGraphView.animation.toValue = CGFloat(deckCellCompletionPercentage[indexPath.item])/100
return cell
}
The setup within my cell class:
let progressGraphView: ProgressCirclePath = {
let circlePath = ProgressCirclePath(frame: CGRect(x:0, y:0, width: 86, height: 86))
circlePath.progressLayer.position = circlePath.center
circlePath.progressBackgroundLayer.position = circlePath.center
circlePath.translatesAutoresizingMaskIntoConstraints = false
return circlePath
}()
And here my ProgressCirclePath()
class ProgressCirclePath: UIView {
let progressLayer = CAShapeLayer()
let progressBackgroundLayer = CAShapeLayer()
let animation = CABasicAnimation(keyPath: "strokeEnd")
// var percentageValue = CGFloat()
override init(frame: CGRect) {
super.init(frame: frame)
layer.addSublayer(progressBackgroundLayer)
layer.addSublayer(progressLayer)
let circularPath = UIBezierPath(arcCenter: .zero, radius: 43, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
progressBackgroundLayer.path = circularPath.cgPath
progressBackgroundLayer.lineWidth = 10
progressBackgroundLayer.strokeStart = 0
progressBackgroundLayer.strokeEnd = 1
progressBackgroundLayer.strokeColor = UIColor(red: 221/255.0, green: 240/255.0, blue: 226/255.0, alpha: 1.0).cgColor
progressBackgroundLayer.fillColor = UIColor.clear.cgColor
progressBackgroundLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
progressLayer.path = circularPath.cgPath
progressLayer.lineWidth = 10
progressLayer.lineCap = kCALineCapRound
progressLayer.strokeColor = UIColor(red: 72/255.0, green: 172/255.0, blue: 104/255.0, alpha: 1.0).cgColor
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
animation.duration = 1
animation.fromValue = 0
// animation.toValue = percentageValue
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
progressLayer.add(animation, forKey: "animateGraph")
print("animation.toValue \(animation.toValue)")
}
required init?(coder aDecoder: NSCoder) {
fatalError("has not been implemented")
}
}
You're handling the injection of data and animation too early in the lifecycle of the custom view. Instead of handling them in the object's initializer, move them to a later and more appropriate method, such as layoutSubviews:
override open func layoutSubviews() {
super.layoutSubviews()
// handle post-init stuff here, like animations and passing in data
// when it doesn't get passed in on init
}

Set gradient on UIView partially, half color is gradient and half is single color

Trying to show the progress bar made it custom, took an UIView set it frame to percent of progress. Say 20% of frame width and made gradient but remaining 80% should be white color and text on it defining percentage.
Problems facing is not able to display text set UILabel instead of UIView but text not displaying. Please guide.
Below is what i have tried.
let view: UILabel = UILabel(frame: CGRectMake(0.0, self.scrollMainView.frame.size.height-50, self.view.frame.size.width/5, 50))
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = view.bounds
gradient.locations = [0.0 , 1.0]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
let color0 = UIColor(red:71.0/255, green:198.0/255, blue:134.0/255, alpha:1.0).CGColor
let color1 = UIColor(red:25.0/255, green:190.0/255, blue: 205.0/255, alpha:1.0).CGColor
// let color2 = UIColor(red:0.0/255, green:0.0/255, blue: 0.0/255, alpha:1.0).CGColor
gradient.colors = [color1, color0]
view.layer.insertSublayer(gradient, atIndex: 0)
self.scrollMainView.addSubview(view)
view.text = "20%"
view.textColor = UIColor.blackColor()
view.layer.shadowColor = UIColor.blackColor().CGColor
view.layer.shadowOpacity = 1
view.layer.shadowOffset = CGSizeZero
view.layer.shadowRadius = 2
I cant understand your problem from your question exactly but I will answer based on question title only. To get half color gradient and half single color you have to use three colors gradient and set their locations accordingly :
gradient.colors = [color1,color1, color0]
gradient.locations = [0.0, 0.5, 1.0]
This way you will draw a gradient from color1 to color1 (which in fact is single color) and fill 50% of frame's area with it and a gradient from color1 to color0 that will fill other half of the frame.
I answer to why you can't see the text.
Forget for one second layers and think about subviews. What should happen if you add a subview to a UILabel? It will be on top of the label's content, of course.
So, the same applies to layers. The UILabel draws its text on its main layer, and any sublayer you add to the main layer is on top of it.
My suggestion is to use a UIView with a CAGradientLayer sublayer and a UILabel subview.
Or even better, subclass a UIView in order to use a CAGradientLayer as backing layer (through class func layerClass() -> AnyClass method) and just add UILabel as subview.
Here is an example:
class CustomView : UIView {
lazy var label : UILabel = {
let l = UILabel(frame: self.bounds)
l.text = "20%"
l.textColor = UIColor.blackColor()
l.backgroundColor = UIColor.clearColor()
return l
}()
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
let gradient: CAGradientLayer = self.layer as! CAGradientLayer
gradient.locations = [0.0 , 1.0]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
let color0 = UIColor(red:71.0/255, green:198.0/255, blue:134.0/255, alpha:1.0).CGColor
let color1 = UIColor(red:25.0/255, green:190.0/255, blue: 205.0/255, alpha:1.0).CGColor
gradient.colors = [color1, color0]
label.frame = bounds
label.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
self.addSubview(label)
}
}

Gradient mask on UIScrollView slow to update

I have a UIScrollView subclass containing a very wide UIView subclass, and I want to fade out the edges of the visible content. Here's what I've got in the UIScrollView subclass:
private var gradientMask = CAGradientLayer()
override func layoutSubviews() {
super.layoutSubviews()
gradientMask.frame = self.bounds
gradientMask.colors = [UIColor(white: 0.0, alpha: 0.18).CGColor,
UIColor(white: 0.0, alpha: 0.95).CGColor,
UIColor(white: 0.0, alpha: 0.95).CGColor,
UIColor(white: 0.0, alpha: 0.0).CGColor
]
let fadeStart:CGFloat = 50.0 / self.bounds.width
gradientMask.startPoint = CGPoint(x:0, y:0.5)
gradientMask.endPoint = CGPoint(x:1, y:0.5)
let fadeEnd = (self.bounds.width - 40.0) / self.bounds.width
gradientMask.locations = [fadeStart,fadeStart+0.1,fadeEnd,1.0]
self.layer.mask = gradientMask
}
It does work, but there's a lag. After you scroll, the gradient appears to scroll with the content for a moment then snaps back to the expected position. How can I ensure the edges of the scroll view fade out—and that the gradient doesn't move when scrolling?
The fix was just to embed the UIScrollView subclass into a view that has the gradient applied. That's it!

Swift function not drawing circle in updated view

I have a stepper in my View Controller that updates variables(redd1, greenn1, bluee1) in my UIView. drawRect draws an initial circle and updateColor is meant to draw a new circle on top of it with an updated color. The variables get updated when I call updateColor and I know that they get passed through because when i have their values printed out in updateColor they are correct. updateColor won't draw a new circle.
class UIView1: UIView {
var redd1 = 0.0;
var greenn1 = 0.0;
var bluee1 = 0.0;
override init(frame: CGRect)
{
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect)
{
let circle2 = UIView(frame: CGRect(x: -25.0, y: 10.0, width: 100.0, height:100.0))
circle2.layer.cornerRadius = 50.0
let startingColor2 = UIColor(red: (CGFloat(redd1))/255, green: (CGFloat (greenn1))/255, blue: (CGFloat(bluee1))/255, alpha: 1.0)
circle2.backgroundColor = startingColor2;
addSubview(circle2);
}
func updateColor()
{
let circle = UIView(frame: CGRect(x: -25.0, y: 10.0, width: 100.0, height: 100.0))
circle.layer.cornerRadius = 50.0;
let startingColor = UIColor(red: (CGFloat(redd1))/255, green: (CGFloat(greenn1))/255, blue: (CGFloat(bluee1))/255, alpha: 1.0)
circle.backgroundColor = startingColor;
addSubview(circle);
}
}
Do not call addSubview in drawRect. Only use drawRect if you were going to draw a circle yourself (e.g. by calling stroke of a UIBezierPath). Or if you're going to add circles as subviews (as CAShapeLayer sublayers to the view's layer), the retire drawRect altogether.
But by calling addSubview in updateColor, you're not changing the color, but rather you're adding another subview every time. Likewise, by calling addSubview in drawRect, you're adding another subview there, too, every time the OS calls drawRect. For example, I ran your code, changing the color from black to red, to green, to blue, and triggered drawRect to be called again a few more times, and when I look at the view hierarchy in the view debugger, you can see all of the views in the view hierarchy:
This is a dangerous practice, because those subviews will add up over time, taking up memory.
If you're going to add subviews, I would suggest (a) drawRect is not the right place to be adding subviews; and (b) if you're determined to go the addSubview approach, decide whether you can remove the previous subviews before adding new ones.
Personally, if I wanted drawRect to draw circles of different colors, I would just stroke the UIBezierPath. For example:
class CircleView: UIView {
var redd1: CGFloat = 0.0 {
didSet {
setNeedsDisplay()
}
}
var greenn1 : CGFloat = 0.0 {
didSet {
setNeedsDisplay()
}
}
var bluee1: CGFloat = 0.0 {
didSet {
setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
let color = UIColor(red: redd1 / 255.0, green: greenn1 / 255.0, blue: bluee1 / 255.0, alpha: 1.0)
color.setStroke()
let path = UIBezierPath(arcCenter: CGPoint(x: 75, y: 75), radius: 50, startAngle: 0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
path.lineWidth = 2
path.lineCapStyle = .Round
path.stroke()
}
}
By the way, you notice that we can retire updateColor entirely. With the above code, if you set either red, green, or blue, it will call setNeedsDisplay, which will trigger the view being redrawn automatically.
You report that the above isn't working. Well, I tried it with this code:
class ViewController: UIViewController {
#IBOutlet weak var circleView: CircleView!
#IBOutlet weak var redSlider: UISlider!
#IBOutlet weak var greenSlider: UISlider!
#IBOutlet weak var blueSlider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
redSlider.value = Float(circleView.redd1) / 255.0
greenSlider.value = Float(circleView.greenn1) / 255.0
blueSlider.value = Float(circleView.bluee1) / 255.0
}
#IBAction func valueChangedForSlider(sender: UISlider) {
circleView.redd1 = 255.0 * CGFloat(redSlider.value)
circleView.greenn1 = 255.0 * CGFloat(greenSlider.value)
circleView.bluee1 = 255.0 * CGFloat(blueSlider.value)
}
}
And it worked fine:
As you mentioned that the both have the values which they should have.
The two circles which you are drawing are exactly on the same postion and both have exactly the same color according to your code snippet. How should they differ? Of course you can't see the two circles when they are having the same color and same position. They are overlayed.

iOS8: Auto-layout and Gradient

Setup:
I have a View Controller that consists of a View and a Container View.
The View (Orange) is pinned to top 0, left 0, and right 0.
The Container View (Gray) is pinned to bottom 0, left 0, and right 0.
The View's Bottom Space to: Container View = 0
The View's Proportional Height to Container View = 1
Desired Results:
I would like to add gradient to the background of the View (Orange)
Tried:
I'm using Auto-layout with class sizes to get different behavior on different screen.
Code:
class ViewController: UIViewController {
#IBOutlet weak var graphView: UIView!
#IBOutlet weak var containerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let backgroundColor = CAGradientLayer().graphViewBackgroundColor()
backgroundColor.frame = self.graphView.frame
self.graphView.layer.addSublayer(backgroundColor)
}
I have a category:
extension CAGradientLayer {
func graphViewBackgroundColor() -> CAGradientLayer {
let topColor = UIColor(red: (160/255.0), green: (160/255.0), blue: (160/255.0), alpha: 1)
let bottomColor = UIColor(red: (52/255.0), green: (53/255.0), blue: (52/255.0), alpha: 1)
let gradientColors: [CGColor] = [topColor.CGColor, bottomColor.CGColor]
let gradientLocations: [Float] = [0.0, 1.0]
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = gradientColors
gradientLayer.locations = gradientLocations
return gradientLayer
}
}
Result:
As you can see gradient did not cover the entire View.
Question: How can I get the gradient to cover the entire View
Update:
When I place the code in viewDidLayoutSubviews() It looks weird when I rotate:
Simply do it this inside viewDidLayoutSubviews:
override func viewDidLayoutSubview() {
super.viewDidLayoutSubviews
backgroundColor.frame = self.graphView.bounds
}
viewDidLayoutSubviews should be called when you rotate the device.
If it is not called, override this method and do it as,
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
backgroundColor.frame = self.graphView.bounds
}
Try putting your gradient code into viewDidLayoutSubviews instead of viewDidLoad
When viewDidLoad is called the views are not laid out (ie do not have their final frames set yet), so this is why you are only seeing a partial coverage of the gradient