How do i make uiview using linear gradient in swift - ios

i have in my figma this style: background: linear-gradient(155.98deg, #F5FEFF 0%, #C5F8FF 0%, rgba(106, 164, 189, 0.5) 47.57%, #0E8B9C 88.75%), #FFFFFF;
i need to make my uiview as linear-gradient

The basic idea is to configure a CAGradientLayer with an array of colors and locations. If you want a diagonal gradient, specify a startPoint and endPoint, too.
But you need to decipher that Figma CSS. The first parameter to linear-gradient is the angle. Linear gradients in iOS are not measured in angles, but rather with startPoint and endPoint, both of which are CGPoint references where 0, 0 represents the upper left corner of the view and 1, 1 represents the lower right corner. Your angle of 155.98deg suggests that you're looking for a “from upper left to lower right” gradient, which would be a startPoint of 0, 0 and an endPoint of 1, 1. (The exact mapping between the angle and these CGPoint values will vary based upon the aspect ratio of the view in question.)
The next parameter to linear-gradient is an array of tuples of colors and location. Solid colors are represented in hex strings. Translucent colors are represented with integer rgb values. So #F5FEFF 0% is at location 0.0 and a color of 0xf5 for red, 0xfe for green, and 0xff for blue. You need to divide all those numbers by 0xff (or 255). (The Xcode color literal will do that conversion for you.) And the 0% at the end of the string is the relative location of this color in the gradient (where 0% is the start of the gradient and 100% is the end). This correlates nicely to the CAGradientLayer property locations (though the % numbers are represented as values between 0 and 1).
The rgba(106, 164, 189, 0.5) 47.57% is red, green, and blue values of 106, 164, and 189, respectively, (each divided by 255). The 0.5 is an alpha of 50%. And the 47.57 is the position (47.57% of the way along the gradient).
Anyway, to render this, define an array of colors, array of positions, and the start and end of the gradient:
#IBDesignable class GradientView: UIView {
override class var layerClass: AnyClass { CAGradientLayer.self }
var gradientLayer: CAGradientLayer { layer as! CAGradientLayer }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
func configure() {
gradientLayer.colors = [#colorLiteral(red: 0.9607843137, green: 0.9960784314, blue: 1, alpha: 1), #colorLiteral(red: 0.7725490196, green: 0.9725490196, blue: 1, alpha: 1), #colorLiteral(red: 0.4156862745, green: 0.6431372549, blue: 0.7411764706, alpha: 0.5), #colorLiteral(red: 0.05490196078, green: 0.5450980392, blue: 0.6117647059, alpha: 1)].map { $0.cgColor }
gradientLayer.locations = [0, 0, 0.4757, 0.8875]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
}
}
If you want to see either the hex representation or integer representation of those colors, just double click on the color and tap on "Other" to see the RGB representations. E.g. I double clicked on the second color and I can see the #C5F8FF value:
A couple of final observations:
Note, I didn't just add a CAGradientLayer and set its frame. Instead, I created a view whose backing layer is a gradient. That way, if the view size changes because of constraints (or if you animate the size change), the size of the CAGradientLayer changes dynamically, too.
I made it #IBDesignable so I could see it in the storyboard. That’s not necessary, but is useful if using storyboards.
I wonder if your designer really meant to have both #F5FEFF and #C5F8FF at the 0% position. I suspect that was a mistake. (Figma makes it too easy to have multiple color data points in the gradient overlap with each other at either 0% or 100%.)
The CSS you've provided does not look syntactically correct. Notably, I don't know what to make of that #FFFFFF floating at the end (and your gradient points don’t go all the way to 100%). But, I don't think I need to know your full intent here ... you can just update the CAGradientLayer colors and locations arrays as you see fit. Just make sure they both have the same number of entries.
As you have noted, if you tap on the rightmost “Code” tab in the panel on the right in Figma, you can see an iOS code snippet. I would be wary of using that code, verbatim, in your app, because it (a) won’t use the UIView subclass outlined above, breaking if the view changes size or is animated; and (b) it will tend to use hard-coded values whereas you generally want to use constraints to dictate the layouts within iOS. Use this code snippet for inspiration, suggestions of possible API, etc., but I would not be inclined to use that exact code. Like all auto-generated code snippets, Figma’s suggested iOS code is not ideal.

let layerGradient = CAGradientLayer()
layerGradient.colors = [hexStringToUIColor(hex: "yourhex").cgColor, hexStringToUIColor(hex: "yourhex").cgColor]
layerGradient.startPoint = CGPoint(x: 0, y: 0.5)
layerGradient.endPoint = CGPoint(x: 1, y: 0.5)
layerGradient.frame = CGRect(x: 0, y: 0, width: yourView.bounds.width, height: yourView.bounds.height)
self.yourView.layer.addSublayer(layerGradient)
Essentially you add a layer on top of your view which you then assign a start point and end point and provide colors to use.
This is the function used to convert hex strings to colors
func hexStringToUIColor (hex:String) -> UIColor {
var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if (cString.hasPrefix("#")) {
cString.remove(at: cString.startIndex)
}
if ((cString.count) != 6) {
return UIColor.gray
}
var rgbValue:UInt64 = 0
Scanner(string: cString).scanHexInt64(&rgbValue)
return UIColor(
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
alpha: CGFloat(1.0)
)
}

Related

How to generate color palette that's gradually changing color

I am making a game similar to the stack game on iOS (https://apps.apple.com/us/app/stack/id1080487957).
In the screenshot, each box is in a color slightly different from the box below. The background is also changing, in case of conflict of background and boxes.
I am wondering how to generate this sequence of color palette for the boxes, and a corresponding background color that helps to make the box stand out.
What I have tried is very naive:
UIColor(
red: ((0 + boxCount) % 255)/255,
green: ((100 + boxCount) % 255)/255,
blue: ((200 + boxCount) % 255)/255)
)
This doesn't look good, and I think the reasons are:
unable to cover lots of good colors (e.g. having the same red & green, but different blue value)
I don't know how to get the background color based on the box color under this naive algorithm.
But what's good about this is that it avoids grayscale color such as completely black, so that the boxes are always colorful.
Is there any better way for this problem?
EDIT:
To clarify, the color is not just lighter or darker. It can actually change color (e.g. the first screenshot changes from sky blue to greenish-yellow).
A very long time ago, in a different programming language, I spent way too much time trying to come up with a blending algorithm which "worked" the way I expected it to.
Sooo, I spent a lot of time fiddling with it, until I came up with an approach which worked for me. So I've dragged the concept over to UIKit, for example.
Now, this is a little basic. I'd considered writing an extension to make it easier to get the RGB colors, in a more "structured" way, as well as adding the blending algorithm as an extensions to UIColor, but you get the basic idea.
I've also included a basic darken and lighten algorithm, based on the same concept as the blend. In fact, if you have the color and know the number of samples (ie total number of bricks), you could easily just darken or lighten the base color by that factor, but where's the fun in that :P
Put the code in a playground and see what you get
import UIKit
public extension UIColor {
func darken(by: Double) -> UIColor {
let rgb = cgColor.components
let red = max(0, (rgb?[0])! - CGFloat(1.0 * by))
let green = max(0, (rgb?[1])! - CGFloat(1.0 * by))
let blue = max(0, (rgb?[2])! - CGFloat(1.0 * by))
let alpha = cgColor.alpha
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
func brighten(by: Double) -> UIColor {
let rgb = cgColor.components
let red = min(1.0, (rgb?[0])! + CGFloat(1.0 * by))
let green = min(1.0, (rgb?[1])! + CGFloat(1.0 * by))
let blue = min(1.0, (rgb?[2])! + CGFloat(1.0 * by))
let alpha = cgColor.alpha
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
}
func blend(from lhs: UIColor, to rhs: UIColor, by weight: Double) -> UIColor {
var lhsRed: CGFloat = 0
var lhsGreen: CGFloat = 0
var lhsBlue: CGFloat = 0
var lhsAlpha: CGFloat = 0
lhs.getRed(&lhsRed, green: &lhsGreen, blue: &lhsBlue, alpha: &lhsAlpha)
var rhsRed: CGFloat = 0
var rhsGreen: CGFloat = 0
var rhsBlue: CGFloat = 0
var rhsAlpha: CGFloat = 0
rhs.getRed(&rhsRed, green: &rhsGreen, blue: &rhsBlue, alpha: &rhsAlpha)
let inverseWeight = 1.0 - weight
let red = min(1.0, max(0.0, lhsRed * weight + rhsRed * inverseWeight))
let green = min(1.0, max(0.0, lhsGreen * weight + rhsGreen * inverseWeight))
let blue = min(1.0, max(0.0, lhsBlue * weight + rhsBlue * inverseWeight))
let alpha = min(1.0, max(0.0, lhsAlpha * weight + rhsAlpha * inverseWeight))
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
let from = UIColor.red.darken(by: 0.5)
let to = UIColor.red.brighten(by: 0.5)
blend(from: from, to: to, by: 0.1)
blend(from: from, to: to, by: 0.2)
blend(from: from, to: to, by: 0.3)
blend(from: from, to: to, by: 0.4)
blend(from: from, to: to, by: 0.5)
blend(from: from, to: to, by: 0.6)
blend(from: from, to: to, by: 0.7)
blend(from: from, to: to, by: 0.8)
blend(from: from, to: to, by: 0.9)
For example, in the first screenshot in the link, it changes from sky blue on the bottom to greenish-yellow on the top)
Sure, so change the from color to .blue and the end color to .yellow or .green and see what you get
I'd also suggesting looking a things like SceneKit gradients as this might change your approach to the problem
Any idea how to make it an infinite sequence? (given input of boxCount which is number of boxes already placed in the tower). I am thinking about having an "anchorColors" array, then gradually blending from ith anchorColor to i+1th anchorColor using your blend function. What do you think?
Oh, the boxCount will be incrementing as we keep placing the box on top of the tower. And the color of a given box will never change.
Much of this will come down to your own choices, designs and long term intentions.
If you have a unknown number of elements, you could start with a series of "base" colors. Then you could allocate n number of deviations between them, giving your a different gradient over a very wide range of colors.
For example, if you had 64 base colors, with 128 segments between each base colour you get something like 8192 colors ... So each time you use up a series of segments, you'd move onto the next base color pair (the old end color would become the new start color and the next color would become the end color)

Crop Part of CALayer

.
I want to crop out the Red triangle part from the CAshapelayer.
This is the code i used for drawing it:
Radial Gradient background in Swift and my code is:
let bgLayer = RadialGradientLayer(center: speed_dial_center_point, radius: speed_dial_size.width/2, colors: [UIColor.clear.cgColor,UIColor(red: 211/255, green: 211/255, blue: 211/255, alpha: 0.5).cgColor],speed_scale_margin: speed_scale_margin)//CALayer()
bgLayer.frame = CGRect(origin: rect.origin, size: speed_dial_size)
bgLayer.contentsScale = self.layer.contentsScale
bgLayer.setNeedsDisplay()
backgondArc_Layer.addSublayer(bgLayer)
self.layer.insertSublayer(backgondArc_Layer, below: speed_dial_layer)
So add a mask layer to your gradient layer that masks out the part you want to remove.

Stroke a CGPath with a gradient along its length [duplicate]

I have a relatively straight forward implementation of a progress view set up with CALayer objects. The progress view itself is a subview of UIView.
Here is the code that sets up the progress ring:
self.progressRingLayer = CAShapeLayer()
let innerRect = CGRectInset(bounds, CGFloat(self.lineWidth) / 2, CGFloat(self.lineWidth) / 2)
let innerPath = UIBezierPath(ovalInRect: innerRect)
self.progressRingLayer.path = innerPath.CGPath
self.progressRingLayer.fillColor = UIColor.clearColor().CGColor
self.progressRingLayer.strokeColor = kProgressColor.CGColor
self.progressRingLayer.anchorPoint = CGPointMake(0.5, 0.5)
self.progressRingLayer.transform = CATransform3DRotate(self.progressRingLayer.transform, (CGFloat(M_PI))*1, 0, 0, 1)
self.progressRingLayer.lineCap = kCALineCapRound
self.progressRingLayer.lineWidth = CGFloat(self.lineWidth)
self.layer.addSublayer(self.progressRingLayer)
What I am trying to do now is add a gradient to the progressRingLayer that follows (or bends with) the path. I have been successful in adding a linear gradient to the fill, but not to just the path.
Here is an example of what effect I want:
So far everything I have found requires a bunch of additional steps with CoreGraphics and CGContext that don't quite fit with my implementation. Any help would be great, thanks!
What I would do is draw a gradient layer, then draw on top of that a layer that is black with the arc erased.
Here's my attempt at roughly the image you provided (I omitted the white label in the center, but that's trivial):
And here's the code that generated it:
let r = CGRectMake(100,100,130,100)
let g = CAGradientLayer()
g.frame = r
let c1 = UIColor(
red: 151.0/255.0, green: 81.0/255.0, blue: 227.0/255.0, alpha: 1)
let c2 = UIColor(
red: 36.0/255.0, green: 176.0/255.0, blue: 233.0/255.0, alpha: 1)
g.colors = [c1.CGColor as AnyObject, c2.CGColor as AnyObject];
self.view.layer.addSublayer(g)
let percent = CGFloat(0.64) // percentage of circle
UIGraphicsBeginImageContextWithOptions(r.size, false, 0)
let con = UIGraphicsGetCurrentContext()
CGContextFillRect(con, CGRect(origin: CGPoint(), size: r.size))
CGContextSetLineWidth(con, 5)
CGContextSetLineCap(con, kCGLineCapRound)
CGContextSetBlendMode(con, kCGBlendModeClear)
let pi = CGFloat(M_PI)
CGContextAddArc(con, r.size.width/2.0, r.size.height/2.0, 30,
-pi/2.0, -pi/2.0 + percent*pi*2.0, 0)
CGContextStrokePath(con)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let b = CALayer()
b.frame = r
b.contents = im.CGImage
self.view.layer.addSublayer(b)
The gradient layer (the first part of the code) is just a "serving suggestion". If that is not the gradient you want, you can design your own. You could draw it in Photoshop and use an image as the content of the gradient layer. Or you could make an "angular" layer in code, using third-party code such as https://github.com/paiv/AngleGradientLayer. The point of the example is merely to show how it is possible to "erase" an arc in a black layer so as to reveal the gradient concealed behind it, and thus appear to paint with a gradient.

iOS: How to ensure cell maintains its colour when reuse cells are on

I'm building an app where my tableview cells transition in colour from Red to Dark blue (Hot to Cold), this dictates an order of priority for tasks to be completed in.
In my cellForRowAtIndexPath method, I have written some logic to ensure that the color of each cell is equal depending on the size of a task collection. The problem I am facing is that my global variables which track the current index mutate each time a new cell is generated, meaning my cells all become one block color at some given point (as the user scrolls).
Here is my current global code:
// Difference is calculated by dividing the bgColors size by the task collection size
var currDiff = 0
var currIndex = 0
var tasks = [PFObject]()
var bgColors = [
UIColor(red: 255, green: 51, blue: 51),
UIColor(red: 255, green: 153, blue: 51),
UIColor(red: 255, green: 255, blue: 51),
UIColor(red: 153, green: 255, blue: 51),
UIColor(red: 102, green: 255, blue: 51),
UIColor(red: 51, green: 255, blue: 204),
UIColor(red: 51, green: 204, blue: 255),
UIColor(red: 51, green: 102, blue: 255),
]
Then here is the portion from cellForRowAtIndexPath which sets the colours based on the collection size:
// Compute the cell colour
let totalCells = tasks.count
let totalColors = bgColors.count
// If we exceed the number of colours, then evenly distribute them
if(totalCells > totalColors) {
let diff = Int(ceil(CGFloat(totalCells) / CGFloat(totalColors)))
if(currDiff > diff) {
currDiff = 0
currIndex++
}
if currIndex < 0 {
currIndex = 0
}
if(currIndex >= bgColors.count) {
currIndex = 0
}
cell.backgroundColor = bgColors[currIndex]
currDiff++
} else {
cell.backgroundColor = bgColors[indexPath.row]
}
In the scenario that I had 28 tasks in a collection and 8 colors, this code would compute 28 / 8 = 3.5 and then round it to 4, this would then render blocks of 4 by using currDiff to track the amount of cells rendered in that specific colour, and currIndex to track which index in the array we are.
I hope this makes sense, if any further details are needed please ask.
Since you want the colors evenly distributed through the rows of the table, you can do some simple math on the cell's indexPath.row to calculate the needed color index.
I'm not proficient in Swift so the following syntax could be a bit off but it should be close:
var colorIndex = Int((CGFloat(indexPath.row) / CGFloat(tasks.count)) * CGFloat(bgColors.count));
The first part gives you the percentage through the table for the given row and then that percentage is applied to the color count.
Keep in mind that if the number of rows isn't a whole multiple of the color count, then obviously not every color will appear the same number of times.

Swift: Gradient along a bezier path (using CALayers)

I have a relatively straight forward implementation of a progress view set up with CALayer objects. The progress view itself is a subview of UIView.
Here is the code that sets up the progress ring:
self.progressRingLayer = CAShapeLayer()
let innerRect = CGRectInset(bounds, CGFloat(self.lineWidth) / 2, CGFloat(self.lineWidth) / 2)
let innerPath = UIBezierPath(ovalInRect: innerRect)
self.progressRingLayer.path = innerPath.CGPath
self.progressRingLayer.fillColor = UIColor.clearColor().CGColor
self.progressRingLayer.strokeColor = kProgressColor.CGColor
self.progressRingLayer.anchorPoint = CGPointMake(0.5, 0.5)
self.progressRingLayer.transform = CATransform3DRotate(self.progressRingLayer.transform, (CGFloat(M_PI))*1, 0, 0, 1)
self.progressRingLayer.lineCap = kCALineCapRound
self.progressRingLayer.lineWidth = CGFloat(self.lineWidth)
self.layer.addSublayer(self.progressRingLayer)
What I am trying to do now is add a gradient to the progressRingLayer that follows (or bends with) the path. I have been successful in adding a linear gradient to the fill, but not to just the path.
Here is an example of what effect I want:
So far everything I have found requires a bunch of additional steps with CoreGraphics and CGContext that don't quite fit with my implementation. Any help would be great, thanks!
What I would do is draw a gradient layer, then draw on top of that a layer that is black with the arc erased.
Here's my attempt at roughly the image you provided (I omitted the white label in the center, but that's trivial):
And here's the code that generated it:
let r = CGRectMake(100,100,130,100)
let g = CAGradientLayer()
g.frame = r
let c1 = UIColor(
red: 151.0/255.0, green: 81.0/255.0, blue: 227.0/255.0, alpha: 1)
let c2 = UIColor(
red: 36.0/255.0, green: 176.0/255.0, blue: 233.0/255.0, alpha: 1)
g.colors = [c1.CGColor as AnyObject, c2.CGColor as AnyObject];
self.view.layer.addSublayer(g)
let percent = CGFloat(0.64) // percentage of circle
UIGraphicsBeginImageContextWithOptions(r.size, false, 0)
let con = UIGraphicsGetCurrentContext()
CGContextFillRect(con, CGRect(origin: CGPoint(), size: r.size))
CGContextSetLineWidth(con, 5)
CGContextSetLineCap(con, kCGLineCapRound)
CGContextSetBlendMode(con, kCGBlendModeClear)
let pi = CGFloat(M_PI)
CGContextAddArc(con, r.size.width/2.0, r.size.height/2.0, 30,
-pi/2.0, -pi/2.0 + percent*pi*2.0, 0)
CGContextStrokePath(con)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let b = CALayer()
b.frame = r
b.contents = im.CGImage
self.view.layer.addSublayer(b)
The gradient layer (the first part of the code) is just a "serving suggestion". If that is not the gradient you want, you can design your own. You could draw it in Photoshop and use an image as the content of the gradient layer. Or you could make an "angular" layer in code, using third-party code such as https://github.com/paiv/AngleGradientLayer. The point of the example is merely to show how it is possible to "erase" an arc in a black layer so as to reveal the gradient concealed behind it, and thus appear to paint with a gradient.

Resources