Create a Diagonal Custom UIView in Swift - uiview

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
}
}

Related

How to set correct path for triangle along with stroke in CAShapLayer?

I've to create triangle with some border that fit's into another view. But the problem is, Once I set stroke then the Triangle is bigger than the container. So, I decided to calculate the inner triangle size
After applied border we'll have two triangle like Homothetic Transformation. I just want to calculate the size of the small triangle. So, the border will be fit into the container.
I tried to set it up, but I can't get it done for triangle. Because of the inner triangle center is not in the container center.
Please note that this is first try with equilateral Triangle.
Originally I want solve with same solution with any shape of Polygons.
I attached full demo xcode project at here.
Please help me to solve this problem. Thank you...
ShapeView.swift
import UIKit
class ShapeView: UIView {
var path: CGPath? {
didSet{
setNeedsDisplay(bounds)
}
}
var cornerRadius: CGFloat = .zero
var borderWidth: CGFloat{
set{
shapeLayer.lineWidth = newValue
makeTriangle()
}
get{
return shapeLayer.lineWidth
}
}
var borderColor: UIColor{
set{
shapeLayer.strokeColor = newValue.cgColor
}
get{
return UIColor(cgColor: shapeLayer.strokeColor ?? UIColor.clear.cgColor)
}
}
var background: UIColor{
set{
shapeLayer.fillColor = newValue.cgColor
setNeedsDisplay(bounds)
}
get{
return UIColor(cgColor: shapeLayer.fillColor ?? UIColor.clear.cgColor)
}
}
private lazy var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = .zero
layer.addSublayer(shapeLayer)
return shapeLayer
}()
func makeTriangle(){
// Homothetic Transformation
// https://math.stackexchange.com/questions/3952129/calculate-bounds-of-the-inner-rectangle-of-the-polygon-based-on-its-constant-bo
// https://stackoverflow.com/questions/65315093/cashapelayer-stroke-issue
let lineWidth: CGFloat = borderWidth/2
let size = (frame.width)-lineWidth
let rect = CGRect(x: lineWidth, y: lineWidth , width:size, height: size)
var cornerRadius: CGFloat = self.cornerRadius-lineWidth
cornerRadius = max(cornerRadius, 0)
let path = CGMutablePath()
path.move(to: CGPoint(x: rect.width/2.0, y: lineWidth))
path.addLine(to: CGPoint(x: lineWidth, y: rect.height - lineWidth))
path.addLine(to: CGPoint(x: rect.width - lineWidth, y: rect.height - lineWidth))
path.closeSubpath()
self.path = path
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let path = path else{
return
}
shapeLayer.path = path
}
}
Current Out:
Note that the expressions for the coordinates of the bounds of the inner triangle in the Math.SE post that you linked is not very useful here, because that post assumes an equilateral triangle. However, your triangle is not equilateral. Its base has the same length as its height. The view that it is in is a square after all.
After doing some trigonometry, I've found that the top of the inner triangle's bounding rect is insetted by lineWidth divided by sin(atan(0.5)). The bottom is insetted by lineWidth, obviously. And since the inner bounding rect also has to be a square, the left and right insets are both half of the sum of the top and bottom insets.
Here is a GeoGebra screenshot illustrating where sin(atan(0.5)) comes from (I'm rather bad with GeoGebra, so please forgive the mess):
The right half of the outer triangle has a base that is half of its height, so beta = atan(0.5). Then consider the triangle E'EA2, and notice that sin(beta) = lineWidth / EE', and EE' is the top inset that we want.
// makeTriangle
let lineWidth: CGFloat = borderWidth / 2
let top = lineWidth / sin(atan(0.5))
let bottom = lineWidth
let horizontal = (top + bottom) / 2
let rect = bounds.inset(by:
UIEdgeInsets(
top: top,
left: horizontal,
bottom: bottom,
right: horizontal
)
)
let path = CGMutablePath()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.closeSubpath()()
self.path = path
Now the outer triangle should fit neatly inside the square. But note that by default, CALayer property changes are animated, and the outer triangle could exceed the bounds of the square if you are sliding the slider too quickly, and the animations can't keep up. You can disable the animations by wrapping the code that sets CALayer properties into a CATransaction, and setDisableActions(true). For example in the setter of borderWidth:
set{
CATransaction.begin()
CATransaction.setDisableActions(true)
shapeLayer.lineWidth = newValue
makeTriangle()
CATransaction.commit()
}

Can't draw a line on a UIView

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

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.

Swift draw an arc for view background

I'd like to draw an arc at the bottom of a view like this:
The background pattern is another matter, but I think this can be done with something like uibezierpath? I haven't had much experience with it, any starter points? Thanks
Assign this custom class to the view in IB
class CurveView:UIView {
var once = true
override func layoutSubviews() {
super.layoutSubviews()
if once {
let bb = UIBezierPath()
bb.move(to: CGPoint(x: 0, y: self.frame.height))
// the offset here is 40 you can play with it to increase / decrease the curve height
bb.addQuadCurve(to: CGPoint(x: self.frame.width, y: self.frame.height), controlPoint: CGPoint(x: self.frame.width / 2 , y: self.frame.height + 40 ))
bb.close()
let l = CAShapeLayer()
l.path = bb.cgPath
l.fillColor = self.backgroundColor!.cgColor
self.layer.insertSublayer(l,at:0)
once = false
}
}
}
//

How to draw at the centre of a subview

I want to draw an arrow at the center of a SubView which is created by
{let arrowView = ArrowView()
arrowView.frame = CGRect(x: selectButton.frame.minX, y: selectButton.frame.maxY, width: selectButton.frame.width, height: processButton.frame.minY - selectButton.frame.maxY)
arrowView.backgroundColor = UIColor.white
arrowView.viewWithTag(100)
view.addSubview(arrowView)
} //these codes are in the view controller
To draw the arrow in the subview I use the
let arrowPath = UIBezierPath.bezierPathWithArrowFromPoint(startPoint: startPoint, endPoint: endPoint, tailWidth: 4, headWidth: 8, headLength: 6)
// the error occurs here
override func draw(_ rect: CGRect) {
let fillColor = UIColor.white
fillColor.setFill()
arrowPath.lineWidth = 1.0
let strokeColor = UIColor.blue
strokeColor.setStroke()
arrowPath.stroke()
arrowPath.fill()
//these codes are in the subclass of UIView
Since I want to draw the arrow at the center of the subview,
I define the startPoint and the endPoint as
private var startPoint: CGPoint {
return CGPoint(x: bounds.midX, y: bounds.minY)
}
private var endPoint: CGPoint {
return CGPoint(x: bounds.midX, y: bounds.maxY)
}
//try to find the point at the centre of Subview
However, this code does not compile and the error says:
Cannot use instance member 'startPoint' within property initializer; property initializers run before 'self' is available
Edit:
I would like to know how the get the bounds of a subview created by code.
I try the "bounds" variable but get the above error.
arrowPath.center = CGPoint(x:arrowView.frame.size.width/2, y: arrowView.frame.size.height/2)
arrowView.addSubview(arrowPath)

Resources