How to curve a view edge to looks like an arc - ios

I want to make an view to looks like this:
If there is a way, something like define point A, point B, with a predefined angle.
The only solution that I found is to make a giant rounded view and insert it as a subview of another view with clipToBounds = true. But this has some problems in multiple screen sizes because I'm using constraints.
Edit1: After some search, i'm trying to create that view with CAShapeLayer, without success. I'm creating that view by storyboard, with constraints, that view is connected by IBOutlet and your leading constraint too. Here's the code:
On viewDidLoad:
self.cnstRoundedLeading.constant = -(self.vwRounded.frame.width/3)
let maskPath : UIBezierPath = UIBezierPath(roundedRect: CGRect(x: self.vwRounded.bounds.minX*4,
y: self.vwRounded.bounds.minY*4,
width: self.vwRounded.bounds.width*4,
height: self.vwRounded.bounds.height*4),
byRoundingCorners: .topLeft,
cornerRadii: CGSize(width: self.vwRounded.frame.size.width*2,
height: self.vwRounded.frame.size.height))
let maskLayer : CAShapeLayer = CAShapeLayer()
maskLayer.frame = self.vwRounded.bounds
maskLayer.path = maskPath.cgPath
self.vwRounded.layer.mask = maskLayer
And on viewWillLayoutSubviews:
gradient2.colors = [startColorBlue.cgColor, endColorBlue.cgColor]
gradient2.locations = [0.0, 1.0]
gradient2.startPoint = CGPoint(x: 0, y: 1)
gradient2.endPoint = CGPoint(x: 1, y: 0)
vwRounded.applyGradient(gradient2)
applyGradient it's a extension of UIView:
func applyGradient(_ gradient: CAGradientLayer) -> Void {
gradient.frame = self.bounds
self.layer.insertSublayer(gradient, at: 0)
}
Does not work properly, i don't know the right way to construct that 'arc edge' effect

