Implement Like what's app story progress circle - swift

I'm trying to do the below progress
Using MKMagneticProgress, I was able to apply the bottom space, but now I'm stuck on how to do the spaces in the circle, for example in this image we have the progress of 7 items out of 10.
What I want is to update the MKMagneticProgress code to add the spaces.
the code taken from MKMagneticProgress with my update (I added the total)
import UIKit
// MARK: - Line Cap Enum
public enum LineCap : Int{
case round, butt, square
public func style() -> String {
switch self {
case .round:
return CAShapeLayerLineCap.round.rawValue
case .butt:
return CAShapeLayerLineCap.butt.rawValue
case .square:
return CAShapeLayerLineCap.square.rawValue
}
}
}
// MARK: - Orientation Enum
public enum Orientation: Int {
case left, top, right, bottom
}
#IBDesignable
open class CircleProgress: UIView {
// MARK: - Variables
private let titleLabelWidth:CGFloat = 100
private let percentLabel = UILabel(frame: .zero)
#IBInspectable open var titleLabel = UILabel(frame: .zero)
/// Stroke background color
#IBInspectable open var clockwise: Bool = true {
didSet {
layoutSubviews()
}
}
/// Stroke background color
#IBInspectable open var backgroundShapeColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
updateShapes()
}
}
/// Progress stroke color
#IBInspectable open var progressShapeColor: UIColor = .blue {
didSet {
updateShapes()
}
}
/// Line width
#IBInspectable open var lineWidth: CGFloat = 8.0 {
didSet {
updateShapes()
}
}
/// Space value
#IBInspectable open var spaceDegree: CGFloat = 45.0 {
didSet {
// if spaceDegree < 45.0{
// spaceDegree = 45.0
// }
//
// if spaceDegree > 135.0{
// spaceDegree = 135.0
// }
layoutSubviews()
updateShapes()
}
}
/// The progress shapes line width will be the `line width` minus the `inset`.
#IBInspectable open var inset: CGFloat = 0.0 {
didSet {
updateShapes()
}
}
// The progress percentage label(center label) format
#IBInspectable open var percentLabelFormat: String = "%.f %%" {
didSet {
percentLabel.text = String(format: percentLabelFormat, progress * 100)
}
}
#IBInspectable open var percentColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
percentLabel.textColor = percentColor
}
}
/// progress text (progress bottom label)
#IBInspectable open var title: String = "" {
didSet {
titleLabel.text = title
}
}
#IBInspectable open var titleColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
titleLabel.textColor = titleColor
}
}
// progress text (progress bottom label)
#IBInspectable open var font: UIFont = .systemFont(ofSize: 13) {
didSet {
titleLabel.font = font
percentLabel.font = font
}
}
// progress Orientation
open var orientation: Orientation = .bottom {
didSet {
updateShapes()
}
}
/// Progress shapes line cap.
open var lineCap: LineCap = .round {
didSet {
updateShapes()
}
}
/// Returns the total Items
private var _total: CGFloat = 1.0
#IBInspectable open var total: CGFloat {
set {
self._total = newValue
self.setProgress(progress: self.progress)
}
get {
return self._total
}
}
/// Returns the current progress.
#IBInspectable open private(set) var progress: CGFloat {
set {
progressShape?.strokeEnd = newValue
}
get {
return progressShape.strokeEnd
}
}
/// Duration for a complete animation from 0.0 to 1.0.
open var completeDuration: Double = 2.0
private var backgroundShape: CAShapeLayer!
private var progressShape: CAShapeLayer!
private var progressAnimation: CABasicAnimation!
// MARK: - Init
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
backgroundShape = CAShapeLayer()
backgroundShape.fillColor = nil
backgroundShape.strokeColor = backgroundShapeColor.cgColor
layer.addSublayer(backgroundShape)
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = 0.0
progressShape.strokeEnd = 0.1
layer.addSublayer(progressShape)
progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
percentLabel.frame = self.bounds
percentLabel.textAlignment = .center
// percentLabel.textColor = self.progressShapeColor
self.addSubview(percentLabel)
percentLabel.text = String(format: "%.1f%%", progress * 100)
titleLabel.frame = CGRect(x: (self.bounds.size.width-titleLabelWidth)/2, y: self.bounds.size.height-21, width: titleLabelWidth, height: 21)
titleLabel.textAlignment = .center
// titleLabel.textColor = self.progressShapeColor
titleLabel.text = title
titleLabel.contentScaleFactor = 0.3
// textLabel.adjustFontSizeToFit()
titleLabel.numberOfLines = 2
//textLabel.adjustFontSizeToFit()
titleLabel.adjustsFontSizeToFitWidth = true
self.addSubview(titleLabel)
}
// MARK: - Progress Animation
public func setProgress(progress: CGFloat, animated: Bool = true) {
let actualProgress = progress/self._total
if actualProgress > 1.0 {
return
}
var start = progressShape.strokeEnd
if let presentationLayer = progressShape.presentation(){
if let count = progressShape.animationKeys()?.count, count > 0 {
start = presentationLayer.strokeEnd
}
}
let duration = abs(Double(progress - start)) * completeDuration
percentLabel.text = String(format: percentLabelFormat, actualProgress * 100)
progressShape.strokeEnd = actualProgress
if animated {
progressAnimation.fromValue = start
progressAnimation.toValue = actualProgress
progressAnimation.duration = duration
progressShape.add(progressAnimation, forKey: progressAnimation.keyPath)
}
}
// MARK: - Layout
open override func layoutSubviews() {
super.layoutSubviews()
backgroundShape.frame = bounds
progressShape.frame = bounds
let rect = rectForShape()
backgroundShape.path = pathForShape(rect: rect).cgPath
progressShape.path = pathForShape(rect: rect).cgPath
self.titleLabel.frame = CGRect(x: (self.bounds.size.width - titleLabelWidth)/2, y: self.bounds.size.height-50, width: titleLabelWidth, height: 42)
updateShapes()
percentLabel.frame = self.bounds
}
private func updateShapes() {
backgroundShape?.lineWidth = lineWidth
backgroundShape?.strokeColor = backgroundShapeColor.cgColor
backgroundShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
progressShape?.strokeColor = progressShapeColor.cgColor
progressShape?.lineWidth = lineWidth - inset
progressShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
switch orientation {
case .left:
titleLabel.isHidden = true
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi / 2, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1.0)
case .right:
titleLabel.isHidden = true
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi * 1.5, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 1.5, 0, 0, 1.0)
case .bottom:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: temp.bounds.size.height-50, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi * 2, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 2, 0, 0, 1.0)
case .top:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: 0, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi, 0, 0, 1.0)
}
}
// MARK: - Helper
private func rectForShape() -> CGRect {
return bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
}
private func pathForShape(rect: CGRect) -> UIBezierPath {
let startAngle:CGFloat!
let endAngle:CGFloat!
if clockwise{
startAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
endAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
}else{
startAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
endAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
}
let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY), radius: rect.size.width / 2.0, startAngle: startAngle, endAngle: endAngle
, clockwise: clockwise)
return path
}
}
My output:
Thanks.

