Make 2 contradictory methods work in drawRect
Asked Answered
N

3

5

I'm writing an app building elements consisting of CGPoints. I have 2 buttons: makeRectangle and makeTriangle. For building/drawing stage I use three methods for rectangle and three methods for triangle inside drawRect.

I'm stuck with my code in drawRect. In if-else-statement each method swaps the building/drawing scheme for previous element every time a button pressed.

If I already have built rectangle and then I click makeTriangle button, I get new triangle but my rectangle turns into triangle with one unconnected point.

Is there a workaround or I shouldn't use drawRect method?

Here's an SO post on the drawRect topic: To drawRect or not to drawRect

Animation of incorrectly-drawn trapezoid and triangle

Image of correctly-drawn trapezoid and triangle

Element declaration:

enum Element {
    case point1(point: CGPoint)
    case point2(point: CGPoint)
    case point3(point: CGPoint)
    case point4(point: CGPoint)

    func coord() -> [CGPoint] {    
        switch self {  
        case .point1(let point): return [point]
        case .point2(let point): return [point]
        case .point3(let point): return [point]
        case .point4(let point): return [point]
        }
    }
    func buildQuadPath(path: CGMutablePath) {
        switch self {
        case .point1(let point): CGPathMoveToPoint(path, nil, point.x, point.y)
        case .point2(let point): CGPathAddLineToPoint(path, nil, point.x, point.y)
        case .point3(let point): CGPathAddLineToPoint(path, nil, point.x, point.y)
        case .point4(let point): CGPathAddLineToPoint(path, nil, point.x, point.y)
        CGPathCloseSubpath(path)
        }
    }
    func buildTriPath(path: CGMutablePath) {
        switch self {
        case .point1(let point): CGPathMoveToPoint(path, nil, point.x, point.y)
        case .point2(let point): CGPathAddLineToPoint(path, nil, point.x, point.y)
        case .point3(let point): CGPathAddLineToPoint(path, nil, point.x, point.y)
        default:
        CGPathCloseSubpath(path)
        }
    }
}

Methods for building and drawing triangle and rectangle:

func buildTriPath() -> CGMutablePath {
    let path = CGPathCreateMutable()
    _ = array.map { $0.buildTriPath(path) }
    return path
}
func drawTriPath() {
    let path = buildTriPath()
    GraphicsState {
        CGContextAddPath(self.currentContext, path)
        CGContextStrokePath(self.currentContext)
    }
}
func drawTriFill() {
    let fill = buildTriPath()
    GraphicsState {
        CGContextAddPath(self.currentContext, fill)
        CGContextFillPath(self.currentContext)
    }
}

///////////////////////////////////////////////////////

func buildQuadPath() -> CGMutablePath {
    let path = CGPathCreateMutable()
    _ = array.map { $0.buildQuadPath(path) }
    return path
}
func drawQuadPath() {
    let path = buildQuadPath()
    GraphicsState {
        CGContextAddPath(self.currentContext, path)
        CGContextStrokePath(self.currentContext)
    }
}
func drawQuadFill() {
    let fill = buildQuadPath()
    GraphicsState {
        CGContextAddPath(self.currentContext, fill)
        CGContextFillPath(self.currentContext)
    }
}

Two variables help determine whether button is pressed:

var squareB: Int = 0
var triangleB: Int = 0

@IBAction func makeTriangle(sender: AnyObject?) {
    ....................
    ....................
    triangleB += 1
    squareB = 0
}
@IBAction func makeRectangle(sender: AnyObject?) {
    ....................
    ....................
    triangleB = 0
    squareB += 1
}

drawRect method:

override func drawRect(dirtyRect: NSRect) {
    super.drawRect(dirtyRect)

    drawBG()
    GraphicsState { self.drawMyPoints() }

    if squareB >= 1 && triangleB == 0 {
        buildQuadPath()
        drawQuadPath()
        drawQuadFill()
        needsDisplay = true
    }
    else if triangleB >= 1 && squareB == 0 {
        buildTriPath()
        drawTriPath()
        drawTriFill()
        needsDisplay = true
    }
    drawBorder()
}

...and at last a Context.swift file:

import Cocoa
import CoreGraphics

extension NSView {

    var currentContext : CGContext? {

        get {

            let unsafeContextPointer = NSGraphicsContext.currentContext()?.graphicsPort

            if let contextPointer = unsafeContextPointer {
            let opaquePointer = COpaquePointer(contextPointer)
            let context: CGContextRef = Unmanaged.fromOpaque(opaquePointer).takeUnretainedValue()
            return context }
            else { return nil }
        }
    }

