I'm trying to draw rectangles pattern using DrawRect like this:
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!
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...
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 CAShapeLayer
s and/or CAReplicatorLayer
s... 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.
Edit 2
Additional modifications... settable colors and number of boxes.
Also, using this extension:
// extension to round CGFloat values to floor/nearest CGFloat
// so, for example
// if f == 10.6
// f.floor(nearest: 0.5) = 10.5
// f.floor(nearest: 0.3333) = 10.3333
// f.round(nearest: 0.5) = 10.5
// f.round(nearest: 0.3333) = 10.66666
extension CGFloat {
func round(nearest: CGFloat) -> CGFloat {
let n = 1/nearest
let numberToRound = self * n
return numberToRound.rounded() / n
}
func floor(nearest: CGFloat) -> CGFloat {
let intDiv = CGFloat(Int(self / nearest))
return intDiv * nearest
}
}
We can round the coordinates to match the screen scale.
PatternView class
class PatternView: UIView {
var c1: UIColor = .white { didSet { setNeedsDisplay() } }
var c2: UIColor = .lightGray { didSet { setNeedsDisplay() } }
var numberOfBoxesPerRow = 21 { didSet { setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let sc: CGFloat = 1.0 // / CGFloat(UIScreen.main.scale)
// use a whole number
let boxSide: CGFloat = (rect.width / CGFloat(numberOfBoxesPerRow)).floor(nearest: sc)
// inset because numBoxes * boxSide may not be exactly equal to rect
let inset: CGFloat = ((rect.width - boxSide * CGFloat(numberOfBoxesPerRow)) * 0.5).floor(nearest: sc)
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()
}
}
Example Controller View class
class PatternTestVC: UIViewController {
let pvA = PatternView()
let pvB = PatternView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 8
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
stack.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
[pvA, pvB].forEach { v in
v.backgroundColor = .red
v.numberOfBoxesPerRow = 7
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
stack.addArrangedSubview(v)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
pvB.numberOfBoxesPerRow += 1
}
}
Sets up two pattern views... both start at 7 boxes... each tap anywhere increments the boxes per row in the bottom view.
Here's how it looks with 21 boxes per row (actual size - so really big image):
and zoomed-in 1600%:
Note the red borders... I set the background of the view to red, so we can see that the grid must be inset to account for the non-whole-number box size.
Edit 3
Options to avoid "blurry edges" ...
Suppose we have a view width of 209
and we want 10
boxes.
That gives us a box width of 20.9 ... which results in "blurry edges" -- so we know we need to get to a whole number.
If we round it, we'll get 21
-- 21 x 10 = 210
which will exceed the width of the view. So we need to round it down (floor()
).
So...
Option 1:
Option 2:
Option 3: