Can I draw to the same CGContextRef from multiple threads? - ios

I'm making an app where I want to draw a lot of shapes - circles, boxes, lines, etc.
Millions of them.
To test the performance of this, I threw together this simple UIView. Note that credit is due - I got inspired by this project.
import UIKit
let qkeyString = "label" as NSString
var QKEY = qkeyString.UTF8String
let qvalString = "com.hanssjunnesson.Draw" as NSString
var QVAL = qvalString.UTF8String
public class RenderImageView: UIView {
var bitmapContext: CGContext?
let drawQueue: dispatch_queue_attr_t = {
let q = dispatch_queue_create(QVAL, nil)
dispatch_queue_set_specific(q, QKEY, &QVAL, nil)
return q
}()
public override init() {
super.init()
render()
}
required public init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
render()
}
override required public init(frame: CGRect) {
super.init(frame: frame)
render()
}
public override func drawRect(rect: CGRect) {
if let bitmapContext = self.bitmapContext {
let context = UIGraphicsGetCurrentContext()
let image = CGBitmapContextCreateImage(bitmapContext)
CGContextDrawImage(context, self.bounds, image)
}
}
private func render() {
dispatch_async(drawQueue) {
let startDate = NSDate()
let bounds = self.bounds
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()
self.bitmapContext = context
CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor)
CGContextFillRect(context, bounds)
CGContextSetFillColorWithColor(context, UIColor(red: 0.15, green: 0.4, blue: 0.8, alpha: 1.0).CGColor)
for i in 1...1000000 {
CGContextFillEllipseInRect(context, bounds)
}
UIGraphicsEndImageContext()
self.setNeedsDisplay()
let benchmark = startDate.timeIntervalSinceNow
println("Rendering took: \(-benchmark*1000) Ms")
}
}
}
This works just fine. On my iOS simulator, it takes little over a minute to draw a million circles on top of each other.
I wanted to speed this up, so I tried drawing to the bitmap context from multiple threads.
let group = dispatch_group_create()
for i in 1...100 {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
dispatch_group_enter(group)
CGContextFillEllipseInRect(context, bounds)
dispatch_group_leave(group)
}
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
This did not work, however. I get an EXC_BAD_ACCESS when calling CGContextFillEllipseInRect(context, bounds).
Drawing to a CGContext in a background thread seems fine as long as it's the same thread that created it.
Anyone know of a way of getting this to work?

