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.
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).
I am trying to detect Taps inside BezierPaths in a UIView and have found many references to the containsPoint method. However I cannot seem to find how to actually reference the BezierPaths from my ViewController.
I have set up:
class func drawSA(frame targetFrame: CGRect = CGRect(x: 0, y: 0, width: 69, height: 107), resizing: ResizingBehavior = .aspectFit, SACountries: [String: ViewController.CountryStruct])
{
let myPath = UIBezierPath()
myPath.move(to: CGPoint(x: 32.24, y: 8.61))
myPath.addLine(to: CGPoint(x: 31.99, y: 8.29))
myPath.addLine(to: CGPoint(x: 31.78, y: 8.19))
myPath.close()
}
The Beziers are drawn in this function, called by:
override func draw(_ rect: CGRect)
In the main ViewController, I have the following function for detecting a Tap on the UIView:
#objc func SATap(sender: UITapGestureRecognizer)
{
let location = sender.location(in: self.SAView)
// how do I call containsPoint from here?
}
How do I call containsPoint from here?
The bezierPaths are drawn correctly at runtime.
As the Paths are created in a Class Function within a separate Class, I cannot save them to the View directly.
I solved this by putting the UIBezierPaths into a Dictionary and then return the Dictionary. An Array would work too, but this way I can access specific Paths easily.
class func drawSA(frame targetFrame: CGRect = CGRect(x: 0, y: 0, width: 71, height: 120), resizing: ResizingBehavior = .aspectFit, SACountries: [String: ViewController.CountryStruct]) -> ([String: UIBezierPath])
{
var Paths = [String: UIBezierPath]()
let myPath = UIBezierPath()
myPath.move(to: CGPoint(x: 32.24, y: 8.61))
myPath.addLine(to: CGPoint(x: 31.99, y: 8.29))
myPath.addLine(to: CGPoint(x: 31.78, y: 8.19))
myPath.close()
Paths["mP"] = myPath
let myPath2 = UIBezierPath()
myPath2.move(to: CGPoint(x: 32.24, y: 8.61))
myPath2.addLine(to: CGPoint(x: 31.99, y: 8.29))
myPath2.addLine(to: CGPoint(x: 31.78, y: 8.19))
myPath2.close()
Paths["mP2"] = myPath2
return Paths
}
I then used View.addLayer to create Layers in the View and added the Layer.name property to each Layer when creating them with a For In Loop on the Dictionary:
for path in Paths.keys.sorted()
{
self.layer.addSublayer(CreateLayer(path)) // The Function simply creates a CAShapeLayer
}
I then used the Dictionary in the Gesture Function:
#objc func ATap(sender: UITapGestureRecognizer)
{
let location = sender.location(in: self.SAView)
// You need to Scale Location if your CAShapeLayers are Scaled
for path in SAView.Paths
{
let value = path.value
value.contains(location)
if value.contains(location)
{
for (index, layer) in SAView.layer.sublayers!.enumerated()
{
if layer.name == path.key
{
// Do something when specific layer is Tapped
}
}
}
}
}
There may well be better ways of doing this, but it's all working and performs well.
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!
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
}
}
I am trying to replicate the layer or view in iOS swift(the one saying follow and or the view with the followers)that is in the red box but i dont know really how to approach this.Should I try by making a view to then making it triangular?
-Try the following code
import UIKit
class animationView: UIView {
override func drawRect(rect: CGRect) {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 80, y: 50))
path.addLineToPoint(CGPoint(x: 140, y:150))
path.addLineToPoint(CGPoint(x: 10, y:150))
path.closePath()
UIColor.greenColor().setFill()
UIColor.redColor().setStroke()
path.lineWidth = 3
path.fill()
path.stroke()
}
}
You should have a triangle after this. Adjust the point's x and y value to correctly match your requirements.