`UIBezierPath.fill()` does not fill inside of `draw()` - uiview

I'm cutting a rectangle out of a semi-transparent UIView. When I use UIRectFill() it works as expected. However when I create a UIBezierPath with rounded corners, fill() does not seem to do anything. Oddly, calling stroke() on that same path works fine.
WORKS:
override func draw(_ rect: CGRect) {
screenColor.set()
UIRectFill(self.bounds)
let viewfinderRect = CGRect(x: 50, y: 50, width: 100, height: 100)
UIColor.clear.setFill()
UIRectFill(viewfinderRect) //Works!
}
DOES NOT WORK:
override func draw(_ rect: CGRect) {
screenColor.set()
UIRectFill(self.bounds)
let viewfinderRect = CGRect(x: 50, y: 50, width: 100, height: 100)
let path = UIBezierPath(roundedRect: viewFinderRect, cornerRadius: 10.0)
UIColor.clear.setFill()
path.fill() // Does not work!
//path.stroke() // Works!
}

You're not drawing with the same blend mode.
The documentation for UIRectFill says:
Fills the specified rectangle using the fill color of the current graphics context and the kCGBlendModeCopy blend mode.
(In Swift, the name for this blend mode is CGBlendMode.copy.)
When you use this blend mode, the specified fill color is copied into the context, replacing whatever was underneath. Since your fill color is clear, you effectively cut a hole in the view.
However, when you call path.fill(), it uses the default blend mode in the context, which is CGBlendMode.normal. That draws the fill color over whatever was already in the context. Since your fill color is clear, that has no visible effect.
Try this:
UIColor.clear.setFill()
path.fill(with: CGBlendMode.copy, alpha: 1.0)
Or this:
UIColor.clear.setFill()
UIGraphicsGetCurrentContext()?.setBlendMode(CGBlendMode.copy)
path.fill()
Or you can even do it in one line:
path.fill(with: CGBlendMode.clear, alpha: 1.0)

Related

How to add class UIView in addSubview

I have a code that draws an ellipse, but it's in a separate class that inherits from UIView
class DRAW: UIView {
override func draw(_ rect: CGRect) {
var path = UIBezierPath()
path = UIBezierPath(ovalIn: CGRect(x: 36.62, y: 77.54, width: 303.19, height: 495.93))
UIColor.green.setFill()
path.stroke()
path.fill()
}
}
If I try to add it via addSubview(), then it is not on the screen, and if I give it the dimensions, then a black crawl for the entire display and only in the middle is the ellipse I need.
view.addSubview(DRAW.init(CGRect(x: 36.62, y: 77.54, width: 303.19, height: 495.93)))
How do I display only an ellipse without a black square. I would be grateful for your help
You can just simply set the clear background color of your view
let yourView = DRAW.init(frame: CGRect(x: 36.62, y: 77.54, width: 303.19, height: 495.93))
yourView.backgroundColor = .clear
self.view.addSubview(yourView)

Invert simple UIView mask (cut hole instead of clip to circle) [duplicate]

