I'm an Objective-C noob, have searched high and low not finding the answer to this yet:
In my RubyMotion project I have a UIView subclass called StatusGuage, which contains a method called drawLinearGradient as follows:
def drawLinearGradient(context, rect, startColor, endColor)
colorspace = CGColorSpaceCreateDeviceRGB()
locations = [0.0, 1.0]
# colors = NSArray.arrayWithObjects(startColor, endColor, nil)
# ptrColors = Pointer.new(:object, colors)
colors = [startColor, endColor, nil]
# CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef) colors, locations);
CGGradientCreateWithColors(colorspace, colors, locations)
end
I'd like to know how to call CGGradientCreateWithColors. It clearly expects a (CFArrayRef) pointer but I cannot figure out how to pass that in. One of the iterations that I've tried is commented out.
Here is the error message:
2012-05-11 16:57:36.331 HughesNetMeter[34906:17903]
*** Terminating app due to uncaught exception 'TypeError',
reason: 'status_guage.rb:43:in `drawLinearGradient:': expected
instance of Pointer, got `[0.0, 1.0]' (Array) (TypeError)
from status_guage.rb:13:in `drawRect:'
Thanks for any help.
A couple of things. The error is not talking about the colours, it is referring to the const CGFloat locations[] argument.
This should be a pointer which can be achieved like this (Reference on Pointer class)
locations = Pointer.new(:float, 2)
locations[1] = 1.0
Next up your array does not need the nil termination. In Ruby this would create an array with 3 objects, which is not what you want as it will most likely cause the CGGradientCreateWithColors() function to mess up
This looks like the example from http://www.raywenderlich.com/, so here's the rest from that
def drawLinearGradient(context, rect, startColor, endColor)
colorspace = CGColorSpaceCreateDeviceRGB()
locations = Pointer.new(:float, 2)
locations[1] = 1.0
colors = [startColor, endColor]
gradient = CGGradientCreateWithColors(colorspace, colors, locations)
startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect))
endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect))
CGContextSaveGState(context)
CGContextAddRect(context, rect)
CGContextClip(context)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0)
CGContextRestoreGState(context)
end
Final side note
The locations argument isn't even required in this case as CGGradientCreateWithColors() will automatically set the values to 0.0 and 1.0 for the first and last colour. Check the CGGradient Reference
locations
The location for each color provided in components. Each location must be a CGFloat value in the range of 0 to 1, inclusive. If 0 and 1 are not in the locations array, Quartz uses the colors provided that are closest to 0 and 1 for those locations.
If locations is NULL, the first color in colors is assigned to location 0, the last color incolors is assigned to location 1, and intervening colors are assigned locations that are at equal intervals in between.
Related
I am trying to draw a circle shaded with a gradient from white to transparent. I am using Core Graphics.
Here is what I have which draws a gradient from white to black:
let colorSpace = CGColorSpaceCreateDeviceRGB();
let colors = [UIColor.white.cgColor, UIColor.black.cgColor] as CFArray;
let locations : [CGFloat] = [0.0, 1.0];
let glowGradient : CGGradient = CGGradient.init(colorsSpace: colorSpace, colors: colors, locations: locations)!;
let ctx = UIGraphicsGetCurrentContext()!;
ctx.drawRadialGradient(glowGradient, startCenter: rectCenter, startRadius: 0, endCenter: rectCenter, endRadius: imageWidthPts/2, options: []);
However, I do not want to draw white-to-black; I want to draw white-to-transparent.
To do so, I first tried changing the end color to UIColor.white.cgColor.copy(alpha: 0.0) (i.e., transparent white). However, this failed with:
fatal error: unexpectedly found nil while unwrapping an Optional value
I assume this error is due to the color being outside the specified RGB color space (CGColorSpaceCreateDeviceRGB()).
The fix would seem to be to change the specified color space to one with an alpha component, such as RGBA. However, such color spaces do not appear to exist! There are only CGColorSpaceCreateDeviceRGB, CGColorSpaceCreateDeviceCMYK, and CGColorSpaceCreateDeviceGray.
But it makes no sense for there to be no available color spaces with an alpha component. The documentation explicitly describes support for alpha in gradients. The documentation for CGGradient.init says:
For example, if the color space is an RGBA color space and you want to use two colors in the gradient (one for a starting location and another for an ending location), then you need to provide 8 values in components—red, green, blue, and alpha values for the first color, followed by red, green, blue, and alpha values for the second color.
This RGBA encoding makes perfect sense, but it's impossible to tell Core Graphics that I'm using such an RGBA encoding, because there is no RGBA color space!
Where is the CGColorSpace for RGBA?
You don't need the RGBA color space to draw transparent radial/linear gradients. The RGB color space is enough. If it's not drawing it transparently, you probably have the background color of the view or the context misconfigured.
If you're creating a context you want to make sure that you pass in false for opaque: Iphone How to make context background transparent?
If you're using a CALayer on a UIView, you need to make sure that the UIView's background color is set to UIColor.clear. If it's set to nil, you'll end up with the gradient blending with black instead.
A slightly unsatisfactory answer: you can get the color space of a context with .colorSpace. In my case, it seems to give me an RGBA space, but I can't see any guarantee of this.
Here's the gradient using .colorSpace:
let ctx = UIGraphicsGetCurrentContext()!;
let colorSpace = ctx.colorSpace!;
let colorComponents : [CGFloat] = [
// R G B A
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 0.0,
];
let locations : [CGFloat] = [0.0, 1.0];
let glowGradient : CGGradient = CGGradient.init(
colorSpace: colorSpace,
colorComponents: colorComponents,
locations: locations,
count: locations.count
)!;
ctx.drawRadialGradient(glowGradient, startCenter: rectCenter, startRadius: 0, endCenter: rectCenter, endRadius: imageWidthPts/2, options: []);
It's particularly confusing that in my case, the colorSpace.numberOfComponents evaluates to 3, i.e. not RGBA, and yet it still correctly interprets the alpha component in the gradient. ¯\_(ツ)_/¯
I have creating a circle using CGContextFillEllipseInRect from CoreGraphic.
I'm using this circle (which is actually a disk) to replace the thumbImage of an UISlider. The antialiasing is applied by default.
But the result on my iPhone 6 is clearly not good. I can clearly see the pixels ,not as much as with the antialiasing off, but way more than the pixels of a normal UISlider.
Maybe I'm doing something wrong. So my question is, is there a way to get programmatically the same nice disk than the one used by default for an UISlider?
EDIT:
Here is how I create the disk:
class func drawDisk(rgbValue:UInt32, rectForDisk: CGRect = CGRectMake(0.0, 0.0, 1.0, 1.0)) -> UIImage {
let color = uicolorFromHex(rgbValue)
UIGraphicsBeginImageContext(rectForDisk.size)
let context = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, color.CGColor)
CGContextFillEllipseInRect(context, rectForDisk)
let rectForCircle = CGRectMake(0.5, 0.5, rectForDisk.size.width - 1, rectForDisk.size.height - 1)
CGContextSetLineWidth(context, 1.0)
CGContextSetStrokeColorWithColor(context,
UIColor.blackColor().CGColor)
CGContextAddEllipseInRect(context, rectForCircle)
CGContextStrokePath(context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
The problem is that you are creating a non-retina graphics context when using UIGraphicsBeginImageContext, as mentioned in the documentation:
This function is equivalent to calling the UIGraphicsBeginImageContextWithOptions function with the opaque parameter set to NO and a scale factor of 1.0.
Instead you should be using UIGraphicsBeginImageContextWithOptions to create your image context. You can keep passing NO for the opaque parameter if you want an image that supports transparency (same as what you are implicitly doing now).
In most cases you can pass 0.0 for the scale factor. This sets the scale factor to that of the device's main screen. Again, as mentioned in the documentation:
If you specify a value of 0.0, the scale factor is set to the scale factor of the device’s main screen.
So, in short, you should create your image context like this:
UIGraphicsBeginImageContextWithOptions(rectForDisk.size, false, 0.0) // false for Swift
I have a UIBezierPath which is an arc. I only want to draw the stroke to the graphics context (that is, the stroke of the arc, NOT including the line connecting the end and start points of the arc)
Once I’ve done that, I can then add a linear gradient to the context, effectively drawing a gradient on the stroke of an arc.
Here is my drawRect:
let context = UIGraphicsGetCurrentContext()
CGContextSaveGState(context)
CGContextAddPath(context, _progressPathForProgress(_progressToDrawForProgress(progress)).CGPath)
CGContextStrokePath(context)
CGContextClip(context)
let colours = [self.startColour.CGColor, self.endColour.CGColor]
let colourSpace = CGColorSpaceCreateDeviceRGB()
let colourLocations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradientCreateWithColors(colourSpace, colours, colourLocations)
var startPoint = CGPoint.zeroPoint
var endPoint = CGPoint(x: 0, y: bounds.height)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, CGGradientDrawingOptions.allZeros)
CGContextRestoreGState(context)
But all this does is add a gradient to the entirety of my UIView. Where have I gone wrong?
The path needs to be closed in order to get the clipping to work.
If you don't want to see the closing line you can set the stroke color to UIColor.clearColor() and then close the path.
CGContextSaveGState(context)
CGContextAddPath(context, _progressPathForProgress(_progressToDrawForProgress(progress)).CGPath)
CGContextStrokePath(context)
CGContextSetStrokeColorWithColor(context, UIColor.clearColor().CGColor)
// close the path, you will need to add some more points here, otherwise
// the end point is simply connected to the start point
CGPathCloseSubpath(...)
CGContextClip(context)
// draw the gradient
I'll explain what I mean by adding more points to close the path. The following images are a screenshot from one of my apps, it is an altitude profile with a gradient below the graph.
Closing the path directly would result in this, meaning the gradient would not be drawn down to the x-axis:
To close the path correctly I add points from the last coordinate (x,y) down to the x axis (x,0), then to (0,0) and finally close the path with the first point, like so:
I don't want to see the closing lines, so I use UIColor.clearColor() here.
Hope you get the idea.
I'm trying to customize grouped UITableViewCell's backgroundView with a gradient, based on code I find on this blog. It's a subclass of UIView for use on cell.backgroundView.
The colors of the background's gradient are defined like this on the original code :
#define TABLE_CELL_BACKGROUND { 1, 1, 1, 1, 0.866, 0.866, 0.866, 1} // #FFFFFF and #DDDDDD
And then, used like this on the drawRect of the subclassed backgroundView:
CGFloat components[8] = TABLE_CELL_BACKGROUND;
myGradient = CGGradientCreateWithColorComponents(myColorspace, components , locations, 2);
I'm trying to implement a function to set start and end color for the gradient, which takes two UIColors and then fill in a global float array float startAndEndColors[8] (in .h / #interface) for later use:
-(void)setColorsFrom:(UIColor*)start to:(UIColor*)end{
float red = 0.0, green = 0.0, blue = 0.0, alpha =0.0, red1 = 0.0, green1 = 0.0, blue1 = 0.0, alpha1 =0.0;
[start getRed:&red green:&green blue:&blue alpha:&alpha];
[end getRed:&red1 green:&green1 blue:&blue1 alpha:&alpha1];
//This line works fine, my array is successfully filled, just for test
float colorsTest[8] = {red, green, blue, alpha, red1, green1, blue1, alpha1};
//But for this one, I just have an error.
//"Expected expression"
// \
// v
startAndEndColors = {red, green, blue, alpha, red1, green1, blue1, alpha1};
}
But it throw me this error "Expected expression" at assignation.
I tried with CGFloat, desperately adding random const, but I quickly ran out of ideas.
I simply don't get it, why can't I fill my float array this way? What am I doing wrong?
Comment added as answer:
The only way of creating an array that way is dynamically in code. If you are adding to an iVar (class variable) you need to go through one by one because the memory has already been allocated at initialization. So use startAndEndColors[0] = ..., etc.
As for your follow up question: No, there is no way to assign values in that way to memory that has already been initialized in the allocation phase. If you used std::vector or other objects then it would be possible.
A way around that would be something like this in your header
CGFloat *startAndEndColors;
And then something like this in your implementation
float colorsTest[8] = {red, green, blue, alpha, red1, green1, blue1, alpha1};
startAndEndColors = colorsTest;
That way you can initialize it the way you want to, but you have no guarantee of the number of objects in your startAndEndColors object. You could later assign it to something of the wrong size and cause crashes if you try to access outside of it's bounds.
Sorry for noobish question about iPhone and Quartz programming. Just started my conversion from C++ to Objective-C :)
So, I have such a class method
+(CGGradientRef)CreateGradient:(UIColor*)startColor endColor:(UIColor*)endColor
{
CGGradientRef result;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat locations[2] = {0.0f, 1.0f};
CGFloat startRed, startGreen, startBlue, startAlpha;
CGFloat endRed, endGreen, endBlue, endAlpha;
[endColor getRed:&endRed green:&endGreen blue:&endBlue alpha:&endAlpha];
[startColor getRed:&startRed green:&startGreen blue:&startBlue alpha:&startAlpha];
CGFloat componnents[8] = {
startRed, startGreen, startBlue, startAlpha,
endRed, endGreen, endBlue, endAlpha
};
result = CGGradientCreateWithColorComponents(colorSpace, componnents, locations, 2);
CGColorSpaceRelease(colorSpace);
return result;
}
and its usage.
-(void)FillGradientRect:(CGRect)area startColor:(UIColor *)startColor endColor:(UIColor *)endColor isVertical:(BOOL)isVertical
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIGraphicsPushContext(context);
CGGradientRef gradient = [Graphics CreateGradient:startColor endColor:endColor];
CGPoint startPoint, endPoint;
if (isVertical) {
startPoint = CGPointMake(CGRectGetMinX(area), area.origin.y);
endPoint = CGPointMake(startPoint.x, area.origin.y + area.size.height);
}else{
startPoint = CGPointMake(0, area.size.height / 2.0f);
endPoint = CGPointMake(area.size.width, startPoint.y);
}
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGGradientRelease(gradient);
UIGraphicsPopContext();
}
everything works as expected. But, when I run the Analyze tool from Xcode 4, I'm getting a warning about memory leak in method CreateGradient for result variable. Well, I understand what's that about, but in my calling method I'm releasing the gradient object (CGGradientRelease(gradient);).
So, who is wrong and how to make Analyze tool happy?
Thx
Since CGGradientRef is a Core Foundation type of object, you can autorelease it. Just add this line before returning the gradient:
[(id)result autorelease];
If the goal is solely to keep the analyzer happy in ARC, then just make it a C function rather than objective-C - i.e.:
CGGradientRef CreateGradient(UIColor *startColor, UIColor * endColor)
The Core Foundation naming scheme then applies which says that a function with Create in the name is treated as returning a retained object (and it is the caller's responsibility to release it). This satisfies the analyser.
If you want an autoreleased variable, then transfer ownership of the CG type to ARC:
id arc_result = (__bridge_transfer id)result
However, if you do that, you need to return the objective-c type (arc_result), not the CG-type. If you return the CG type, there will be no retained references to arc_result, and so the compiler will clean it up as you return from the function.
You could use this hack to effect a CG-type autorelease:
dispatch_async(dispatch_get_main_queue(), ^{
CGGradientRelease(result);
});
It would satisfy the analyser and probably work - though I would consider it to be pretty unsafe!