Changing the shape of a UIView border - ios

I am wondering how I would go about implementing a wave-like border of a UIView. Is this possible through UIView's alone? Or would creating this appearance through a UIImageView be the way to go?
An example might be something similar to:
Thanks for your help!

Here is a code based solution that doesn't require any images. This creates a custom view using UIBezierPath to create the sine waves.
import UIKit
class WavyView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
return nil // TODO
}
override func draw(_ rect: CGRect) {
// Fill the whole background with the darkest blue color
UIColor(red: 0.329, green: 0.718, blue: 0.875, alpha: 1).set()
let bg = UIBezierPath(rect: rect)
bg.fill()
// Add the first sine wave filled with a very transparent white
let top1: CGFloat = 17.0
let wave1 = wavyPath(rect: CGRect(x: 0, y: top1, width: frame.width, height: frame.height - top1), periods: 1.5, amplitude: 21, start: 0.55)
UIColor(white: 1.0, alpha: 0.1).set()
wave1.fill()
// Add the second sine wave over the first
let top2: CGFloat = 34.0
let wave2 = wavyPath(rect: CGRect(x: 0, y: top2, width: frame.width, height: frame.height - top2), periods: 1.5, amplitude: 21, start: 0.9)
UIColor(white: 1.0, alpha: 0.15).set()
wave2.fill()
// Add the text
let paraAttrs = NSMutableParagraphStyle()
paraAttrs.alignment = .center
let textRect = CGRect(x: 0, y: frame.maxY - 64, width: frame.width, height: 24)
let textAttrs = [NSFontAttributeName: UIFont.boldSystemFont(ofSize: 20), NSForegroundColorAttributeName: UIColor(white: 1.0, alpha: 0.9), NSParagraphStyleAttributeName: paraAttrs]
("New user? Register here." as NSString).draw(in: textRect, withAttributes: textAttrs)
}
// This creates the desired sine wave bezier path
// rect is the area to fill with the sine wave
// periods is how may sine waves fit across the width of the frame
// amplitude is the height in points of the sine wave
// start is an offset in wavelengths for the left side of the sine wave
func wavyPath(rect: CGRect, periods: Double, amplitude: Double, start: Double) -> UIBezierPath {
let path = UIBezierPath()
// start in the bottom left corner
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
let radsPerPoint = Double(rect.width) / periods / 2.0 / Double.pi
let radOffset = start * 2 * Double.pi
let xOffset = Double(rect.minX)
let yOffset = Double(rect.minY) + amplitude
// This loops through the width of the frame and calculates and draws each point along the size wave
// Adjust the "by" value as needed. A smaller value gives smoother curve but takes longer to draw. A larger value is quicker but gives a rougher curve.
for x in stride(from: 0, to: Double(rect.width), by: 6) {
let rad = Double(x) / radsPerPoint + radOffset
let y = sin(rad) * amplitude
path.addLine(to: CGPoint(x: x + xOffset, y: y + yOffset))
}
// Add the last point on the sine wave at the right edge
let rad = Double(rect.width) / radsPerPoint + radOffset
let y = sin(rad) * amplitude
path.addLine(to: CGPoint(x: Double(rect.maxX), y: y + yOffset))
// Add line from the end of the sine wave to the bottom right corner
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
// Close the path
path.close()
return path
}
}
// This creates the view with the same size as the image posted in the question
let wavy = WavyView(frame: CGRect(x: 0, y: 0, width: 502, height: 172))
The result of running this code in a Swift playground gives the following:
Obviously you can adjust any of the values in the code above to tweak the result.

The easiest approach would be to use a UIImageView. However, it is also possible by creating a custom border for the UIView but that will require a lot of code to draw the shapes.

Related

How do I draw stuff under the text of a UILabel?

