Create custom Line graph with shadow using CAShapeLayer swift - 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?

Related

EmitterCells not showing in CAEmitterLayer

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

Custom UIButton Causes Storyboard Agent Crash Swift

I created a custom UIButton class and it causing the storyboard agent to fail.
I'm Including my extensions cause I really don't know what the problem is.
I tried to debug this view from the storyboard but it sends me straight to assembly code.
I tried to make it a without #IBDesignable, but it still cause a crash.
Also if you tips for improving how I'm writing my class I'll be glad to hear them.
I'll be glad if you can help me
This is my class:
#IBDesignable class customButton: UIButton{
private let imagesPadding: CGFloat = 2
private var ArrowSymbleImageView: UIImageView!
#IBInspectable var iconImageInspectable: UIImage = UIImage(systemName: "globe")!{
willSet {
if (ArrowSymbleImageView != nil) {
ArrowSymbleImageView.image = newValue
}
}
}
#IBInspectable var BackgroundColorInspectable: UIColor = .white {
willSet {
self.backgroundColor = newValue
if (ArrowSymbleImageView != nil) {
if (self.BackgroundColorInspectable.isDarkColor) {
ArrowSymbleImageView.tintColor = .white
}else{
ArrowSymbleImageView.tintColor = .black
}
}
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.generalInit()
}
private func generalDeinit() {
ArrowSymbleImageView.removeFromSuperview()
}
private func generalInit() {
self.backgroundColor = self.BackgroundColorInspectable
self.roundCorners(corners: [.bottomLeft], radius: self.width() / 2 * 0.7)
self.dropShadow()
let sizePartFromView: CGFloat = 4
ArrowSymbleImageView = UIImageView(frame: CGRect(x: self.width() / 2 - (self.width() / sizePartFromView / 2),
y: self.height() / 2 - (self.height() / sizePartFromView / 2),
width: self.width() / sizePartFromView,
height: self.height() / sizePartFromView))
ArrowSymbleImageView.image = self.iconImageInspectable
if (self.BackgroundColorInspectable.isDarkColor) {
ArrowSymbleImageView.tintColor = .white
}else{
ArrowSymbleImageView.tintColor = .black
}
ArrowSymbleImageView.contentMode = .scaleAspectFill
self.addSubview(ArrowSymbleImageView)
}
}
internal extension UIView {
func roundCorners(corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
internal extension UIView {
func dropShadow(scale: Bool = true, size: CGSize = CGSize(width: -2, height: 2)) {
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.5
layer.shadowOffset = size
layer.shadowRadius = 1
layer.shadowPath = UIBezierPath(rect: bounds).cgPath
layer.shouldRasterize = true
layer.rasterizationScale = scale ? UIScreen.main.scale : 1
}
func dropShadow(color: UIColor, opacity: Float = 0.5, offSet: CGSize, radius: CGFloat = 1, scale: Bool = true) {
layer.masksToBounds = false
layer.shadowColor = color.cgColor
layer.shadowOpacity = opacity
layer.shadowOffset = offSet
layer.shadowRadius = radius
layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath
layer.shouldRasterize = true
layer.rasterizationScale = scale ? UIScreen.main.scale : 1
}
}
internal extension UIColor
{
var isDarkColor: Bool {
var r, g, b, a: CGFloat
(r, g, b, a) = (0, 0, 0, 0)
self.getRed(&r, green: &g, blue: &b, alpha: &a)
let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
return lum < 0.50 ? true : false
}
}
I noticed this:
UIImage(systemName: "globe")!
Is there a reason why you instantiate a custom image this way? Is "globe" an apple provided default image?
You should be really using, if this isn't a default image.
UIImage(named:"globe")!
Dont use Force unwrapping when you do not have confirmation about data always use optional binding and do change following line in code from this UIImage(systemName: "globe")! to UIImage(named:"globe")! .

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

Display a fraction in one UILabel

I am trying to display a fraction as something like 1/2 but I want to line to be horizontal so the fraction looks more fraction like, some thing like this:
1
_
2
However with no massive space and in a way that I can use it inside one UILabel (which can have multiple lines). I also need to be able to put a fraction as the numerator/denominator of a fraction and so on. Any suggestions? (Fonts, characters...)
Thank you,
Gleb Koval
Here's a UIView subclass that draws a fraction:
class FractionView: UIView {
var font: UIFont = UIFont.systemFont(ofSize: 16) {
didSet { self.setNeedsDisplay() }
}
var numerator: Int = 1 {
didSet { self.setNeedsDisplay() }
}
var denominator: Int = 2{
didSet { self.setNeedsDisplay() }
}
var spacing: CGFloat = 5{
didSet { self.setNeedsDisplay() }
}
override func draw(_ rect: CGRect) {
let numString = "\(numerator)" as NSString
let numWidth = numString.size(withAttributes: [.font: font]).width
let denomString = "\(denominator)" as NSString
let denomWidth = denomString.size(withAttributes: [.font: font]).width
let numX: CGFloat
let denomX: CGFloat
if numWidth <= denomWidth {
denomX = 0
numX = (denomWidth - numWidth) / 2
} else {
denomX = (numWidth - denomWidth) / 2
numX = 0
}
numString.draw(at: CGPoint(x: numX, y: 0), withAttributes: [.font : font])
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: font.lineHeight + spacing))
path.addLine(to: CGPoint(x: self.frame.maxX, y: font.lineHeight + spacing))
UIColor.black.setStroke()
path.lineWidth = 1
path.stroke()
denomString.draw(at: CGPoint(x: denomX, y: font.lineHeight + spacing * 2), withAttributes: [.font: font])
}
}
// usage:
let width = ("12" as NSString).size(withAttributes: [.font: UIFont.systemFont(ofSize: 16)]).width
let view = FractionView(frame: CGRect(x: 0, y: 0, width: width, height: 48))
view.backgroundColor = .white
view.denominator = 12
As Sulthan in comments has suggested, using an NSAttributedString would be an easier approach:
let attrString = NSMutableAttributedString(string: "1", attributes: [NSAttributedStringKey.underlineStyle : 1])
attrString.append(NSAttributedString(string: "\n2"))
attrString
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
label.attributedText = attrString
label.numberOfLines = 2

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