I was able to figure out how to update the code to add multiple arcs here is the updated code
import UIKit
// MARK: - Line Cap Enum
public enum LineCap : Int{
case round, butt, square
public func style() -> String {
switch self {
case .round:
return CAShapeLayerLineCap.round.rawValue
case .butt:
return CAShapeLayerLineCap.butt.rawValue
case .square:
return CAShapeLayerLineCap.square.rawValue
}
}
}
// MARK: - Orientation Enum
public enum Orientation: Int {
case left, top, right, bottom
}
#IBDesignable
open class CircleProgress: UIView {
// MARK: - Variables
private let titleLabelWidth:CGFloat = 100
private let percentLabel = UILabel(frame: .zero)
#IBInspectable open var titleLabel = UILabel(frame: .zero)
/// Stroke background color
#IBInspectable open var clockwise: Bool = true {
didSet {
layoutSubviews()
}
}
/// Stroke background color
#IBInspectable open var backgroundShapeColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
updateShapes()
}
}
/// Progress stroke color
#IBInspectable open var progressShapeColor: UIColor = .blue {
didSet {
updateShapes()
}
}
/// Line width
#IBInspectable open var lineWidth: CGFloat = 5.0 {
didSet {
updateShapes()
}
}
/// Space value
#IBInspectable open var spaceDegree: CGFloat = 45.0 {
didSet {
// if spaceDegree < 45.0{
// spaceDegree = 45.0
// }
//
// if spaceDegree > 135.0{
// spaceDegree = 135.0
// }
layoutSubviews()
updateShapes()
}
}
/// The progress shapes line width will be the `line width` minus the `inset`.
#IBInspectable open var inset: CGFloat = 0.0 {
didSet {
updateShapes()
}
}
// The progress percentage label(center label) format
#IBInspectable open var percentLabelFormat: String = "%.f %%" {
didSet {
percentLabel.text = String(format: percentLabelFormat, progress * 100)
}
}
#IBInspectable open var percentColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
percentLabel.textColor = percentColor
}
}
/// progress text (progress bottom label)
#IBInspectable open var title: String = "" {
didSet {
titleLabel.text = title
}
}
#IBInspectable open var titleColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
titleLabel.textColor = titleColor
}
}
// progress text (progress bottom label)
#IBInspectable open var font: UIFont = .systemFont(ofSize: 13) {
didSet {
titleLabel.font = font
percentLabel.font = font
}
}
// progress Orientation
open var orientation: Orientation = .bottom {
didSet {
updateShapes()
}
}
/// Progress shapes line cap.
open var lineCap: LineCap = .round {
didSet {
updateShapes()
}
}
/// Returns the total Items
private var _total: Int = 1
#IBInspectable open var total: Int {
set {
self._total = newValue
self.addProgressShapes()
}
get {
return self._total
}
}
/// Returns the current progress.
private var _progress: CGFloat = 0.0
#IBInspectable open private(set) var progress: CGFloat {
set {
self._progress = newValue
}
get {
return self._progress
}
}
/// Duration for a complete animation from 0.0 to 1.0.
open var completeDuration: Double = 1.0
private var backgroundShape: CAShapeLayer!
private var progressShapes: [CAShapeLayer]!
private var progressAnimation: CABasicAnimation!
// MARK: - Init
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
backgroundShape = CAShapeLayer()
backgroundShape.fillColor = nil
backgroundShape.strokeColor = backgroundShapeColor.cgColor
layer.addSublayer(backgroundShape)
progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
percentLabel.frame = self.bounds
percentLabel.textAlignment = .center
// percentLabel.textColor = self.progressShapeColor
self.addSubview(percentLabel)
percentLabel.text = String(format: "%.1f%%", progress * 100)
titleLabel.frame = CGRect(x: (self.bounds.size.width-titleLabelWidth)/2, y: self.bounds.size.height-21, width: titleLabelWidth, height: 21)
titleLabel.textAlignment = .center
// titleLabel.textColor = self.progressShapeColor
titleLabel.text = title
titleLabel.contentScaleFactor = 0.3
// textLabel.adjustFontSizeToFit()
titleLabel.numberOfLines = 2
//textLabel.adjustFontSizeToFit()
titleLabel.adjustsFontSizeToFitWidth = true
self.addSubview(titleLabel)
}
// MARK: - Progress Animation
private func addProgressShapes(){
self.progressShapes = []
let progressSize = 1.0
var size:CGFloat = CGFloat(progressSize / Double(self.total))
let padingPercent = 0.2
let pading: Double = padingPercent * Double(self.total)/10 * Double(progressSize / Double(self.total - 1))
size = size - CGFloat(pading)
print("")
print("size: \(size) | pading: \(pading)")
print("------------------------")
var start: CGFloat = 0.0
var end: CGFloat = size
print("start: \(start) | end: \(end) ")
print("------------------------")
let progressShape: CAShapeLayer!
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = start
progressShape.strokeEnd = end
layer.addSublayer(progressShape)
self.progressShapes.append(progressShape)
for _ in 1..<self._total {
start = CGFloat(end + CGFloat(pading))
end = CGFloat(Double(start + size))
print("------------------------")
print("start: \(start) | end: \(end) ")
let progressShape: CAShapeLayer!
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = start
progressShape.strokeEnd = end
layer.addSublayer(progressShape)
self.progressShapes.append(progressShape)
}
}
private func setProgressShapeFrame(){
guard self.progressShapes != nil else { return }
for shape in self.progressShapes {
shape.frame = bounds
let rect = rectForShape()
shape.path = pathForShape(rect: rect).cgPath
}
}
private func updateShapesWidth(){
guard self.progressShapes != nil else { return }
for i in 0..<self._total {
let shape = self.progressShapes[i]
let color = CGFloat(i) >= self.progress ? UIColor.white.withAlphaComponent(0.1).cgColor:progressShapeColor.cgColor
shape.strokeColor = color
shape.lineWidth = lineWidth - inset
shape.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
}
}
private func transformShapes(_ transform: CATransform3D){
guard self.progressShapes != nil else { return }
for shape in self.progressShapes {
shape.transform = transform
}
}
public func setProgress(progress: CGFloat, animated: Bool = true) {
self._progress = progress
// let actualProgress = progress/self._total
// if actualProgress > 1.0 {
// return
// }
//
// var start = progressShape.strokeEnd
// if let presentationLayer = progressShape.presentation(){
// if let count = progressShape.animationKeys()?.count, count > 0 {
// start = presentationLayer.strokeEnd
// }
// }
//
// let duration = abs(Double(progress - start)) * completeDuration
// percentLabel.text = String(format: percentLabelFormat, actualProgress * 100)
// progressShape.strokeEnd = actualProgress
//
// if animated {
// progressAnimation.fromValue = start
// progressAnimation.toValue = actualProgress
// progressAnimation.duration = duration
// progressShape.add(progressAnimation, forKey: progressAnimation.keyPath)
// }
}
// MARK: - Layout
open override func layoutSubviews() {
super.layoutSubviews()
backgroundShape.frame = bounds
let rect = rectForShape()
backgroundShape.path = pathForShape(rect: rect).cgPath
setProgressShapeFrame()
self.titleLabel.frame = CGRect(x: (self.bounds.size.width - titleLabelWidth)/2, y: self.bounds.size.height-50, width: titleLabelWidth, height: 42)
updateShapes()
percentLabel.frame = self.bounds
}
private func updateShapes() {
backgroundShape?.lineWidth = lineWidth
backgroundShape?.strokeColor = backgroundShapeColor.cgColor
backgroundShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
self.updateShapesWidth()
switch orientation {
case .left:
titleLabel.isHidden = true
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi / 2, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1.0)
case .right:
titleLabel.isHidden = true
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi * 1.5, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 1.5, 0, 0, 1.0)
case .bottom:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: temp.bounds.size.height-50, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi * 2, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 2, 0, 0, 1.0)
case .top:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: 0, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi, 0, 0, 1.0)
}
}
// MARK: - Helper
private func rectForShape() -> CGRect {
return bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
}
private func pathForShape(rect: CGRect) -> UIBezierPath {
let startAngle:CGFloat!
let endAngle:CGFloat!
if clockwise{
startAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
endAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
}else{
startAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
endAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
}
let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY), radius: rect.size.width / 2.0, startAngle: startAngle, endAngle: endAngle
, clockwise: clockwise)
return path
}
}