I created a custom UILabel subclass that has a circle in the middle, and the label's text (which is a number) will be on top of the circle.
I initially thought of doing this using layer.cornerRadius, but that will not create a circle when the label's width and height are not equal.
What I mean is, for a label with width 100 and height 50, I still want a circle with radius 50 and centre at (50, 25).
Therefore, I tried to use UIBezierPath to draw the circle. This is what I have tried:
override func draw(_ rect: CGRect) {
super.draw(rect)
if bounds.height > bounds.width {
let y = (bounds.height - bounds.width) / 2
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: y, width: bounds.width, height: bounds.width))
circleColor.setFill()
path.fill()
} else {
let x = (bounds.width - bounds.height) / 2
let path = UIBezierPath(ovalIn: CGRect(x: x, y: 0, width: bounds.height, height: bounds.height))
circleColor.setFill()
path.fill()
}
}
I have put super.draw(rect) because I thought that would draw the label's text, but when I run the app, I only see the circle and not my label text.
I am very confused because why hasn't super.draw(rect) drawn the label's text?
The text is not seen because the "z-index" of UIBezierPaths depends on the order in which they are drawn. In other words, UIBezierPaths are drawn on top of each other.
super.draw(rect) indeed draws the text. But when you put it as the first statement, it will get drawn first, so everything you draw after that, goes on top of the text. To fix this, you should call super.draw(rect) last:
override func draw(_ rect: CGRect) {
if bounds.height > bounds.width {
let y = (bounds.height - bounds.width) / 2
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: y, width: bounds.width, height: bounds.width))
circleColor.setFill()
path.fill()
} else {
let x = (bounds.width - bounds.height) / 2
let path = UIBezierPath(ovalIn: CGRect(x: x, y: 0, width: bounds.height, height: bounds.height))
circleColor.setFill()
path.fill()
}
super.draw(rect) // <------- here!
}
Alternatively, just subclass UIView, draw the circle in draw(_:), and add a UILabel as a subview of that. The advantage if this approach is that it does not depend on the implementation of super.draw(_:), which might change in the future,

Drawing a 3D cone shape in Swift using a UIBezierPath() and CAShapeLayer()

