How to Draw a Uilabel with a Different Blend Mode in Draw(_ Rect: Cgrect) in Swift

How to draw a UILabel with a different blend mode in draw(_ rect: CGRect) in Swift

You can't blend views this way; they have separate layer hierarchies that are resolved independently. Move the gradient and text on CALayer objects and blend those (or draw them by hand inside the same view). For example:

class MyView: UIView {
let gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.red.cgColor,
UIColor.blue.cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
return gradientLayer
}()

let textLayer: CATextLayer = {
let textLayer = CATextLayer()
let astring = NSAttributedString(string: "Text Example",
attributes: [.font: UIFont(name: "MarkerFelt-Wide", size: 60)!])
textLayer.string = astring
textLayer.alignmentMode = kCAAlignmentCenter
textLayer.bounds = astring.boundingRect(with: CGSize(width: .max, height: .max),
options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
return textLayer
}()

override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
gradientLayer.bounds = bounds
gradientLayer.render(in: context)

context.saveGState()
context.setBlendMode(.softLight)

context.translateBy(x: center.x - textLayer.bounds.width / 2,
y: center.y - textLayer.bounds.height / 2)

textLayer.position = center
textLayer.render(in: context)
context.restoreGState()
}
}
}

Sample Image

Can't get good performance with Draw(_ rect: CGRect)

The major observation is that you should not be adding subviews in draw. That is intended for rendering a single frame and you shouldn't be adding/removing things from the view hierarchy inside draw. Because you're adding subviews roughly ever 0.035 seconds, your view hierarchy is going to explode, with adverse memory and performance impact.

You should either have a draw method that merely calls stroke on your updated UIBezierPath for the seconds hand. Or, alternatively, if using CAShapeLayer, just update its path (and no draw method is needed at all).

So, a couple of peripheral observations:

  1. You are incrementing your "percentage" for every tick of your timer. But you are not guaranteed the frequency with which your timer will be called. So, instead of incrementing some "percentage", you should save the start time when you start the timer, and then upon every tick of the timer, you should recalculate the amount of time elapsed between then and now when figuring out how much time has elapsed.

  2. You are using a Timer which is good for most purposes, but for the sake of optimal drawing, you really should use a CADisplayLink, which is optimally timed to allow you to do whatever you need before the next refresh of the screen.

So, here is an example of a simple clock face with a sweeping second hand:

open class StopWatchView: UIView {

let faceLineWidth: CGFloat = 5 // LineWidth of the face
private var startTime: Date? { didSet { self.updateHand() } } // When was the stopwatch resumed
private var oldElapsed: TimeInterval? { didSet { self.updateHand() } } // How much time when stopwatch paused

public var elapsed: TimeInterval { // How much total time on stopwatch
guard let startTime = startTime else { return oldElapsed ?? 0 }

return Date().timeIntervalSince(startTime) + (oldElapsed ?? 0)
}

private weak var displayLink: CADisplayLink? // Display link animating second hand
public var isRunning: Bool { return displayLink != nil } // Is the timer running?

private var clockCenter: CGPoint { // Center of the clock face
return CGPoint(x: bounds.midX, y: bounds.midY)
}

private var radius: CGFloat { // Radius of the clock face
return (min(bounds.width, bounds.height) - faceLineWidth) / 2
}

private lazy var face: CAShapeLayer = { // Shape layer for clock face
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = self.faceLineWidth
shapeLayer.strokeColor = #colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1).cgColor
shapeLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor
return shapeLayer
}()

private lazy var hand: CAShapeLayer = { // Shape layer for second hand
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 3
shapeLayer.strokeColor = #colorLiteral(red: 0.2196078449, green: 0.007843137719, blue: 0.8549019694, alpha: 1).cgColor
shapeLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor
return shapeLayer
}()

override public init(frame: CGRect = .zero) {
super.init(frame: frame)

configure()
}

required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)

configure()
}

// add necessary shape layers to layer hierarchy

private func configure() {
layer.addSublayer(face)
layer.addSublayer(hand)
}

// if laying out subviews, make sure to resize face and update hand

override open func layoutSubviews() {
super.layoutSubviews()

updateFace()
updateHand()
}

// stop display link when view disappears
//
// this prevents display link from keeping strong reference to view after view is removed

override open func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
pause()
}
}

// MARK: - DisplayLink routines

/// Start display link

open func resume() {
// cancel any existing display link, if any

pause()

// start new display link

startTime = Date()
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .commonModes)

// save reference to it

self.displayLink = displayLink
}

/// Stop display link

open func pause() {
displayLink?.invalidate()

// calculate floating point number of seconds

oldElapsed = elapsed
startTime = nil
}

open func reset() {
pause()

oldElapsed = nil
}

/// Display link handler
///
/// Will update path of second hand.

@objc func handleDisplayLink(_ displayLink: CADisplayLink) {
updateHand()
}

/// Update path of clock face

private func updateFace() {
face.path = UIBezierPath(arcCenter: clockCenter, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
}

/// Update path of second hand

private func updateHand() {
// convert seconds to an angle (in radians) and figure out end point of seconds hand on the basis of that

let angle = CGFloat(elapsed / 60 * 2 * .pi - .pi / 2)
let endPoint = CGPoint(x: clockCenter.x + cos(angle) * radius * 0.9, y: clockCenter.y + sin(angle) * radius * 0.9)

// update path of hand

let path = UIBezierPath()
path.move(to: clockCenter)
path.addLine(to: endPoint)
hand.path = path.cgPath
}
}

