Drawing in cocoa swift - swift

I've been trying to draw a simple line in a cocoa application using swift, however I fail to fully understand how to use the CGContext class.
I've created this class:
import Cocoa
class Drawing: NSView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
let context = NSGraphicsContext.currentContext()?.CGContext;
CGContextBeginPath(context)
CGContextMoveToPoint(context, 0, 0)
CGContextAddLineToPoint(context, 100, 100)
CGContextSetRGBStrokeColor(context, 1, 1, 1, 1)
CGContextSetLineWidth(context, 5.0)
CGContextStrokePath(context)
}
}
and used it in the main window like so
override func viewDidLoad() {
super.viewDidLoad()
let dr = Drawing()
self.view.addSubview(dr)
dr.drawRect(NSRect(x: 0, y: 0, width: 100, height: 100))
}
but it didn't do a thing

You need to initialise your Drawing view with a frame otherwise the system won't know where to draw it, like so:
override func viewDidLoad() {
super.viewDidLoad()
let dr = Drawing(frame: NSRect(x: 0, y: 0, width: 100, height: 100))
self.view.addSubview(dr)
}
Also as David said, you do not need to call drawRect yourself as it is automatically called by the system.

my 2 cents for swift 5.x Xcode 10:
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current?.cgContext else{
return
}
context.beginPath()
context.move(to: CGPoint.zero)
context.addLine(to: CGPoint( x: 100, y: 100))
context.setStrokeColor(CGColor( red: 1, green: 0, blue: 0, alpha: 1))
context.setLineWidth(5.0)
context.strokePath()
}

You don't need to add a view or use CoreGraphics instructions.
Simply do like this: ( EDIT: I added update exemple code )
class View: NSView {
var p1: NSPoint = .zero {
didSet { needsDisplay = true }
}
var p2: NSPoint = NSPoint(x: 100, y: 100) {
didSet { needsDisplay = true }
}
// Returns the rect using the two points
var rect: NSRect {
NSRect(x: p1.x, y: p1.y,
width: p2.x - p1.x, height: p2.y - p1.y)
}
// Draw a rectangle and its diagonal
override func draw(_ dirtyRect: NSRect) {
// Draw rect
let rect = self.rect
NSColor.red.setFill()
NSColor.black.setStroke()
rect.fill()
rect.stroke()
// Draw line
let path = NSBezierPath()
path.move(to: p1)
path.line(to: p2)
path.stroke()
}
}
Don't forget to call myView.needsDisplay = true to update content
From there, check auto completion to discover Cocoa drawing functions to draw paths, ovals, rounded rects. Cheers :)

Related

Using SwiftUI or UIKit to position view elements?

This is more or less a repost of a question that I think could have been reproduced more minimally. I'm trying to form a text bubble using the triangle created by the overriden draw() function and the rest of the callout. I removed all the code that couldn't possibly affect the positioning of either the triangle or the box.
Recap: I'd like to move the triangle created by the draw function outside of the frame created in the initialization (Currently, it's inside the frame).
If I can add anything to clarify or make this question better, let me know.
class CustomCalloutView: UIView, MGLCalloutView {
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
lazy var leftAccessoryView = UIView()
lazy var rightAccessoryView = UIView()
weak var delegate: MGLCalloutViewDelegate?
//MARK: Subviews -
required init() {
// init with 75% of width and 120px tall
super.init(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width:
UIScreen.main.bounds.width * 0.75, height: 120)))
setup()
}
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}
func setup() {
// setup this view's properties
self.backgroundColor = UIColor.white
}
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect,
animated: Bool) {
//Always, Slightly above center
self.center = view.center.applying(CGAffineTransform(translationX: 0, y: -self.frame.height))
view.addSubview(self)
}
override func draw(_ rect: CGRect) {
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .black
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y +
rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}

How to draw a line between two views in Swift 3?