Background
I’m attempting to draw a 3D cone shape as shown in the image below.
I have a method that I use to draw a simple triangle and fill it with a solid color in Swift using a UIBezierPath() and CAShapeLayer().
Question
In Swift code, how can I draw a 3D cone shape or fill the triangle
shape I’ve drawn with a complex gradient that gives the triangle shape
a 3D effect and effectively a cone appearance?
Code
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
override func viewDidLoad() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 100))
path.addLineToPoint(CGPoint(x: 200, y: 100))
path.addLineToPoint(CGPoint(x: 100, y: 0))
path.closePath()
let shape = CAShapeLayer()
shape.path = path.CGPath
shape.fillColor = UIColor.grayColor().CGColor
myView.layer.insertSublayer(shape, atIndex: 0)
}
}
Image
I was able to achieve your goal making use of CGGradientLayer with a type of conic. The trick is to use the triangle shape as a mask for the gradient.
Here is an example that can be run in a Swift Playground. Comments in the code.
import UIKit
import PlaygroundSupport
class ConeView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
// Just for display
backgroundColor = .yellow
// Create the triangle to be used as the gradient's mask
// Base the size of the triangle on the view's frame
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: frame.height))
path.addLine(to: CGPoint(x: frame.width, y: frame.height))
path.addLine(to: CGPoint(x: frame.width / 2, y: 0))
path.close()
// Create a shape from the path
let shape = CAShapeLayer()
shape.path = path.cgPath
// Create a conical gradient and mask it with the triangle shape
let gradientLayer = CAGradientLayer()
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: -0.5)
gradientLayer.type = .conic
// Change the white value of the center color to adjust the center highlight brightness as desired
gradientLayer.colors = [UIColor.black.cgColor, UIColor(white: 0.9, alpha: 1.0).cgColor, UIColor.black.cgColor]
// Tweak the 1st and 3rd number as desired.
// The closer to 0.5 the darker the cone edges will be.
// The further from 0.5 the lighter the cone edges will be.
gradientLayer.locations = [ 0.38, 0.5, 0.62 ]
gradientLayer.frame = bounds
gradientLayer.mask = shape
layer.insertSublayer(gradientLayer, at: 0)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// Set whatever size you want for the view
PlaygroundPage.current.liveView = ConeView(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
Here's the result (the black border is not part of the view, just part of the screen capture):

CATransform3DMakeRotation anchorpoint and position in Swift

Hi I am trying to rotate a triangle shaped CAShapeLayer. The northPole CAShapeLayer is defined in a custom UIView class. I have two functions in the class one to setup the layer properties and one to animate the rotation. The rotation animate works fine but after the rotation completes it reverts back to the original position. I want the layer to stay at the angle given (positionTo = 90.0) after the rotation animation.
I am trying to set the position after the transformation to retain the correct position after the animation. I have also tried to get the position from the presentation() method but this has been unhelpful. I have read many articles on frame, bounds, anchorpoints but I still cannot seem to make sense of why this transformation rotation is not working.
private func setupNorthPole(shapeLayer: CAShapeLayer) {
shapeLayer.frame = CGRect(x: self.bounds.width / 2 - triangleWidth / 2, y: self.bounds.height / 2 - triangleHeight, width: triangleWidth, height: triangleHeight)//self.bounds
let polePath = UIBezierPath()
polePath.move(to: CGPoint(x: 0, y: triangleHeight))
polePath.addLine(to: CGPoint(x: triangleWidth / 2, y: 0))
polePath.addLine(to: CGPoint(x: triangleWidth, y: triangleHeight))
shapeLayer.path = polePath.cgPath
shapeLayer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
The animate function:
private func animate() {
let positionTo = CGFloat(DegreesToRadians(value: degrees))
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = 0.0
rotate.toValue = positionTo //CGFloat(M_PI * 2.0)
rotate.duration = 0.5
northPole.add(rotate, forKey: nil)
let present = northPole.presentation()!
print("presentation position x " + "\(present.position.x)")
print("presentation position y " + "\(present.position.y)")
CATransaction.begin()
northPole.transform = CATransform3DMakeRotation(positionTo, 0.0, 0.0, 1.0)
northPole.position = CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)
CATransaction.commit()
}
To fix the problem, in the custom UIView class I had to override layoutSubviews() with the following:
super.layoutSubviews()
let northPoleTransform: CATransform3D = northPole.transform
northPole.transform = CATransform3DIdentity
setupNorthPole(northPole)
northPole.transform = northPoleTransform

How to draw a sloping oval with UIBezierPath

I wanna draw a sloping oval with UIBezierPath. There is only a method UIBezierPath.init(ovalInRect: rect) to draw a oval without slop.What can I do to draw this? thanks
Here is an example of a rotated oval:
class MyView: UIView {
override func draw(_ rect: CGRect) {
// create oval centered at (0, 0)
let path = UIBezierPath(ovalIn: CGRect(x: -75, y: -50, width: 150, height: 100))
// rotate it 30 degrees
path.apply(CGAffineTransform(rotationAngle: 30 * .pi / 180))
// translate it to where you want it
path.apply(CGAffineTransform(translationX: self.bounds.width / 2, y: self.bounds.height / 2))
// set stroke color
UIColor.blue.setStroke()
// stroke the path
path.stroke()
}
}
let view = MyView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = .white
Here it is running in a Playground:

Swift bezier curve fill gradient fill

Trying to create this curve in Swift using CG drawing on a view in my storyboard.
I've done the white curve but having trouble with filling it with a gradient as show in the image
There are three parts to this image.
The white curve.
The gradient.
The mask that blocks out the gradient.
Your best approach would be to create these as separate layers.
First create a CAGradientLayer with your desired colours.
Then create a path curve in white and add it above the gradient.
Now create a third layer with a closed path (similar to the curve but closed at the bottom. Use this as a maskLayer on the gradient layer.
Here is working code
class FillCurve: UIView {
override func draw(_ rect: CGRect) {
// Drawing ARC
let center = CGPoint(x: bounds.width / 2 , y: bounds.height / 2)
let curve = UIBezierPath()
curve.move(to:CGPoint(x: 10, y: bounds.height - 20))
curve.addQuadCurve(to: CGPoint(x: bounds.width - 20 , y: bounds.height - 20), controlPoint:center )
UIColor.white.setStroke()
curve.lineWidth = 2.0
curve.stroke()
let fillPath = curve.copy() as! UIBezierPath
fillPath.addLine(to: CGPoint(x: bounds.width , y: bounds.height - 2))
fillPath.addLine(to: CGPoint(x: 0, y: bounds.height - 2))
fillPath.close()
fillPath.fill()
fillPath.addClip()
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colors = [UIColor.cyan.cgColor,UIColor.black.cgColor]
let location:[CGFloat] = [0.0,1.0]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: location)!
UIGraphicsGetCurrentContext()?.drawLinearGradient(gradient, start: CGPoint(x: 0, y: fillPath.bounds.height), end: CGPoint(x: 0, y: bounds.height), options: [])
}
}
output

Resources