1) You aren't actually waiting for the group you created to finish -- dispatch_group_wait is going to be called in that code before any of the blocks have been executed, so the enter / leave calls inside them won't have any effect. Use dispatch_group_async instead (see below).
2) You can't draw to a CGContext from two different threads at the same time -- you can see this if you add a println inside your drawing loop. It will work a few times, with varying results, but eventually you'll be end up with an error.
let group = dispatch_group_create()
for i in 1...10 {
dispatch_group_async(group, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
for j in 1...100 {
println("i:\(i), j:\(j)")
CGContextFillEllipseInRect(context, bounds)
}
}
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
Sample output:
iiii::::4123,,,, jjjj::::1111
ii:1, j:2
:5, j:1
i:6, j:1
EXC_BAD_ACCESS
The only solution to this is to jump back on a single thread for drawing, but that defeats what you were trying to do any way. If you have to do lots of calculations to decide what to draw, that could happen on separate threads, but drawing to CGContext itself isn't thread safe.

Related

Constantly animated view in Swift

I'm creating a subclass of AVPlayerController, that observes the player and handles player's states. If AVPlayerItemStatus is .failed, I add a custom UIView over the player's frame. At present, the important parts of my custom view's code looks like this:
class NoiseView: UIView {
...
var timer: Timer?
func animate() {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
}
func stopAnimating() {
timer?.invalidate()
}
#objc func timerAction() {
self.setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let context = UIGraphicsGetCurrentContext()!
let h = rect.height
let w = rect.width
var val: CGFloat!
var color: UIColor!
for i in 0...Int(w-1) {
for j in 0...Int(h-1) {
val = .random
color = UIColor(red: val, green: val, blue: val, alpha: 1.0)
context.setFillColor(color.cgColor)
context.fill(CGRect(x: i, y: j, width: 1, height: 1))
}
}
context.flush()
}
}
I'm calling the method animate() from other ViewController that keeps the NoiseView object.
The thing is, it's working as it's supposed to work (view is animating and creating a white noise) but the app starts to work slowly. What should be a proper approach to redraw the view without causing such a performance drop?
By the way, the drop happened with 0.1s interval (10 FPS). What I want to accomplish is having a constant white noise with at least 30 FPS to look like a legit tv noise.
There are a number of strategies you can try to optimize the drawing code, but to me the most obvious one is that you should pre-calculate the CGRects you need outside the drawing code.
The loops you're running require w^h iterations for each frame of animation to calculate all the CGRects you need. That's a lot, and totally unnecessary, because the CGRects will always be the same so long as the width and height are unchanged. Instead, you should do something like this:
var cachedRects = [CGRect]()
override var frame: CGRect {
didSet {
calculateRects()
}
}
func calculateRects() {
cachedRects = []
let w = frame.width
let h = frame.height
for i in 0...Int(w-1) {
for j in 0...Int(h-1) {
let rect = CGRect(x: i, y: j, width: 1, height: 1)
cachedRects += [rect]
}
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let context = UIGraphicsGetCurrentContext()!
var val: CGFloat!
var color: UIColor!
for rect in cachedRects {
val = .random
color = UIColor(red: val, green: val, blue: val, alpha: 1.0)
context.setFillColor(color.cgColor)
context.fill(rect)
}
context.flush()
}
By pre-caching the rects, you only have to do (w * h) number of iterations, which is a vast improvement.
If that isn't enough to improve performance, you can further optimize using similar pre-caching strategies, like instead of randomizing the each pixel, pre-calculate tiles of random colors outside the draw code, and then randomly assemble them in drawRect(). If the randomizer is what's the performance problem, this would cut down on the amount of work it would have to.
The key strategy is to try and minimize the amount of work your drawRect() method has to do, because it run on each frame of animation.
Good luck!
Instead of using the timer and calling setNeedsDisplay which in result will keep calling draw method, hence slowing the app.
Use CAAnimation and create CALayer or CAreplicator layer and animate that.
If you need code you can check this link Color Picker or I can post some demo code after sometime, Its fifa time :-p.
CAReplicatorlayer CALayer
Custom draw(rect:) methods can cause performance hits.
I suggest looking into adding a CAAnimation or CAAnimationGroup to your white noise view.
CABasicAnimation Documentation
CAAnimationGroup StackOverflow Post

How to fix the Gradient on text when using an image in swift, as the gradient restarts

I'm trying to create a gradient on text, I have used UIGraphics to use a gradient image to create this. The problem I'm having is that the gradient is restarting. Does anyone know how I can scale the gradient to stretch to the text?
The text is on a wireframe and will be altered a couple of times. Sometimes it will be perfect but other times it is not.
The gradient should go yellow to blue but it restarts see photo below:
import UIKit
func colourTextWithGrad(label: UILabel) {
UIGraphicsBeginImageContext(label.frame.size)
UIImage(named: "testt.png")?.drawInRect(label.bounds)
let myGradient: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
label.textColor = UIColor(patternImage: myGradient)
}
You'll have to redraw the image each time the label size changes
This is because a pattered UIColor is only ever tiled. From the documentation:
During drawing, the image in the pattern color is tiled as necessary to cover the given area.
Therefore, you'll need to change the image size yourself when the bounds of the label changes – as pattern images don't support stretching. To do this, you can subclass UILabel, and override the layoutSubviews method. Something like this should achieve the desired result:
class GradientLabel: UILabel {
let gradientImage = UIImage(named:"gradient.png")
override func layoutSubviews() {
guard let grad = gradientImage else { // skip re-drawing gradient if it doesn't exist
return
}
// redraw your gradient image
UIGraphicsBeginImageContext(frame.size)
grad.drawInRect(bounds)
let myGradient = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// update text color
textColor = UIColor(patternImage: myGradient)
}
}
Although it's worth noting that I'd always prefer to draw a gradient myself – as you can have much more flexibility (say you want to add another color later). Also the quality of your image might be degraded when you redraw it at different sizes (although due to the nature of gradients, this should be fairly minimal).
You can draw your own gradient fairly simply by overriding the drawRect of your UILabel subclass. For example:
override func drawRect(rect: CGRect) {
// begin new image context to let the superclass draw the text in (so we can use it as a mask)
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
do {
// get your image context
let ctx = UIGraphicsGetCurrentContext()
// flip context
CGContextScaleCTM(ctx, 1, -1)
CGContextTranslateCTM(ctx, 0, -bounds.size.height)
// get the superclass to draw text
super.drawRect(rect)
}
// get image and end context
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// get drawRect context
let ctx = UIGraphicsGetCurrentContext()
// clip context to image
CGContextClipToMask(ctx, bounds, img.CGImage)
// define your colors and locations
let colors = [UIColor.orangeColor().CGColor, UIColor.redColor().CGColor, UIColor.purpleColor().CGColor, UIColor.blueColor().CGColor]
let locs:[CGFloat] = [0.0, 0.3, 0.6, 1.0]
// create your gradient
let grad = CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), colors, locs)
// draw gradient
CGContextDrawLinearGradient(ctx, grad, CGPoint(x: 0, y:bounds.size.height*0.5), CGPoint(x:bounds.size.width, y:bounds.size.height*0.5), CGGradientDrawingOptions(rawValue: 0))
}
Output:
Swift 4 & as subclass
class GradientLabel: UILabel {
// MARK: - Colors to create gradient from
#IBInspectable open var gradientFrom: UIColor?
#IBInspectable open var gradientTo: UIColor?
override func draw(_ rect: CGRect) {
// begin new image context to let the superclass draw the text in (so we can use it as a mask)
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
do {
// get your image context
guard let ctx = UIGraphicsGetCurrentContext() else { super.draw(rect); return }
// flip context
ctx.scaleBy(x: 1, y: -1)
ctx.translateBy(x: 0, y: -bounds.size.height)
// get the superclass to draw text
super.draw(rect)
}
// get image and end context
guard let img = UIGraphicsGetImageFromCurrentImageContext(), img.cgImage != nil else { return }
UIGraphicsEndImageContext()
// get drawRect context
guard let ctx = UIGraphicsGetCurrentContext() else { return }
// clip context to image
ctx.clip(to: bounds, mask: img.cgImage!)
// define your colors and locations
let colors: [CGColor] = [UIColor.orange.cgColor, UIColor.red.cgColor, UIColor.purple.cgColor, UIColor.blue.cgColor]
let locs: [CGFloat] = [0.0, 0.3, 0.6, 1.0]
// create your gradient
guard let grad = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: locs) else { return }
// draw gradient
ctx.drawLinearGradient(grad, start: CGPoint(x: 0, y: bounds.size.height*0.5), end: CGPoint(x:bounds.size.width, y: bounds.size.height*0.5), options: CGGradientDrawingOptions(rawValue: 0))
}
}