You can create that shape - and use it as a mask - with a UIBezierPath (the black border is showing the actual view frame):
Basically,
Find the midpoint of the line from pt1 to pt2.
Find the point perpendicular to that line, with a distance from the line that makes the curve look the way you want. Using the same length as half-the-line-length is what's shown here.
Create a UIBezierPath, connecting pt1 to pt2 with a quadratic curve.
Here is example code that you can run directly in a Playground page. I based the pt1 and pt2 y-positions based on the image you posted... If you change the frame of the view, it will maintain the proportions you've shown.
import PlaygroundSupport
import UIKit
class TestViewController: UIViewController {
override public var preferredContentSize: CGSize {
get { return CGSize(width: 800, height: 800) }
set { super.preferredContentSize = newValue }
}
let myPlainView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let myBorderView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
func customCurvedPath(for rect: CGRect) -> UIBezierPath {
// curve start point Y is 490/544ths of the height of the view
let curveStartPoint = CGPoint(x: 0.0, y: rect.size.height * 490.0 / 544.0)
// curve end point Y is 22/544ths of the height of the view
let curveEndPoint = CGPoint(x: rect.size.width, y: rect.size.height * 22.0 / 544.0)
var x1 = curveStartPoint.x
var y1 = curveStartPoint.y
let x2 = curveEndPoint.x
let y2 = curveEndPoint.y
// get the midpoint of the line from x1,y1 to x2,y2
x1 = (x1 + x2) / 2.0
y1 = (y1 + y2) / 2.0
// get the length of half the line (midpoint to endpoint)
var dx = x1 - x2
var dy = y1 - y2
let dist = sqrt(dx*dx + dy*dy)
// use length of helf the line for distance from line
// increase or decrease this value to get the desired curve
let distFromLine = dist
dx /= dist
dy /= dist
// get perpendicular point at distFromLine
let x3 = x1 - (distFromLine/2)*dy
let y3 = y1 + (distFromLine/2)*dx
let curveControlPoint = CGPoint(x: x3, y: y3)
let myBezier = UIBezierPath()
// pt1
myBezier.move(to: curveStartPoint)
// quad curve to pt2
myBezier.addQuadCurve(to: curveEndPoint, controlPoint: curveControlPoint)
// line to pt3 (bottom right corner)
myBezier.addLine(to: CGPoint(x: rect.width, y: rect.height))
// line to pt4 (bottom left corner)
myBezier.addLine(to: CGPoint(x: 0.0, y: rect.height))
// close the path (automatically add a line from bottom left corner to curve start point)
myBezier.close()
return myBezier
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let vwWidth = CGFloat(710.0)
let vwHeight = CGFloat(544.0)
view.addSubview(myBorderView)
myBorderView.backgroundColor = .clear
NSLayoutConstraint.activate([
myBorderView.widthAnchor.constraint(equalToConstant: vwWidth + 2.0),
myBorderView.heightAnchor.constraint(equalToConstant: vwHeight + 2.0),
myBorderView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myBorderView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
myBorderView.layer.borderWidth = 2.0
// comment this next line (or set to false) to see the actual view frame
myBorderView.isHidden = true
view.addSubview(myPlainView)
myPlainView.backgroundColor = .red
NSLayoutConstraint.activate([
myPlainView.widthAnchor.constraint(equalToConstant: vwWidth),
myPlainView.heightAnchor.constraint(equalToConstant: vwHeight),
myPlainView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myPlainView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
let bezPath = customCurvedPath(for: CGRect(x: 0, y: 0, width: vwWidth, height: vwHeight))
// add the bezier path as a layer mask
let maskForPath = CAShapeLayer()
maskForPath.path = bezPath.cgPath
myPlainView.layer.mask = maskForPath
}
}
let viewController = TestViewController()
PlaygroundPage.current.liveView = viewController
As I mentioned, this would work better as part of a custom view class, as you could override layoutSubviews() to keep the path shape consistent.
Here's an example using a gradient layer + a layer mask in a custom view, set to 300 x 250:
And the Playground-runnable source:
import PlaygroundSupport
import UIKit
class MaskedGradientView: UIView {
var gradLayer: CAGradientLayer!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
gradLayer = CAGradientLayer()
gradLayer.colors = [UIColor.blue.cgColor, UIColor.cyan.cgColor]
gradLayer.locations = [0.0, 1.0]
gradLayer.startPoint = CGPoint(x: 0, y: 1)
gradLayer.endPoint = CGPoint(x: 1, y: 0)
layer.addSublayer(gradLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
let rect = self.bounds
gradLayer.frame = self.bounds
// curve start point Y is 490/544ths of the height of the view
let curveStartPoint = CGPoint(x: 0.0, y: rect.size.height * 490.0 / 544.0)
// curve end point Y is 22/544ths of the height of the view
let curveEndPoint = CGPoint(x: rect.size.width, y: rect.size.height * 22.0 / 544.0)
var x1 = curveStartPoint.x
var y1 = curveStartPoint.y
let x2 = curveEndPoint.x
let y2 = curveEndPoint.y
// get the midpoint of the line from x1,y1 to x2,y2
x1 = (x1 + x2) / 2.0
y1 = (y1 + y2) / 2.0
// get the length of half the line (midpoint to endpoint)
var dx = x1 - x2
var dy = y1 - y2
let dist = sqrt(dx*dx + dy*dy)
// use length of helf the line for distance from line
// increase or decrease this value to get the desired curve
let distFromLine = dist
dx /= dist
dy /= dist
// get perpendicular point at distFromLine
let x3 = x1 - (distFromLine/2)*dy
let y3 = y1 + (distFromLine/2)*dx
let curveControlPoint = CGPoint(x: x3, y: y3)
let myBezier = UIBezierPath()
// pt1
myBezier.move(to: curveStartPoint)
// quad curve to pt2
myBezier.addQuadCurve(to: curveEndPoint, controlPoint: curveControlPoint)
// line to pt3 (bottom right corner)
myBezier.addLine(to: CGPoint(x: rect.width, y: rect.height))
// line to pt4 (bottom left corner)
myBezier.addLine(to: CGPoint(x: 0.0, y: rect.height))
// close the path (automatically add a line from bottom left corner to curve start point)
myBezier.close()
// add the bezier path as a layer mask
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
class TestViewController: UIViewController {
override public var preferredContentSize: CGSize {
get { return CGSize(width: 400, height: 400) }
set { super.preferredContentSize = newValue }
}
let myMaskedGradientView: MaskedGradientView = {
let v = MaskedGradientView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let myPlainView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(myMaskedGradientView)
NSLayoutConstraint.activate([
myMaskedGradientView.widthAnchor.constraint(equalToConstant: 300.0),
myMaskedGradientView.heightAnchor.constraint(equalToConstant: 250.0),
myMaskedGradientView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myMaskedGradientView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
let viewController = TestViewController()
PlaygroundPage.current.liveView = viewController

Related

Swift 5 and UIKit draw, animate and split lines between 2 to 3 UIViews [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 1 year ago.
Improve this question
I have a view that may contains 2 or 3 UIViews.
I want to draw (and possibly animate a line from the bottom MidX of the higher view to the bottom one.
If I have 3 views I want the line to split and animate to both of them.
If I have a single view I want the line to go directly to the middle top of the bottom view all this using UIBezierPath and CAShapeLayer
All this considering screen height (4.7" -> 6.2") I have attached images to illustrate what I want to achieve.
Thanks for the help.
You're on the right track...
The problem with drawing a "split" line is that there is one start point and TWO end points. So, the resulting animation may not be what you really want.
Another approach would be to use TWO layers - one with the "left-side" split line and one with the "right-side" split line, then animate them together.
Here's an example of wrapping things into a "Connect" view subclass.
We'll use 3 layers: 1 for the single vertical connecting line and one each for the right-side and left-side lines.
We can also set the path points to the center of the view, and the left and right edges. That way we can constrain the Leading edge to the center of the left-box, and the trailing edge to the center of the right-box.
This view, by itself, will look like this (with a yellow background so we can see its frame):
or:
With the lines will be animated from the top.
class ConnectView: UIView {
// determines whether we want a single box-to-box line, or
// left and right split / stepped lines to two boxes
public var single: Bool = true
private let singleLineLayer = CAShapeLayer()
private let leftLineLayer = CAShapeLayer()
private let rightLineLayer = CAShapeLayer()
private var durationFactor: CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add and configure sublayers
[singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
layer.addSublayer(lay)
lay.lineWidth = 4
lay.strokeColor = UIColor.blue.cgColor
lay.fillColor = UIColor.clear.cgColor
}
}
override func layoutSubviews() {
super.layoutSubviews()
// for readablility, define the points for our lines
let topCenter = CGPoint(x: bounds.midX, y: 0)
let midCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let botCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let midLeft = CGPoint(x: bounds.minX, y: bounds.midY)
let midRight = CGPoint(x: bounds.maxX, y: bounds.midY)
let botLeft = CGPoint(x: bounds.minX, y: bounds.maxY)
let botRight = CGPoint(x: bounds.maxX, y: bounds.maxY)
let singleBez = UIBezierPath()
let leftBez = UIBezierPath()
let rightBez = UIBezierPath()
// vertical line
singleBez.move(to: topCenter)
singleBez.addLine(to: botCenter)
// split / stepped line to the left
leftBez.move(to: topCenter)
leftBez.addLine(to: midCenter)
leftBez.addLine(to: midLeft)
leftBez.addLine(to: botLeft)
// split / stepped line to the right
rightBez.move(to: topCenter)
rightBez.addLine(to: midCenter)
rightBez.addLine(to: midRight)
rightBez.addLine(to: botRight)
// set the layer paths
// initializing strokeEnd to 0 for all three
singleLineLayer.path = singleBez.cgPath
singleLineLayer.strokeEnd = 0
leftLineLayer.path = leftBez.cgPath
leftLineLayer.strokeEnd = 0
rightLineLayer.path = rightBez.cgPath
rightLineLayer.strokeEnd = 0
// calculate total line lengths (in points)
// so we can adjust the "draw speed" in the animation
let singleLength = botCenter.y - topCenter.y
let doubleLength = singleLength + (midCenter.x - midLeft.x)
durationFactor = singleLength / doubleLength
}
public func doAnim() -> Void {
// reset the animations
[singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
lay.removeAllAnimations()
lay.strokeEnd = 0
}
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
if self.single {
// we want the apparent drawing speed to be the same
// for a single line as for a split / stepped line
// so change the animation duration
animation.duration *= durationFactor
// animate the single line layer
self.singleLineLayer.add(animation, forKey: animation.keyPath)
} else {
// animate the both left and right line layers
self.leftLineLayer.add(animation, forKey: animation.keyPath)
self.rightLineLayer.add(animation, forKey: animation.keyPath)
}
}
}
and a sample view controller showing it in action:
class ConnectTestViewController: UIViewController {
let vTop = UIView()
let vLeft = UIView()
let vCenter = UIView()
let vRight = UIView()
let testConnectView = ConnectView()
override func viewDidLoad() {
super.viewDidLoad()
// give the 4 views different background colors
// add them as subviews
// make them all 100x100 points
let colors: [UIColor] = [
.systemYellow,
.systemRed, .systemGreen, .systemBlue,
]
for (v, c) in zip([vTop, vLeft, vCenter, vRight], colors) {
v.backgroundColor = c
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
v.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
}
// add the clear-background Connect View
testConnectView.backgroundColor = .clear
testConnectView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testConnectView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// horizontally center the top box near the top
vTop.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
vTop.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// horizontally center the center box, 200-pts below the top box
vCenter.topAnchor.constraint(equalTo: vTop.bottomAnchor, constant: 200.0),
vCenter.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// align tops of left and right boxes with center box
vLeft.topAnchor.constraint(equalTo: vCenter.topAnchor),
vRight.topAnchor.constraint(equalTo: vCenter.topAnchor),
// position left and right boxes to left and right of center box
vLeft.trailingAnchor.constraint(equalTo: vCenter.leadingAnchor, constant: -20.0),
vRight.leadingAnchor.constraint(equalTo: vCenter.trailingAnchor, constant: 20.0),
// constrain Connect View
// Top to Bottom of Top box
testConnectView.topAnchor.constraint(equalTo: vTop.bottomAnchor),
// Bottom to Top of the row of 3 boxes
testConnectView.bottomAnchor.constraint(equalTo: vCenter.topAnchor),
// Leading to CenterX of Left box
testConnectView.leadingAnchor.constraint(equalTo: vLeft.centerXAnchor),
// Trailing to CenterX of Right box
testConnectView.trailingAnchor.constraint(equalTo: vRight.centerXAnchor),
])
// add a couple buttons at the bottom
let stack = UIStackView()
stack.spacing = 20
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
["Run Anim", "Show/Hide"].forEach { str in
let b = UIButton()
b.setTitle(str, for: [])
b.backgroundColor = .red
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.addTarget(self, action: #selector(buttonTap(_:)), for: .touchUpInside)
stack.addArrangedSubview(b)
}
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
stack.heightAnchor.constraint(equalToConstant: 50.0),
])
}
#objc func buttonTap(_ sender: Any?) -> Void {
guard let b = sender as? UIButton,
let t = b.currentTitle
else {
return
}
if t == "Run Anim" {
// tap button to toggle between
// Top-to-Middle box line or
// Top-to-SideBoxes split / stepped line
testConnectView.single.toggle()
// run the animation
testConnectView.doAnim()
} else {
// toggle background of Connect View between
// clear and yellow
testConnectView.backgroundColor = testConnectView.backgroundColor == .clear ? .yellow : .clear
}
}
}
Running that will give this result:
The first button at the bottom will toggle the connection between Top-Center and Top-Left-Right (re-running the animation each time). The second button will toggle the view's background color between clear and yellow so we can see its frame.
Edit
If you want rounded "step" corners that look like this:
replace the layoutSubviews() code above with this:
override func layoutSubviews() {
super.layoutSubviews()
// for readablility, define the points for our lines
let topCenter = CGPoint(x: bounds.midX, y: 0)
let midCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let botCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let midLeft = CGPoint(x: bounds.minX, y: bounds.midY)
let midRight = CGPoint(x: bounds.maxX, y: bounds.midY)
let botLeft = CGPoint(x: bounds.minX, y: bounds.maxY)
let botRight = CGPoint(x: bounds.maxX, y: bounds.maxY)
let singleBez = UIBezierPath()
let leftBez = UIBezierPath()
let rightBez = UIBezierPath()
// vertical line
singleBez.move(to: topCenter)
singleBez.addLine(to: botCenter)
// rounded "step" corners
let radius: CGFloat = 20.0
let leftArcP = CGPoint(x: midLeft.x + radius, y: midLeft.y)
let leftArcC = CGPoint(x: midLeft.x + radius, y: midLeft.y + radius)
let rightArcP = CGPoint(x: midRight.x - radius, y: midRight.y)
let rightArcC = CGPoint(x: midRight.x - radius, y: midRight.y + radius)
// split / stepped line to the left
leftBez.move(to: topCenter)
leftBez.addLine(to: midCenter)
leftBez.addLine(to: leftArcP)
leftBez.addArc(withCenter: leftArcC, radius: radius, startAngle: .pi * 1.5, endAngle: .pi, clockwise: false)
leftBez.addLine(to: botLeft)
// split / stepped line to the right
rightBez.move(to: topCenter)
rightBez.addLine(to: midCenter)
rightBez.addLine(to: rightArcP)
rightBez.addArc(withCenter: rightArcC, radius: radius, startAngle: .pi * 1.5, endAngle: 0, clockwise: true)
rightBez.addLine(to: botRight)
// set the layer paths
// initializing strokeEnd to 0 for all three
singleLineLayer.path = singleBez.cgPath
singleLineLayer.strokeEnd = 0
leftLineLayer.path = leftBez.cgPath
leftLineLayer.strokeEnd = 0
rightLineLayer.path = rightBez.cgPath
rightLineLayer.strokeEnd = 0
// calculate total line lengths (in points)
// so we can adjust the "draw speed" in the animation
let singleLength = botCenter.y - topCenter.y
let doubleLength = singleLength + (midCenter.x - midLeft.x)
durationFactor = singleLength / doubleLength
}
Well after some research I have come up with this solution for Swift 5:
class ViewController: UIViewController {
#IBOutlet weak var someView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let start = CGPoint(x: self.someView.bounds.midX, y: self.someView.bounds.maxY)
let end = CGPoint(x: self.someView.layer.bounds.midX, y: (UIScreen.main.bounds.height / 2) - 100)
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
linePath.addLine(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.move(to: end)
linePath.addLine(to: CGPoint(x: 250, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.addLine(to: CGPoint(x: lowerViewA.x, y: lowerViewA.y))
linePath.move(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.addLine(to: CGPoint(x: lowerViewB.x, y: lowerViewB.y))
let shapeLayer = CAShapeLayer()
shapeLayer.path = linePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 2
shapeLayer.lineJoin = CAShapeLayerLineJoin.bevel
self.someView.layer.addSublayer(shapeLayer)
//Basic animation if you want to animate the line drawing.
let pathAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 4.0
pathAnimation.fromValue = 0.0
pathAnimation.toValue = 1.0
//Animation will happen right away
shapeLayer.add(pathAnimation, forKey: "strokeEnd")
}
}

Corner radius image Swift

I'm trying to make this corner radius image...it's not exactly the same shape of the image..any easy answer instead of trying random numbers of width and height ?
thanks alot
let rectShape = CAShapeLayer()
rectShape.bounds = self.mainImg.frame
rectShape.position = self.mainImg.center
rectShape.path = UIBezierPath(roundedRect: self.mainImg.bounds, byRoundingCorners: [.bottomLeft , .bottomRight ], cornerRadii: CGSize(width: 50, height: 4)).cgPath
You can use QuadCurve to get the design you want.
Here is a Swift #IBDesignable class that lets you specify the image and the "height" of the rounding in Storyboard / Interface Builder:
#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.scaleAspectFill
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 + roundingValue
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
}
}
Result with 300 x 200 image view, rounding set to 40:
Edit - (3.5 years later)...
To answer #MiteshDobareeya comment, we can switch the rounded edge from Bottom to Top by transforming the bezier path:
let c = CGAffineTransform(scaleX: 1, y: -1).concatenating(CGAffineTransform(translationX: 0, y: bounds.size.height))
myBezier.apply(c)
It's been quite a while since this answer was originally posted, so a few changes:
subclass UIImageView directly - no need to make it a UIView with an embedded UIImageView
add a Bool roundTop var
if set to False (the default), we round the Bottom
if set to True, we round the Top
re-order and "name" our path points for clarity
So, the basic principle:
We create a UIBezierPath and:
move to pt1
add a line to pt2
add a line to pt3
add a quad-curve to pt4 with controlPoint
close the path
use that path for a CAShapeLayer mask
the result:
If we want to round the Top, after closing the path we can apply apply a scale transform using -1 as the y value to vertically mirror it. Because that transform mirror it at "y-zero" we also apply a translate transform to move it back down into place.
That gives us:
Here's the updated class:
#IBDesignable
class RoundedTopBottomImageView: UIImageView {
#IBInspectable var roundingValue: CGFloat = 0.0 {
didSet {
self.setNeedsLayout()
}
}
#IBInspectable var roundTop: Bool = false {
didSet {
self.setNeedsLayout()
}
}
override func layoutSubviews() {
super.layoutSubviews()
let r = bounds
let myBezier = UIBezierPath()
let pt1: CGPoint = CGPoint(x: r.minX, y: r.minY)
let pt2: CGPoint = CGPoint(x: r.maxX, y: r.minY)
let pt3: CGPoint = CGPoint(x: r.maxX, y: r.maxY - roundingValue)
let pt4: CGPoint = CGPoint(x: r.minX, y: r.maxY - roundingValue)
let controlPoint: CGPoint = CGPoint(x: r.midX, y: r.maxY + roundingValue)
myBezier.move(to: pt1)
myBezier.addLine(to: pt2)
myBezier.addLine(to: pt3)
myBezier.addQuadCurve(to: pt4, controlPoint: controlPoint)
myBezier.close()
if roundTop {
// if we want to round the Top instead of the bottom,
// flip the path vertically
let c = CGAffineTransform(scaleX: 1, y: -1) //.concatenating(CGAffineTransform(translationX: 0, y: bounds.size.height))
myBezier.apply(c)
}
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
You can try with UIView extension. as
extension UIView {
func setBottomCurve(){
let offset = CGFloat(self.frame.size.height + self.frame.size.height/1.8)
let bounds = self.bounds
let rectBounds = CGRect(x: bounds.origin.x,
y: bounds.origin.y ,
width: bounds.size.width,
height: bounds.size.height / 2)
let rectPath = UIBezierPath(rect: rectBounds)
let ovalBounds = CGRect(x: bounds.origin.x - offset / 2,
y: bounds.origin.y ,
width: bounds.size.width + offset,
height: bounds.size.height)
let ovalPath = UIBezierPath(ovalIn: ovalBounds)
rectPath.append(ovalPath)
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = rectPath.cgPath
self.layer.mask = maskLayer
}
}
& use it in viewWillAppear like methods where you can get actual frame of UIImageView.
Usage:
override func viewWillAppear(_ animated: Bool) {
//use it in viewWillAppear like methods where you can get actual frame of UIImageView
myImageView.setBottomCurve()
}

CAShapeLayers added to a Stackview subviews is rendering only on one subview

The calorieView and timeView are expected to display a black circle. But
the distanceView is displaying the expected black circle.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a
let allViews: [UIView] = [distanceView, calorieView, timeView]
let stackView = UIStackView(arrangedSubviews: allViews)
stackView.frame = view.frame
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.spacing = 0
let y1 = CGFloat(0)
let y2 = view.frame.height / 3
let y3 = y2 * 2
distanceView.frame = CGRect(x: 0, y: y1, width: view.frame.width, height: y2)
calorieView.frame = CGRect(x: 0, y: y2, width: view.frame.width, height: y2)
timeView.frame = CGRect(x: 0, y: y3, width: view.frame.width, height: y2)
distanceView.layer.addSublayer(self.createViewLayer(localView: distanceView))
calorieView.layer.addSublayer(self.createViewLayer(localView: calorieView))
timeView.layer.addSublayer(createViewLayer(localView: timeView))
view.addSubview(stackView)
}
func createCircleLayer(center: CGPoint, radius: CGFloat, clockWise:Bool) -> CAShapeLayer {
let shapeLayer = CAShapeLayer()
let path = UIBezierPath
(arcCenter: center, radius: radius, startAngle: 0,
endAngle: 2 * CGFloat.pi, clockwise: clockWise)
shapeLayer.path = path.cgPath
return shapeLayer
}
func createViewLayer(localView: UIView) -> CAShapeLayer {
print(localView)
var viewLayer = CAShapeLayer()
viewLayer.frame = localView.frame
let center = localView.center
print(center)
let radius = localView.frame.height / 3
let isClockwise = true
viewLayer = self.createCircleLayer
(center: center, radius: radius, clockWise: isClockwise)
return viewLayer
}
}
What you are doing wrong is setting y coordinates for your views, basically doing the work that vertical UIStackView will do.
When using a stack view all views should have x and y coordinates set to 0,0, since it will be up to UIStackView to position arrangedViews according to it's settings.
Just change your frames to have origin = (0,0) or don't use UIStackView and use the calculated y coordinates...

Creating a curly bracket curve from two points

I'm trying to create a curly bracket in Swift, from two points. The idea works fine, with a straight line, because it's currently not dynamic in anyway. My issue lies in finding the dynamic control points and center depending on the location of p1 and p2 points.
This is my current code:
override func viewDidLoad() {
super.viewDidLoad()
let path = UIBezierPath()
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 100)
let c1 = CGPointMake(150, 80)
let c2 = CGPointMake(250, 80)
var midPoint = midPointForPoints(p1, p2: p2)
var midP1 = midPoint
midP1.x -= 10
var midP2 = midPoint
midP2.x += 10
midPoint.y -= 20
path.moveToPoint(p1)
path.addQuadCurveToPoint(midP1, controlPoint: c1)
path.addLineToPoint(midPoint)
path.addLineToPoint(midP2)
path.addQuadCurveToPoint(p2, controlPoint: c2)
let shape = CAShapeLayer()
shape.lineWidth = 5
shape.strokeColor = UIColor.redColor().CGColor
shape.fillColor = UIColor.clearColor().CGColor
shape.path = path.CGPath
self.view.layer.addSublayer(shape)
}
func midPointForPoints(p1: CGPoint, p2: CGPoint)->CGPoint{
let deltaX = (p1.x + p2.x)/2
let deltaY = (p1.y + p2.y)/2
let midPoint = CGPointMake(deltaX, deltaY)
return midPoint
}
This doesen't take the degrees of the points into account, so if I were to create the two points as:
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 300)
It would not find the proper control points and midpoint.
Hope someone can help me in the right direction. The idea is of course in the end to just know the two points (p1, p2) and dynamically create every other points, I just typed in values for the moment, to make it easier for myself. I've added images of the issue to better show you.
First create a path for a brace that starts at (0, 0) and ends at (1, 0). Then apply an affine transformation that moves, scales, and rotates the path to span your designed endpoints. It needs to transform (0, 0) to your start point and (1, 0) to your end point. Creating the transformation efficiently requires some trigonometry, but I've done the homework for you:
extension UIBezierPath {
class func brace(from start: CGPoint, to end: CGPoint) -> UIBezierPath {
let path = self.init()
path.move(to: .zero)
path.addCurve(to: CGPoint(x: 0.5, y: -0.1), controlPoint1: CGPoint(x: 0, y: -0.2), controlPoint2: CGPoint(x: 0.5, y: 0.1))
path.addCurve(to: CGPoint(x: 1, y: 0), controlPoint1: CGPoint(x: 0.5, y: 0.1), controlPoint2: CGPoint(x: 1, y: -0.2))
let scaledCosine = end.x - start.x
let scaledSine = end.y - start.y
let transform = CGAffineTransform(a: scaledCosine, b: scaledSine, c: -scaledSine, d: scaledCosine, tx: start.x, ty: start.y)
path.apply(transform)
return path
}
}
Result:
Here's the entire Swift playground I used to make the demo:
import UIKit
import PlaygroundSupport
extension UIBezierPath {
class func brace(from start: CGPoint, to end: CGPoint) -> UIBezierPath {
let path = self.init()
path.move(to: .zero)
path.addCurve(to: CGPoint(x: 0.5, y: -0.1), controlPoint1: CGPoint(x: 0, y: -0.2), controlPoint2: CGPoint(x: 0.5, y: 0.1))
path.addCurve(to: CGPoint(x: 1, y: 0), controlPoint1: CGPoint(x: 0.5, y: 0.1), controlPoint2: CGPoint(x: 1, y: -0.2))
let scaledCosine = end.x - start.x
let scaledSine = end.y - start.y
let transform = CGAffineTransform(a: scaledCosine, b: scaledSine, c: -scaledSine, d: scaledCosine, tx: start.x, ty: start.y)
path.apply(transform)
return path
}
}
class ShapeView: UIView {
override class var layerClass: Swift.AnyClass { return CAShapeLayer.self }
lazy var shapeLayer: CAShapeLayer = { self.layer as! CAShapeLayer }()
}
class ViewController: UIViewController {
override func loadView() {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 600, height: 200))
view.backgroundColor = .white
for (i, handle) in handles.enumerated() {
handle.autoresizingMask = [ .flexibleTopMargin, .flexibleTopMargin, .flexibleBottomMargin, .flexibleRightMargin ]
let frame = CGRect(x: view.bounds.width * 0.1 + CGFloat(i) * view.bounds.width * 0.8 - 22, y: view.bounds.height / 2 - 22, width: 44, height: 44)
handle.frame = frame
handle.shapeLayer.path = CGPath(ellipseIn: handle.bounds, transform: nil)
handle.shapeLayer.lineWidth = 2
handle.shapeLayer.lineDashPattern = [2, 6]
handle.shapeLayer.lineCap = kCALineCapRound
handle.shapeLayer.strokeColor = UIColor.blue.cgColor
handle.shapeLayer.fillColor = nil
view.addSubview(handle)
let panner = UIPanGestureRecognizer(target: self, action: #selector(pannerDidFire(panner:)))
handle.addGestureRecognizer(panner)
}
brace.shapeLayer.lineWidth = 2
brace.shapeLayer.lineCap = kCALineCapRound
brace.shapeLayer.strokeColor = UIColor.black.cgColor
brace.shapeLayer.fillColor = nil
view.addSubview(brace)
setBracePath()
self.view = view
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setBracePath()
}
private let handles: [ShapeView] = [
ShapeView(),
ShapeView()
]
private let brace = ShapeView()
private func setBracePath() {
brace.shapeLayer.path = UIBezierPath.brace(from: handles[0].center, to: handles[1].center).cgPath
}
#objc private func pannerDidFire(panner: UIPanGestureRecognizer) {
let view = panner.view!
let offset = panner.translation(in: view)
panner.setTranslation(.zero, in: view)
var center = view.center
center.x += offset.x
center.y += offset.y
view.center = center
setBracePath()
}
}
let vc = ViewController()
PlaygroundPage.current.liveView = vc.view
The key to the problem is when the figure is rotated your base vectors will rotate. When your figure is axis-aligned your base vectors are u (1, 0) and v (0, 1).
So when you are performing midPoint.y -= 20 you can see it as the same as midPoint.x -= v.x * 20; midPoint.y -= v.y * 20 where v is (0, 1). The results are the same, check for yourself.
This implementation will do what your code does, only axis independent.
let path = UIBezierPath()
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 100)
let o = p1.plus(p2).divide(2.0) // origo
let u = p2.minus(o) // base vector 1
let v = u.turn90() // base vector 2
let c1 = o.minus(u.times(0.5)).minus(v.times(0.2)) // CGPointMake(150, 80)
let c2 = o.plus(u.times(0.5)).minus(v.times(0.2)) // CGPointMake(250, 80)
var midPoint = o.minus(v.times(0.2))
var midP1 = o.minus(u.times(0.2))
var midP2 = o.plus(u.times(0.2))
Note: I set the factors to match the initial values in your implementation.
Also added this CGPoint extension for convenience. Hope it helps.
extension CGPoint {
public func plus(p: CGPoint) -> (CGPoint)
{
return CGPoint(x: self.x + p.x, y: self.y + p.y)
}
public func minus(p: CGPoint) -> (CGPoint)
{
return CGPoint(x: self.x - p.x, y: self.y - p.y)
}
public func times(f: CGFloat) -> (CGPoint)
{
return CGPoint(x: self.x * f, y: self.y * f)
}
public func divide(f: CGFloat) -> (CGPoint)
{
return self.times(1.0/f)
}
public func turn90() -> (CGPoint)
{
return CGPoint(x: -self.y, y: x)
}
}

EXC_BAD_INSTRUCTION when trying to pass data to UIViews

I created a call of a UIView in which I draw a graph. I am trying to pass it new data and have it update.
When I run the app on the simulator and click the tab in which the the controller housing the view is in, I receive this error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_1386_INVOP, subcode=0x0)
at the line:
let maxValue = graphPoints.maxElement()
Here is my code for the view:
#IBDesignable class GraphView: UIView {
var graphPoints :[Int]!
override init(frame: CGRect) {
super.init(frame: frame)
}
init(graphPoints: [Int]) {
self.graphPoints = graphPoints
super.init(frame: CGRectZero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
#IBInspectable var startColor: UIColor = UIColor.redColor()
#IBInspectable var endColor: UIColor = UIColor.greenColor()
override func drawRect(rect: CGRect) {
let width = rect.width
let height = rect.height
//set up background clipping area
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: UIRectCorner.AllCorners,
cornerRadii: CGSize(width: 8.0, height: 8.0))
path.addClip()
//2 - get the current context
let context = UIGraphicsGetCurrentContext()
let colors = [startColor.CGColor, endColor.CGColor]
//3 - set up the color space
let colorSpace = CGColorSpaceCreateDeviceRGB()
//4 - set up the color stops
let colorLocations:[CGFloat] = [0.0, 1.0]
//5 - create the gradient
let gradient = CGGradientCreateWithColors(colorSpace,
colors,
colorLocations)
//6 - draw the gradient
var startPoint = CGPoint.zeroPoint
var endPoint = CGPoint(x:0, y:self.bounds.height)
CGContextDrawLinearGradient(context,
gradient,
startPoint,
endPoint,
.DrawsBeforeStartLocation)
//calculate the x point
let rightMargin:CGFloat = 40
let leftMargin : CGFloat = 10
let columnXPoint = { (column:Int) -> CGFloat in
//Calculate gap between points
let spacer = (width - rightMargin - leftMargin - 4) /
CGFloat((self.graphPoints.count - 1))
var x:CGFloat = CGFloat(column) * spacer
x += leftMargin + 2
print(x)
return x
}
// calculate the y point
let topBorder:CGFloat = 30
let bottomBorder:CGFloat = 50
let graphHeight = height - topBorder - bottomBorder
let maxValue = graphPoints.maxElement()
let columnYPoint = { (graphPoint:Int) -> CGFloat in
var y:CGFloat = CGFloat(graphPoint) /
CGFloat(maxValue!) * graphHeight
y = graphHeight + topBorder - y // Flip the graph
print(y)
return y
}
// draw the line graph
UIColor.whiteColor().setFill()
UIColor.whiteColor().setStroke()
//set up the points line
let graphPath = UIBezierPath()
//go to start of line
graphPath.moveToPoint(CGPoint(x:columnXPoint(0), y:columnYPoint(graphPoints[0])))
//add points for each item in the graphPoints array
//at the correct (x, y) for the point
for i in 1..<graphPoints.count {
let nextPoint = CGPoint(x:columnXPoint(i),
y:columnYPoint(graphPoints[i]))
graphPath.addLineToPoint(nextPoint)
}
//Create the clipping path for the graph gradient
//1 - save the state of the context (commented out for now)
CGContextSaveGState(context)
//2 - make a copy of the path
let clippingPath = graphPath.copy() as! UIBezierPath
//3 - add lines to the copied path to complete the clip area
clippingPath.addLineToPoint(CGPoint(
x: columnXPoint(graphPoints.count - 1),
y:height))
clippingPath.addLineToPoint(CGPoint(
x:columnXPoint(0),
y:height))
clippingPath.closePath()
//4 - add the clipping path to the context
clippingPath.addClip()
let highestYPoint = columnYPoint(maxValue!)
startPoint = CGPoint(x:leftMargin, y: highestYPoint)
endPoint = CGPoint(x:rightMargin, y:self.bounds.height)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, .DrawsBeforeStartLocation)
CGContextRestoreGState(context)
//draw the line on top of the clipped gradient
graphPath.lineWidth = 2.0
graphPath.stroke()
//Draw the circles on top of graph stroke
for i in 0..<graphPoints.count {
var point = CGPoint(x:columnXPoint(i), y:columnYPoint(graphPoints[i]))
point.x -= 5.0/2
point.y -= 5.0/2
let circle = UIBezierPath(ovalInRect:
CGRect(origin: point,
size: CGSize(width: 5.0, height: 5.0)))
circle.fill()
}
//Draw horizontal graph lines on the top of everything
let linePath = UIBezierPath()
//top line
linePath.moveToPoint(CGPoint(x:leftMargin, y: topBorder))
linePath.addLineToPoint(CGPoint(x: width - rightMargin,
y:topBorder))
//center line
linePath.moveToPoint(CGPoint(x:leftMargin,
y: graphHeight/2 + topBorder))
linePath.addLineToPoint(CGPoint(x:width - rightMargin,
y:graphHeight/2 + topBorder))
//bottom line
linePath.moveToPoint(CGPoint(x:leftMargin,
y:height - bottomBorder))
linePath.addLineToPoint(CGPoint(x:width - rightMargin,
y:height - bottomBorder))
let color = UIColor(white: 1.0, alpha: 0.3)
color.setStroke()
linePath.lineWidth = 1.0
linePath.stroke()
}
And here is the code for my view controller in which I pass data to the view.
import UIKit
import QuartzCore
class ProgressViewController: UIViewController {
#IBOutlet weak var graphView: UIView!
var firstGraph : GraphView!
override func viewDidLoad() {
self.graphView = self.firstGraph
self.firstGraph = GraphView(graphPoints: [2240, 1983, 2171, 2017, 1842, 1992, 2347])
I'm new to Swift and I'm stumped on this problem after looking everywhere for an answer. Any help is appreciated.

Resources