Swift for Xojo Developers : Part 6 - Graphics : Canvas

Note : all the code I am going to be posting under the GRAPHICS category is EXPERIMENTAL, and may required modifications if you plan to use it.
I plan on (over time) making this as close to the operation of the Xojo Canvas as I can.

There will also be posts in this same category for PDF and Picture methods both subclassed from this Canvas Object

/* ********************************* */
/* ****                         **** */
/* **** C A N V A S   C L A S S **** */
/* **** Super : none            **** */
/* ****                         **** */
/* ********************************* */
class Canvas {
    private let sysFontName : String = "HelveticaNeue"
    private let styleDASH   : [CGFloat]=[4,4] // 11110000
    private let styleDOT    : [CGFloat]=[2,2] // 1100
    private let styleNORMAL : [CGFloat]=[1]   // 1
    private let styleNONE   : [CGFloat]=[]
    //
    private var zBold       : Bool = false
    private var zItalic     : Bool = false
    private var zUnderline  : Bool = false
    private let zAlign      = NSMutableParagraphStyle()
    private var zTextFont   : String  = ""
    private var zTextSize   : CGFloat = 13
    private var zPenSize    : CGFloat = 0.5
    private var zPenStyle   = lineStyle.solid
    private var zForecolor  : UIColor = UIColor.black
    //

    private var zFontInfo   : UIFont  = UIFont(name: "HelveticaNeue", size:13)!
    //
    private var zContext    : CGContext?
    private var zOFFSET     : CGPoint = CGPoint.zero
    private var zPosition   : CGPoint = CGPoint.zero // Keep Track of "pen" position
    //
    // Create Graphics Object
    //
    init() {}

    init(context: CGContext,offset:CGPoint=CGPoint.zero) {
        zContext  = context
        zOFFSET   = CGPoint(x: offset.x,y: offset.y)
        textFont=""
        textSize=(-1)
        updateFont(1)
        zAlign.alignment = .left
        zContext?.setLineWidth(zPenSize)
    }

    var context : CGContext { get { return zContext! } }

    var Offset : CGPoint {
        get { return zOFFSET }
        set { zOFFSET=newValue }
    }

    //
    // Set/Get FontName
    //
    var textFont : String {
        get { return zTextFont }
        set {
            zTextFont=newValue
            if zTextFont.isEmpty || zTextFont.uppercased()=="SYSTEM" { zTextFont=sysFontName }
            updateFont(2)
        }
    }

    //
    // Set/Get FontSize
    //
    var textSize : CGFloat {
        get { return zTextSize }
        set {
            zTextSize=newValue
            if zTextSize<=0 { zTextSize=13 }
            updateFont(3)
        }
    }

    //
    // Set/Get Bold Trait
    //
    var bold : Bool {
        set { zBold=newValue;updateFont(4) }
        get { return zBold}
    }

    //
    // Set/Get Italic Trait
    //
    var italic : Bool {
        set { zItalic=newValue;updateFont(5)}
        get { return zItalic}
    }

    //
    // Set/Get Underline Trait
    //
    var underline : Bool {
        set { zUnderline=newValue }
        get { return zUnderline}
    }

    var textHeight : CGFloat { get { return zFontInfo.ascender-zFontInfo.descender } }
    var textAscent : CGFloat { get { return zFontInfo.ascender } }
    func stringWidth(_ text:String)  -> CGFloat { return stringInfo(text).width }
    func stringHeight(_ text:String) -> CGFloat { return stringInfo(text).height }

    //
    // Set/Get FontSize
    //
    var foreColor : UIColor {
        get { return zForecolor }
        set {
            zForecolor=newValue
            zContext?.setFillColor(zForecolor.cgColor)
            zContext?.setStrokeColor(zForecolor.cgColor)
        }
    }

    //
    // Set/Get Text Alignment
    //
    var align : NSTextAlignment {
        get { return zAlign.alignment }
        set { zAlign.alignment = newValue }
    }

    //
    // Set the PenSize [ie. Line Drawing Width]
    //
    var penSize : CGFloat {
        get { return zPenSize }
        set {
            if zPenSize != newValue {
                zPenSize=newValue
                // zPenSize = max(0.25,newValue)
                zContext?.setLineWidth(zPenSize)
            }
        }
    }

    //
    // Set Pen Style [solid, dotted, dashed]
    //
    var penStyle :lineStyle {
        get { return zPenStyle }
        set {
            if zPenStyle != newValue {
                zPenStyle=newValue
                switch zPenStyle {
                case .none : zContext?.setLineDash(phase: 0,lengths: styleNONE)
                case .solid: zContext?.setLineDash(phase:0,lengths: styleNORMAL)
                case .dot  : zContext?.setLineDash(phase:0,lengths: styleDOT)
                case .dash : zContext?.setLineDash(phase:0,lengths: styleDASH)
                }
            }
        }
    }

    //
    // Track "pen" Position, this is required by some PDF routines
    //
    var position : CGPoint {
        get { return zPosition }
        set { zPosition = newValue }
    }

    //
    // Insert an IMAGE into document
    //
    func drawPicture(_ img:UIImage,_ rect:CGRect) { img.draw(in: adjRect(rect)) }