I need help with drawing a simple line between two Outline Views in Swift 3 (Xcode 8).
My situation:
Main ViewController
|--- Main View
|--- Outline View
|--- Outline View
So I Need help to get the coordinates of both Outline Views and draw a line with them (the line itself is not that difficult, more to get the coordinates). The goal is to draw a line (programmatically) that connects both Outline Views (f.ex. from one edge to the other, or from the top, ...).
I already tried following:
class Line: NSView{
var origin = CGPoint()
var destination = CGPoint()
required init?(coder aDecoder: NSCoder){
super.init(coder: aDecoder)
}
init(fromPoint: CGPoint, toPoint: CGPoint){
self.origin = fromPoint
self.destination = toPoint
super.init(frame: CGRect(origin: fromPoint, size: CGSize(width: destination.x - origin.x, height: destination.y - origin.y)))
}
override func draw(_ dirtyRect: NSRect){
let myPath = NSBezierPath()
myPath.move(to: CGPoint(x: origin.x, y: origin.y))
myPath.line(to: CGPoint(x: destination.x - origin.x, y: destination.y - origin.y))
myPath.stroke()
}
}
class ViewController: NSViewController{
override func viewDidLoad(){
super.viewDidLoad()
let line = Line(fromPoint: self.view.convert(CGPoint.zero, to: self.view.viewWithTag(1)), toPoint: self.view.convert(CGPoint.zero, to: self.view.viewWithTag(2)))
view.addSubview(line)
}
}
But that didn't do anything.
I would appreciate your help!
Thank you
I now solved my problem (more or less) as following:
class Line: NSView{
var fromPoint = CGPoint()
var toPoint = CGPoint()
func setPoints(fromPoint: CGPoint, toPoint: CGPoint){
self.fromPoint = fromPoint
self.toPoint = toPoint
}
override func draw(_ dirtyRect: NSRect) {
let path = NSBezierPath()
NSColor.green.setFill()
path.move(to: fromPoint)
path.line(to: toPoint)
path.stroke()
}
}
class ViewController: NSViewController{
override function viewDidLoad(){
super.viewDidLoad()
let subview3 = Line(frame: self.view.bounds)
subview3.setPoints(fromPoint: subview1.convert(CGPoint(x: subview1.bounds.maxX, y: subview1.bounds.maxY), to: self.view), toPoint: subview2.convert(CGPoint(x: subview2.bounds.minX, y: subview2.bounds.minY), to: self.view))
self.view.addSubview(subview3)
}
}
I need to know how to do this on runtime. Do I always have to create a new view in order to draw a path?
A full example:
//
// ViewController.swift
// DrawConnectViews
//
// Created by T M on 17.06.17.
// Copyright © 2017 TM. All rights reserved.
//
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let subview1 = CustomViewWithColor(frame: NSRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 200.0, height: 200.0)))
let subview2 = CustomViewWithColor(frame: NSRect(origin: CGPoint(x: 360.0, y: 360.0), size: CGSize(width: 200.0, height: 200.0)))
// create a subview programatically:
let subview3 = Line(frame: self.view.bounds)
subview3.setPoints(fromPoint: subview1.convert(CGPoint(x: subview1.bounds.maxX, y: subview1.bounds.maxY), to: self.view), toPoint: subview2.convert(CGPoint(x: subview2.bounds.minX, y: subview2.bounds.minY), to: self.view))
self.view.addSubview(subview3)
subview1.setColor(color: NSColor.red)
subview2.setColor(color: NSColor.blue)
self.view.addSubview(subview1)
self.view.addSubview(subview2)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
class CustomViewWithColor: NSView{
var color = NSColor()
func setColor(color: NSColor){
self.color = color
}
override func draw(_ dirtyRect: NSRect) {
let path = NSBezierPath(rect: self.bounds)
self.color.setFill()
path.fill()
}
}
class Line: NSView{
var fromPoint = CGPoint()
var toPoint = CGPoint()
func setPoints(fromPoint: CGPoint, toPoint: CGPoint){
self.fromPoint = fromPoint
self.toPoint = toPoint
}
override func draw(_ dirtyRect: NSRect) {
let path = NSBezierPath()
NSColor.green.setFill()
path.move(to: fromPoint)
path.line(to: toPoint)
path.stroke()
}
}
That produces following:
Output of program
A lot of people will think that this is overkill, but what I do is add a subview whose background is the line color, whose height is constrained to the desired line thickness, and whose leading and trailing edges are constrained to the superview's leading and trailing edges. This ensures that the border will always adjust with the superview's size, which is why adding a border as a layer or customizing the view's draw(in:) to draw the border as a path don't work as well.

Swift 3: Drawing a rectangle

