This is my first time in which I've encountered problems with drawRect. I have a simple UIView subclass with a path to fill. Everything is fine. The path gets filled properly, but the background remains black no matter what. What should I do? My code is listed below:
var color: UIColor = UIColor.red {didSet{setNeedsDisplay()}}
private var spaceshipBezierPath: UIBezierPath{
let path = UIBezierPath()
let roomFromTop: CGFloat = 10
let roomFromCenter: CGFloat = 7
path.move(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.minY+roomFromTop))
path.addLine(to: CGPoint(x: bounds.midX-roomFromCenter, y: bounds.minY+roomFromTop))
path.addLine(to: CGPoint(x: bounds.midX-roomFromCenter, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.midX+roomFromCenter, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.midX+roomFromCenter, y: bounds.minY+roomFromTop))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY+roomFromTop))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
return path
}
override func draw(_ rect: CGRect) {
color.setFill()
spaceshipBezierPath.fill()
}
Here is what the view looks like:
I have a simple UIView subclass ... but the background remains black no matter what
Typically that sort of thing is because you have forgotten to set the UIView subclass's isOpaque to false. It is a good idea to do this in the UIView subclass's initializer in order for it to be early enough.
For example, here I've adapted your code very slightly. This is the complete code I'm using:
class MyView : UIView {
private var spaceshipBezierPath: UIBezierPath{
let path = UIBezierPath()
// ... identical to your code
return path
}
override func draw(_ rect: CGRect) {
UIColor.red.setFill()
spaceshipBezierPath.fill()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let v = MyView(frame:CGRect(x: 100, y: 100, width: 200, height: 200))
self.view.addSubview(v)
}
}
Notice the black background:
Now I add these lines to MyView:
override init(frame:CGRect) {
super.init(frame:frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
See the difference that makes?
In my case what I solved was to switch from Background-Color "No Color", to Background-Color "Clear Color"
For future seekers....
I had the same problem but #matt solution didn’t work for me.
In my scenario, I had a UIView (A) behind another UIView (B).
UIView (A) had a background color and UIView (B) was transparent. Worth to note that B was subclassed to create a custom shape using UIBezier by overriding draw method.
Turned out that in this situation a transparent UIView means nothing and I had to set the same background color for B or either remove background color of A.
there was no other way, so don’t waste your time if you have the same scenario!
Related
I have a UIView that contains two UITextfields, I want to create a 1.0 pixel width separator between the two, but I just can't get it to work.
I tried overriding the draw func, but it didn't work, I also tried using bezierPath and shapeLayer, but I don't have the UIView's frame, so the bezierPath can't be drawn.
This is the draw override I have:
override func draw(_ rect: CGRect) {
super.draw(rect)
if let context = UIGraphicsGetCurrentContext() {
context.setStrokeColor(UIColor.black.cgColor)
context.setLineWidth(1)
context.move(to: CGPoint(x: bounds.width / 2, y: 0))
context.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height))
context.strokePath()
}
}
This is the bezierPath func:
private func drawSeparator() {
let path = UIBezierPath()
path.addLine(to: CGPoint(x: bounds.width / 2, y: 0))
path.move(to: CGPoint(x: bounds.width / 2, y: self.bounds.height))
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.strokeColor = UIColor.appGray.cgColor
shape.lineWidth = 1.0
shape.fillColor = UIColor.appGray.cgColor
self.layer.addSublayer(shape)
}
I also tried moving the path variable to the class level, and using it as lazy var to try and get the frame/bounds that way, but it also didn't work.
I'm trying to create a simple, vertical separator.
While there are refinements I would suggest, your draw(:_) rendition works fine. (Your shape layer rendition is wrong ... you want to decouple the creation of the shape layer and the updating of the path; but let’s set that aside.)
If you’re not seeing your view’s separator line rendered in your draw(_:) rendition, the problem rests elsewhere. Try inserting log statement in the draw method and make sure you’re hitting this code like you think it should and that the bounds is what you think it should be. Or use the view debugger to confirm the base class is set correctly and that the frame is what you think it is, etc.
I might suggest using midX, minY and maxY. Personally, I’d also not drop into CoreGraphics unless necessary:
#IBDesignable
class VerticalSplitView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.midX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY))
path.lineWidth = 1
UIColor.black.setStroke()
path.stroke()
}
}
By the way, if you use this draw(_:) approach, make sure you set your view’s content mode to be redraw. In the absence of that, you can get strange behavior (e.g. if the constraints link the width of this view to its superview and user rotates device, it’s going to stretch the width of this vertical line).
Personally, I like to get out of manual drawing and hand that off to a CAShapeLayer. That way I don’t have to worry about the contentMode of the view and I offload the drawing to Apple’s code (which undoubtedly has optimizations that we don’t consider here).
As I mentioned, you have to decouple the creation of the shape layer (to be done during initialization) from the updating of its path (which should be done in layoutSubviews).
Personally, I might have a view whose backing layer is a CAShapeLayer:
#IBDesignable
class VerticalShapeLayer: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
lazy var shapeLayer: CAShapeLayer = layer as! CAShapeLayer
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.midX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY))
shapeLayer.strokeColor = UIColor { traitCollection in
switch traitCollection.userInterfaceStyle {
case .dark: return .white
default: return .black
}
}.cgColor
shapeLayer.lineWidth = 1
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.path = path.cgPath
}
}
Also, as a refinement, I would set the color of the stroke based upon whether the device is in dark mode or not. That obviously presumes that you set the background colors accordingly, too. Do whatever you want regarding the stroke color.
One final suggestion once you have your immediate problem behind you: If you’re using NIBs or storyboards, I’d designate this view class as #IBDesignable. That way I can see it right in Interface Builder, eliminating a lot of these questions. We can see it rendered during the design process, removing ambiguity about whether the base class was set right, that the frame is correct, etc.
It’s certainly not necessary, but #IBDesignable helps identify issues during the design process.
It's unclear what you're hoping to do, but here's a complete working version based directly on your code:
class MyView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
if let context = UIGraphicsGetCurrentContext() {
context.setStrokeColor(UIColor.black.cgColor)
context.setLineWidth(1)
context.move(to: CGPoint(x: bounds.width / 2, y: 0))
context.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height))
context.strokePath()
}
}
}
class ViewController: UIViewController {
let myView = MyView()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if myView.superview == nil {
self.view.addSubview(myView)
}
myView.frame = self.view.bounds
myView.setNeedsDisplay()
}
}
That looks like this:
Okay, so now that you've seen a working example, adapt it to your own situation and you're all set.
For the line you can also use UIView between the textField as a separator(set width of UIView).
Suppose we have two UIBezierPaths, path1 and path2... (that have already been defined relative to view bounds at runtime and already exist as properties of the same view).
We want to get a new path of type UIBezierPath, path3, that is the result of subtracting path2 from path1:
The way this is done (as seen here) is to do this:
path1.append(path2.reversing())
BUT, that only seems to work for circumstances where path1 fully encompasses path2.
For example, consider the case where there is only a partial intersection -- path1 does not fully encompass path2. This is what happens if we apply the same method as above:
In Android, the answer is:
path1.op(path2, Path.Op.DIFFERENCE);
So... is there an equivalent simple operation in IOS?
If not, is there a function that could be written as:
func returnPath2CutOutOfPath1(path1: UIBezierPath, path2: UiBezierPath) -> UIBezierPath {
// the mystery lies within these here parts. :)
}
There is no direct way to get the differences of UIBezierPaths as a new path on iOS.
Testcase
private func path2() -> UIBezierPath {
return UIBezierPath(rect: CGRect(x: 100, y: 50, width: 200, height: 200))
}
private func path1() -> UIBezierPath {
return UIBezierPath(rect: CGRect(x: 50, y: 100, width: 200, height: 200))
}
Starting point: to simply show which path represents what: path 1 is yellow and path 2 is green:
Possiblity 1
You have probably already seen this possibility: In the link you mentioned in your question, there is also a clever solution if you only have to perform a fill operation.
The code was taken from this answer (from the link you posted) https://stackoverflow.com/a/8860341 - only converted to Swift:
func fillDifference(path2: UIBezierPath, path1: UIBezierPath) {
let clipPath = UIBezierPath.init(rect: .infinite)
clipPath.append(path2)
clipPath.usesEvenOddFillRule = true
UIGraphicsGetCurrentContext()?.saveGState()
clipPath.addClip()
path1.fill()
UIGraphicsGetCurrentContext()?.restoreGState()
}
So this fills a path, but does not return a UIBezierPath, which means that you cannot apply outlines, backgrounds, contour widths, etc., because the result is not a UIBezierPath.
It looks like this:
Possiblity 2
You can use a third-party library, e.g. one from an author named Adam Wulf here: https://github.com/adamwulf/ClippingBezier.
The library is written in Objective-C, but can be called from Swift.
In Swift, it would look like this:
override func draw(_ rect: CGRect) {
let result = self.path1().difference(with: self.path2())
for p in result ?? [] {
p.stroke()
}
}
If you want to use this library, you have to note a small hint: in Other Linker Flags in the project settings, as described by the readme, "-ObjC++ -lstdc++" must be added, otherwise it would be built without complaints, but would silently not load the frameworks and eventually crash, since the UIBEzierPath categories are not found.
The result looks like this:
So this would actually give your desired result, but you would have to use a 3rd party library.
I have achieved you desired solution, but I have created code for static view of size 200x200.
Let me show you what I have tried and tell me how useful for you.
First of all, I have create class of custom view where I have wrote code to draw view. See the following code:
class MyCustomView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
// Create a CAShapeLayer
let shapeLayer = CAShapeLayer()
let path1 = self.path1()
let path2 = self.path2()
// Append path2 to path1
path1.append(path2.reversing())
// Set true of Even Odd Fill Rule
path1.usesEvenOddFillRule = true
// Call this method to add clip in path
path1.addClip()
shapeLayer.path = path1.cgPath
// Apply other properties related to the path
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.fillColor = UIColor.black.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.position = CGPoint(x: 0, y: 0)
// Add the new layer to our custom view
self.layer.addSublayer(shapeLayer)
}
//-----------------------------------------------------
// This is static code as I have already told you first.
// Create First UIBezierPath,
func path1() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: 200))
path.addLine(to: CGPoint(x: 200, y: 200))
path.addLine(to: CGPoint(x: 200, y: 0))
path.close()
return path
}
// Create Second UIBezierPath
func path2() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: -50))
path.addLine(to: CGPoint(x: 50, y: 150))
path.addLine(to: CGPoint(x: 250, y: 150))
path.addLine(to: CGPoint(x: 250, y: -50))
path.close()
return path
}
}
This custom class code will create share layer with your actual result as you have described in figure 4.
Now to use this class I have create instance of above class with fixed height & width.
override func viewDidLoad() {
super.viewDidLoad()
// Create a new UIView and add it to the view controller
let myView = MyCustomView()
// Must set clipsToBounds true to remove extra layer which are display out side of view as like your **actual** result in figure 4.
myView.clipsToBounds = true
myView.frame = CGRect(x: 50, y: 100, width: 200, height: 200)
myView.backgroundColor = UIColor.orange
view.addSubview(myView)
}
This will display view as follow which is your desired result.
I hope this will help you, let me know still if you have any query.
I wanted to make a white color view with a triangle shaped pointer being grooved inside like this:
As shown in the image above, goal is to create a "rounded groove" inset into the whiteview
let pointerRadius:CGFloat = 4
pointerLayer = CAShapeLayer()
pointerLayer.path = pointerPathForContentSize(contentSize: bounds.size).cgPath
pointerLayer.lineJoin = kCALineJoinRound
pointerLayer.lineWidth = 2*pointerRadius
pointerLayer.fillColor = UIColor.white.cgColor
pointerLayer.strokeColor = UIColor.black.cgColor
pointerLayer.backgroundColor = UIColor.white.cgColor
layer.addSublayer(pointerLayer)
But what I get is this:
But,if I set the stroke color to white
pointerLayer.strokeColor = UIColor.white.cgColor
In the groove I wanted to have a rounded edge in bottom (just like in the first pic) which no more remains visible when fillColor and strokeColor get matched (both white).
How can I fix it?
Is there any other way to achieve this?
Here is the code for pointer path:
private func pointerPathForContentSize(contentSize: CGSize) -> UIBezierPath
{
let rect = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
let width:CGFloat = 20
let height:CGFloat = 20
let path = UIBezierPath()
let startX:CGFloat = 50
let startY:CGFloat = rect.minY
path.move(to: CGPoint(x: startX , y: startY))
path.addLine(to: CGPoint(x: (startX + width*0.5), y: startY + height))
path.addLine(to: CGPoint(x: (startX + width), y: startY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
path.close()
return path
}
Since you have already outlined the shape you want by stroking the path, I think the simplest solution is probably to use the stroked and filled path as a mask.
For example, here is a rectangular red view:
And here is the same red view with the notch cut out of the top. This seems to be the sort of thing you're after:
What I did there was to mask the red view with a special mask view that draws the notch using .clear blend mode:
class MaskView : UIView {
override init(frame: CGRect) {
super.init(frame:frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()!
con.fill(self.bounds)
con.setBlendMode(.clear)
con.move(to: CGPoint(x:0, y:-4))
con.addLine(to: CGPoint(x:100, y:-4))
con.addLine(to: CGPoint(x:110, y:15))
con.addLine(to: CGPoint(x:120, y:-4))
con.addLine(to: CGPoint(x: self.bounds.maxX, y:-4))
con.addLine(to: CGPoint(x: self.bounds.maxX, y:-20))
con.addLine(to: CGPoint(x: 0, y:-20))
con.closePath()
con.setLineJoin(.round)
con.setLineWidth(10)
con.drawPath(using: .fillStroke) // stroke it and fill it
}
}
So then when I'm ready to cut out the notch on the red view, I just say:
self.redView.mask = MaskView(frame:self.redView.bounds)
I am trying to add a rectangular shape (curve as shown in picture) to my existing UIView.
.
This is the code I have implemented:
func drawRect(rect: CGRect) {
let y:CGFloat = 20
let curveTo:CGFloat = 0
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: rect.height))
myBezier.addLine(to: CGPoint(x: 0, y: rect.height))
myBezier.close()
let context = UIGraphicsGetCurrentContext()
context!.setLineWidth(4.0)
UIColor.yellow.setFill()
myBezier.fill()
}
Just to try and get a rectangle path to appear, however, when I open it in the simulator there is nothing. I believe it is because my other elements are overlayed on top of it is this correct? My full code is here
Thanks for your help.
Looking at your code. The UIViewController don't have overridden message drawRect. You should create custom class derived from UIView and override message drawRect there.
This could be made much more flexible, but it might suit your needs. If it doesn't, it could be a good starting point for you to get to what you want.
If you haven't looked at custom components / subclassing UIView / using IBInspectable and IBDesignable, this probably won't make much sense, so you might have some reading to do :)
//
// RoundedBottomImageView.swift
// SW3IBDesign
//
// Created by Don Mag on 2/27/17.
// Copyright © 2017 DonMag. All rights reserved.
//
import UIKit
#IBDesignable
class RoundedBottomImageView: UIView {
var imageView: UIImageView!
#IBInspectable var image: UIImage? {
didSet { self.imageView.image = image }
}
#IBInspectable var roundingValue: CGFloat = 0.0 {
didSet {
self.setNeedsLayout()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
doMyInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
doMyInit()
}
func doMyInit() {
imageView = UIImageView()
imageView.backgroundColor = UIColor.red
imageView.contentMode = UIViewContentMode.scaleToFill
addSubview(imageView)
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = self.bounds
let rect = self.bounds
let y:CGFloat = rect.size.height - roundingValue
let curveTo:CGFloat = rect.size.height
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
Example app: https://github.com/DonMag/IBDesignInspect
Edit: Example now includes buttons to demonstrate changing the image via code.
Edit2: Example app now has an Alternate Storyboard to show how the image can be "path clipped" without using a custom subclass.
How can I draw custom view as shown in image.
I want to draw a small rectangle in main view.
plaese refer any link if you have.
Image
Thank you
It could be as simple as below
import UIKit
class Rectangle:UIView{
override func draw(_ rect: CGRect) {
let path = UIBezierPath(rect: rect)
path.move(to: CGPoint(x:0,y:rect.height))
path.addLine(to: CGPoint(x:rect.width/2,y:rect.height/1.4))
path.addLine(to: CGPoint(x:rect.width,y:rect.height))
UIColor.red.setStroke()
path.stroke()
}
}
let rect=Rectangle(frame: CGRect(x: 0, y: 0, width: 100, height: 100))