Display a fraction in one UILabel - swift

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

Related

How can I draw text in Swift's CoreGraphics context in a UIImageView?

I'm trying to draw a string in a UIImageView in Swift's CoreGraphics for a dynamic geometry software app. I'm using context graphics for graphing points and lines, but when I try to draw a text string (for example, the length of a line segment), I either get no text (such as in these two examples), or everything else I'm drawing is erased except the text string (sorry, I didn't save examples of that). In the latter case, I could draw other stuff on top of the string, but that isn't really helpful either, since I could only draw a single string (trying to draw a second string erased the first).
Here are two attempts, both of which result in no text string drawn:
UIGraphicsBeginImageContext(canvas.frame.size)
canvas.draw(CGRect(x: 0, y: 0, width: canvas.frame.width, height: canvas.frame.height))
let context = UIGraphicsGetCurrentContext()
let text="\(value)"
let font=UIFont(name: "Helvetica", size: 12)!
let text_style=NSMutableParagraphStyle()
text_style.alignment=NSTextAlignment.center
let text_color=UIColor.black
let attributes=[NSAttributedString.Key.font:font, NSAttributedString.Key.paragraphStyle:text_style, NSAttributedString.Key.foregroundColor:text_color]
let text_x=coordinates.x
let text_y=coordinates.y
let text_rect=CGRect(x: text_x, y: text_y, width: 100, height: font.lineHeight)
text.draw(in: text_rect.integral, withAttributes: attributes)
UIGraphicsEndImageContext()
}```
```override func draw(_ canvas: UIImageView, _ isRed: Bool) {
UIGraphicsBeginImageContext(canvas.frame.size)
canvas.draw(CGRect(x: 0, y: 0, width: canvas.frame.width, height: canvas.frame.height))
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 512, height: 512))
let img = renderer.image { ctx in
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attrs = [NSAttributedString.Key.font: UIFont(name: "HelveticaNeue-Thin", size: 12)!, NSAttributedString.Key.paragraphStyle: paragraphStyle]
let string = "\(value)"
string.draw(with: CGRect(x: coordinates.x, y: coordinates.y, width: 100, height: 16), options: .usesLineFragmentOrigin, attributes: attrs, context: nil)
}
}```
I have also found a page that says something to the effect that it is impossible to draw text in a UIImageView. However, it is hard to believe that a graphics system as mature as core graphics wouldn't allow drawing text.
I am working on a barChart in MacOS Cocoa, using core Graphic. The program embed some usefull link for SwiftUI. You should be able to adapt my code to your program.
// ViewController.swift
// Chart_test
//
// Created by slareau on 2022-10-10.
//
//https://stackoverflow.com/questions/38079917/drawing-in-cocoa-swift
//https://www.raywenderlich.com/1101-core-graphics-on-macos-tutorial
//https://stackoverflow.com/questions/10627557/mac-os-x-drawing-into-an-offscreen-nsgraphicscontext-using-cgcontextref-c-funct
//https://nshipster.com/cggeometry/
//https://www.hackingwithswift.com/example-code/core-graphics/how-to-draw-a-text-string-using-core-graphics
import Cocoa
import CoreGraphics
class ChartsVC: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("ChartsVC")
let dr = Drawing(frame: NSRect(x: 50, y: 50, width: 550, height: 500))
self.view.addSubview(dr)
}
}
class Drawing: NSView {
//var facts_list: [myfact] = [] //complete database
//var fact_total: [Double] = [0.0, 0.0, 0.0, 0.0, 0.0]
var fact_total: [Double] = [12454, 22365,18989, 14656, 16777]
//var comm_list: [myComm] = []
// var comm_total: [Double] = [0.0, 0.0, 0.0, 0.0, 0.0]
var comm_total: [Double] = [8976, 14989, 13989, 12065, 15040]
var Current_Year : Int = 0
var Current_Month : Int = 0
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let todays_date = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy"
Current_Year = Int(dateFormatter.string(from: todays_date))!
// print ("VC60:", dateFormatter.string(from: todays_date), Current_Year!) OK
dateFormatter.dateFormat = "MM"
Current_Month = Int(dateFormatter.string(from: todays_date))!
// Load_CommDB()
// Load_FactDB()
var j = 0
while j < 5 { // last 5 years
print ( "VCH165: ", Current_Year - j, fact_total[j], comm_total[j] )
j += 1
}
// frame background
myRect(x: 0, y: 0, wd: 500, ht: 400, color: NSColor.lightGray)
for i in 0...4 {
let factT = String(fact_total[4 - i])
let commT = String(comm_total[4 - i])
let diffT = String(fact_total[4 - i] - comm_total[4 - i])
myRect(x: Double(50 + 80 * i) , y: 80, wd: 50, ht: Double(fact_total[4 - i]/100), color: NSColor.blue)
myRect(x: Double(60 + 80 * i) , y: 80, wd: 50, ht: Double(comm_total[4 - i]/100), color: NSColor.orange)
myRect(x: Double(70 + 80 * i) , y: 80, wd: 50, ht: Double((fact_total[4 - i] - comm_total[4 - i])/100), color: NSColor.green)
mytext(txt: factT.toCurrencyFormat(), x: Double(45 + 80 * i), y: 60)
mytext(txt: commT.toCurrencyFormat(), x: Double(45 + 80 * i), y: 40)
mytext(txt: diffT.toCurrencyFormat(), x: Double(45 + 80 * i), y: 20)
mytext(txt: String(Current_Year - (4 - i)), x: Double(45 + 80 * i), y: 3)
}
let context = NSGraphicsContext.current!.cgContext;
context.beginPath()
context.move(to: CGPoint(x: 10.0, y: 78.0))
context.addLine(to: CGPoint(x: 420.0, y: 78.0))
//context.setStrokeColor(red: 1, green: 0, blue: 0, alpha: 1)
context.setStrokeColor(NSColor.black.cgColor)
context.setLineWidth(2.0)
context.strokePath()
}
func myRect( x: Double, y: Double, wd: Double, ht :Double, color: NSColor) {
let context = NSGraphicsContext.current!.cgContext;
let rectangle = CGRect(x: x, y: y, width: wd, height: ht) // NSRect or CGRect work
//context.setFillColor(NSColor.yellow.cgColor)
context.setFillColor(color.cgColor)
context.setStrokeColor(NSColor.black.cgColor)
context.setLineWidth(2)
context.addRect(rectangle)
context.drawPath(using: .fillStroke)
}
func mytext ( txt: String, x: Double, y: Double) {
// let myfont = CTFontCreateWithName("Papyrus" as CFString, 12, nil)
let myfont = CTFontCreateWithName("Arial" as CFString, 12, nil)
let attributedString = NSAttributedString(string: txt, attributes: [NSAttributedString.Key.font: myfont])
// let attributedString = NSAttributedString(string: "allo le monde")
let line = CTLineCreateWithAttributedString(attributedString)
// Draw text
if let context = NSGraphicsContext.current?.cgContext {
context.textPosition = .init(x: x , y: y)
CTLineDraw(line, context)
}
//https://blog.krzyzanowskim.com/2020/07/09/coretext-academy-part-1/
//https://blog.krzyzanowskim.com/2020/07/10/coretext-swift-academy-part-2/
//https://blog.krzyzanowskim.com/2020/07/13/coretext-swift-academy-part-3/
}
}

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

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?

Getting SpriteKit texture from SKView returning nil after 104 calls

I am having some odd behavior in SpriteKit when creating a texture. The function below shows you what I am doing. In short, I'm in SceneKit and making a SCNNode out of an Array of colors (think pixel/voxels). It works like a charm. However, after exactly 104 calls the texture returned is nil. Afterwards, it is hit or miss whether the texture will be nil or not. I am also providing the exact color information. Thoughts?
func create2dModelSK(with colors: [String]) -> SCNNode? {
let geo = SCNBox(width: 1.0, height: 1.0, length: 0.1, chamferRadius: 0.0)
let base = SCNNode(geometry: geo)
let view = SKSpriteNode(color: .white, size: CGSize(width: 160, height: 160))
view.anchorPoint = CGPoint(x: 0, y: 1)
var xOffset = 0
var yOffset = 0
var count = 0
for _ in 0...15 {
for _ in 0...15 {
guard let newColor = UIColor(hexString: "#" + colors[count] + "ff") else { return base }
let n = SKSpriteNode(color: newColor, size: CGSize(width: 10, height: 10))
n.anchorPoint = CGPoint(x: 0, y: 1)
n.position = CGPoint(x: xOffset, y: yOffset)
view.addChild(n)
xOffset += 10
count += 1
}
xOffset = 0
yOffset -= 10
}
let skView = SKView(frame: CGRect(x: 0, y: 0, width: 160, height: 160))
let texture = skView.texture(from: view)
//AFTER being called 104 times, texture is nil.
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = texture
let sideMaterial = SCNMaterial()
sideMaterial.diffuse.contents = UIColor.white
let materialsForBox = [faceMaterial,sideMaterial,faceMaterial,sideMaterial,sideMaterial,sideMaterial]
base.geometry?.materials = materialsForBox
let scale = SCNVector3(x: 0.1, y: 0.1, z: 0.1)
base.scale = scale
return base
}
This is where autoreleasepool comes in handy, it allows you to release the memory when the autoreleasepool is finished so that you do not run out of space before using it again.
Of course this is not going to solve your main problem, where you are creating too many textures and running out of memory space, but it will allow you to at least make some more because it will release the temporary memory that view.texture(from:node) is holding on to.
func create2dModelSK(with colors: [String]) -> SCNNode? {
let geo = SCNBox(width: 1.0, height: 1.0, length: 0.1, chamferRadius: 0.0)
let base = SCNNode(geometry: geo)
let view = SKSpriteNode(color: .white, size: CGSize(width: 160, height: 160))
view.anchorPoint = CGPoint(x: 0, y: 1)
var xOffset = 0
var yOffset = 0
var count = 0
for _ in 0...15 {
for _ in 0...15 {
guard let newColor = UIColor(hexString: "#" + colors[count] + "ff") else { return base }
let n = SKSpriteNode(color: newColor, size: CGSize(width: 10, height: 10))
n.anchorPoint = CGPoint(x: 0, y: 1)
n.position = CGPoint(x: xOffset, y: yOffset)
view.addChild(n)
xOffset += 10
count += 1
}
xOffset = 0
yOffset -= 10
}
autoreleasepool{
let skView = SKView(frame: CGRect(x: 0, y: 0, width: 160, height: 160))
let texture = skView.texture(from: view)
//AFTER being called 104 times, texture is nil.
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = texture
let sideMaterial = SCNMaterial()
sideMaterial.diffuse.contents = UIColor.white
let materialsForBox = [faceMaterial,sideMaterial,faceMaterial,sideMaterial,sideMaterial,sideMaterial]
base.geometry?.materials = materialsForBox
}
let scale = SCNVector3(x: 0.1, y: 0.1, z: 0.1)
base.scale = scale
return base
}

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