What is the right way of creating circle animation?

I just saw this image and it's interesting to me, how to create such type of animation in Swift:
So, I have many gray teeth in circle and when I set the angle, for example 45degree it will fill these gray teeth into blue within 0..45 degree.
You can just explain me the right way of doing it or you can show different snippets(it would be great). And later I will search or read about it.
Thanks in advance!
If you only need the individual 'teeth' to change color, instead of using the teeth as masks for a solid fill, you can use Core Graphics instead of Core Animation (although Core Animation is generally preferred). So in order to do this, we should be doing the following:
Subclass UIView to insert our drawing code
Create an array of path objects, wrapped in UIBezierPath
Setup a timer to update a progress value and setNeedsDisplay
In drawRect:, draw the paths and assign a fill to each depending on the progress
First of all, lets define the variables we're going to be working with in this UIView subclass.
class TeethLoaderView : UIView {
let numberOfTeeth = UInt(60) // Number of teeth to render
let teethSize = CGSize(width:8, height:45) // The size of each individual tooth
let animationDuration = NSTimeInterval(5.0) // The duration of the animation
let highlightColor = UIColor(red: 29.0/255.0, green: 175.0/255.0, blue: 255.0/255.0, alpha: 1) // The color of a tooth when it's 'highlighted'
let inactiveColor = UIColor(red: 233.0/255.0, green: 235.0/255.0, blue: 236.0/255.0, alpha: 1) // The color of a tooth when it isn't 'hightlighted'
var progress = NSTimeInterval(0.0) // The progress of the loader
var paths = [UIBezierPath]() // The array containing the UIBezier paths
var displayLink = CADisplayLink() // The display link to update the progress
var teethHighlighted = UInt(0) // Number of teeth highlighted
...
Now let's add a function to create our paths.
func getPaths(size:CGSize, teethCount:UInt, teethSize:CGSize, radius:CGFloat) -> [UIBezierPath] {
let halfHeight = size.height*0.5;
let halfWidth = size.width*0.5;
let deltaAngle = CGFloat(2*M_PI)/CGFloat(teethCount); // The change in angle between paths
// Create the template path of a single shape.
let p = CGPathCreateWithRect(CGRectMake(-teethSize.width*0.5, radius, teethSize.width, teethSize.height), nil);
var pathArray = [UIBezierPath]()
for i in 0..<teethCount { // Copy, translate and rotate shapes around
let translate = CGAffineTransformMakeTranslation(halfWidth, halfHeight);
var rotate = CGAffineTransformRotate(translate, deltaAngle*CGFloat(i))
let pathCopy = CGPathCreateCopyByTransformingPath(p, &rotate)!
pathArray.append(UIBezierPath(CGPath: pathCopy)) // Populate the array
}
return pathArray
}
This is fairly simple. We just create a path for a single 'tooth' and then copy this path for how many teeth we need, translating and rotating the path for each one.
Next we want to setup our view. I'm going to a CADisplayLink for the timer so that the animation performs at the same speed on all devices.
override init(frame: CGRect) {
super.init(frame: frame)
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonSetup()
}
private func commonSetup() {
self.backgroundColor = UIColor.whiteColor()
paths = getPaths(frame.size, teethCount: numberOfTeeth, teethSize: teethSize, radius: ((frame.width*0.5)-teethSize.height))
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire));
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
Here we just set the background color, as well as setup our timer and initialise the paths we're going to be using. Next we want to setup a function to change the progress of the view when the CADisplayLink fires.
func displayLinkDidFire() {
progress += displayLink.duration/animationDuration
if (progress > 1) {
progress -= 1
}
let t = teethHighlighted
teethHighlighted = UInt(round(progress*NSTimeInterval(numberOfTeeth))) // Calculate the number of teeth to highlight
if (t != teethHighlighted) { // Only call setNeedsDisplay if the teethHighlighted changed
setNeedsDisplay()
}
}
Nothing complicated here, we just update the progress and teethHighlighted and call setNeedsDisplay() to redraw the view, if teethHighlighted changed.
Finally, we want to draw the view.
override func drawRect(rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
CGContextScaleCTM(ctx, -1, -1) // Flip the context to the correct orientation
CGContextTranslateCTM(ctx, -rect.size.width, -rect.size.height)
for (index, path) in paths.enumerate() { // Draw each 'tooth'
CGContextAddPath(ctx, path.CGPath);
let fillColor = (UInt(index) <= teethHighlighted) ? highlightColor:inactiveColor;
CGContextSetFillColorWithColor(ctx, fillColor.CGColor)
CGContextFillPath(ctx)
}
}
If you wanted to go down the Core Animation path, I adapted this code into a Core Animation layer
Final Result
Full project: https://github.com/hamishknight/Circle-Loader
Well, in the spirit of "go big or go home" (and because I'm actually having some fun doing this), I created a Core Animation version of my Core Graphics answer. It's quite a bit less code and animates smoother, so I'd actually prefer to use this.
First off, let's subclass a UIView again (this isn't strictly necessary, but it's nice to contain everything in a single view) and define our variables:
class TeethLoaderViewCA : UIView {
let numberOfTeeth = UInt(60) // Number of teetch to render
let teethSize = CGSize(width:8, height:45) // The size of each individual tooth
let animationDuration = NSTimeInterval(5.0) // The duration of the animation
let highlightColor = UIColor(red: 29.0/255.0, green: 175.0/255.0, blue: 255.0/255.0, alpha: 1) // The color of a tooth when it's 'highlighted'
let inactiveColor = UIColor(red: 233.0/255.0, green: 235.0/255.0, blue: 236.0/255.0, alpha: 1) // The color of a tooth when it isn't 'hightlighted'
let shapeLayer = CAShapeLayer() // The teeth shape layer
let drawLayer = CAShapeLayer() // The arc fill layer
let anim = CABasicAnimation(keyPath: "strokeEnd") // The stroke animation
...
This is mostly the same as the Core Graphics version, but with a couple of Core Animation objects and without the timing logic. Next, we can pretty much copy the getPaths function we created in the other version, except with a few tweaks.
func getPathMask(size:CGSize, teethCount:UInt, teethSize:CGSize, radius:CGFloat) -> CGPathRef? {
let halfHeight = size.height*0.5
let halfWidth = size.width*0.5
let deltaAngle = CGFloat(2*M_PI)/CGFloat(teethCount); // The change in angle between paths
// Create the template path of a single shape.
let p = CGPathCreateWithRect(CGRectMake(-teethSize.width*0.5, radius, teethSize.width, teethSize.height), nil)
let returnPath = CGPathCreateMutable()
for i in 0..<teethCount { // Copy, translate and rotate shapes around
let translate = CGAffineTransformMakeTranslation(halfWidth, halfHeight)
var rotate = CGAffineTransformRotate(translate, deltaAngle*CGFloat(i))
CGPathAddPath(returnPath, &rotate, p)
}
return CGPathCreateCopy(returnPath)
}
This time, all the paths are grouped into one big path and the function returns that path.
Finally, we just have to create our layer objects & setup the animation.
private func commonSetup() {
// set your background color
self.backgroundColor = UIColor.whiteColor()
// Get the group of paths we created.
shapeLayer.path = getPathMask(frame.size, teethCount: numberOfTeeth, teethSize: teethSize, radius: ((frame.width*0.5)-teethSize.height))
let halfWidth = frame.size.width*0.5
let halfHeight = frame.size.height*0.5
let halfDeltaAngle = CGFloat(M_PI/Double(numberOfTeeth))
// Creates an arc path, with a given offset to allow it to be presented nicely
drawLayer.path = UIBezierPath(arcCenter: CGPointMake(halfWidth, halfHeight), radius: halfWidth, startAngle: CGFloat(-M_PI_2)-halfDeltaAngle, endAngle: CGFloat(M_PI*1.5)+halfDeltaAngle, clockwise: true).CGPath
drawLayer.frame = frame
drawLayer.fillColor = inactiveColor.CGColor
drawLayer.strokeColor = highlightColor.CGColor
drawLayer.strokeEnd = 0
drawLayer.lineWidth = halfWidth
drawLayer.mask = shapeLayer
layer.addSublayer(drawLayer)
// Optional, but looks nice
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
}
All we're doing here is assigning the group of paths to a CAShapeLayer, which we will use as a mask over the drawLayer, which we will be animating around the view (using a stroke on an arch path).
Final Result
Full project: https://github.com/hamishknight/Circle-Loader

Programmatic "fuzzy" style background for UIView

Of course, it's trivial to set a plain color for a background:
These days, instead of using "plain gray", it is popular to use a "fuzzy" or "cloudy" background, as a design feature in apps.
For example, here's a couple "fuzzy" backgrounds - it's just a plain color with perhaps some noise and maybe blur on that.
You can see backgrounds something like this all over, consider popular feed apps (whassapp etc). It's a "fad" of our day.
It occurred to me, it would be fantastic if you could do this in code in Swift
Note: starting with a PNG is not an elegant solution:
Hopefully it is possible to generate everything programmatically from scratch.
It would be great if the Inspector had a slider in the IBDesignable style, "Add faddish 'grainy' background..." - Should be possible in the new era!
This will get you started, based on something I wrote a long time ago:
#IBInspectable properties:
noiseColor: the noise/grain color, this is applied over the view's backgroundColor
noiseMinAlpha: the minimum alpha the randomized noise can be
noiseMaxAlpha: the maximum alpha the randomized noise can be
noisePasses: how many times to apply the noise, more passes will be slower but can result in a better noise effect
noiseSpacing: how common the randomized noise occurs, higher spacing means the noise will be less frequent
Explanation:
When any of the designable noise properties change the view is flagged for redraw. In the draw function the UIImage is generated (or pulled from NSCache if available).
In the generation method each pixel is iterated over and if the pixel should be noise (depending on the spacing parameter), the noise color is applied with a randomized alpha channel. This is done as many times as the number of passes.
.
// NoiseView.swift
import UIKit
let noiseImageCache = NSCache()
#IBDesignable class NoiseView: UIView {
let noiseImageSize = CGSizeMake(128, 128)
#IBInspectable var noiseColor: UIColor = UIColor.blackColor() {
didSet { setNeedsDisplay() }
}
#IBInspectable var noiseMinAlpha: CGFloat = 0 {
didSet { setNeedsDisplay() }
}
#IBInspectable var noiseMaxAlpha: CGFloat = 1 {
didSet { setNeedsDisplay() }
}
#IBInspectable var noisePasses: Int = 1 {
didSet {
noisePasses = max(0, noisePasses)
setNeedsDisplay()
}
}
#IBInspectable var noiseSpacing: Int = 1 {
didSet {
noiseSpacing = max(1, noiseSpacing)
setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
UIColor(patternImage: currentUIImage()).set()
UIRectFillUsingBlendMode(bounds, .Normal)
}
private func currentUIImage() -> UIImage {
// Key based on all parameters
let cacheKey = "\(noiseImageSize),\(noiseColor),\(noiseMinAlpha),\(noiseMaxAlpha),\(noisePasses)"
var image = noiseImageCache.objectForKey(cacheKey) as! UIImage!
if image == nil {
image = generatedUIImage()
#if !TARGET_INTERFACE_BUILDER
noiseImageCache.setObject(image, forKey: cacheKey)
#endif
}
return image
}
private func generatedUIImage() -> UIImage {
UIGraphicsBeginImageContextWithOptions(noiseImageSize, false, 0)
let accuracy: CGFloat = 1000.0
for _ in 0..<noisePasses {
for y in 0..<Int(noiseImageSize.height) {
for x in 0..<Int(noiseImageSize.width) {
if random() % noiseSpacing == 0 {
let alpha = (CGFloat(random() % Int((noiseMaxAlpha - noiseMinAlpha) * accuracy)) / accuracy) + noiseMinAlpha
noiseColor.colorWithAlphaComponent(alpha).set()
UIRectFill(CGRectMake(CGFloat(x), CGFloat(y), 1, 1))
}
}
}
}
let image = UIGraphicsGetImageFromCurrentImageContext() as UIImage
UIGraphicsEndImageContext()
return image
}
}
in Swift 3
import UIKit
let noiseImageCache = NSCache<AnyObject, AnyObject>()
#IBDesignable class NoiseView: UIView {
let noiseImageSize = CGSize(width: 128.0, height: 128.0)
#IBInspectable var noiseColor: UIColor = UIColor.black {
didSet { setNeedsDisplay() }
}
#IBInspectable var noiseMinAlpha: CGFloat = 0 {
didSet { setNeedsDisplay() }
}
#IBInspectable var noiseMaxAlpha: CGFloat = 0.5 {
didSet { setNeedsDisplay() }
}
#IBInspectable var noisePasses: Int = 3 {
didSet {
noisePasses = max(0, noisePasses)
setNeedsDisplay()
}
}
#IBInspectable var noiseSpacing: Int = 1 {
didSet {
noiseSpacing = max(1, noiseSpacing)
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor(patternImage: currentUIImage()).set()
UIRectFillUsingBlendMode(bounds, .normal)
}
private func currentUIImage() -> UIImage {
// Key based on all parameters
let cacheKey = "\(noiseImageSize),\(noiseColor),\(noiseMinAlpha),\(noiseMaxAlpha),\(noisePasses)"
var image = noiseImageCache.object(forKey: cacheKey as AnyObject) as? UIImage
if image == nil {
image = generatedUIImage()
#if !TARGET_INTERFACE_BUILDER
noiseImageCache.setObject(image!, forKey: cacheKey as AnyObject)
#endif
}
return image!
}
private func generatedUIImage() -> UIImage {
UIGraphicsBeginImageContextWithOptions(noiseImageSize, false, 0)
let accuracy: CGFloat = 1000.0
for _ in 0..<noisePasses {
for y in 0..<Int(noiseImageSize.height) {
for x in 0..<Int(noiseImageSize.width) {
if Int(arc4random()) % noiseSpacing == 0 {
let alpha = (CGFloat(arc4random() % UInt32((noiseMaxAlpha - noiseMinAlpha) * accuracy)) / accuracy) + noiseMinAlpha
noiseColor.withAlphaComponent(alpha).set()
UIRectFill(CGRect(x: x, y: y, width: 1, height: 1))
}
}
}
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
You could easily build something up using GPUImage. It comes with a huge set of blurs, noise generators and filters.. You can connect them together in sequence and build up complex GPU accelerated effects.
To give you an good starting point. Here's a quick dirty prototype of a function that uses GPUImage to do something like what you want. If you set 'orUseNoise' to YES it will create a blurred image based on perlin noise INSTEAD if the image. Tweak the values pointed out to change the desired effect.
- (UIImage *)blurWithGPUImage:(UIImage *)sourceImage orUseNoise:(bool) useNoise {
GPUImagePicture *stillImageSource = [[GPUImagePicture alloc] initWithImage:sourceImage];
GPUImageGaussianBlurFilter *gaussFilter = [[GPUImageGaussianBlurFilter alloc] init];
[gaussFilter setBlurRadiusInPixels:6]; //<<-------TWEAK
[gaussFilter setBlurPasses:1]; //<<-------TWEAK
if(useNoise) {
GPUImagePerlinNoiseFilter* perlinNouse = [[GPUImagePerlinNoiseFilter alloc] init];
[perlinNouse setColorStart:(GPUVector4){1.0, 1.0, 1.0f, 1.0}]; //<<-------TWEAK
[perlinNouse setColorFinish:(GPUVector4){0.5,0.5, 0.5f, 1.0}]; //<<-------TWEAK
[perlinNouse setScale:200]; //<<-------TWEAK
[stillImageSource addTarget:perlinNouse];
[perlinNouse addTarget:gaussFilter];
} else {
[stillImageSource addTarget:gaussFilter];
}
[gaussFilter useNextFrameForImageCapture];
[stillImageSource processImage];
UIImage *outputImage = [gaussFilter imageFromCurrentFramebuffer];
// Set up output context.
UIGraphicsBeginImageContext(self.view.frame.size);
CGContextRef outputContext = UIGraphicsGetCurrentContext();
// Invert image coordinates
CGContextScaleCTM(outputContext, 1.0, -1.0);
CGContextTranslateCTM(outputContext, 0, -self.view.frame.size.height);
// Draw base image.
CGContextDrawImage(outputContext, self.view.frame, outputImage.CGImage);
// Apply tint
CGContextSaveGState(outputContext);
UIColor* tint = [UIColor colorWithWhite:1.0f alpha:0.6]; //<<-------TWEAK
CGContextSetFillColorWithColor(outputContext, tint.CGColor);
CGContextFillRect(outputContext, self.view.frame);
CGContextRestoreGState(outputContext);
// Output image
outputImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return outputImage;
}
This is a simple stack of:
GPUImagePicture -> GPUImagePerlinNoiseFilter -> GPUImageGaussianBlurFilter
..with a bit of handling code to make into an image properly.
You can try changing the stack to use some of the many other filters.
NOTE: Even if you use the noise instead of the image. You will still need to provide an image until you cut that part out.
We use great component KGNoise. It is really easy to use. I think it can help you
KGNoise generates random black and white pixels into a static 128x128 image that is then tiled to fill the space. The random pixels are seeded with a value that has been chosen to look the most random, this also means that the noise will look consistent between app launches.
I agree with answer about GPUImage and since you don't want to provide image, you could create blank image like this:
func createNoiseImage(size: CGSize, color: UIColor) -> UIImage {
UIGraphicsBeginImageContext(size)
let context = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, color.CGColor)
CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
let filter = GPUImagePerlinNoiseFilter()
return filter.imageByFilteringImage(image)
}
The main advantage of using GPUImage is speed.
While the question asks for a "programmatic" solution, it comes to mind that what you are trying to do and refer as "fuzzy" sounds a lot like UIBlurEffect, UIVisualEffectView and UIVibrancyEffect which were introduced in iOS 8.
In order to use these, you can drag a UIVisualEffectView on your Storyboard scene to add a blur or vibrancy effect to a specific part of the screen.
If you would like to have an entire scene appearing with the visual effect on top of the previous scene, you should configure the following:
Set either the View Controller or presentation segue to Presentation = Over Current Context and make the background color of the "fuzzy"
Set the background color of the presented view controller to clearColor.
Embed the entire content of the presented view controller inside a UIVisualEffectView
With that, you can get effects like this:

NSAttributedString background color and rounded corners

I have a question regarding rounded corners and text background color for a custom UIView.
Basically, I need to achieve an effect like this (image attached - notice the rounded corners on one side) in a custom UIView:
I'm thinking the approach to use is:
Use Core Text to get glyph runs.
Check highlight range.
If the current run is within the highlight range, draw a background rectangle with rounded corners and desired fill color before drawing the glyph run.
Draw the glyph run.
However, I'm not sure whether this is the only solution (or for that matter, whether this is the most efficient solution).
Using a UIWebView is not an option, so I have to do it in a custom UIView.
My question being, is this the best approach to use, and am I on the right track? Or am I missing out something important or going about it the wrong way?
TL;DR; Create a custom-view, which renders same old NSAttributedString, but with rounded-corners.
Unlike Android's SpannableString, iOS does not support "custom-render for custom-string-attributes", at least not without an entire custom-view (at time of writing, 2022).
I managed to achieve the above effect, so thought I'd post an answer for the same.
If anyone has any suggestions about making this more effective, please feel free to contribute. I'll be sure to mark your answer as the correct one. :)
For doing this, you'll need to add a "custom attribute" to NSAttributedString.
Basically, what that means is that you can add any key-value pair, as long as it is something that you can add to an NSDictionary instance. If the system does not recognize that attribute, it does nothing. It is up to you, as the developer, to provide a custom implementation and behavior for that attribute.
For the purposes of this answer, let us assume I've added a custom attribute called: #"MyRoundedBackgroundColor" with a value of [UIColor greenColor].
For the steps that follow, you'll need to have a basic understanding of how CoreText gets stuff done. Check out Apple's Core Text Programming Guide for understanding what's a frame/line/glyph run/glyph, etc.
So, here are the steps:
Create a custom UIView subclass.
Have a property for accepting an NSAttributedString.
Create a CTFramesetter using that NSAttributedString instance.
Override the drawRect: method
Create a CTFrame instance from the CTFramesetter.
You will need to give a CGPathRef to create the CTFrame. Make that CGPath to be the same as the frame in which you wish to draw the text.
Get the current graphics context and flip the text coordinate system.
Using CTFrameGetLines(...), get all the lines in the CTFrame you just created.
Using CTFrameGetLineOrigins(...), get all the line origins for the CTFrame.
Start a for loop - for each line in the array of CTLine...
Set the text position to the start of the CTLine using CGContextSetTextPosition(...).
Using CTLineGetGlyphRuns(...) get all the Glyph Runs (CTRunRef) from the CTLine.
Start another for loop - for each glyphRun in the array of CTRun...
Get the range of the run using CTRunGetStringRange(...).
Get typographic bounds using CTRunGetTypographicBounds(...).
Get the x offset for the run using CTLineGetOffsetForStringIndex(...).
Calculate the bounding rect (let's call it runBounds) using the values returned from the aforementioned functions.
Remember - CTRunGetTypographicBounds(...) requires pointers to variables to store the "ascent" and "descent" of the text. You need to add those to get the run height.
Get the attributes for the run using CTRunGetAttributes(...).
Check if the attribute dictionary contains your attribute.
If your attribute exists, calculate the bounds of the rectangle that needs to be painted.
Core text has the line origins at the baseline. We need to draw from the lowermost point of the text to the topmost point. Thus, we need to adjust for descent.
So, subtract the descent from the bounding rect that we calculated in step 16 (runBounds).
Now that we have the runBounds, we know what area we want to paint - now we can use any of the CoreGraphis/UIBezierPath methods to draw and fill a rect with specific rounded corners.
UIBezierPath has a convenience class method called bezierPathWithRoundedRect:byRoundingCorners:cornerRadii: that let's you round specific corners. You specify the corners using bit masks in the 2nd parameter.
Now that you've filled the rect, simply draw the glyph run using CTRunDraw(...).
Celebrate victory for having created your custom attribute - drink a beer or something! :D
Regarding detecting that the attribute range extends over multiple runs, you can get the entire effective range of your custom attribute when the 1st run encounters the attribute. If you find that the length of the maximum effective range of your attribute is greater than the length of your run, you need to paint sharp corners on the right side (for a left to right script). More math will let you detect the highlight corner style for the next line as well. :)
Attached is a screenshot of the effect. The box on the top is a standard UITextView, for which I've set the attributedText. The box on the bottom is the one that has been implemented using the above steps. The same attributed string has been set for both the textViews.
Again, if there is a better approach than the one that I've used, please do let me know! :D
Hope this helps the community. :)
Cheers!
Just customize NSLayoutManager and override drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:) Apple API Document
In this method, you can draw underline by yourself, Swift code,
override func drawUnderline(forGlyphRange glyphRange: NSRange,
underlineType underlineVal: NSUnderlineStyle,
baselineOffset: CGFloat,
lineFragmentRect lineRect: CGRect,
lineFragmentGlyphRange lineGlyphRange: NSRange,
containerOrigin: CGPoint
) {
let firstPosition = location(forGlyphAt: glyphRange.location).x
let lastPosition: CGFloat
if NSMaxRange(glyphRange) < NSMaxRange(lineGlyphRange) {
lastPosition = location(forGlyphAt: NSMaxRange(glyphRange)).x
} else {
lastPosition = lineFragmentUsedRect(
forGlyphAt: NSMaxRange(glyphRange) - 1,
effectiveRange: nil).size.width
}
var lineRect = lineRect
let height = lineRect.size.height * 3.5 / 4.0 // replace your under line height
lineRect.origin.x += firstPosition
lineRect.size.width = lastPosition - firstPosition
lineRect.size.height = height
lineRect.origin.x += containerOrigin.x
lineRect.origin.y += containerOrigin.y
lineRect = lineRect.integral.insetBy(dx: 0.5, dy: 0.5)
let path = UIBezierPath(rect: lineRect)
// let path = UIBezierPath(roundedRect: lineRect, cornerRadius: 3)
// set your cornerRadius
path.fill()
}
Then construct your NSAttributedString and add attributes .underlineStyle and .underlineColor.
addAttributes(
[
.foregroundColor: UIColor.white,
.underlineStyle: NSUnderlineStyle.single.rawValue,
.underlineColor: UIColor(red: 51 / 255.0, green: 154 / 255.0, blue: 1.0, alpha: 1.0)
],
range: range
)
That's it!
I did it by checking frames of text fragments. In my project I needed to highlight hashtags while a user is typing text.
class HashtagTextView: UITextView {
let hashtagRegex = "#[-_0-9A-Za-z]+"
private var cachedFrames: [CGRect] = []
private var backgrounds: [UIView] = []
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
configureView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configureView()
}
override func layoutSubviews() {
super.layoutSubviews()
// Redraw highlighted parts if frame is changed
textUpdated()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc private func textUpdated() {
// You can provide whatever ranges needed to be highlighted
let ranges = resolveHighlightedRanges()
let frames = ranges.compactMap { frame(ofRange: $0) }.reduce([], +)
if cachedFrames != frames {
cachedFrames = frames
backgrounds.forEach { $0.removeFromSuperview() }
backgrounds = cachedFrames.map { frame in
let background = UIView()
background.backgroundColor = UIColor.hashtagBackground
background.frame = frame
background.layer.cornerRadius = 5
insertSubview(background, at: 0)
return background
}
}
}
/// General setup
private func configureView() {
NotificationCenter.default.addObserver(self, selector: #selector(textUpdated), name: UITextView.textDidChangeNotification, object: self)
}
/// Looks for locations of the string to be highlighted.
/// The current case - ranges of hashtags.
private func resolveHighlightedRanges() -> [NSRange] {
guard text != nil, let regex = try? NSRegularExpression(pattern: hashtagRegex, options: []) else { return [] }
let matches = regex.matches(in: text, options: [], range: NSRange(text.startIndex..<text.endIndex, in: text))
let ranges = matches.map { $0.range }
return ranges
}
}
There is also a helper extension to determine frames of ranges:
extension UITextView {
func convertRange(_ range: NSRange) -> UITextRange? {
let beginning = beginningOfDocument
if let start = position(from: beginning, offset: range.location), let end = position(from: start, offset: range.length) {
let resultRange = textRange(from: start, to: end)
return resultRange
} else {
return nil
}
}
func frame(ofRange range: NSRange) -> [CGRect]? {
if let textRange = convertRange(range) {
let rects = selectionRects(for: textRange)
return rects.map { $0.rect }
} else {
return nil
}
}
}
Result text view:
I wrote the below code following the #codeBearer answer.
import UIKit
class CustomAttributedTextView: UITextView {
override func layoutSubviews() {
super.layoutSubviews()
}
func clearForReuse() {
setNeedsDisplay()
}
var lineCountUpdate: ((Bool) -> Void)?
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.clear.setFill()
UIColor.clear.setFill()
guard let context = UIGraphicsGetCurrentContext() else { return }
context.textMatrix = .identity
context.translateBy(x: 0, y: bounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)
let path = CGMutablePath()
let size = sizeThatFits(CGSize(width: self.frame.width, height: .greatestFiniteMagnitude))
path.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height), transform: .identity)
let framesetter = CTFramesetterCreateWithAttributedString(attributedText as CFAttributedString)
let frame: CTFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedText.length), path, nil)
let lines: [CTLine] = frame.lines
var origins = [CGPoint](repeating: .zero, count: lines.count)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)
for lineIndex in 0..<lines.count {
let line = lines[lineIndex]
let runs: [CTRun] = line.ctruns
var tagCountInOneLine = 0
for run in runs {
var cornerRadius: CGFloat = 3
let attributes: NSDictionary = CTRunGetAttributes(run)
var imgBounds: CGRect = .zero
if let value: UIColor = attributes.value(forKey: NSAttributedString.Key.customBackgroundColor.rawValue) as? UIColor {
var ascent: CGFloat = 0
imgBounds.size.width = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, nil, nil) + 4)
imgBounds.size.height = ascent + 6
let xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil)
imgBounds.origin.x = origins[lineIndex].x + xOffset + 3
imgBounds.origin.y = origins[lineIndex].y - 13
if lineIndex != 0 {
imgBounds.origin.y = imgBounds.origin.y - 1
}
let path = UIBezierPath(roundedRect: imgBounds, cornerRadius: cornerRadius)
value.setFill()
path.fill()
value.setStroke()
}
}
}
}
}
extension CTFrame {
var lines: [CTLine] {
let linesAO: [AnyObject] = CTFrameGetLines(self) as [AnyObject]
guard let lines = linesAO as? [CTLine] else {
return []
}
return lines
}
}
extension CTLine {
var ctruns: [CTRun] {
let linesAO: [AnyObject] = CTLineGetGlyphRuns(self) as [AnyObject]
guard let lines = linesAO as? [CTRun] else {
return []
}
return lines
}
}

Resources