Can't draw a line on a UIView - ios

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).

Related

How to get center point (CGPoint) of a UIView that has been set using autolayout

So I haven't seen any other SO question about this. Basically, I have set a UIView with autolayout, programmatically.
The issue occurs when I try to create a UIBezierPath:
let circularPath = UIBezierPath(arcCenter: myView.center, radius: (91*(1.2))/2 - (8/2), startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
The issue is that the myView.center prints out: 0.0, 0.0 since I did not set a CGRect .frame for the myView, and instead used autolayout (which is below):
NSLayoutConstraint.activate([
myView.heightAnchor.constraint(equalToConstant: 91),
myView.widthAnchor.constraint(equalToConstant: 91),
myView.centerXAnchor.constraint(equalToSystemSpacingAfter: myButton.centerXAnchor, multiplier: 1),
myView.centerYAnchor.constraint(equalToSystemSpacingBelow: myButton.centerYAnchor, multiplier: 1),
])
and also perhaps I should note that myView is added as a subview of myButton (though this shouldn't really matter..)
So how do I make this work? how could I get the .center (a type CGPoint) to use in the UIBezierPath? THANKS!
A few observations:
The center property is where the center of the current view is within the coordinate system of the superview. That’s not likely what you intended. You want the CGPoint(x: myView.bounds.midX, y: myView.bounds.midY), which is the midpoint of the current view.
Because you’re using constraints, you need to defer this creation of the path until after the constraints are applied, because that’s when the view’s bounds are set. The typical solution is to implement a UIButton subclass, and in that subclass you can implement layoutSubviews, and set the path there.
For example, a simple circular button might be defined as follows:
#IBDesignable
class CircleButton: UIButton {
let normalColor: UIColor = .red
let highlightedColor: UIColor = .init(red: 0.5, green: 0, blue: 0, alpha: 1)
lazy var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = normalColor.cgColor
return shapeLayer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
func configure() {
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.height, bounds.width) / 2
shapeLayer.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
}
override var isHighlighted: Bool {
didSet {
if isHighlighted {
shapeLayer.fillColor = highlightedColor.cgColor
} else {
shapeLayer.fillColor = normalColor.cgColor
}
}
}
}
The virtue of this approach is that:
Your view controller is not encumbered with lots of code for rendering of circles.
This will work regardless of size that is rendered (which can be useful if your constraints are not for a fixed size, but might respond to the orientation of the device, size classes, etc.).
If you want, because this is #IBDesignable, you can render it in your storyboards/NIBs.
You can try using bounds instead of frames. They may help you give the center coordinates of the UIVIew. The center property is where the center of the current view is within the coordinate system of the superview.  You can try to use the below code which is the midpoint of the current view.
CGPoint(x: myView.bounds.midX, y: myView.bounds.midY),

How to subtract the intersection of UIBezierPath A&B from A?

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.

Cant Change UIView Background Color From Black

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!

Create a Diagonal Custom UIView in Swift

I am working on designing custom UIimageview in swift. I want to create a UIimageview using beizerpath similar to this
The coding should be in swift.
Any help is appreciated. Thanks
Create a CAShapeLayer and supply it a path and a fillColor:
#IBDesignable
public class AngleView: UIView {
#IBInspectable public var fillColor: UIColor = .blue { didSet { setNeedsLayout() } }
var points: [CGPoint] = [
.zero,
CGPoint(x: 1, y: 0),
CGPoint(x: 1, y: 1),
CGPoint(x: 0, y: 0.5)
] { didSet { setNeedsLayout() } }
private lazy var shapeLayer: CAShapeLayer = {
let _shapeLayer = CAShapeLayer()
self.layer.insertSublayer(_shapeLayer, at: 0)
return _shapeLayer
}()
override public func layoutSubviews() {
shapeLayer.fillColor = fillColor.cgColor
guard points.count > 2 else {
shapeLayer.path = nil
return
}
let path = UIBezierPath()
path.move(to: convert(relativePoint: points[0]))
for point in points.dropFirst() {
path.addLine(to: convert(relativePoint: point))
}
path.close()
shapeLayer.path = path.cgPath
}
private func convert(relativePoint point: CGPoint) -> CGPoint {
return CGPoint(x: point.x * bounds.width + bounds.origin.x, y: point.y * bounds.height + bounds.origin.y)
}
}
Now, I made this designable (so if you put it in a separate framework target, you can add this view right in your storyboard and see it rendered there). It still works if you're not using storyboards. It just can be convenient to do so:
I also used relative coordinates (with values ranging from zero to one) and have a method to convert those to actual coordinates, but you can hard code your coordinates if you want. But using this as values from zero to one, you have an angular view that can participate in auto-layout without needing to worry about changing specific coordinate values.
Finally, minor things that might seem trivial, but I construct the path in layoutSubviews: That way, as the view changes size (whether via auto-layout or programmatic changes), the view will be correctly re-rendered. Likewise, by using didSet for fillColor and points, if you change either of those, the view will be re-rendered for you.
Feel free to change this as you see fit, but hopefully this illustrates the basic idea of just having a CAShapeLayer with a custom path.
If you use insertSublayer, you can then combine this with other subviews of the AngleView, e.g.:
I'm using something like this and it worked fine, you can add any thing you want to the view
import UIKit
class CustomView: UIView {
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
// Get Height and Width
let layerHeight = layer.frame.height
let layerWidth = layer.frame.width
// Create Path
let bezierPath = UIBezierPath()
// Points
let pointA = CGPoint(x: 0, y: 0)
let pointB = CGPoint(x: layerWidth, y: 0)
let pointC = CGPoint(x: layerWidth, y: layerHeight)
let pointD = CGPoint(x: 0, y: layerHeight*2/3)
// Draw the path
bezierPath.move(to: pointA)
bezierPath.addLine(to: pointB)
bezierPath.addLine(to: pointC)
bezierPath.addLine(to: pointD)
bezierPath.close()
// Mask to Path
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
layer.mask = shapeLayer
}
}

