I'm trying to make an app based on image editing and I'm founding some problems coding a resizing by corners dragging crop area.
I have croppingView.swift where the crop rectangle is drawn when launched:
override func viewDidAppear(_ animated: Bool) {
let imagePoint = CGPoint(x: contentView.center.x, y: contentView.center.y)
let imageSize = CGSize(width: cropImageView.image!.size.width, height: cropImageView.image!.size.height)
let widthScale = cropImageView.frame.width / imageSize.width
let heightScale = cropImageView.frame.height / imageSize.height
let scale = min(widthScale, heightScale)
let width = imageSize.width * scale
let height = imageSize.height * scale
let size = CGSize(width: width, height: height)
let originViewFrame = CGPoint(x: imagePoint.x - width/2.0, y: imagePoint.y - height/2.0)
let cropSize = CGSize(width: width * 0.7, height: height * 0.7)
let cropViewFrame = CGRect(origin: originViewFrame, size: cropSize)
cropView = ShapeView(frame: cropViewFrame)
cropView.backgroundColor = UIColor(red: 224.0/255.0, green: 224.0/255.0, blue: 224.0/255.0, alpha: 0.3)
cropView.layer.borderColor = UIColor(red: 97.0/255.0, green: 97.0/255.0, blue: 97.0/255.0, alpha: 1.0).cgColor
cropView.layer.borderWidth = 1.0
cropView.center = imagePoint
self.view.addSubview(cropView)
}
The ShapeView.swift to draw the cropping area is as follows:
class ShapeView: UIView {
var previousLocation = CGPoint.zero
var topLeft = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var topRight = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var bottomLeft = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var bottomRight = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
override func didMoveToSuperview() {
superview?.addSubview(topLeft)
superview?.addSubview(topRight)
superview?.addSubview(bottomLeft)
superview?.addSubview(bottomRight)
var pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
topLeft.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
topRight.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
bottomLeft.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
bottomRight.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handleMove))
self.addGestureRecognizer(pan)
self.updateDragHandles()
}
func updateDragHandles() {
topLeft.center = self.transformedTopLeft()
topRight.center = self.transformedTopRight()
bottomLeft.center = self.transformedBottomLeft()
bottomRight.center = self.transformedBottomRight()
}
//Gesture Methods
#IBAction func handleMove(gesture:UIPanGestureRecognizer) {
let translation = gesture.translation(in: self.superview!)
var center = self.center
center.x += translation.x
center.y += translation.y
self.center = center
gesture.setTranslation(CGPoint.zero, in: self.superview!)
updateDragHandles()
}
#IBAction func handlePan(gesture:UIPanGestureRecognizer) {
let translation = gesture.translation(in: self)
switch gesture.view! {
case topLeft:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 1, y: 1))
}
self.bounds.size.width -= translation.x
self.bounds.size.height -= translation.y
case topRight:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 0, y: 1))
}
self.bounds.size.width += translation.x
self.bounds.size.height -= translation.y
case bottomLeft:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 1, y: 0))
}
self.bounds.size.width -= translation.x
self.bounds.size.height += translation.y
case bottomRight:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint.zero)
}
self.bounds.size.width += translation.x
self.bounds.size.height += translation.y
default:()
}
gesture.setTranslation(CGPoint.zero, in: self)
updateDragHandles()
if gesture.state == .ended {
self.setAnchorPoint(anchorPoint: CGPoint(x: 0.5, y: 0.5))
}
}
}
As you can see I'm adding one circle in each corner of the rectangle. These circles are used to resize the cropping area by dragging. Here the key is the updateDragHandles()function which updates the positions of all other circle keeping them on the corners.
So when launching the cropping area appears like:
And when user moves for example the bottom-right corner by dragging everything is working fine:
Now the problem is when I click on the portrait button to change cropping area orientation. With the next code into croppingView.swift the grey rectangle correctly changes the size but the circles remain on the their position:
#IBAction func portButton(_ sender: Any) {
portButton.tintColor = UIColor.systemBlue
landButton.tintColor = UIColor.systemGray
isPortait = 1
let inWidth = cropView.frame.size.width
let inHeight = cropView.frame.size.height
if inWidth > inHeight {
let scaleW = cropView.frame.size.height / cropView.frame.size.width
let scaleH = cropView.frame.size.width / cropView.frame.size.height
let fixPoint = CGPoint(x: 0.5, y: 0.5)
cropView.setAnchorPoint(anchorPoint: fixPoint)
cropView.transform = cropView.transform.scaledBy(x: scaleW, y: scaleH)
let vc = ShapeView()
vc.updateDragHandles()
}
}
It's like updateDragHandles is not working from this ViewController...
Probably it's a basic mistake but I can't find out the solution.
Any suggestion?
Thank you in advance!
DragHandle.swift
let diameter:CGFloat = 30
import UIKit
class DragHandle: UIView {
var fillColor = UIColor.darkGray
var strokeColor = UIColor.lightGray
var strokeWidth: CGFloat = 1.0
required init(coder aDecoder: NSCoder) {
fatalError("Use init(fillColor:, strokeColor:)")
}
init(fillColor: UIColor, strokeColor: UIColor, strokeWidth width: CGFloat = 1.0) {
super.init(frame: CGRect(x: 0, y: 0, width: diameter, height: diameter))
self.fillColor = fillColor
self.strokeColor = strokeColor
self.strokeWidth = width
self.backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect)
{
super.draw(rect)
let handlePath = UIBezierPath(ovalIn: rect.insetBy(dx: 10 + strokeWidth, dy: 10 + strokeWidth))
fillColor.setFill()
handlePath.fill()
strokeColor.setStroke()
handlePath.lineWidth = strokeWidth
handlePath.stroke()
}
}
UIViewExtensions.swift
import Foundation
import UIKit
extension UIView {
func offsetPointToParentCoordinates(point: CGPoint) -> CGPoint {
return CGPoint(x: point.x + self.center.x, y: point.y + self.center.y)
}
func pointInViewCenterTerms(point:CGPoint) -> CGPoint {
return CGPoint(x: point.x - self.center.x, y: point.y - self.center.y)
}
func pointInTransformedView(point: CGPoint) -> CGPoint {
let offsetItem = self.pointInViewCenterTerms(point: point)
let updatedItem = offsetItem.applying(self.transform)
let finalItem = self.offsetPointToParentCoordinates(point: updatedItem)
return finalItem
}
func originalFrame() -> CGRect {
let currentTransform = self.transform
self.transform = .identity
let originalFrame = self.frame
self.transform = currentTransform
return originalFrame
}
func transformedTopLeft() -> CGPoint {
let frame = self.originalFrame()
let point = frame.origin
return self.pointInTransformedView(point: point)
}
func transformedTopRight() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.x += frame.size.width
return self.pointInTransformedView(point: point)
}
func transformedBottomRight() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.x += frame.size.width
point.y += frame.size.height
return self.pointInTransformedView(point: point)
}
func transformedBottomLeft() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.y += frame.size.height
return self.pointInTransformedView(point: point)
}
func setAnchorPoint(anchorPoint: CGPoint) {
var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: self.bounds.size.width * self.layer.anchorPoint.x, y: self.bounds.size.height * self.layer.anchorPoint.y)
newPoint = newPoint.applying(self.transform)
oldPoint = oldPoint.applying(self.transform)
var position = self.layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
self.layer.position = position
self.layer.anchorPoint = anchorPoint
}
}
Related
I'm using CGAffineTransform to rotate a collectionView cell. The rotation hinge is located in the bottom-left. As you can see in the images, here is how it looks
The moving progress
The issue is, I want it to rotate exactly on bottom-left`, like a fixed hinge, but now its corner is moved a bit while it's rotating.
here is my code
private func setupGestures() {
let swipeToLeft = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeLeft))
swipeToLeft.direction = .left
container.addGestureRecognizer(swipeToLeft)
}
#objc func respondToSwipeLeft(gesture: UIGestureRecognizer) {
UIView.animate(withDuration: 0.6, animations: { [weak self] in
guard let this = self else { return }
this.backgroundView.backgroundColor = UIColor.systemRed
this.container.setAnchorPoint(anchorPoint: .init(x: 0, y: 1))
this.container.transform = CGAffineTransform(rotationAngle: -30.degreesToRadians)
haptic.impactOccurred()
}, completion: { [weak self] _ in
guard let this = self else { return }
this.rotateContainerToInitialPosition()
})
}
private func rotateContainerToInitialPosition() {
UIView.animate(withDuration: 0.6) {
self.container.transform = .identity
self.container.setAnchorPoint(anchorPoint: .init(x: 0.5, y: 0.5))
self.backgroundView.backgroundColor = UIColor.background
}
}
and
extension UIView{
func setAnchorPoint(anchorPoint: CGPoint) {
var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: self.bounds.size.width * self.layer.anchorPoint.x, y: self.bounds.size.height * self.layer.anchorPoint.y)
newPoint = newPoint.applying(self.transform)
oldPoint = oldPoint.applying(self.transform)
var position : CGPoint = self.layer.position
position.x -= oldPoint.x
position.x += newPoint.x;
position.y -= oldPoint.y;
position.y += newPoint.y;
self.layer.position = position;
self.layer.anchorPoint = anchorPoint;
}
}
Your help and suggestions will be greatly appreciated.
I can't seem to get the CAEmitterLayer display CAEmitterCells. I add the ConfettiEmitterView in Storyboard and the view is being displayed with a red background as intended. But the cells are not showing. No Errors or warinings either.
The code:
import UIKit
class ConfettiEmitterView: UIView {
override class var layerClass:AnyClass {
return CAEmitterLayer.self
}
func makeEmmiterCell(color:UIColor, velocity:CGFloat, scale:CGFloat)-> CAEmitterCell {
let cell = CAEmitterCell()
cell.birthRate = 10
cell.lifetime = 20.0
cell.lifetimeRange = 0
cell.velocity = velocity
cell.velocityRange = velocity / 4
cell.emissionLongitude = .pi
cell.emissionRange = .pi / 8
cell.scale = scale
cell.scaleRange = scale / 3
cell.contents = roundImage(with: .yellow)
return cell
}
override func layoutSubviews() {
let emitter = self.layer as! CAEmitterLayer
emitter.frame = self.bounds
emitter.masksToBounds = true
emitter.emitterShape = .line
emitter.emitterPosition = CGPoint(x: bounds.midX, y: 0)
emitter.emitterSize = CGSize(width: bounds.size.width, height: 1)
emitter.backgroundColor = UIColor.red.cgColor // Setting the background to red
let near = makeEmmiterCell(color: UIColor(white: 1, alpha: 1), velocity: 100, scale: 0.3)
let middle = makeEmmiterCell(color: UIColor(white: 1, alpha: 0.66), velocity: 80, scale: 0.2)
let far = makeEmmiterCell(color: UIColor(white: 1, alpha: 0.33), velocity: 60, scale: 0.1)
emitter.emitterCells = [near, middle, far]
}
func roundImage(with color: UIColor) -> UIImage {
let rect = CGRect(origin: .zero, size: CGSize(width: 12.0, height: 12.0))
return UIGraphicsImageRenderer(size: rect.size).image { context in
context.cgContext.setFillColor(color.cgColor)
context.cgContext.addPath(UIBezierPath(ovalIn: rect).cgPath)
context.cgContext.fillPath()
}
}
}
Adding a view of type ConfettiEmitterView to the UIViewController in Storyboard should be enough to recreate the issue.
Change
cell.contents = roundImage(with: .yellow)
To
cell.contents = roundImage(with: .yellow).cgImage
I have a problem while animating the CAGradientLayer angle.
Angle in CAGradientLayer is represented through start and end point properties.
I want to animate the gradient in a circular fashion.
When I set it inside an animationGroup it doesn't work. No animation is happening.
When I am changing the properties in
DispatchQueue.main.asyncAfter(deadline: now() + 1.0) {
// change properties here
}
it works. But a very fast animation is happening. Which is not good enough.
On the internet the only thing there is is locations and color changes, but no angle change.
Below you can find a Playground project to play with
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
// Gradient layer specification
lazy var gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.white.withAlphaComponent(0.5).cgColor, UIColor.orange.withAlphaComponent(0.5).cgColor, UIColor.orange.cgColor]
gradientLayer.locations = [0, 0.27, 1]
gradientLayer.frame = view.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
return gradientLayer
}()
func animationMapFunction(points: [(CGPoint, CGPoint)], keyPath: String) -> [CABasicAnimation] {
return points.enumerated().map { (arg) -> CABasicAnimation in
let (offset, element) = arg
let gradientStartPointAnimation = CABasicAnimation(keyPath: keyPath)
gradientStartPointAnimation.fromValue = element.0
gradientStartPointAnimation.toValue = element.1
gradientStartPointAnimation.beginTime = CACurrentMediaTime() + Double(offset)
return gradientStartPointAnimation
}
}
lazy var gradientAnimation: CAAnimation = {
let startPointAnimationPoints = [(CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y:0.0)),
(CGPoint(x: 1.0, y:0.0), CGPoint(x: 1.0, y:1.0)),
(CGPoint(x: 1.0, y:1.0), CGPoint(x: 0.0, y:1.0)),
(CGPoint(x: 0.0, y:1.0), CGPoint(x: 0.0, y:0.0))]
let endPointAnimatiomPoints = [(CGPoint(x: 1.0, y:1.0), CGPoint(x: 0.0, y:1.0)),
(CGPoint(x: 0.0, y:1.0), CGPoint(x: 0.0, y:0.0)),
(CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y:0.0)),
(CGPoint(x: 1.0, y:0.0), CGPoint(x: 1.0, y:1.0))]
let startPointAnimations = animationMapFunction(points: startPointAnimationPoints, keyPath: "startPoint")
let endPointAnimations = animationMapFunction(points: startPointAnimationPoints, keyPath: "endPoint")
let animationGroup = CAAnimationGroup()
animationGroup.duration = 5.0
animationGroup.repeatCount = Float.infinity
animationGroup.animations = startPointAnimations + endPointAnimations
return animationGroup
}()
override func loadView() {
let view = UIView(frame: UIScreen.main.bounds)
self.view = view
view.layer.addSublayer(gradientLayer)
}
func animate() {
view.layer.removeAllAnimations()
gradientLayer.add(gradientAnimation, forKey: nil)
}
}
// Present the view controller in the Live View window
let vc = MyViewController()
PlaygroundPage.current.liveView = vc
vc.animate()
So what I did was a timer, that triggers start and end points change.
I rotate from 0 to 360 degrees, while incrementing the angle with a given constant (in my case 6)
I created a function mapping: angle → (startPoint, endPoint)
func animate() {
stopAnimation()
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { timer in
self.angle += 6.0
self.angle.formTruncatingRemainder(dividingBy: 360)
CATransaction.begin()
// disable implicit animation
CATransaction.setDisableActions(true)
let pos = points(from: self.angle)
self.gradientLayer.startPoint = pos.0
self.gradientLayer.endPoint = pos.1
CATransaction.commit()
})
}
func stopAnimation() {
timer?.invalidate()
}
And here are the utility functions
extension CGPoint {
var inverse: CGPoint {
return CGPoint(x: 1 - x, y: 1 - y)
}
}
fileprivate func points(from angle: Double) -> (CGPoint, CGPoint) {
let start: CGPoint
switch angle {
case let x where 0 <= x && x < 90:
start = CGPoint(x: x / 180, y: 0.5 - x / 180)
case let x where 90 <= x && x < 180:
start = CGPoint(x: x / 180, y: x / 180 - 0.5)
case let x where 180 <= x && x < 270:
start = CGPoint(x: 2.0 - x / 180, y: 0.5 + (x - 180) / 180)
case let x where 270 <= x && x < 360:
start = CGPoint(x: 2.0 - x / 180, y: 0.5 + (360 - x) / 180)
default:
start = CGPoint.zero
}
return (start, start.inverse)
}
I am trying to have an animation in a view controller in which the circle rotates with animation. The circle should rotate until a process completed like a gif below. I have implemented the circle animation but couldn't reach to the point what I want to achieve.
import UIKit
class ViewController: UIViewController {
var circle : Circle?;
override func viewDidLoad() {
super.viewDidLoad();
view.backgroundColor = UIColor.white;
setupViews();
}
func setupViews(){
circle = Circle(frame: self.view.frame);
view.addSubview(circle!);
circle?.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true;
circle?.topAnchor.constraint(equalTo: view.topAnchor).isActive = true;
circle?.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true;
circle?.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true;
}
}
class Circle : UIView{
override init(frame: CGRect) {
super.init(frame: frame);
self.backgroundColor = .blue;
self.translatesAutoresizingMaskIntoConstraints = false;
setupCircle();
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
let gradientLayer = CAGradientLayer();
func setupCircle(){
layer.addSublayer(shapeLayer);
let circlePath = UIBezierPath(arcCenter: CGPoint(x: self.frame.width / 2 - 50, y: self.frame.height / 2 - 50), radius: 50, startAngle: CGFloat(Double.pi * (0 / 4)), endAngle: CGFloat(Double.pi * 2), clockwise: true);
shapeLayer.path = circlePath.cgPath;
let group = CAAnimationGroup()
group.animations = [animateStrokeEnd, animateOpacity]
group.duration = 0.8
group.repeatCount = HUGE // repeat forver
shapeLayer.add(group, forKey: nil)
}
let shapeLayer: CAShapeLayer = {
let layer = CAShapeLayer();
layer.strokeColor = UIColor.white.cgColor;
layer.lineWidth = 5;
layer.fillColor = UIColor.clear.cgColor;
layer.strokeStart = 0
layer.strokeEnd = 1;
return layer;
}();
let animateOpacity : CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "opacity");
animation.fromValue = 0;
animation.toValue = 0.8;
animation.byValue = 0.01;
animation.repeatCount = Float.infinity;
return animation
}();
let animateStrokeEnd: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "strokeEnd");
animation.fromValue = 0;
animation.repeatCount = Float.infinity;
animation.toValue = 1;
return animation;
}();
}
I am using strokeEnd animation to implement the animation. And opacity to animate the color. But when the circle reaches 360 degree, its makes a lag before starting a new circle.
Does anybody know how to remove this effect and get smooth animation?
The above code produces this animation
But i want to achieve this animation
Also the stroke colour is different from the original animation. Can we achieve this animation using the CABasicAnimation?
Rather than trying to animate the actual drawing, just draw the view once and then animate it.
Here is a custom PadlockView and a custom CircleView which mimic the animation you showed. To use it, add the code below to your project. Add a UIView to your Storyboard, change its class to PadlockView, and make an #IBOutlet to it (called padlock perhaps). When you want the view to animate, set padlock.circle.isAnimating = true. To stop animating, set padlock.circle.isAnimating = false.
CircleView.swift
// This UIView extension was borrowed from #keval's answer:
// https://stackoverflow.com/a/41160100/1630618
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 3) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat.pi * 2
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = duration
rotateAnimation.repeatCount = Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
}
class CircleView: UIView {
var foregroundColor = UIColor.white
var lineWidth: CGFloat = 3.0
var isAnimating = false {
didSet {
if isAnimating {
self.isHidden = false
self.rotate360Degrees(duration: 1.0)
} else {
self.isHidden = true
self.layer.removeAllAnimations()
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
self.isHidden = true
self.backgroundColor = .clear
}
override func draw(_ rect: CGRect) {
let width = bounds.width
let height = bounds.height
let radius = (min(width, height) - lineWidth) / 2.0
var currentPoint = CGPoint(x: width / 2.0 + radius, y: height / 2.0)
var priorAngle = CGFloat(360)
for angle in stride(from: CGFloat(360), through: 0, by: -2) {
let path = UIBezierPath()
path.lineWidth = lineWidth
path.move(to: currentPoint)
currentPoint = CGPoint(x: width / 2.0 + cos(angle * .pi / 180.0) * radius, y: height / 2.0 + sin(angle * .pi / 180.0) * radius)
path.addArc(withCenter: CGPoint(x: width / 2.0, y: height / 2.0), radius: radius, startAngle: priorAngle * .pi / 180.0 , endAngle: angle * .pi / 180.0, clockwise: false)
priorAngle = angle
foregroundColor.withAlphaComponent(angle/360.0).setStroke()
path.stroke()
}
}
}
PadlockView.swift
class PadlockView: UIView {
var circle: CircleView!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
self.backgroundColor = .clear
circle = CircleView()
circle.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(circle)
circle.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
circle.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
circle.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
circle.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
}
override func draw(_ rect: CGRect) {
let width = bounds.width
let height = bounds.height
let lockwidth = width / 3
let lockheight = height / 4
let boltwidth = lockwidth * 2 / 3
UIColor.white.setStroke()
let path = UIBezierPath()
path.move(to: CGPoint(x: (width - lockwidth) / 2, y: height / 2))
path.addLine(to: CGPoint(x: (width + lockwidth) / 2, y: height / 2))
path.addLine(to: CGPoint(x: (width + lockwidth) / 2, y: height / 2 + lockheight))
path.addLine(to: CGPoint(x: (width - lockwidth) / 2, y: height / 2 + lockheight))
path.close()
path.move(to: CGPoint(x: (width - boltwidth) / 2, y: height / 2))
path.addLine(to: CGPoint(x: (width - boltwidth) / 2, y: height / 2 - boltwidth / 4))
path.addArc(withCenter: CGPoint(x: width/2, y: height / 2 - boltwidth / 4), radius: boltwidth / 2, startAngle: .pi, endAngle: 0, clockwise: true)
path.lineWidth = 2.0
path.stroke()
}
}
Note: Continuous animation code courtesy of this answer.
Here is a demo that I setup with the following code in my ViewController:
#IBOutlet weak var padlock: PadlockView!
#IBAction func startStop(_ sender: UIButton) {
if sender.currentTitle == "Start" {
sender.setTitle("Stop", for: .normal)
padlock.circle.isAnimating = true
} else {
sender.setTitle("Start", for: .normal)
padlock.circle.isAnimating = false
}
}
I designed a custom header view that masks an image and draws a border on the bottom edge, which is an arc. It looks like this:
Here's the code for the class:
class HeaderView: UIView
{
private let imageView = UIImageView()
private let dimmerView = UIView()
private let arcShape = CAShapeLayer()
private let maskShape = CAShapeLayer() // Masks the image and the dimmer
private let titleLabel = UILabel()
#IBInspectable var image: UIImage? { didSet { self.imageView.image = self.image } }
#IBInspectable var title: String? { didSet {self.titleLabel.text = self.title} }
#IBInspectable var arcHeight: CGFloat? { didSet {self.setupLayers()} }
// MARK: Initialization
override init(frame: CGRect)
{
super.init(frame:frame)
initMyStuff()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder:aDecoder)
initMyStuff()
}
override func prepareForInterfaceBuilder()
{
backgroundColor = UIColor.clear()
}
internal func initMyStuff()
{
backgroundColor = UIColor.clear()
titleLabel.font = Font.AvenirNext_Bold(24)
titleLabel.text = "TITLE"
titleLabel.textColor = UIColor.white()
titleLabel.layer.shadowColor = UIColor.black().cgColor
titleLabel.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
titleLabel.layer.shadowRadius = 0.0;
titleLabel.layer.shadowOpacity = 1.0;
titleLabel.layer.masksToBounds = false
titleLabel.layer.shouldRasterize = true
imageView.contentMode = UIViewContentMode.scaleAspectFill
addSubview(imageView)
dimmerView.frame = self.bounds
dimmerView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
addSubview(dimmerView)
addSubview(titleLabel)
// Add the shapes
self.layer.addSublayer(arcShape)
self.layer.addSublayer(maskShape)
self.layer.masksToBounds = true // This seems to be unneeded...test more
// Set constraints
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView .autoPinEdgesToSuperviewEdges()
titleLabel.autoCenterInSuperview()
}
func setupLayers()
{
let aHeight = arcHeight ?? 10
// Create the arc shape
arcShape.path = AppocalypseUI.createHorizontalArcPath(CGPoint(x: 0, y: bounds.size.height), width: bounds.size.width, arcHeight: aHeight)
arcShape.strokeColor = UIColor.white().cgColor
arcShape.lineWidth = 1.0
arcShape.fillColor = UIColor.clear().cgColor
// Create the mask shape
let maskPath = AppocalypseUI.createHorizontalArcPath(CGPoint(x: 0, y: bounds.size.height), width: bounds.size.width, arcHeight: aHeight, closed: true)
maskPath.moveTo(nil, x: bounds.size.width, y: bounds.size.height)
maskPath.addLineTo(nil, x: bounds.size.width, y: 0)
maskPath.addLineTo(nil, x: 0, y: 0)
maskPath.addLineTo(nil, x: 0, y: bounds.size.height)
//let current = CGPathGetCurrentPoint(maskPath);
//print(current)
let mask_Dimmer = CAShapeLayer()
mask_Dimmer.path = maskPath.copy()
maskShape.fillColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1.0).cgColor
maskShape.path = maskPath
// Apply the masks
imageView.layer.mask = maskShape
dimmerView.layer.mask = mask_Dimmer
}
override func layoutSubviews()
{
super.layoutSubviews()
// Let's go old school here...
imageView.frame = self.bounds
dimmerView.frame = self.bounds
setupLayers()
}
}
Something like this will cause it to just snap to the new size without gradually changing its frame:
UIView.animate(withDuration: 1.0)
{
self.headerView.arcHeight = self.new_headerView_arcHeight
self.headerView.frame = self.new_headerView_frame
}
I figure it must have something to do with the fact that I'm using CALayers, but I don't really know enough about what's going on behind the scenes.
EDIT:
Here's the function I use to create the arc path:
class func createHorizontalArcPath(_ startPoint:CGPoint, width:CGFloat, arcHeight:CGFloat, closed:Bool = false) -> CGMutablePath
{
// http://www.raywenderlich.com/33193/core-graphics-tutorial-arcs-and-paths
let arcRect = CGRect(x: startPoint.x, y: startPoint.y-arcHeight, width: width, height: arcHeight)
let arcRadius = (arcRect.size.height/2) + (pow(arcRect.size.width, 2) / (8*arcRect.size.height));
let arcCenter = CGPoint(x: arcRect.origin.x + arcRect.size.width/2, y: arcRect.origin.y + arcRadius);
let angle = acos(arcRect.size.width / (2*arcRadius));
let startAngle = CGFloat(M_PI)+angle // (180 degrees + angle)
let endAngle = CGFloat(M_PI*2)-angle // (360 degrees - angle)
// let startAngle = radians(180) + angle;
// let endAngle = radians(360) - angle;
let path = CGMutablePath();
path.addArc(nil, x: arcCenter.x, y: arcCenter.y, radius: arcRadius, startAngle: startAngle, endAngle: endAngle, clockwise: false);
if(closed == true)
{path.addLineTo(nil, x: startPoint.x, y: startPoint.y);}
return path;
}
BONUS:
Setting the arcHeight property to 0 results in no white line being drawn. Why?
The Path property can't be animated. You have to approach the problem differently. You can draw an arc 'instantly', any arc, so that tells us that we need to handle the animation manually. If you expect the entire draw process to take say 3 seconds, then you might want to split the process to 1000 parts, and call the arc drawing function 1000 times every 0.3 miliseconds to draw the arc again from the beginning to the current point.
self.headerView.arcHeight is not a animatable property. It is only UIView own properties are animatable
you can do something like this
let displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode
let expectedFramesPerSecond = 60
var diff : CGFloat = 0
func update() {
let diffUpdated = self.headerView.arcHeight - self.new_headerView_arcHeight
let done = (fabs(diffUpdated) < 0.1)
if(!done){
self.headerView.arcHeight -= diffUpdated/(expectedFramesPerSecond*0.5)
self.setNeedsDisplay()
}
}