Make 2 contradictory methods work in drawRect

2019-01-12 04:06发布

问题:

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

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

回答1:

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.



回答2:

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



回答3:

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