Related

Swift: Make the outline of a UIView sketch-like

I want to make the outlines of a UIView look "wavey" like someone drew them.
I have this example from PowerPoint, which allows to do it (should work with any size and corner radius):
Currently this is what I have:
myView.layer.borderWidth = 10
myView.layer.borderColor = UIColor.blue.cgColor
myView.layer.cornerRadius = 5 // Optional
Thank
You can create "wavy" lines by using a UIBezierPath with a combination of quad-curves, lines, arcs, etc.
We'll start with a simple line, one-quarter of the width of the view:
Our path would consist of:
move to 0,0
add line to 80,0
If we change that to a quad-curve:
Now we're doing:
move to 0,0
add quad-curve to 80,0 with control point 40,40
If we add another quad-curve going the other way:
Now we're doing:
move to 0,0
add quad-curve to 80,0 with control point 40,40
add quad-curve to 160,0 with control point 120,-40
and we can extend that the width of the view:
of course, that doesn't look like your "sketch" target, so let's change the control-point offsets from 40 to 2:
Now it looks a bit more like a hand-draw "sketched" line.
It's too uniform, though, and it's partially outside the bounds of the view, so let's inset it by 8-pts and, instead of four 25% segments, we'll use (for example) five segments of these widths:
0.15, 0.2, 0.2, 0.27, 0.18
If we take the same approach to go down the right-hand side, back across the bottom, and up the left-hand side, we can get this:
Here's some example code to produce that view:
class SketchBorderView: UIView {
let borderLayer: CAShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = UIColor.blue.cgColor
layer.addSublayer(borderLayer)
backgroundColor = .yellow
}
override func layoutSubviews() {
let incrementVals: [CGFloat] = [
0.15, 0.2, 0.2, 0.27, 0.18,
]
let lineOffsets: [[CGFloat]] = [
[ 1.0, -2.0],
[-1.0, 2.0],
[-1.0, -2.0],
[ 1.0, 2.0],
[ 0.0, -2.0],
]
let pth: UIBezierPath = UIBezierPath()
// inset bounds by 8-pts so we can draw the "wavy border"
// inside our bounds
let r: CGRect = bounds.insetBy(dx: 8.0, dy: 8.0)
var ptDest: CGPoint = .zero
var ptControl: CGPoint = .zero
// start at top-left
ptDest = r.origin
pth.move(to: ptDest)
// we're at top-left
for i in 0..<incrementVals.count {
ptDest.x += r.width * incrementVals[i]
ptDest.y = r.minY + lineOffsets[i][0]
ptControl.x = pth.currentPoint.x + ((ptDest.x - pth.currentPoint.x) * 0.5)
ptControl.y = r.minY + lineOffsets[i][1]
pth.addQuadCurve(to: ptDest, controlPoint: ptControl)
}
// now we're at top-right
for i in 0..<incrementVals.count {
ptDest.y += r.height * incrementVals[i]
ptDest.x = r.maxX + lineOffsets[i][0]
ptControl.y = pth.currentPoint.y + ((ptDest.y - pth.currentPoint.y) * 0.5)
ptControl.x = r.maxX + lineOffsets[i][1]
pth.addQuadCurve(to: ptDest, controlPoint: ptControl)
}
// now we're at bottom-right
for i in 0..<incrementVals.count {
ptDest.x -= r.width * incrementVals[i]
ptDest.y = r.maxY + lineOffsets[i][0]
ptControl.x = pth.currentPoint.x - ((pth.currentPoint.x - ptDest.x) * 0.5)
ptControl.y = r.maxY + lineOffsets[i][1]
pth.addQuadCurve(to: ptDest, controlPoint: ptControl)
}
// now we're at bottom-left
for i in 0..<incrementVals.count {
ptDest.y -= r.height * incrementVals[i]
ptDest.x = r.minX + lineOffsets[i][0]
ptControl.y = pth.currentPoint.y - ((pth.currentPoint.y - ptDest.y) * 0.5)
ptControl.x = r.minX + lineOffsets[i][1]
pth.addQuadCurve(to: ptDest, controlPoint: ptControl)
}
borderLayer.path = pth.cgPath
}
}
and an example controller:
class SketchTestVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let v = SketchBorderView()
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
v.centerXAnchor.constraint(equalTo: g.centerXAnchor),
v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
v.widthAnchor.constraint(equalToConstant: 320.0),
v.heightAnchor.constraint(equalTo: v.widthAnchor),
])
}
}
Using that code, though, we still have too much uniformity, so in actual use we'd want to randomize the number of segments, the widths of the segments, and the control-point offsets.
Of course, to get your "rounded rect" you'd want to add arcs at the corners.
I expect this should get you on your way though.
use this extension to solve the issue
import Foundation
import UIKit
extension UIView {
func dropShadow(scale: Bool = true) {
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.2
layer.shadowOffset = .zero
layer.shadowRadius = 5
layer.shouldRasterize = true
layer.rasterizationScale = scale ? UIScreen.main.scale : 1
}
#IBInspectable
var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
#IBInspectable
var borderWidth: CGFloat {
get {
return layer.borderWidth
}
set {
layer.borderWidth = newValue
}
}
#IBInspectable
var borderColor: UIColor? {
get {
let color = UIColor.init(cgColor: layer.borderColor!) //UIColor.init(CGColor: layer.borderColor!)
return color
}
set {
layer.borderColor = newValue?.cgColor
}
}
#IBInspectable
var shadowRadius: CGFloat {
get {
return layer.shadowRadius
}
set {
layer.shadowRadius = newValue
}
}
#IBInspectable
var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
#IBInspectable
var shadowOffset: CGSize {
get {
return layer.shadowOffset
}
set {
layer.shadowOffset = newValue
}
}
#IBInspectable
var shadowColor: UIColor? {
get {
if let color = layer.shadowColor {
return UIColor(cgColor: color)
}
return nil
}
set {
if let color = newValue {
layer.shadowColor = color.cgColor
} else {
layer.shadowColor = nil
}
}
}
}
it will look like this

