Home > database >  Drawing multiple rectangle using DrawRect efficiently
Drawing multiple rectangle using DrawRect efficiently

Time:10-27

I'm trying to draw rectangles pattern using DrawRect like this:

enter image description here

Currently, I'm doing this like so:

class PatternView: UIView {
    
    override func draw(_ rect: CGRect) {

        let context = UIGraphicsGetCurrentContext()
        
        let numberOfBoxesPerRow = 7
        let boxSide: CGFloat = rect.width / CGFloat(numberOfBoxesPerRow)
        var yOrigin: CGFloat = 0
        var xOrigin: CGFloat = 0
        var isBlack = true
        
        for y in 0...numberOfBoxesPerRow - 1 {
            yOrigin = boxSide * CGFloat(y)
            for x in 0...numberOfBoxesPerRow - 1 {

                xOrigin = boxSide * CGFloat(x)
                let color = isBlack ? UIColor.red : UIColor.blue
                isBlack = !isBlack

                context?.setFillColor(color.cgColor)
                
                let rectnagle =  CGRect(origin: .init(x: xOrigin, y: yOrigin), size: .init(width: boxSide, height: boxSide))
                context?.addRect(rectnagle)
                context?.fill([rectnagle])
            }
        }
           
    }
}

It's working but I'm trying to optimize it.
Any help will be highly appreciated!

CodePudding user response:

I think your first move would be to first draw a big red square, then to draw only the blue ones on top of it. It would spare half the computations, even if it does not change the order of magnitude.

EDIT

Note : it is always the drawing itself that consumes time, rarely the other computations. So that is what we have to minimize.

So, my second move would be to replace drawing squares by creating just one complicated BezierPath, that makes all the squares into just one form, and then display it only once. I do not know if it is possible to do the whole in just one form, but it is possible to make two columns of blue squares into one form.

EDIT 2

Also, I do not understant why there are two instructions here :

context?.addRect(rectnagle)
context?.fill([rectnagle])

Shouldn't only the second be enough ?

CodePudding user response:

It's difficult to answer "abstract" questions... which this one is, without knowing if you've run some tests / profiling to determine if this code is slow.

However, a couple things you can do to speed it up...

  • fill the view with one color (red, in this case) and then draw only the other-color boxes
  • add rects to the context's path, and fill the path once

Take a look at this modification:

class PatternView: UIView {
    
    override func draw(_ rect: CGRect) {
        
        guard let context = UIGraphicsGetCurrentContext() else { return }
        
        let numberOfBoxesPerRow = 7
        let boxSide: CGFloat = rect.width / CGFloat(numberOfBoxesPerRow)
        
        context.setFillColor(UIColor.red.cgColor)
        context.fill(bounds)
        
        var r: CGRect = CGRect(origin: .zero, size: CGSize(width: boxSide, height: boxSide))
        
        context.beginPath()
        
        for row in 0..<numberOfBoxesPerRow {
            r.origin.x = 0.0
            for col in 0..<numberOfBoxesPerRow {
                if (row % 2 == 0 && col % 2 == 1) || (row % 2 == 1 && col % 2 == 0) {
                    context.addRect(r)
                }
                r.origin.x  = boxSide
            }
            r.origin.y  = boxSide
        }
        
        context.setFillColor(UIColor.blue.cgColor)
        
        context.fillPath()

    }
}

There are other options... create a "pattern" background color... use CAShapeLayers and/or CAReplicatorLayers... for example.


Edit

The reason you are getting "blurry edges" is because, as you guessed, you're drawing on partial pixels.

If we modify the values to use whole numbers (using floor()), we can avoid that. Note that the wholeNumberBoxSide * numBoxes may then NOT be exactly equal to the view's rect, so we'll also want to inset the "grid":

class PatternView: UIView {
    
    override func draw(_ rect: CGRect) {
        
        guard let context = UIGraphicsGetCurrentContext() else { return }
        
        let c1: UIColor = .white
        let c2: UIColor = .lightGray
        
        let numberOfBoxesPerRow = 7
        
        // use a whole number
        let boxSide: CGFloat = floor(rect.width / CGFloat(numberOfBoxesPerRow))

        // inset because numBoxes * boxSide may not be exactly equal to rect
        let inset: CGFloat = floor((rect.width - boxSide * CGFloat(numberOfBoxesPerRow)) * 0.5)
        
        context.setFillColor(c1.cgColor)
        context.fill(CGRect(x: inset, y: inset, width: boxSide * CGFloat(numberOfBoxesPerRow), height: boxSide * CGFloat(numberOfBoxesPerRow)))
        
        var r: CGRect = CGRect(x: inset, y: inset, width: boxSide, height: boxSide)
        
        context.beginPath()
        
        for row in 0..<numberOfBoxesPerRow {
            r.origin.x = inset
            for col in 0..<numberOfBoxesPerRow {
                if (row % 2 == 0 && col % 2 == 1) || (row % 2 == 1 && col % 2 == 0) {
                    context.addRect(r)
                }
                r.origin.x  = boxSide
            }
            r.origin.y  = boxSide
        }
        
        context.setFillColor(c2.cgColor)
        
        context.fillPath()

    }
}

We could also get the scale of the main screen (which will be 2x or 3x) and round the boxSide to half- or one-third points to align with the pixels... if really desired.

  • Related