    func GraphicsState(drawStuff: () -> Void) {
        CGContextSaveGState(currentContext)
        drawStuff()
        CGContextRestoreGState(currentContext)
    }
}

//the end of code
Niedersachsen answered 4/10, 2016 at 14:47 Comment(22)
Your statement combined with the supplied code makes no sense. If I get you right, your default state is 0. If you press a button (I guess you have some kind of click-listener enabled), the state changes to 1. So only one of the two options should be drawn. Check your code if your default state is properly intialized to 0 and changed back to 0 after (or before new) drawing.Bozo
You have to support us with more code. The way the code is right now, only the one with a valid state should be drawn, and the order of the methos makes no difference hence they are condition blocksBozo
Can you show me the code in GitHub. Really do not understand buildQuadPath() drawQuadPath() drawQuadFill()Honeysweet
@季亨达 I've pasted buildQuadPath section in my post.Niedersachsen
Please just post the code to github. Also make clear exactly what the desired behavior is (I still don't understand what you want to have happen).Seleneselenious
@Seleneselenious github.com/AndyFedoroff/rectangles-trianglesNiedersachsen
@AndyFedoroff: can you add a compiling project to github? It would make it much easier to dig into.Stutz
@AndyFedoroff: I recreated the project to be compiling again. please see the pull request. By looking on the code I wonder why you operate on points and not an a data structure representing a shape composed by points.Stutz
@Stutz Thank you for compiling project. I need to operate on points (enum) for certain reasons.Niedersachsen
This does not explain why you shouldnt group them in a shape structure.Stutz
I think this question suffers from being a XY-Problem.Stutz
I use enums (not structs) due to its ability to encode hierarchy.Niedersachsen
You could be using enums, structs, or classes or dictionaries or what ever you come up representing the shapes. But you should definitely group them to easily get points that belong together and keep a stack of shapes. Classes, structs, enums could also know how to draw themselves. or how to remove themselves.Stutz
@AndyFedoroff I suspect that the variables of an Enumeration are static, which means they only exist once in the program. If you call the drawRect() method via the makeTriangle() method, you are simply redrawing the path and removing the 4th point...which is why the Rectangle turns into a Triangle. I will have a more detailed response later, which I have already begun that recommends a more structured design.Doralynne
@Doralynne I'll be waiting for it.Niedersachsen
@Stutz has mostly pointed out what's the general problem and also explained why this question is not very well asked. The entire approach should be reconsidered. Shapes should get their own representation and be drawn based on them. Keeping all points in a non-separated data structure seems unfeasible here. Also supplying sample code that doesn't even run and illustrates neither concept nor problem at hand is... kinda pointless.Aprylapse
@Aprylapse I've tried a different approach too. It doesn't work for me. What is your decision?Niedersachsen
@Aprylapse Downvoting is not constructive in this case.Niedersachsen
@AndyFedoroff I'm sorry, I really downvote lightly, but I think people are drawn to your question because of the bounty, but then it's not easy to understand what you want. I thought quite a bit on it and still am not sure, especially considering all the comments already trying to address this. As I understand SO questions like this should be downvoted. Regarding your problem at hand I think you should provide a better sample project (that runs, has a few comments perhaps to explain your approach). Personally I'd go with vikingosegundo's approach (use drawRect, shapes as data structures,...).Aprylapse
@Aprylapse You mean using struct instead of enum? It doesn't work.Niedersachsen
@AndyFedoroff, just because you didn't make it work, doesn't mean it doesn't work at all.Stutz
@Stutz I agree. I can't make it work, so I posted a question here.Niedersachsen
A
3

Okay, since I could use the practice I created an example project to show what vikingosegundo and I mean.

Here's the gist of it:
For this example I kept all relevant code except the adding and removing of shapes in a GHShapeDemoView. I used structs to define the shapes, but treat them as one "unit" of data that is handled during drawing, adding to the view, etc. All shapes are kept in an array and during drawing that is iterated and all found shapes are drawn using a simple NSBezierPath.

For simplicity's sake I just chose some random fixed points for each shape, in a real project that would obviously be determined in another way (I was too lazy to add input fields...).

Even here there are a lot of ways to refactor (probably). For example, one could even make each shape a class of its own (or use one class for shapes in general). Maybe even a subclass of NSView, that would then result in the "drawing area" itself not be a custom view, but a normal one and on button presses relevant shape views would be added as subviews. That would then probably also get rid of all this points-calculating stuff (mostly). In a real project I would probably have gone for shapes as layer subclasses that I then add to the subview. I'm no expert, but I think that might have performance benefits depending on how many shapes there are and whether or not I would animate them. (Obviously the highest performance would probably be gained from using OpenGL ES or something, but I have no clue about that and that's far beyond the scope of this question).

