I'm using XCode's playground feature to develop a custom control. However, I'm having issues using sub-routines (methods) to break up the drawRect implementation.
Here's the code:
import UIKit
class CustomView : UIView
{
override func drawRect(rect: CGRect) {
var color = UIColor(red: 0.571, green: 0.857, blue: 1, alpha: 1);
var ovalPath = UIBezierPath(ovalInRect: rect);
color.setFill();
ovalPath.fill();
// if I uncomment this, the code runs in an infinite loop...
// drawCircle(rect);
}
func drawCircle(rect: CGRect) {
var color = UIColor(red: 0.571, green: 0.857, blue: 1, alpha: 1);
var ovalPath = UIBezierPath(ovalInRect: rect);
color.setFill();
ovalPath.fill();
}
}
var cv = CustomView(frame: CGRect(x: 0, y: 0, width: 400, height: 400));
If I uncomment the line noted above that calls the drawCircle function - the code hangs in an infinite loop.
Why is this?
Related
My goal is to make radial gradient extension for UIView. Here is my code:
extension UIView {
func drawRadialGradient() {
let colors = Colors.gradientColors as CFArray
let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
guard let gradientValue = gradient else{ return }
let endRadius: CGFloat? = max(frame.width, frame.height) / 2
guard let endRadiusValue = endRadius else{ return }
let bottomcenterCoordinates = CGPoint(x: frame.width / 2, y: frame.height)
let getCurrentContext = UIGraphicsGetCurrentContext()
guard let currentContext = getCurrentContext else{ return }
currentContext.drawRadialGradient(gradientValue, startCenter: bottomcenterCoordinates, startRadius: 0.0, endCenter: bottomcenterCoordinates, endRadius: endRadiusValue, options: CGGradientDrawingOptions.drawsAfterEndLocation)
let radialGradientLayer = CALayer(layer: currentContext)
radialGradientLayer.frame = bounds
radialGradientLayer.masksToBounds = true
self.layer.insertSublayer(radialGradientLayer, at: 1)
}
}
When I call this function in viewDidLoad() or viewWillAppear() the compiler contains no mistakes and no warnings, the function just does not work out. i call it as following:
override func viewDidLoad() {
super.viewDidLoad()
view.drawRadialGradient()
}
For example, I have created an extension function for drawing a Linear Gradient on the UIView and it works, I call it the same way as radial gradient function:
func drawLinearGradient() {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.frame
gradientLayer.colors = Colors.gradientColors
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.95)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.05)
self.layer.insertSublayer(gradientLayer, at: 0)
}
For colors I have created a structure:
struct Colors {
static let firstColor = colorPicker(red: 70, green: 183, blue: 0)
static let secondColor = colorPicker(red: 0, green: 170, blue: 116)
static let thirdColor = colorPicker(red: 20, green: 0, blue: 204)
static let gradientColors = [firstColor.cgColor, secondColor.cgColor, thirdColor.cgColor]
static func colorPicker(red: CGFloat, green: CGFloat, blue: CGFloat) -> UIColor {
let color = UIColor(red: red / 255, green: green / 255, blue: blue / 255, alpha: 1.0)
return color
}
}
Please, give me a piece of advice on how to realize it as an extension.
One main thing I can see in your code, is that you try to do the drawing in viewDidLoad. Don't do that, on top of other problems, the frame size is not properly set yet at that moment. If you want the UIView to do the drawing, then derive a class from UIView, and do the drawing in the draw method of that class.
If you want the radialGradientLayer CALayer that you created to do the drawing (it currently is just empty), then derive a subclass from CALayer, and implement its drawInContext method.
I want to put a gradient background in all my views.
The way that I thought to do this without the need to create a IBOutlet of a view in all view controllers is to create a new class from UIView, and inside the draw() I put the code that makes a CAGradientLayer in the view.
So, in the interface builder I just need to select the background view and set its class as my custom class. It works so far.
My question is if I can do that without problems. Somebody knows is it ok?
Because the model file that inherit from UIView come with the comment:
//Only override draw() if you perform custom drawing.
And I don't know if create a layer counts. Or if draw() is only to low level drawing or something like that. Do not know nothing about the best usage of the draw() func.
This is the code:
override func draw(_ rect: CGRect) {
let layer = CAGradientLayer()
layer.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
let color1 = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
let color2 = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)
layer.colors = [color1.cgColor, color2.cgColor]
self.layer.addSublayer(layer)
}
You can use #IBDesignable and #IBInspectable to configure the startColor and endColor properties from the Storyboard. Use CGGradient which specifies colors and locations instead of CAGradientLayer if you want to draw in draw(_ rect:) method. The Simple Gradient class and draw(_ rect: CGRect) function would look like this
import UIKit
#IBDesignable
class GradientView: UIView {
#IBInspectable var startColor: UIColor = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
#IBInspectable var endColor: UIColor = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
let colors = [startColor.cgColor, endColor.cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace,
colors: colors as CFArray,
locations: colorLocations)!
let startPoint = CGPoint.zero
let endPoint = CGPoint(x: 0, y: bounds.height)
context.drawLinearGradient(gradient,
start: startPoint,
end: endPoint,
options: [CGGradientDrawingOptions(rawValue: 0)])
}
}
you can read more about it here and tutorial
You shouldn't be adding a CAGradientLayer inside the draw function.
If you want to add a gradient to a view then the easiest way is to do it with a layer (like you are doing) but not like this...
You should add it to your view as part of the init method and then update its frame in the layout subviews method.
Something like this...
class MyView: UIView {
let gradientLayer: CAGradientLayer = {
let l = CAGradientLayer()
let color1 = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
let color2 = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)
l.colors = [color1.cgColor, color2.cgColor]
return l
}()
override init(frame: CGRect) {
super.init(frame: frame)
layer.addSublayer(gradientLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
}
I would create UIView extension and apply it when needed.
extension UIView {
func withGradienBackground(color1: UIColor, color2: UIColor, color3: UIColor, color4: UIColor) {
let layerGradient = CAGradientLayer()
layerGradient.colors = [color1.cgColor, color2.cgColor, color3.cgColor, color4.cgColor]
layerGradient.frame = bounds
layerGradient.startPoint = CGPoint(x: 1.5, y: 1.5)
layerGradient.endPoint = CGPoint(x: 0.0, y: 0.0)
layerGradient.locations = [0.0, 0.3, 0.4, 1.0]
layer.insertSublayer(layerGradient, at: 0)
}
}
Here you can change func declaration to specify startPoint, endPointand locations params as variables.
After that just call this method like
gradienView.withGradienBackground(color1: UIColor.green, color2: UIColor.red, color3: UIColor.blue, color4: UIColor.white)
P.S.
Please note that you can have as much colors as you want in colors array
I would like to create a gradient layer with four given colours. I have a sketch file for the gradients. I have the coordinates for the colour stops. I calculated the normalised coordinates for the start and the end point for the CAGradientLayer. I also calculated the location where the colours change each other. Still the layer does not match with the sketch image. Can you help me what could be wrong? Usage of the exported image is not an option because the layer has to change with animation to other gradient locations.
let imageWidth: CGFloat = 375.0
let imageHeihgt: CGFloat = 293.0
class ViewController: UIViewController {
let homeColors: [UIColor] = [UIColor(red: 62/255, green: 131/255, blue: 255/255, alpha: 1.0),
UIColor(red: 99/255, green: 22/255, blue: 203/255, alpha: 1.0),
UIColor(red: 122/255, green: 5/255, blue: 239/255, alpha: 1.0),
UIColor(red: 122/255, green: 5/255, blue: 240/255, alpha: 1.0)]
let startPoint: CGPoint = CGPoint(x: -0.225/imageWidth*UIScreen.main.bounds.width, y: -0.582*imageHeihgt/UIScreen.main.bounds.height)
let endPoint: CGPoint = CGPoint(x: 1.088/imageWidth*UIScreen.main.bounds.width, y: 1.01*imageHeihgt/UIScreen.main.bounds.height)
let locations: [NSNumber] = [0.0, 0.734, 0.962, 1.0]
var subview: UIView!
override func viewDidLoad() {
super.viewDidLoad()
subview = UIView(frame: CGRect(x: 0, y: 20, width: imageWidth, height: imageHeihgt))
view.addSubview(subview)
view.sendSubview(toBack: subview)
addGradientLayer()
}
func addGradientLayer() {
let gradient = CAGradientLayer()
gradient.frame = subview.bounds
gradient.colors = homeColors.map { $0.cgColor }
gradient.startPoint = startPoint
gradient.endPoint = endPoint
gradient.locations = locations
subview.layer.insertSublayer(gradient, at: 0)
}
}
I have attached some screenshots to make the difference clear. It is not a big difference but still.
Sketch renders graphics differently than iOS. Even if you match the colors and locations perfectly, the end result will still look different.
If you want an exact match to the Sketch file, you'll need to make some optical adjustments to the colors to account for the differences. Your two images are very close, so with some minor color adjustments, it should look a lot closer.
Try making your light blue (top left) and light purple (bottom right) less vibrant. I don't have access to your Sketch file, so I can't suggest new colors to get an exact match, but it shouldn't be too hard, just a little trial and error.
I wrote a script convert Sketch gradient layer to CALayer. Insert this layer by view.layer.insertSublayer(gradientLayer, at: 0).
Link: Convert Sketch Gradient To Swift Code
Select a layer which contains one gradient fill, then press control + shift + K, paste code below and run. you will see:
And on iPhone it looks like:
console.log('Select Layer Then Run Script.');
var sketch = require('sketch');
var UI = require('sketch/ui');
var document = sketch.getSelectedDocument();
var selectedLayers = document.selectedLayers;
var selectedCount = selectedLayers.length;
function hexToRGBA(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
a: parseInt(result[4], 16)
} : null;
}
let precesion = 10000
function parseGradient(layerGradient) {
let colorItems = ""
let locationArray = ""
layerGradient.stops.forEach(elm => {
let rgbaColor = hexToRGBA(elm.color)
colorItems += `UIColor(red: ${rgbaColor.r}/255.0, green: ${rgbaColor.g}/255.0, blue: ${rgbaColor.b}/255.0, alpha: ${rgbaColor.a}/255.0), `
locationArray += `${Math.round(elm.position*precesion)/precesion}, `
});
var codeTemplate = `
import Foundation
import UIKit
class LinearGradientLayer: CALayer {
required override init() {
super.init()
needsDisplayOnBoundsChange = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
required override init(layer: Any) {
super.init(layer: layer)
}
let colors = [${colorItems}].map(\{ \$0.cgColor \})
override func draw(in ctx: CGContext) {
ctx.saveGState()
let colorSpace = CGColorSpaceCreateDeviceRGB()
let locations: [CGFloat] = [${locationArray}]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: locations)!
ctx.drawLinearGradient(gradient, start: CGPoint(x: ${Math.round(layerGradient.from.x*precesion)/precesion}*bounds.width, y: ${Math.round(layerGradient.from.y*precesion)/precesion}*bounds.height), end: CGPoint(x: ${Math.round(layerGradient.to.x*precesion)/precesion}*bounds.width, y: ${Math.round(layerGradient.to.y*precesion)/precesion}*bounds.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
}
}
`;
return codeTemplate
}
if (selectedCount === 0) {
UI.alert('No layers are selected', 'Select one gradient filled layer and rerun.')
} else {
selectedLayers.forEach(function (layer, i) {
console.log((i + 1) + '. ' + layer.name);
let layerGradient = layer.style.fills[0].gradient;
console.log(layerGradient)
UI.getInputFromUser(
"Convert Result",
{
initialValue: parseGradient(layerGradient),
numberOfLines: 12
},
(err, value) => {
if (err) {
// most likely the user canceled the input
return;
}
}
);
});
}
This question already has answers here:
No '|' candidates produce the expected contextual result type 'NSTextStorageEditActions'
(2 answers)
Closed 6 years ago.
Going back through an old project I was going to add some features and ran into this error. Does anyone know how to eliminate this? It happened for the code below:
UInt32(CGGradientDrawingOptions.DrawsBeforeStartLocation) | UInt32(CGGradientDrawingOptions.DrawsAfterEndLocation))
Below is the full file:
import UIKit
class BackgroundView: UIView {
override func drawRect(rect: CGRect) {
// Background View
//// Color Declarations
var lightPurple: UIColor = UIColor(red: 0.377, green: 0.075, blue: 0.778, alpha: 1.000)
var darkPurple: UIColor = UIColor(red: 0.060, green: 0.036, blue: 0.202, alpha: 1.000)
let context = UIGraphicsGetCurrentContext()
//// Gradient Declarations
let purpleGradient = CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), [lightPurple.CGColor, darkPurple.CGColor], [0, 1])
//// Background Drawing
let backgroundPath = UIBezierPath(rect: CGRectMake(0, 0, self.frame.width, self.frame.height))
CGContextSaveGState(context)
backgroundPath.addClip()
CGContextDrawLinearGradient(context, purpleGradient,
CGPointMake(160, 0),
CGPointMake(160, 568),
UInt32(CGGradientDrawingOptions.DrawsBeforeStartLocation) | UInt32(CGGradientDrawingOptions.DrawsAfterEndLocation))
CGContextRestoreGState(context)
}
}
CGGradientDrawingOptions since Swift 2.0 implements OptionSetType. This means that | operator is no longer necessary and you can write it like this:
let options: CGGradientDrawingOptions = [.DrawsBeforeStartLocation, .DrawsAfterEndLocation]
To get more info you can go here
Try to group them in Array instead.
I'm writing an app that generates random gradients and displays them on the screen using a custom UIView Class. Because I have more planned functions for the app, I chose to give the gradient its own class. The code for generating the gradient is here:
import UIKit
class Gradient {
var topColor = UIColor()
var bottomColor = UIColor()
var gradient: CGGradientRef?
func generateGradient() -> CGGradientRef {
var red = CGFloat(arc4random_uniform(256))
var green = CGFloat(arc4random_uniform(256))
var blue = CGFloat(arc4random_uniform(256))
topColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0)
red = CGFloat(arc4random_uniform(256))
green = CGFloat(arc4random_uniform(256))
blue = CGFloat(arc4random_uniform(256))
bottomColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0)
let gradientColors = [topColor.CGColor, bottomColor.CGColor]
let gradientStops:[CGFloat] = [0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let generatedGradient = CGGradientCreateWithColors(colorSpace, gradientColors, gradientStops)
return generatedGradient
}
init() {
gradient = self.generateGradient()
}
}
The gradient viewer class that displays the gradient reads:
import UIKit
#IBDesignable class Gradient_Viewer: UIView {
override func drawRect(rect: CGRect) {
let background = Gradient()
let context = UIGraphicsGetCurrentContext()
let startPoint = CGPoint.zeroPoint
let endPoint = CGPoint(x: 0, y: self.bounds.height)
CGContextDrawLinearGradient(context, background.gradient, startPoint, endPoint, 0)
}
}
Is there something wrong with the gradient generation, or am I not accessing the gradient properly?
#IBDesignable class Gradient_Viewer: UIView {
var random8bit: CGFloat {
return CGFloat(arc4random_uniform(256))/255.0
}
var gradient: CGGradientRef {
return CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), [UIColor(red: random8bit, green: random8bit, blue: random8bit, alpha: 1.0).CGColor, UIColor(red: random8bit, green: random8bit, blue: random8bit, alpha: 1.0).CGColor], [0, 1.0])
}
override func drawRect(rect: CGRect) {
CGContextDrawLinearGradient(UIGraphicsGetCurrentContext(), gradient, CGPoint.zeroPoint, CGPoint(x: 0, y: self.bounds.height), 0)
}
}