I'm 3 days new to swift, and I'm trying to figure out how to draw a rectangle. I'm too new to the language to know the classes to extend and the methods to override, and I've looked around for sample code, but nothing seems to work (which I'm attributing to my use of swift 3).
What I'm trying now is:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let k = Draw(frame: CGRect(
origin: CGPoint(x: 50, y: 50),
size: CGSize(width: 100, height: 100)))
k.draw(CGRect(
origin: CGPoint(x: 50, y: 50),
size: CGSize(width: 100, height: 100)));
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
class Draw: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let h = rect.height
let w = rect.width
var color:UIColor = UIColor.yellow()
var drect = CGRect(x: (w * 0.25),y: (h * 0.25),width: (w * 0.5),height: (h * 0.5))
var bpath:UIBezierPath = UIBezierPath(rect: drect)
color.set()
bpath.stroke()
print("it ran")
NSLog("drawRect has updated the view")
}
}
And that's not doing anything. Help.
In order to see the view, you need to create one and give it frame so that it knows how big to make it.
If you put your code in a Playground, and then add this line:
let d = Draw(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
You'll be able to click on the Quick View on the right, and then you'll see the view.
You can also add the view as a subview of view in your ViewController and then you'll see it on the iPhone:
override func viewDidLoad() {
super.viewDidLoad()
let k = Draw(frame: CGRect(
origin: CGPoint(x: 50, y: 50),
size: CGSize(width: 100, height: 100)))
// Add the view to the view hierarchy so that it shows up on screen
self.view.addSubview(k)
}
Note that you never call draw(_:) directly. It is called for you by Cocoa Touch to display the view.
Create a class, I put it in a separate Swift 3 file.
//
// Plot_Demo.swift
//
// Storyboard is not good in creating self adapting UI
// Plot_Demo creates the drawing programatically.
import Foundation
import UIKit
public class Plot_Demo: UIView
{
override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ frame: CGRect) {
let h = frame.height
let w = frame.width
let color:UIColor = UIColor.yellow
let drect = CGRect(x: (w * 0.25), y: (h * 0.25), width: (w * 0.5), height: (h * 0.5))
let bpath:UIBezierPath = UIBezierPath(rect: drect)
color.set()
bpath.stroke()
print("it ran")
NSLog("drawRect has updated the view")
}
}
Example of use in an UIViewController object:
override func viewDidLoad() {
super.viewDidLoad()
// Instantiate a new Plot_Demo object (inherits and has all properties of UIView)
let k = Plot_Demo(frame: CGRect(x: 75, y: 75, width: 150, height: 150))
// Put the rectangle in the canvas in this new object
k.draw(CGRect(x: 50, y: 50, width: 100, height: 100))
// view: UIView was created earlier using StoryBoard
// Display the contents (our rectangle) by attaching it
self.view.addSubview(k)
}
Run in an iPhone simulator and on an iPhone:
Used XCode Version 8.0 (8A218a), Swift 3, target iOS 10.0
This is another way of drawing Rectangle,
Step 1: Get rectangle path for given points
(Note: arrPathPoints must be 4 in count to draw rectangle),
func getPathPayer(arrPathPoints:[CGPoint]) throws -> CAShapeLayer {
enum PathError : Error{
case moreThan2PointsNeeded
}
guard arrPathPoints.count > 2 else {
throw PathError.moreThan2PointsNeeded
}
let lineColor = UIColor.blue
let lineWidth: CGFloat = 2
let path = UIBezierPath()
let pathLayer = CAShapeLayer()
for (index,pathPoint) in arrPathPoints.enumerated() {
switch index {
//First point
case 0:
path.move(to: pathPoint)
//Last point
case arrPathPoints.count - 1:
path.addLine(to: pathPoint)
path.close()
//Middle Points
default:
path.addLine(to: pathPoint)
}
}
pathLayer.path = path.cgPath
pathLayer.strokeColor = lineColor.cgColor
pathLayer.lineWidth = lineWidth
pathLayer.fillColor = UIColor.clear.cgColor
return pathLayer
}
Step 2: Usage, Call method like this,
override func viewDidLoad() {
super.viewDidLoad()
do {
let rectangleLayer = try getPathPayer(arrPathPoints: [
CGPoint(x: 110, y: 110), //Top-Left
CGPoint(x: 130, y: 110), //Top-Right
CGPoint(x: 130, y: 130), //Bottom-Right
CGPoint(x: 110, y: 130)]) //Bottom-Left
view.layer.addSublayer(rectangleLayer)
} catch {
debugPrint(error)
}
}
My version of how to draw a rectangle using Swift 5.
First create a class to do the drawing. It uses CoreGraphics to do the drawing, rather than UIKit.
import UIKit
class DrawRectangle: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
print("could not get graphics context")
return
}
context.setStrokeColor(UIColor.yellow.cgColor)
context.setLineWidth(2)
context.stroke(rect.insetBy(dx: 10, dy: 10))
}
}
Then put this in your ViewController's viewDidLoad()
let myView = DrawRectangle(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
self.view.addSubview(myView)

How to change color of Triangle figure using drawRect on Swift

I have made triangle view, called UpTriangleView. It is used in order to show vote. When they are tapped, I want to change their color. I wanna UIColor.grayColor().setStroke() from instance, however I have no idea how to do it. Please tell me how to do it, if you know. Thank you for your kindeness.
class UpTriangleView: UIView {
override func drawRect(rect: CGRect) {
self.backgroundColor = UIColor.clearColor()
// Get Height and Width
let layerHeight = self.layer.frame.height
let layerWidth = self.layer.frame.width
// Create Path
let line = UIBezierPath()
// Draw Points
line.moveToPoint(CGPointMake(0, layerHeight))
line.addLineToPoint(CGPointMake(layerWidth, layerHeight))
line.addLineToPoint(CGPointMake(layerWidth/2, 0))
line.addLineToPoint(CGPointMake(0, layerHeight))
line.closePath()
// Apply Color
UIColor.grayColor().setStroke()
UIColor.grayColor().setFill()
line.lineWidth = 3.0
line.fill()
line.stroke()
// Mask to Path
let shapeLayer = CAShapeLayer()
shapeLayer.path = line.CGPath
self.layer.mask = shapeLayer
}
}
class QATableViewCell : UITableViewCell{
#IBOutlet weak var upTriangleView: UpTriangleView!
}
Add a property to your UpTriangleView that is the color you want to draw it. Implement didSet and call setNeedsDisplay() if the color is set:
class UpTriangleView: UIView {
var color = UIColor.gray {
didSet {
self.setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
self.backgroundColor = .clear
// Get Height and Width
let layerHeight = self.layer.bounds.height
let layerWidth = self.layer.bounds.width
// Create Path
let line = UIBezierPath()
// Draw Points
line.move(to: CGPoint(x: 0, y: layerHeight))
line.addLine(to: CGPoint(x: layerWidth, y: layerHeight))
line.addLine(to: CGPoint(x: layerWidth/2, y: 0))
line.addLine(to: CGPoint(x: 0, y: layerHeight))
line.close()
// Apply Color
color.setStroke()
color.setFill()
line.lineWidth = 3.0
line.fill()
line.stroke()
// Mask to Path
let shapeLayer = CAShapeLayer()
shapeLayer.path = line.cgPath
self.layer.mask = shapeLayer
}
}
Now, demo it in a Playground (see picture below for results):
let utv = UpTriangleView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
utv.color = .yellow // now triangle is yellow
utv.color = .red // now triangle is red
setNeedsDisplay will tell iOS your view needs redrawing, and drawRect will be called again using the newly set color.

How do I create a circle with CALayer?

I have the code below tested, but when I give it constraints it becomes a little small circle:
override func drawRect(rect: CGRect) {
var path = UIBezierPath(ovalInRect: rect)
fillColor.setFill()
path.fill()
//set up the width and height variables
//for the horizontal stroke
let plusHeight:CGFloat = 300.0
let plusWidth:CGFloat = 450.0
//create the path
var plusPath = UIBezierPath()
//set the path's line width to the height of the stroke
plusPath.lineWidth = plusHeight
//move the initial point of the path
//to the start of the horizontal stroke
plusPath.moveToPoint(CGPoint(
x:self.bounds.width/2 - plusWidth/2 + 0.5,
y:self.bounds.height/2 + 0.5))
//add a point to the path at the end of the stroke
plusPath.addLineToPoint(CGPoint(
x:self.bounds.width/2 + plusWidth/2 + 0.5,
y:self.bounds.height/2 + 0.5))
}
Change radius and fillColor as you want. :)
import Foundation
import UIKit
class CircleLayerView: UIView {
var circleLayer: CAShapeLayer!
override func draw(_ rect: CGRect) {
super.draw(rect)
if circleLayer == nil {
circleLayer = CAShapeLayer()
let radius: CGFloat = 150.0
circleLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 2.0 * radius, height: 2.0 * radius), cornerRadius: radius).cgPath
circleLayer.position = CGPoint(x: self.frame.midX - radius, y: self.frame.midY - radius)
circleLayer.fillColor = UIColor.blue.cgColor
self.layer.addSublayer(circleLayer)
}
}
}
The rect being passed into drawRect is the area that needs to be updated, not the size of the drawing. In your case, you would probably just ignore the rect being passed in and set the circle to the size you want.
//// Oval Drawing
var ovalPath = UIBezierPath(ovalInRect: CGRectMake(0, 0, 300, 300))
UIColor.whiteColor().setFill()
ovalPath.fill()
The only correct way to do it:
private lazy var whiteCoin: CAShapeLayer = {
let c = CAShapeLayer()
c.path = UIBezierPath(ovalIn: bounds).cgPath
c.fillColor = UIColor.white.cgColor
layer.addSublayer(c)
return c
}()
That literally makes a layer, as you wanted.
In iOS you must correctly resize any layers you are in charge of, when the view is resized/redrawn.
How do you do that? It is the very raison d'etre of layoutSubviews.
class ProperExample: UIView {
open override func layoutSubviews() {
super.layoutSubviews()
whiteCoin.frame = bounds
}
}
It's that simple.
class ProperExample: UIView {
open override func layoutSubviews() {
super.layoutSubviews()
whiteCoin.frame = bounds
}
private lazy var whiteCoin: CAShapeLayer = {
let c = CAShapeLayer()
c.path = UIBezierPath(ovalIn: bounds).cgPath
c.fillColor = UIColor.white.cgColor
layer.addSublayer(c)
return c
}()
}