This question already has answers here:
How can I 'cut' a transparent hole in a UIImage?
(4 answers)
Closed 1 year ago.
I'm trying to avoid CAShapeLayer because I would need to mess with CABasicAnimation, not UIView.animate. So instead, I'm just using UIView's mask property to mask views. Here's my code currently:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let imageView = UIImageView(frame: CGRect(x: 50, y: 50, width: 200, height: 300))
imageView.image = UIImage(named: "TestImage")
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
view.addSubview(imageView)
let maskView = UIView(frame: CGRect(x: 100, y: 100, width: 80, height: 80))
maskView.backgroundColor = UIColor.blue /// ensure opaque
maskView.layer.cornerRadius = 10
imageView.mask = maskView /// set the mask
}
}
Without imageView.mask = maskView
With imageView.mask = maskView
It makes a portion of the image view visible. However, this is what I want:
Instead of making part of the image view visible, how can I cut a hole in it?
You can create an image view and set that as your mask. Note that this does not lend itself to animation. If you want to animate the mask to different shapes, you should add a mask to your view's CALayer and use CALayerAnimation, as you mention. It's not that bad.
Below I outline how to generate an image with a transparent part (a hole) that you can use as a mask in an image view. If your goal is to animate the size, shape, or position of the hole, however, this won't work. You'd have to regenerate the mask image for every frame, which would be really slow.
Here's how you would get the effect your are after for static views using an image view as a mask:
Use UIGraphicsBeginImageContextWithOptions() UIGraphicsImageRenderer to create an image that is opaque for most of your image, and has a transparent "hole" where you want a hole.
Then install that image in your image view, and make that image view your mask.
The code to create a mostly opaque image with a transparent rounded rect "hole" might look like this:
/**
Function to create a UIImage that is mostly opaque, with a transparent rounded rect "knockout" in it. Such an image might be used ask a mask
for another view, where the transparent "knockout" appears as a hole in the view that is being masked.
- Parameter size: The size of the image to create
- Parameter transparentRect: The (rounded )rectangle to make transparent in the middle of the image.
- Parameter cornerRadius: The corner radius ot use in the transparent rectangle. Pass 0 to make the rectangle square-cornered.
*/
func imageWithTransparentRoundedRect(size: CGSize, transparentRect: CGRect, cornerRadius: CGFloat) -> UIImage? {
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { (context) in
let frame = CGRect(origin: .zero, size: size)
UIColor.white.setFill()
context.fill(frame)
let roundedRect = UIBezierPath(roundedRect: transparentRect, cornerRadius: cornerRadius)
context.cgContext.setFillColor(UIColor.clear.cgColor)
context.cgContext.setBlendMode(.clear)
roundedRect.fill()
}
return image
}
And a viewDidLoad method that installs a UIImageView with a mask image view with a hole in it might look like this:
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .cyan
let size = CGSize(width: 200, height: 300)
let origin = CGPoint(x: 50, y: 50)
let frame = CGRect(origin: origin, size: size)
let imageView = UIImageView(frame: frame)
imageView.image = UIImage(named: "TestImage")
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
view.addSubview(imageView)
imageView.layer.borderWidth = 2
//Create a mask image view the same size as the (image) view we will be masking
let maskView = UIImageView(frame: imageView.bounds)
//Build an opaque UIImage with a transparent "knockout" rounded rect inside it.
let transparentRect = CGRect(x: 100, y: 100, width: 80, height: 80)
let maskImage = imageWithTransparentRoundedRect(size: size, transparentRect: transparentRect, cornerRadius: 20)
//Install the image with the "hole" into the mask image view
maskView.image = maskImage
//Make the maskView the ImageView's mask
imageView.mask = maskView /// set the mask
}
}
I created a sample project using the code above. You can download it from Github here:
https://github.com/DuncanMC/UIImageMask.git
I just updated the project to also show how to do the same thing using a CAShapeLayer as a mask on the image view's layer. Doing it that way, it's possible to animate changes to the mask layer's path.
The new version has a segmented control that lets you pick whether to mask the image view using a UIImage in the view's mask property, or via a CAShapeLayer used as a mask on the image view's layer.
For the CAShapeLayer version, the mask layer's path is a rectangle the size of the whole image view, with a second, smaller rounded rectangle drawn inside it. The winding rule on the shape layer is then set to the "even/odd" rule, meaning that if you have to cross an even number of shape boundaries to get to a point, it is considered outside the shape. That enables you to create hollow shapes like we need here.
When you select the layer mask option, it enables an animation button that animates random changes to the "cutout" transparent rectangle in the mask.
The function that creates the mask path looks like this:
func maskPath(transparentRect: CGRect, cornerRadius: CGFloat) -> UIBezierPath {
let fullRect = UIBezierPath(rect: maskLayer.frame)
let roundedRect = UIBezierPath(roundedRect: transparentRect, cornerRadius: cornerRadius)
fullRect.append(roundedRect)
return fullRect
}
And the function that does the animation looks like this:
#IBAction func handleAnimateButton(_ sender: Any) {
//Create a CABasicAnimation that will change the path of our maskLayer
//Use the keypath "path". That tells the animation object what property we are animating
let animation = CABasicAnimation(keyPath: "path")
animation.autoreverses = true //Make the animation reverse back to the oringinal position once it's done
//Use ease-in, ease-out timing, which looks smooth
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animation.duration = 0.3 //Make each step in the animation last 0.3 seconds.
let transparentRect: CGRect
//Randomly either animate the transparent rect to a different shape or shift it
if Bool.random() {
//Make the transparent rect taller and skinnier
transparentRect = self.transparentRect.inset(by: UIEdgeInsets(top: -20, left: 20, bottom: -20, right: 20))
} else {
//Shift the transparent rect to by a random amount that still says inside the image view's bounds.
transparentRect = self.transparentRect.offsetBy(dx: CGFloat.random(in: -100...20), dy: CGFloat.random(in: -100...100))
}
let cornerRadius: CGFloat = CGFloat.random(in: 0...30)
//install the new path as the animation's `toValue`. If we dont specify a `fromValue` the animation will start from the current path.
animation.toValue = maskPath(transparentRect: transparentRect, cornerRadius: cornerRadius).cgPath
//add the animation to the maskLayer. Since the animation's `keyPath` is "path",
//it will animate the layer's "path" property to the "toValue"
maskLayer.add(animation, forKey: nil)
//Since we don't actually change the path on the mask layer, the mask will revert to it's original path once the animation completes.
}
The results (using my own sample image) look like this:
A sample of the CALayer based mask animation looks like this:

How do I draw stuff under the text of a UILabel?

I created a custom UILabel subclass that has a circle in the middle, and the label's text (which is a number) will be on top of the circle.
I initially thought of doing this using layer.cornerRadius, but that will not create a circle when the label's width and height are not equal.
What I mean is, for a label with width 100 and height 50, I still want a circle with radius 50 and centre at (50, 25).
Therefore, I tried to use UIBezierPath to draw the circle. This is what I have tried:
override func draw(_ rect: CGRect) {
super.draw(rect)
if bounds.height > bounds.width {
let y = (bounds.height - bounds.width) / 2
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: y, width: bounds.width, height: bounds.width))
circleColor.setFill()
path.fill()
} else {
let x = (bounds.width - bounds.height) / 2
let path = UIBezierPath(ovalIn: CGRect(x: x, y: 0, width: bounds.height, height: bounds.height))
circleColor.setFill()
path.fill()
}
}
I have put super.draw(rect) because I thought that would draw the label's text, but when I run the app, I only see the circle and not my label text.
I am very confused because why hasn't super.draw(rect) drawn the label's text?
The text is not seen because the "z-index" of UIBezierPaths depends on the order in which they are drawn. In other words, UIBezierPaths are drawn on top of each other.
super.draw(rect) indeed draws the text. But when you put it as the first statement, it will get drawn first, so everything you draw after that, goes on top of the text. To fix this, you should call super.draw(rect) last:
override func draw(_ rect: CGRect) {
if bounds.height > bounds.width {
let y = (bounds.height - bounds.width) / 2
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: y, width: bounds.width, height: bounds.width))
circleColor.setFill()
path.fill()
} else {
let x = (bounds.width - bounds.height) / 2
let path = UIBezierPath(ovalIn: CGRect(x: x, y: 0, width: bounds.height, height: bounds.height))
circleColor.setFill()
path.fill()
}
super.draw(rect) // <------- here!
}
Alternatively, just subclass UIView, draw the circle in draw(_:), and add a UILabel as a subview of that. The advantage if this approach is that it does not depend on the implementation of super.draw(_:), which might change in the future,

How do I use this circle drawing code in a UIView?