    //
    // Insert Text into document
    //
    func drawString(_ text:String,_ rect:CGRect) {
        let myString : NSString = text as NSString
        //
        let textAttributes   = [
            .font         : zFontInfo,
            .paragraphStyle : zAlign,
            .foregroundColor : zForecolor
            ] as [NSAttributedString.Key : NSObject]

        myString.draw(in: adjRect(rect), withAttributes: textAttributes)

        // Bold and Italic are part of actual Font (if available), Underline we "fake"
        if (zUnderline==true) {
            let oldSize  : CGFloat   = penSize
            let oldStyle : lineStyle = penStyle
            var x1 : CGFloat=0
            var x2 : CGFloat=0
            let y  : CGFloat=rect.minY+zFontInfo.ascender+1
            let w=stringWidth(text)
            penSize=0.5
            switch zAlign.alignment {
            case .left:
                x1=rect.minX
                x2=x1+w
            case .center:
                x1=rect.minX+(rect.width-w)/2
                x2=x1+w
            case .right:
                x2=rect.maxX
                x1=x2-w
            default:
                return
            }
            drawLine(x1,y,x2,y)
            penSize  = oldSize
            penStyle = oldStyle
        }
    }

    func drawString(_ text:String,_ x:CGFloat,_ y:CGFloat, width:CGFloat=0) {
        let h:CGFloat=stringHeight(text)+1
        var w:CGFloat=width
        if w<=0 { w=stringWidth(text)+1 }
        drawString(text,CGRect(x: x,y: y,width: w,height: h))
    }

    //
    // Draw a line from (X1,Y1) -> (X2,Y2)
    //
    func drawLine(_ x1:CGFloat,_ y1:CGFloat,_ x2:CGFloat,_ y2:CGFloat) {
        zContext?.move(to: CGPoint(x: x1+zOFFSET.x, y: y1+zOFFSET.y))
        zContext?.addLine(to: CGPoint(x: x2+zOFFSET.x, y: y2+zOFFSET.y))
        zContext?.strokePath()
        position=CGPoint(x: x2,y: y2)
    }

    //
    // OVAL
    //
    func drawOval(_ rect:CGRect) { zContext?.strokeEllipse(in: adjRect(rect)) }
    func drawOval(_ x:CGFloat,_ y:CGFloat,_ width:CGFloat,_ height:CGFloat) { drawOval(CGRect(x: x,y: y,width: width,height: height)) }

    func fillOval(_ rect:CGRect) { zContext?.fillEllipse(in: adjRect(rect))}
    func fillOval(_ x:CGFloat,_ y:CGFloat,_ width:CGFloat,_ height:CGFloat) { fillOval(CGRect(x: x,y: y,width: width,height: height)) }
    //
    // Rectangle
    //
    func drawRect(_ rect:CGRect) { zContext?.stroke(adjRect(rect)) }
    func drawRect(_ x:CGFloat,_ y:CGFloat,_ width:CGFloat,_ height:CGFloat) { drawRect(CGRect(x: x,y: y,width: width,height: height)) }

    func fillRect(_ rect:CGRect) { zContext?.fill(adjRect(rect)) }
    func fillRect(_ x:CGFloat,_ y:CGFloat,_ width:CGFloat,_ height:CGFloat) { fillRect(CGRect(x: x,y: y,width: width,height: height)) }
    //
    // Rounded Rectangle
    //
    func drawRoundrect(_ rect:CGRect,_ radius:CGFloat) { let path=UIBezierPath(roundedRect:rect,cornerRadius: radius); path.stroke() }
    func drawRoundrect(_ x:CGFloat,_ y:CGFloat,_ width:CGFloat,_ height:CGFloat,_ radius:CGFloat) { drawRoundrect(CGRect(x: x,y: y,width: width,height: height),radius) }

    func fillRoundrect(_ rect:CGRect,_ radius:CGFloat) { let path=UIBezierPath(roundedRect:rect,cornerRadius: radius); path.fill()}
    func fillRoundrect(_ x:CGFloat,_ y:CGFloat,_ width:CGFloat,_ height:CGFloat,_ radius:CGFloat) { fillRoundrect(CGRect(x: x,y: y,width: width,height: height),radius) }

    //
    // Polygon
    //
    func drawPolygon(_ points:[CGPoint],fill:Bool=false) {
        for i in(0...points.count-1) {
            if(i == 0) {
                zContext?.move(to: CGPoint(x: points[i].x, y: points[i].y))
            } else {
                zContext?.addLine(to: CGPoint(x: points[i].x, y: points[i].y))
            }
        }
        zContext?.closePath()
        if fill==true {
            zContext?.fillPath()
        } else {
            zContext?.strokePath()
        }
    }

    func fillPolygon(_ points:[CGPoint]) { drawPolygon(points,fill: true) }

    /*--------------------------*/
    /* PRIVATE CANVAS FUNCTIONS */
    /*--------------------------*/
    // create a UIFONT object based on Font/Size properties
    private func updateFont(_ id:Int)  {
        func withTraits(_ traits:UIFontDescriptor.SymbolicTraits...)  {
            let descriptor = zFontInfo.fontDescriptor.withSymbolicTraits(UIFontDescriptor.SymbolicTraits(traits))
            zFontInfo=UIFont(descriptor: descriptor!, size: 0)
        }

        zFontInfo=UIFont(name: zTextFont, size: zTextSize)!

        if (zBold==true) || (zItalic==true) {
            if (zBold==true)  && (zItalic==false) { withTraits( .traitBold) }
            if (zBold==false) && (zItalic==true)  { withTraits( .traitItalic) }
            if (zBold==true)  && (zItalic==true)  { withTraits( .traitItalic , .traitBold) }
        }
    }

    // return String Metrics based on current font
    private func stringInfo(_ text:String) -> CGSize {
        let myString: NSString = text as NSString
        return myString.size(withAttributes: [ .font: zFontInfo ])
    }

    private func adjRect(_ rect:CGRect) -> CGRect {
        position=CGPoint(x: rect.maxX,y: rect.maxY) // DO NOT ADJUST POSITION
        return rect.offsetBy(dx: zOFFSET.x, dy: zOFFSET.y)
    }
}