I hope this provides you with a good starting point to work on your drawing. As stated above I would strongly suggest restructuring your project in a similar way to properly define a flow of what you draw and how you draw it. If you somehow must rely on keeping points data in enums or structs or something, write adequate mappers to your drawing data structure.

Aprylapse answered 21/10, 2016 at 9:27 Comment(6)
Thank you. I'll take a look.Niedersachsen
I'll award the bounty in 10 hours.Niedersachsen
Glad I could help. I realize this means quite a bit of refactoring to get into your project, but I believe that will ultimately help you also in maintaining your code. :) Take care!Aprylapse
I need massive refactoring)) But it's necessary.Niedersachsen
@Gero, nicely done. I'd give the shape an array of points, though, to allow any kind of shape.Stutz
@Stutz Yeah, that would be a neat idea, I hadn't even thought about that. Plus, you could also set an id or a label to keep track of them. Then the collection variable could also be a different collection, depending on what you need. There's a ton of stuff you could still do I guess. The creation methods are also kinda crude atm. :)Aprylapse
S
3

if (makeTriangle != nil) { and if (makeRectangle != nil) { doesnt make much sense. according to your comment, makerRectangle and makeTriangle are buttons. By your statements you are checking their existence — and we can assume they always exists — the first if-clause will always be executed.

what you want instead: create method that will be executed by the buttons. Each of this method will set either a combination of bool values or a single enum value and then tell the view to redraw by calling setNeedsDisplay().

Stutz answered 5/10, 2016 at 9:6 Comment(2)
Do I have to use a tag or it's useless here?Niedersachsen
tags are nearly always useless.Stutz
A
3

Okay, since I could use the practice I created an example project to show what vikingosegundo and I mean.

Here's the gist of it:
For this example I kept all relevant code except the adding and removing of shapes in a GHShapeDemoView. I used structs to define the shapes, but treat them as one "unit" of data that is handled during drawing, adding to the view, etc. All shapes are kept in an array and during drawing that is iterated and all found shapes are drawn using a simple NSBezierPath.

For simplicity's sake I just chose some random fixed points for each shape, in a real project that would obviously be determined in another way (I was too lazy to add input fields...).

Even here there are a lot of ways to refactor (probably). For example, one could even make each shape a class of its own (or use one class for shapes in general). Maybe even a subclass of NSView, that would then result in the "drawing area" itself not be a custom view, but a normal one and on button presses relevant shape views would be added as subviews. That would then probably also get rid of all this points-calculating stuff (mostly). In a real project I would probably have gone for shapes as layer subclasses that I then add to the subview. I'm no expert, but I think that might have performance benefits depending on how many shapes there are and whether or not I would animate them. (Obviously the highest performance would probably be gained from using OpenGL ES or something, but I have no clue about that and that's far beyond the scope of this question).

I hope this provides you with a good starting point to work on your drawing. As stated above I would strongly suggest restructuring your project in a similar way to properly define a flow of what you draw and how you draw it. If you somehow must rely on keeping points data in enums or structs or something, write adequate mappers to your drawing data structure.

Aprylapse answered 21/10, 2016 at 9:27 Comment(6)
Thank you. I'll take a look.Niedersachsen
I'll award the bounty in 10 hours.Niedersachsen
Glad I could help. I realize this means quite a bit of refactoring to get into your project, but I believe that will ultimately help you also in maintaining your code. :) Take care!Aprylapse
I need massive refactoring)) But it's necessary.Niedersachsen
@Gero, nicely done. I'd give the shape an array of points, though, to allow any kind of shape.Stutz
@Stutz Yeah, that would be a neat idea, I hadn't even thought about that. Plus, you could also set an id or a label to keep track of them. Then the collection variable could also be a different collection, depending on what you need. There's a ton of stuff you could still do I guess. The creation methods are also kinda crude atm. :)Aprylapse
N
0

Here is a code written by Gero:

This code works well with Swift 2.2.

