Search code examples
iosswiftuiviewuikituibezierpath

Drawing the squares of a grid in UIView, the alpha of the fill color of UIBezierPath is inconsistent depending on the square location


I was trying to achieve something similar to a chess board. In my class, which inherits from UIView, I'm drawing all the squares of the grid using two nested for loops when overriding the draw() method.

I also want to have some visual indicator when the finger is dragged inside the grid, so I want to draw on top of the selected square another yellow square with an alpha equal to 0.15.

The issue is that, depending on the square location, the alpha property seems to be more or less ignored. In particular, the more you drag up left, the more the alpha of the yellow square tends to 1, the more you go down right the more is respected.

See the gif example here. To better visualize the issue, in this example all the squares are gray, but they should actually be like a chess board.

My drawSquare() method, called in draw():

private func drawSquare(col: Int, row: Int, color: UIColor, isSelected: Bool) {
    let path = UIBezierPath(rect: CGRect(x: CGFloat(col) * cellSide, y: CGFloat(row) * cellSide, width: cellSide, height: cellSide))
    color.setFill()
    path.fill(with: .normal, alpha: isSelected ? 0.15 : 1.0)
    
    // or also
    // let colorWithAlpha = color.withAlphaComponent(isSelected ? 0.15 : 1.0)
    // colorWithAlpha.setFill()
    // path.fill()
}

Down below there is a runnable, simplified sample of my BoardView class. I'm still learning, so for sure I'm missing something here. What is my mistake? Maybe something related to the way the drawings are performed under the hood? How can I achieve a yellow square with my set alpha consistent for each square?

import UIKit

final class BoardView: UIView {
    
    // MARK: - Properties
    private var cellSide: CGFloat = CGFloat.zero
    private var fromCol: Int?
    private var fromRow: Int?
    private var toCol: Int?
    private var toRow: Int?
    
    // MARK: - Lifecycle
    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        backgroundColor = .clear
    }
    
    // MARK: - Draw
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        backgroundColor = .clear
        cellSide = bounds.width / 8
        drawBoard()
    }
    private func drawBoard() {
        for row in 0..<4 {
            for col in 0..<4 {
                drawSquare(col: col * 2, row: row * 2, color: UIColor.gray, isSelected: false)
                drawSquare(col: 1 + col * 2, row: row * 2, color: UIColor.gray, isSelected: false)
                drawSquare(col: col * 2, row: 1 + row * 2, color: UIColor.gray, isSelected: false)
                drawSquare(col: 1 + col * 2, row: 1 + row * 2, color: UIColor.gray, isSelected: false)
                if let fromCol = fromCol, let fromRow = fromRow {
                    drawSquare(col: fromCol, row: fromRow, color: .yellow, isSelected: true)
                }
                if let toCol = toCol, let toRow = toRow {
                    drawSquare(col: toCol, row: toRow, color: .yellow, isSelected: true)
                }
            }
        }
    }
    private func drawSquare(col: Int, row: Int, color: UIColor, isSelected: Bool) {
        let path = UIBezierPath(rect: CGRect(x: CGFloat(col) * cellSide, y: CGFloat(row) * cellSide, width: cellSide, height: cellSide))
        color.setFill()
        path.fill(with: .normal, alpha: isSelected ? 0.15 : 1.0)
        
        // or also
        // let colorWithAlpha = color.withAlphaComponent(isSelected ? 0.15 : 1.0)
        // colorWithAlpha.setFill()
        // path.fill()
    }
    
    // MARK: - Touches
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        if let first = touches.first {
            let fingerLocation = first.location(in: self)
            fromCol = Int(fingerLocation.x / cellSide)
            fromRow = Int(fingerLocation.y / cellSide)
            // intentionally not calling setNeedsDisplay()
        }
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesMoved(touches, with: event)
        if let first = touches.first {
            let fingerLocation = first.location(in: self)
            toCol = Int(fingerLocation.x / cellSide)
            toRow = Int(fingerLocation.y / cellSide)
            setNeedsDisplay()
        }
    }
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        if let first = touches.first {
            fromCol = nil
            fromRow = nil
            self.toCol = nil
            self.toRow = nil
            setNeedsDisplay()
        }
    }
}

Solution

  • You seem to be drawing an 8x8 grid but you only iterate through 4x4. You then draw 4 gray squares in each of those 16 bigger squares. While that means less iterating, it means far more complicated code and code duplication.

    The big issue is that you draw a yellow square at fromRow/fromCol 16 times and a yellow square at toRow/fromRow 16 times. Shouldn't you check row and col with fromRow, toRow, and fromCol, toCol so you only draw a yellow square once in any given square?

    I think you want the following for drawBoard:

    private func drawBoard() {
        for row in 0..<8 {
            for col in 0..<8 {
                drawSquare(col: col, row: row, color: UIColor.gray, isSelected: false)
    
                if let fromCol, let fromRow, let toCol, let toRow, fromCol <= col && col <= toCol, fromRow <= row && row <= toRow {
                    // We have start and end and the current square is within the range
                    drawSquare(col: col, row: row, color: .yellow, isSelected: true)
                }
            }
        }
    }
    

    The above works because you are not calling setNeedsDisplay until you have set all of fromCol, fromRow, toCol, and toRow.

    If you want the first yellow square to appear as soon as the user touches the screen and not wait until the user starts to move their finger, then update touchesBegan as follows:

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        if let first = touches.first {
            let fingerLocation = first.location(in: self)
            fromCol = Int(fingerLocation.x / cellSide)
            fromRow = Int(fingerLocation.y / cellSide)
            toCol = fromCol
            toRow = fromRow
            setNeedsDisplay()
        }
    }