Create custom Line graph with shadow using CAShapeLayer swift

I have modified Line graph of Minh Nguyen to some extend to show two lines one for systolic and othere for diastolic.
The first image show the how the graph should look like and second image is what I have achieved.
struct PointEntry {
let systolic: Int
let diastolic: Int
let label: String
}
extension PointEntry: Comparable {
static func <(lhs: PointEntry, rhs: PointEntry) -> Bool {
return lhs.systolic < rhs.systolic || lhs.systolic < rhs.systolic
}
static func ==(lhs: PointEntry, rhs: PointEntry) -> Bool {
return lhs.systolic == rhs.systolic && lhs.diastolic == rhs.diastolic
}
}
class LineChart: UIView {
/// gap between each point
let lineGap: CGFloat = 30.0
/// preseved space at top of the chart
let topSpace: CGFloat = 20.0
/// preserved space at bottom of the chart to show labels along the Y axis
let bottomSpace: CGFloat = 40.0
/// The top most horizontal line in the chart will be 10% higher than the highest value in the chart
let topHorizontalLine: CGFloat = 110.0 / 100.0
/// Dot inner Radius
var innerRadius: CGFloat = 8
/// Dot outer Radius
var outerRadius: CGFloat = 12
var dataEntries: [PointEntry]? {
didSet {
self.setNeedsLayout()
}
}
/// Contains the main line which represents the data
private let dataLayer: CALayer = CALayer()
/// Contains dataLayer and gradientLayer
private let mainLayer: CALayer = CALayer()
/// Contains mainLayer and label for each data entry
private let scrollView: UIScrollView = {
let view = UIScrollView()
view.showsVerticalScrollIndicator = false
view.showsHorizontalScrollIndicator = false
return view
}()
/// Contains horizontal lines
private let gridLayer: CALayer = CALayer()
/// An array of CGPoint on dataLayer coordinate system that the main line will go through. These points will be calculated from dataEntries array
private var systolicDataPoint: [CGPoint]?
private var daistolicDataPoint: [CGPoint]?
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
convenience init() {
self.init(frame: CGRect.zero)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
mainLayer.addSublayer(dataLayer)
mainLayer.addSublayer(gridLayer)
scrollView.layer.addSublayer(mainLayer)
self.addSubview(scrollView)
}
override func layoutSubviews() {
scrollView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
if let dataEntries = dataEntries {
scrollView.contentSize = CGSize(width: CGFloat(dataEntries.count) * lineGap + 30, height: self.frame.size.height)
mainLayer.frame = CGRect(x: 0, y: 0, width: CGFloat(dataEntries.count) * lineGap + 30, height: self.frame.size.height)
dataLayer.frame = CGRect(x: 0, y: topSpace, width: mainLayer.frame.width, height: mainLayer.frame.height - topSpace - bottomSpace)
systolicGradientLayer.frame = dataLayer.frame
diastolicGradientLayer.frame = dataLayer.frame
systolicDataPoint = convertDataEntriesToPoints(entries: dataEntries, isSystolic: true)
daistolicDataPoint = convertDataEntriesToPoints(entries: dataEntries, isSystolic: false)
gridLayer.frame = CGRect(x: 0, y: topSpace, width: CGFloat(dataEntries.count) * lineGap + 30, height: mainLayer.frame.height - topSpace - bottomSpace)
clean()
drawHorizontalLines()
drawVerticleLine()
drawChart(for: systolicDataPoint, color: .blue)
drawChart(for: daistolicDataPoint, color: .green)
drawLables()
}
}
/// Convert an array of PointEntry to an array of CGPoint on dataLayer coordinate system
/// - Parameter entries: Arrays of PointEntry
private func convertDataEntriesToPoints(entries: [PointEntry], isSystolic: Bool) -> [CGPoint] {
var result: [CGPoint] = []
// let gridValues: [CGFloat] = [0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0, 1.05]
for (index, value) in entries.enumerated() {
let difference: CGFloat = 0.125 / 30
let userValue: CGFloat = isSystolic ? CGFloat(value.systolic) : CGFloat(value.diastolic)
var height = (userValue - 30.0) * difference
height = (1.0 - height) * gridLayer.frame.size.height
let point = CGPoint(x: CGFloat(index)*lineGap + 40, y: height)
result.append(point)
}
return result
}
/// Draw a zigzag line connecting all points in dataPoints
private func drawChart(for points: [CGPoint]?, color: UIColor) {
if let dataPoints = points, dataPoints.count > 0 {
guard let path = createPath(for: points) else { return }
let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.strokeColor = color.cgColor
lineLayer.fillColor = UIColor.clear.cgColor
dataLayer.addSublayer(lineLayer)
}
}
/// Create a zigzag bezier path that connects all points in dataPoints
private func createPath(for points: [CGPoint]?) -> UIBezierPath? {
guard let dataPoints = points, dataPoints.count > 0 else {
return nil
}
let path = UIBezierPath()
path.move(to: dataPoints[0])
for i in 1..<dataPoints.count {
path.addLine(to: dataPoints[i])
}
return path
}
/// Create titles at the bottom for all entries showed in the chart
private func drawLables() {
if let dataEntries = dataEntries,
dataEntries.count > 0 {
for i in 0..<dataEntries.count {
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: lineGap*CGFloat(i) - lineGap/2 + 40, y: mainLayer.frame.size.height - bottomSpace/2 - 8, width: lineGap, height: 16)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.backgroundColor = UIColor.clear.cgColor
textLayer.alignmentMode = CATextLayerAlignmentMode.center
textLayer.contentsScale = UIScreen.main.scale
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
textLayer.fontSize = 11
textLayer.string = dataEntries[i].label
mainLayer.addSublayer(textLayer)
}
}
}
/// Create horizontal lines (grid lines) and show the value of each line
private func drawHorizontalLines() {
let gridValues: [CGFloat] = [1.05, 1.0, 0.875, 0.75, 0.625, 0.5, 0.375, 0.25, 0.125]
let gridText = ["", "30", "60", "90", "120", "150", "180", "210", "240"]
for (index, value) in gridValues.enumerated() {
let height = value * gridLayer.frame.size.height
let path = UIBezierPath()
if value == gridValues.first! {
path.move(to: CGPoint(x: 30, y: height))
} else {
path.move(to: CGPoint(x: 28, y: height))
}
path.addLine(to: CGPoint(x: gridLayer.frame.size.width, y: height))
let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = UIColor.black.cgColor
if value != gridValues.first! {
lineLayer.lineDashPattern = [4, 4]
}
lineLayer.lineWidth = 0.5
gridLayer.addSublayer(lineLayer)
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: 4, y: height-8, width: 50, height: 16)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.backgroundColor = UIColor.clear.cgColor
textLayer.contentsScale = UIScreen.main.scale
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
textLayer.fontSize = 12
textLayer.string = gridText[index]
gridLayer.addSublayer(textLayer)
}
}
private func drawVerticleLine() {
let height = gridLayer.frame.size.height * 1.05
let path = UIBezierPath()
path.move(to: CGPoint(x: 30, y: 0))
path.addLine(to: CGPoint(x: 30, y: height))
let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = UIColor.black.cgColor
lineLayer.lineWidth = 0.5
gridLayer.addSublayer(lineLayer)
}
private func clean() {
mainLayer.sublayers?.forEach({
if $0 is CATextLayer {
$0.removeFromSuperlayer()
}
})
dataLayer.sublayers?.forEach({$0.removeFromSuperlayer()})
gridLayer.sublayers?.forEach({$0.removeFromSuperlayer()})
}
}
How can I add shadow to lines like shown in the first image and add simple line drawing animation to the Graph?