I am making an app including some breathing techniques for a client. What he wants is to have a circle in the middle. For breathing in it becomes bigger, for breathing out tinier. The thing is, that he would like to have a cool animated circle in the middle, not just a standard one. I showed him this picture from YouTube:
The code used in the video looks like this:
func drawRotatedSquares() {
UIGraphicsBeginImageContextWithOptions(CGSize(width: 512, height: 512), false, 0)
let context = UIGraphicsGetCurrentContext()
context!.translateBy(x: 256, y: 256)
let rotations = 16
let amount = M_PI_2 / Double(rotations)
for i in 0 ..< rotations {
context!.rotate(by: CGFloat(amount))
//context!.addRect(context, CGRect(x: -128, y: -128, width: 256, height: 256))
context!.addRect(CGRect(x: -128, y: -128, width: 256, height: 256))
}
context!.setStrokeColor(UIColor.black as! CGColor)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
imageView.image = img
}
But if I run it, my simulator shows just a white screen. How do I get this circle into my Swift 3 app and how would the code look like? And is it possible not to show it in an ImageView but simply in a view?
Thank you very much!
Here is an implementation as a UIView subclass.
To set up:
Add this class to your Swift project.
Add a UIView to your Storyboard and change the class to Circle.
Add an outlet to your viewController
#IBOutlet var circle: Circle!
Change the value of multiplier to change the size of the circle.
circle.multiplier = 0.5 // 50% of size of view
class Circle: UIView {
var multiplier: CGFloat = 1.0 {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
// Calculate size of square edge that fits into the view
let size = min(bounds.width, bounds.height) * multiplier / CGFloat(sqrt(2)) / 2
// Move origin to center of the view
context.translateBy(x: center.x, y: center.y)
// Create a path to draw a square
let path = UIBezierPath()
path.move(to: CGPoint(x: -size, y: -size))
path.addLine(to: CGPoint(x: -size, y: size))
path.addLine(to: CGPoint(x: size, y: size))
path.addLine(to: CGPoint(x: size, y: -size))
path.close()
UIColor.black.setStroke()
let rotations = 16
let amount = .pi / 2 / Double(rotations)
for _ in 0 ..< rotations {
// Rotate the context
context.rotate(by: CGFloat(amount))
// Draw a square
path.stroke()
}
}
}
Here it is running in a Playground:
You posted a singe method that generates a UIImage and installs it in an image view. If you don't have the image view on-screen then it won't show up.
If you create an image view in your view controller and connect an outlet to the image view then the above code should install the image view into your image and draw it on-screen.
You could rewrite the code you posted as the draw(_:) method of a custom subclass of UIView, in which case you'd get rid of the context setup and UIImage stuff, and simply draw in the current context. I suggest you search on UIView custom draw(_:) methods for more guidance.

How to change the color of a UIBezierPath in Swift?

I have an instance of UIBezierPath and I want to change the color of the stroke to something other than black. Does anyone know how to do this in Swift?
With Swift 5, UIColor has a setStroke() method. setStroke() has the following declaration:
func setStroke()
Sets the color of subsequent stroke operations to the color that the receiver represents.
Therefore, you can use setStroke() like this:
strokeColor.setStroke() // where strokeColor is a `UIColor` instance
The Playground code below shows how to use setStroke() alongside UIBezierPath in order to draw a circle with a green fill color and a light grey stroke color inside a UIView subclass:
import UIKit
import PlaygroundSupport
class MyView: UIView {
override func draw(_ rect: CGRect) {
// UIBezierPath
let newRect = CGRect(
x: bounds.minX + ((bounds.width - 79) * 0.5 + 0.5).rounded(.down),
y: bounds.minY + ((bounds.height - 79) * 0.5 + 0.5).rounded(.down),
width: 79,
height: 79
)
let ovalPath = UIBezierPath(ovalIn: newRect)
// Fill
UIColor.green.setFill()
ovalPath.fill()
// Stroke
UIColor.lightGray.setStroke()
ovalPath.lineWidth = 5
ovalPath.stroke()
}
}
let myView = MyView(frame: CGRect(x: 0, y: 0, width: 200, height: 300))
PlaygroundPage.current.liveView = myView
Let's assume you want to use the color red instead to stroke a rounded rectangle;
This is how you do it in Swift 3 :
// Drawing the border of the rounded rectangle:
let redColor = UIColor.red
redColor.setStroke() // Stroke subsequent views with a red color
let roundedRectagle = CGRect(x: 0,y: 0, width: 90,height: 20)
let rectangleBorderPath = UIBezierPath(roundedRect: roundedRectangle,cornerRadius: 5)
roundedRectangle.borderWidth = 1
roundedRectangle.stroke() // Apply the red color stroke on this view
The second and last lines of the above code are important in answering your question.I hope this answer is helpful.

Resources