UIView subclassed with CAShapeLayer does not animate during device rotation

TL;DR:
Basically, my problem is the same as this question.
I followed the excellent answer (as always) by Rob Mayoff. My problem is that I am not using a CAGradientLayer but a CAShapeLayer and it does not work.
I use a CAShapeLayer instead of a UIBezierPath because I need to animate the path colour and shadow.
Though, the problem regards the shape of the path when the device rotates: the animation of CAShapeLayer looks awful, in fact, it does not even animate, It just immediately arrange the path to the new bounds.
I used to add the CAShapeLayer as a sublayer, but reading some answers here I tried to subclass UIView and set its layer to CAShapeLayer so that the view would manage the rotation animation automagically, but the result is the same.
If I draw using UIBezierPath it does work perfectly (but I need CAShapeLayer features, unfortunately)
Here is my code:
import UIKit
class CaShapeLayerView: UIView {
override class func layerClass () -> AnyClass{
return CAShapeLayer.self
}
override func drawRect(rect: CGRect) {
var bezierPath = UIBezierPath()
// create a random scribble :)
bezierPath.moveToPoint(CGPoint(x: 0, y: 0))
bezierPath.addCurveToPoint(CGPoint(x:self.bounds.width, y:self.bounds.height), controlPoint1: CGPoint(x: self.bounds.width/50, y: self.bounds.height/2), controlPoint2: CGPoint(x: self.bounds.width/40, y: self.bounds.height/8*10))
// draw UIBezierPath
UIColor.redColor().set()
bezierPath.lineWidth = 20
bezierPath.stroke()
// draw CAShapeLayer
var shapeLayer = self.layer as CAShapeLayer
shapeLayer.strokeColor = UIColor.blueColor().CGColor
shapeLayer.fillColor = nil
shapeLayer.lineWidth = 16
shapeLayer.path = bezierPath.CGPath
}
}
Rotating the device you will see the difference between the UIBezierPath in red and the CAShapeLayer in blue.
I assure the with my actual path the "animation" with CAShapeLayer is awful.
Am I missing something?
Thank you very much everybody.
This code at your drawRect isn't animatable:
UIColor.redColor().set()
bezierPath.lineWidth = 20
bezierPath.stroke()
That's why your image partially redraw on device rotation
You need something like this:
class CAShapeLayerView: UIView {
var firstShapeLayer, secondShapeLayer:CAShapeLayer
required init(coder aDecoder: NSCoder) {
firstShapeLayer = CAShapeLayer()
secondShapeLayer = CAShapeLayer()
super.init(coder: aDecoder)
}
override class func layerClass () -> AnyClass{
return CAShapeLayer.self
}
override func awakeFromNib() {
self.firstShapeLayer.lineWidth = 20
self.firstShapeLayer.strokeColor = UIColor.redColor().CGColor
self.firstShapeLayer.fillColor = nil
self.secondShapeLayer.strokeColor = UIColor.blueColor().CGColor
self.secondShapeLayer.fillColor = nil
self.secondShapeLayer.lineWidth = 16
self.layer.addSublayer(self.firstShapeLayer)
self.layer.addSublayer(self.secondShapeLayer)
}
override func layoutSubviews() {
super.layoutSubviews();
var bezierPath = UIBezierPath()
// create a random scribble :)
bezierPath.moveToPoint(CGPoint(x: 0, y: 0))
bezierPath.addCurveToPoint(CGPoint(x:self.bounds.width, y:self.bounds.height), controlPoint1: CGPoint(x: self.bounds.width/50, y: self.bounds.height/2), controlPoint2: CGPoint(x: self.bounds.width/40, y: self.bounds.height/8*10))
self.firstShapeLayer.path = bezierPath.CGPath
self.secondShapeLayer.path = bezierPath.CGPath
}
}

Resources