I have a CATiledLayer inside a NSView which is a documentView property of NSScrollView. Storyboard setup is pretty straitforward: add NSScrollView to the default view controller and assign View class to the NSView of clipping view.
The following code draws a number of squares of random color. Scrolling works exactly as it should in CATiledLayer but zooming doesn't work very well:
Found tons of CATiledLayer problems and all the proposed solutions don't work for me (like subclassing with 0 fadeDuration or disabling CATransaction actions). I guess that setNeedsDisplay() screws it all but can't figure out the proper way to do that. If I use CALayer then I don't see the flashing issues but then I can't deal with large layers of thousands of boxes inside.
The View class source:
import Cocoa
import CoreGraphics
import Combine
let rows = 1000
let columns = 1000
let width = 50.0
let height = 50.0
class View: NSView {
typealias Coordinate = (x: Int, y: Int)
private let colors: [[CGColor]]
private let rect = CGRect(origin: .zero, size: CGSize(width: width, height: height))
private var store = Set<AnyCancellable>()
private var scale: CGFloat {
guard let scrollView = self.superview?.superview as? NSScrollView else { fatalError() }
return NSScreen.main!.backingScaleFactor * scrollView.magnification
}
required init?(coder: NSCoder) {
colors = (0..<rows).map { _ in (0..<columns).map { _ in .random } }
super.init(coder: coder)
setFrameSize(NSSize(width: width * CGFloat(columns), height: height * CGFloat(rows)))
wantsLayer = true
NotificationCenter.default.publisher(for: NSScrollView.didEndLiveMagnifyNotification).sink { [unowned self] _ in
self.layer?.contentsScale = scale
self.layer?.setNeedsDisplay()
}.store(in: &store)
}
override func makeBackingLayer() -> CALayer {
let layer = CATiledLayer()
layer.tileSize = CGSize(width: 1000, height: 1000)
return layer
}
override func draw(_ dirtyRect: NSRect) {
guard let context = NSGraphicsContext.current?.cgContext else { return }
let (min, max) = coordinates(in: dirtyRect)
context.translateBy(x: CGFloat(min.x) * width, y: CGFloat(min.y) * height)
(min.y...max.y).forEach { row in
context.saveGState()
(min.x...max.x).forEach { column in
context.setFillColor(colors[row][column])
context.addRect(rect)
context.drawPath(using: .fillStroke)
context.translateBy(x: width, y: 0)
}
context.restoreGState()
context.translateBy(x: 0, y: height)
}
}
private func coordinates(in rect: NSRect) -> (Coordinate, Coordinate) {
var minX = Int(rect.minX / width)
var minY = Int(rect.minY / height)
var maxX = Int(rect.maxX / width)
var maxY = Int(rect.maxY / height)
if minX >= columns {
minX = columns - 1
}
if maxX >= columns {
maxX = columns - 1
}
if minY >= rows {
minY = rows - 1
}
if maxY >= rows {
maxY = rows - 1
}
return ((minX, minY), (maxX, maxY))
}
}
extension CGColor {
class var random: CGColor {
let random = { CGFloat(arc4random_uniform(255)) / 255.0 }
return CGColor(red: random(), green: random(), blue: random(), alpha: random())
}
}