Need help whenIntegrating complex UIView with SwiftUI failed

I tried to test a waving animated UIView that is runloop based on SwiftUI using ''UIViewRepresentable'' but it does not appear to be animating at all.
Using UIViewRepresentable Protocol to connect swiftui to UIView.
Swift UI Code:
import SwiftUI
struct WaveView: UIViewRepresentable {
func makeUIView(context: Context) -> WaveUIView {
WaveUIView(frame: .init(x: 0, y: 0, width: 300, height: 300))
}
func updateUIView(_ view: WaveUIView, context: Context) {
view.start()
}
}
struct WaveView_Previews: PreviewProvider {
static var previews: some View {
WaveView()
}
}
The "Waving" UIView that I tested working on UIViewController way of doing it.
import Foundation
import UIKit
class WaveUIView:UIView {
/// wave curvature (default: 1.5)
open var waveCurvature: CGFloat = 1.5
/// wave speed (default: 0.6)
open var waveSpeed: CGFloat = 0.6
/// wave height (default: 5)
open var waveHeight: CGFloat = 5
/// real wave color
open var realWaveColor: UIColor = UIColor.red {
didSet {
self.realWaveLayer.fillColor = self.realWaveColor.cgColor
}
}
/// mask wave color
open var maskWaveColor: UIColor = UIColor.red {
didSet {
self.maskWaveLayer.fillColor = self.maskWaveColor.cgColor
}
}
/// float over View
open var overView: UIView?
/// wave timmer
fileprivate var timer: CADisplayLink?
/// real aave
fileprivate var realWaveLayer :CAShapeLayer = CAShapeLayer()
/// mask wave
fileprivate var maskWaveLayer :CAShapeLayer = CAShapeLayer()
/// offset
fileprivate var offset :CGFloat = 0
fileprivate var _waveCurvature: CGFloat = 0
fileprivate var _waveSpeed: CGFloat = 0
fileprivate var _waveHeight: CGFloat = 0
fileprivate var _starting: Bool = false
fileprivate var _stoping: Bool = false
/**
Init view
- parameter frame: view frame
- returns: view
*/
override init(frame: CGRect) {
super.init(frame: frame)
var frame = self.bounds
frame.origin.y = frame.size.height
frame.size.height = 0
maskWaveLayer.frame = frame
realWaveLayer.frame = frame
// test
self.backgroundColor = UIColor.blue
}
/**
Init view with wave color
- parameter frame: view frame
- parameter color: real wave color
- returns: view
*/
public convenience init(frame: CGRect, color:UIColor) {
self.init(frame: frame)
self.realWaveColor = color
self.maskWaveColor = color.withAlphaComponent(0.4)
realWaveLayer.fillColor = self.realWaveColor.cgColor
maskWaveLayer.fillColor = self.maskWaveColor.cgColor
self.layer.addSublayer(self.realWaveLayer)
self.layer.addSublayer(self.maskWaveLayer)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/**
Add over view
- parameter view: overview
*/
open func addOverView(_ view: UIView) {
overView = view
overView?.center = self.center
overView?.frame.origin.y = self.frame.height - (overView?.frame.height)!
self.addSubview(overView!)
}
/**
Start wave
*/
open func start() {
if !_starting {
_stop()
_starting = true
_stoping = false
_waveHeight = 0
_waveCurvature = 0
_waveSpeed = 0
timer = CADisplayLink(target: self, selector: #selector(wave))
timer?.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
}
}
/**
Stop wave
*/
open func _stop(){
if (timer != nil) {
timer?.invalidate()
timer = nil
}
}
open func stop(){
if !_stoping {
_starting = false
_stoping = true
}
}
/**
Wave animation
*/
#objc func wave() {
// when view is not visible
// if overView?.window == nil {
// print("not playing cause not visible")
// return
// }
if _starting {
print("started")
if _waveHeight < waveHeight {
_waveHeight = _waveHeight + waveHeight/100.0
var frame = self.bounds
frame.origin.y = frame.size.height-_waveHeight
frame.size.height = _waveHeight
maskWaveLayer.frame = frame
realWaveLayer.frame = frame
_waveCurvature = _waveCurvature + waveCurvature / 100.0
_waveSpeed = _waveSpeed + waveSpeed / 100.0
} else {
_starting = false
}
}
if _stoping {
if _waveHeight > 0 {
_waveHeight = _waveHeight - waveHeight/50.0
var frame = self.bounds
frame.origin.y = frame.size.height
frame.size.height = _waveHeight
maskWaveLayer.frame = frame
realWaveLayer.frame = frame
_waveCurvature = _waveCurvature - waveCurvature / 50.0
_waveSpeed = _waveSpeed - waveSpeed / 50.0
} else {
_stoping = false
_stop()
}
}
offset += _waveSpeed
let width = frame.width
let height = CGFloat(_waveHeight)
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: height))
var y: CGFloat = 0
let maskpath = CGMutablePath()
maskpath.move(to: CGPoint(x: 0, y: height))
let offset_f = Float(offset * 0.045)
let waveCurvature_f = Float(0.01 * _waveCurvature)
for x in 0...Int(width) {
y = height * CGFloat(sinf( waveCurvature_f * Float(x) + offset_f))
path.addLine(to: CGPoint(x: CGFloat(x), y: y))
maskpath.addLine(to: CGPoint(x: CGFloat(x), y: -y))
}
if (overView != nil) {
let centX = self.bounds.size.width/2
let centY = height * CGFloat(sinf(waveCurvature_f * Float(centX) + offset_f))
let center = CGPoint(x: centX , y: centY + self.bounds.size.height - overView!.bounds.size.height/2 - _waveHeight - 1 )
overView?.center = center
}
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.closeSubpath()
self.realWaveLayer.path = path
maskpath.addLine(to: CGPoint(x: width, y: height))
maskpath.addLine(to: CGPoint(x: 0, y: height))
maskpath.closeSubpath()
self.maskWaveLayer.path = maskpath
}
}
I expect the SwiftUI to have the view animating and correctly have the frame/border changes according animation. But it is not animating at all right now.
Following is the animated view with UIViewController:
override func viewWillAppear(_ animated: Bool) {
cardView.start()
}
func viewdidload(){
let frame = CGRect(x: 0, y: 0, width: self.view.bounds.size.width * 0.8, height: view.bounds.height * 0.5)
cardView = HomeCardView(frame: frame, color: .gray)
cardView.addOverView(someUIView())
cardView.realWaveColor = UIColor.white.withAlphaComponent(0.7)
cardView.maskWaveColor = UIColor.white.withAlphaComponent(0.3)
cardView.waveSpeed = 1.2
cardView.waveHeight = 10
view.addSubview(cardView)
}
You forget to addOverview in update uimethod
func updateUIView(_ view: WaveUIView, context: Context) {
let overView: UIView = UIView(frame: CGRect.init(x: 0, y: 0, width: 300, height: 300))
overView.backgroundColor = UIColor.green
view.addOverView(overView)
view.start()
}