And

class ViewController: UIViewController {

@IBOutlet weak var stopWatchView: StopWatchView!

@IBAction func didTapStartStopButton () {
if stopWatchView.isRunning {
stopWatchView.pause()
} else {
stopWatchView.resume()
}
}

@IBAction func didTapResetButton () {
stopWatchView.reset()
}

}

Now, in the above example, I'm just updating the CAShapeLayer of the seconds hand in my CADisplayLink handler. Like I said at the start, you can alternatively have a draw method that simply strokes the paths you need for single frame of animation and then call setNeedsDisplay in your display link handler. But if you do that, don't change the view hierarchy within draw, but rather do any configuration you need in init and draw should just stroke whatever the path should be at that moment.

drawRect drawing 'transparent' text?

I've rewritten it as a UILabel subclass using barely any code and posted it on GitHub

The gist of it is you override drawRect but call [super drawRect:rect] to let the UILabel render as normal. Using a white label color lets you easily use the label itself as a mask.

- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();

// let the superclass draw the label normally
[super drawRect:rect];

CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, CGRectGetHeight(rect)));

// create a mask from the normally rendered text
CGImageRef image = CGBitmapContextCreateImage(context);
CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(image), CGImageGetHeight(image), CGImageGetBitsPerComponent(image), CGImageGetBitsPerPixel(image), CGImageGetBytesPerRow(image), CGImageGetDataProvider(image), CGImageGetDecode(image), CGImageGetShouldInterpolate(image));

CFRelease(image); image = NULL;

// wipe the slate clean
CGContextClearRect(context, rect);

CGContextSaveGState(context);
CGContextClipToMask(context, rect, mask);

CFRelease(mask); mask = NULL;

[self RS_drawBackgroundInRect:rect];

CGContextRestoreGState(context);

}

iOS Core Graphics How to erase a drawing

solved this problem by swapping tempImageView and drawImageView when touch began if it's erasing

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
_mouseSwiped = NO;
UITouch *touch = [touches anyObject];
_lastPoint = [touch locationInView:self.tempImageView];

if (_isErasing) {
self.tempImageView.image = self.drawImageView.image;
self.drawImageView.image = nil;
}

}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {

_mouseSwiped = YES;
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.tempImageView];

UIGraphicsBeginImageContext(self.tempImageView.frame.size);
[self.tempImageView.image drawInRect:CGRectMake(0, 0, self.tempImageView.frame.size.width, self.tempImageView.frame.size.height)];
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), _lastPoint.x, _lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), _width );

if (_isErasing) {
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeClear);
}
else {
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), _red, _green, _blue, 1.0);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(),kCGBlendModeNormal);
}

CGContextStrokePath(UIGraphicsGetCurrentContext());
self.tempImageView.image = UIGraphicsGetImageFromCurrentImageContext();
[self.tempImageView setAlpha:_alpha];
UIGraphicsEndImageContext();

_lastPoint = currentPoint;
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

CGSize size = self.tempImageView.frame.size;

if(!_mouseSwiped) {
UIGraphicsBeginImageContext(self.tempImageView.frame.size);
[self.tempImageView.image drawInRect:CGRectMake(0, 0, size.width, size.height)];
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), _width);

if (_isErasing) {
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeClear);
}
else {
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), _red, _green, _blue, _alpha);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(),kCGBlendModeNormal);
}

CGContextMoveToPoint(UIGraphicsGetCurrentContext(), _lastPoint.x, _lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), _lastPoint.x, _lastPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
CGContextFlush(UIGraphicsGetCurrentContext());
self.tempImageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}

UIGraphicsBeginImageContext(self.drawImageView.frame.size);
[self.drawImageView.image drawInRect:CGRectMake(0, 0, size.width, size.height) blendMode:kCGBlendModeNormal alpha:1.0];
[self.tempImageView.image drawInRect:CGRectMake(0, 0, size.width, size.height) blendMode:kCGBlendModeNormal alpha:_alpha];
self.drawImageView.image = UIGraphicsGetImageFromCurrentImageContext();
self.tempImageView.image = nil;
UIGraphicsEndImageContext();
}

How to achieve a thick stroked border around text UILabel

Use this designable class to render labels with the stroke on the storyboard. Most of the fonts I tried look bad (with CGLineJoin.miter), I found the "PingFang TC" font most closely resembles the desired output. Though CGLineJoin.round lineJoin looks fine on most of the font.

@IBDesignable
class StrokeLabel: UILabel {
@IBInspectable var strokeSize: CGFloat = 0
@IBInspectable var strokeColor: UIColor = .clear

override func drawText(in rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let textColor = self.textColor
context?.setLineWidth(self.strokeSize)
context?.setLineJoin(CGLineJoin.miter)
context?.setTextDrawingMode(CGTextDrawingMode.stroke)
self.textColor = self.strokeColor
super.drawText(in: rect)
context?.setTextDrawingMode(.fill)
self.textColor = textColor
super.drawText(in: rect)
}
}

Output: (Check used values in the attribute inspector for reference)

Sample Image

I want to imitate Photoshop layer 'soft light' blending in iOS

You can't apply different blending modes between CALayers (at least, on iOS), but if you're implementing -drawRect: or otherwise creating an image, you can certainly use soft light blending with the kCGBlendModeSoftLight CoreGraphics blend mode.



Related Topics



Leave a reply



Submit