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