Custom View Broken When Window is Resized (Cocoa)

I am experiencing this weird bug with my custom view. The custom view is supposed to show meters of rating distribution. It gets added to a cell view of an outline view.
When I resize the window, the custom view somehow gets squished and looks broken. I have pasted the drawRect of the custom view below.
override func drawRect(r: NSRect) {
super.drawRect(r)
var goodRect: NSRect?
var okRect: NSRect?
var badRect: NSRect?
let barHeight = CGFloat(10.0)
if self.goodPercent != 0.0 {
goodRect = NSRect(x: 0, y: 0, width: r.width * CGFloat(goodPercent), height: barHeight)
let goodPath = NSBezierPath(roundedRect: goodRect!, xRadius: 6, yRadius: 6)
RatingDistributionView.goodColor.setFill()
goodPath.fill()
}
if self.okPercent != 0.0 {
let okX = CGFloat(goodRect?.width ?? 0.0)
okRect = NSRect(x: okX, y: 0, width: r.width * CGFloat(okPercent), height: barHeight)
let okPath = NSBezierPath(roundedRect: okRect!, xRadius: 6, yRadius: 6)
RatingDistributionView.okColor.setFill()
okPath.fill()
}
if self.badPercent != 0.0 {
var badX: CGFloat
//Cases:
//Good persent and OK present - badX = okRect.x + okRect.width
//Good persent and OK missing - badX = goodRect.x + goodRect.width
//Good missing and OK present - badX = okRect.x + okRect.width
//Both missing -
if okRect != nil {
badX = okRect!.origin.x + okRect!.width
}else if goodRect != nil {
badX = goodRect!.origin.x + goodRect!.width
} else {
badX = 0.0
}
badRect = NSRect(x: badX, y: 0, width: r.width * CGFloat(badPercent), height: barHeight)
let badPath = NSBezierPath(roundedRect: badRect!, xRadius: 6, yRadius: 6)
RatingDistributionView.badColor.setFill()
badPath.fill()
}
//Draw dividers
let divWidth = CGFloat(6.75)
if self.goodPercent != 0.0 && (self.okPercent != 0.0 || self.badPercent != 0.0) {
let divX = goodRect!.origin.x + goodRect!.width
let divRect = NSRect(x: divX - (divWidth / 2.0), y: 0.0, width: divWidth, height: barHeight)
let divPath = NSBezierPath(roundedRect: divRect, xRadius: 0, yRadius: 0)
NSColor.whiteColor().setFill()
divPath.fill()
}
if self.okPercent != 0.0 && self.badPercent != 0.0 {
let divX = okRect!.origin.x + okRect!.width
let divRect = NSRect(x: divX - (divWidth / 2.0), y: 0.0, width: divWidth, height: barHeight)
let divPath = NSBezierPath(roundedRect: divRect, xRadius: 0, yRadius: 0)
NSColor.whiteColor().setFill()
divPath.fill()
}
}
AN alternative solution for your problem is to use NSView. You can have a container view with rounded corner and then drawing subviews (red, orange, green) in that container. like this;
I have written a class for it that you may customise according to your requirements;
public class CProgressView:NSView {
private lazy var goodView:NSView = {
let viw:NSView = NSView(frame: NSRect.zero);
viw.layer = CALayer();
viw.layer?.backgroundColor = NSColor.greenColor().CGColor;
self.addSubview(viw)
return viw;
} ();
private lazy var okView:NSView = {
let viw:NSView = NSView(frame: NSRect.zero);
viw.layer = CALayer();
viw.layer?.backgroundColor = NSColor.orangeColor().CGColor;
self.addSubview(viw)
return viw;
} ();
private lazy var badView:NSView = {
let viw:NSView = NSView(frame: NSRect.zero);
viw.layer = CALayer();
viw.layer?.backgroundColor = NSColor.redColor().CGColor;
self.addSubview(viw)
return viw;
} ();
private var _goodProgress:CGFloat = 33;
private var _okProgress:CGFloat = 33;
private var _badProgress:CGFloat = 34;
private var goodViewFrame:NSRect {
get {
let rect:NSRect = NSRect(x: 0, y: 0, width: (self.frame.size.width * (_goodProgress / 100.0)), height: self.frame.size.height);
return rect;
}
}
private var okViewFrame:NSRect {
get {
let rect:NSRect = NSRect(x: self.goodViewFrame.size.width, y: 0, width: (self.frame.size.width * (_okProgress / 100.0)), height: self.frame.size.height);
return rect;
}
}
private var badViewFrame:NSRect {
get {
let width:CGFloat = (self.frame.size.width * (_badProgress / 100.0));
let rect:NSRect = NSRect(x: self.frame.size.width - width, y: 0, width: width, height: self.frame.size.height);
return rect;
}
}
override public init(frame frameRect: NSRect) {
super.init(frame: frameRect);
//--
self.commonInit();
}
required public init?(coder: NSCoder) {
super.init(coder: coder);
}
override public func awakeFromNib() {
super.awakeFromNib();
//--
self.commonInit();
}
private func commonInit() {
self.layer = CALayer();
self.layer!.cornerRadius = 15;
self.layer!.masksToBounds = true
//-
self.updateFrames();
}
public func updateProgress(goodProgressV:Int, okProgressV:Int, badProgressV:Int) {
guard ((goodProgressV + okProgressV + badProgressV) == 100) else {
NSLog("Total should be 100%");
return;
}
_goodProgress = CGFloat(goodProgressV);
_okProgress = CGFloat(okProgressV);
_badProgress = CGFloat(badProgressV);
//--
self.updateFrames();
}
private func updateFrames() {
self.layer?.backgroundColor = NSColor.grayColor().CGColor;
self.goodView.frame = self.goodViewFrame;
self.okView.frame = self.okViewFrame;
self.badView.frame = self.badViewFrame;
}
public override func resizeSubviewsWithOldSize(oldSize: NSSize) {
super.resizeSubviewsWithOldSize(oldSize);
//--
self.updateFrames();
}
}
Note: Call updateProgress() method for changing progress default is 33, 33 & 34 (33+33+34 = 100);
You may also download a sample project from the link below;
http://www.filedropper.com/osxtest
From the documentation of drawRect(_ dirtyRect: NSRect):
dirtyRect:
A rectangle defining the portion of the view that requires redrawing. This rectangle usually represents the portion of the view that requires updating. When responsive scrolling is enabled, this rectangle can also represent a nonvisible portion of the view that AppKit wants to cache.
Don't use dirtyRect for your calculations, use self.bounds.

Why doesn't UIView.animateWithDuration affect this custom view?

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