//
//  GHShapeDemoView.swift
//  GHShapeDrawingExample
//
//  Created by Gero Herkenrath on 21/10/2016.
//  Copyright © 2016 Gero Herkenrath. All rights reserved.
//

import Cocoa

class GHShapeDemoView: NSView {

    struct Shape {

        var p1:CGPoint = NSMakePoint(0.0, 0.0)
        var p2:CGPoint = NSMakePoint(0.0, 0.0)
        var p3:CGPoint = NSMakePoint(0.0, 0.0)
        var p4:CGPoint?
    }

    var shapes:[Shape] = []

    override internal var flipped: Bool {
        return true
    }

    override func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)

        NSColor.whiteColor().setFill()
        let updatedRect = NSBezierPath.init(rect: dirtyRect)
        updatedRect.fill()

        for shape in shapes {
            drawShape(shape)
        }
    }

    func drawShape(shape:Shape) {
        let shapePath = NSBezierPath()
        shapePath.moveToPoint(shape.p1)
        shapePath.lineToPoint(shape.p2)
        shapePath.lineToPoint(shape.p3)

        if let lastPoint = shape.p4 {
            shapePath.lineToPoint(lastPoint)
        }

        shapePath.closePath()
        NSColor.blackColor().setStroke()
        shapePath.stroke()
    }

    func addTrapezoid(p1:NSPoint, p2:NSPoint, p3:NSPoint, p4:NSPoint) {
        var shape = Shape()
        shape.p1 = p1
        shape.p2 = p2
        shape.p3 = p3
        shape.p4 = p4
        shapes.append(shape)
    }

    func addTriangle(p1:NSPoint, p2:NSPoint, p3:NSPoint) {
        var shape = Shape()
        shape.p1 = p1
        shape.p2 = p2
        shape.p3 = p3
        shapes.append(shape)
    }

    func removeShapeAt(index:Int) {
        if index < shapes.count {
            shapes.removeAtIndex(index)
        }
    }
}

/////////////////////////////////////////////////

//
//  ViewController.swift
//  GHShapeDrawingExample
//
//  Created by Gero Herkenrath on 21/10/2016.
//  Copyright © 2016 Gero Herkenrath. All rights reserved.
//

import Cocoa

class ViewController: NSViewController {

    override func viewDidLoad() {

        if #available(OSX 10.10, *) {
            super.viewDidLoad()
        } 
        else {
            // Fallback on earlier versions
        }
        // Do any additional setup after loading the view.
    }

    override var representedObject: AnyObject? {
        didSet {
        // Update the view, if already loaded.
        }
    }

    // first Shape
    let pointA1 = NSMakePoint(115.0, 10.0)
    let pointA2 = NSMakePoint(140.0, 10.0)
    let pointA3 = NSMakePoint(150.0, 40.0)
    let pointA4 = NSMakePoint(110.0, 40.0)

    // second Shape
    let pointB1 = NSMakePoint(230.0, 10.0)
    let pointB2 = NSMakePoint(260.0, 40.0)
    let pointB3 = NSMakePoint(200.0, 40.0)

    // thirdShape
    let pointC1 = NSMakePoint(115.0, 110.0)
    let pointC2 = NSMakePoint(140.0, 110.0)
    let pointC3 = NSMakePoint(150.0, 140.0)
    let pointC4 = NSMakePoint(110.0, 140.0)


    @IBOutlet weak var shapeHolderView: GHShapeDemoView!

    @IBAction func newTrapezoid(sender: AnyObject) {

        if shapeHolderView.shapes.count < 1 {
            shapeHolderView.addTrapezoid(pointA1, p2: pointA2, p3: pointA3, p4: pointA4)
        } 
        else {
        shapeHolderView.addTrapezoid(pointC1, p2: pointC2, p3: pointC3, p4: pointC4)
        }
    shapeHolderView.setNeedsDisplayInRect(shapeHolderView.bounds)
    }

    @IBAction func newTriangle(sender: AnyObject) {
        shapeHolderView.addTriangle(pointB1, p2: pointB2, p3: pointB3)
        shapeHolderView.setNeedsDisplayInRect(shapeHolderView.bounds)
    }

    @IBAction func removeLastShape(sender: AnyObject) {
        if shapeHolderView.shapes.count > 0 {
            shapeHolderView.removeShapeAt(shapeHolderView.shapes.count - 1)
            shapeHolderView.setNeedsDisplayInRect(shapeHolderView.bounds)
        }
    }
}
Niedersachsen answered 21/10, 2016 at 11:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.