In SwiftUI / AppKit I build a simple grid view composed of blue squares. Once a square is hovered, the square turns red, re-drawing only the updated square. Here is my code:
struct CoreGraphicsSquaresView: NSViewRepresentable {
let rows: Int
let cols: Int
let squareSize: CGFloat
let spacing: CGFloat
func makeNSView(context: Context) -> NSView {
return SquaresNSView(rows: rows, columns: cols, squareSize: squareSize, spacing: spacing)
}
func updateNSView(_ nsView: NSView, context: Context) {
nsView.setNeedsDisplay(nsView.bounds) // Trigger a redraw if needed
}
}
class SquaresNSView: NSView {
let rows: Int
let columns: Int
let squareSize: CGFloat
let spacing: CGFloat
override var acceptsFirstResponder: Bool { return true }
var cellColors: [IndexPath: NSColor] = [:]
var lastHovered: IndexPath? = nil
init(rows: Int, columns: Int, squareSize: CGFloat, spacing: CGFloat) {
self.rows = rows
self.columns = columns
self.squareSize = squareSize
self.spacing = spacing
super.init(frame: .zero)
let trackingArea = NSTrackingArea(
rect: self.bounds,
options: [.mouseMoved, .activeAlways, .inVisibleRect],
owner: self,
userInfo: nil
)
self.addTrackingArea(trackingArea)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
guard let context = NSGraphicsContext.current?.cgContext else { return }
let cornerRadius: CGFloat = 2
let path = NSBezierPath() // Reusable path object
for row in 0..<rows {
for col in 0..<columns {
let x = CGFloat(col) * (squareSize + spacing)
let y = CGFloat(row) * (squareSize + spacing)
let squareRect = CGRect(x: x, y: y, width: squareSize, height: squareSize)
if dirtyRect.intersects(squareRect) {
let color = cellColors[IndexPath(item: row, section: col)] ?? NSColor.blue
context.setFillColor(color.cgColor)
path.removeAllPoints()
path.appendRoundedRect(squareRect, xRadius: cornerRadius, yRadius: cornerRadius)
path.fill()
}
}
}
}
func updateSquare(atRow row: Int, column: Int, to color: NSColor) {
let indexPath = IndexPath(item: row, section: column)
if cellColors[indexPath] == color {
return // No need to redraw if the color hasn't changed
}
cellColors[indexPath] = color // Update color
let x = CGFloat(column) * (squareSize + spacing)
let y = CGFloat(row) * (squareSize + spacing)
let squareRect = CGRect(x: x, y: y, width: squareSize, height: squareSize)
self.setNeedsDisplay(squareRect)
}
override func mouseMoved(with event: NSEvent) {
let locationInView = convert(event.locationInWindow, from: nil)
let y = Int(round(locationInView.y)) // use round() because it has pixel perfect accuracy on y axis
let x = Int(ceil(locationInView.x)) // use ceil() because it has pixel perfect accuracy on x axis
let isInYRange = (1 <= y % 12) && (y % 12 <= 10) // Accounts for 2px spacing
let isInXRange = (1 <= x % 12) && (x % 12 <= 10) // Accounts for 2px spacing
if isInXRange && isInYRange {
let colIdx: Int = (x - 1) / 12
let rowIdx: Int = (y - 1) / 12
let indexPath = IndexPath(item: rowIdx, section: colIdx)
if lastHovered == indexPath {
return
} else {
lastHovered = indexPath
}
print("ColIdx: \(colIdx), RowIdx: \(rowIdx)")
updateSquare(atRow: rowIdx, column: colIdx, to: .red)
}
}
}
struct CGHeatMap: View {
var body: some View {
CoreGraphicsSquaresView(rows: 7, cols: 40, squareSize: 10, spacing: 2)
.frame(width: 40*12 - 2, height: 7*12 - 2) // Force SwiftUI to respect size
.background(Color.white)
}
}
And in my SwiftUI ContentView
I create 36 of these CGHeatMap
views.
When profiling the application, it starts at around 35MiB of persistent memory allocation. As I hover on squares on different grids (turning them red) the persistent allocation increases (expected since, for example, we add elements to the cellColors
dictionary) but I believe it increases too much. For example when turning red less than 200 squares, the allocation settles at around 61MiB. Why is so much memory consumed?
EDIT: When studying the behavior more deeply, I noticed a strange behavior. When coloring the first cell of a new grid the consumption increases by 1/2 MiB, while when coloring successive cells for the same grid increases it by much less (0.01/0.02 MiB).