I have a UIScrollView
and I draw a line and a string in it's contentView
of type DrawView
. I want to maintain the width of the drawn elements relative to the zoomScale
. Below is my code.
class ViewController: UIViewController {
@IBOutlet private weak var scrollView: UIScrollView!
@IBOutlet private weak var drawView: DrawView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.maximumZoomScale = 20.0
scrollView.minimumZoomScale = 0.1
scrollView.zoomScale = 1.0
scrollView.backgroundColor = .lightGray
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
drawView.setNeedsDisplay()
}
}
extension ViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return drawView
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
drawView.zoomScale = scale
drawView.setNeedsDisplay()
}
}
and this is my DrawView
public class DrawView: UIView {
public var zoomScale: CGFloat = 1.0
public override func draw(_ rect: CGRect) {
drawLine()
drawString()
}
private func drawLine() {
let path = UIBezierPath()
path.move(to: CGPoint(x:100, y:300))
path.addLine(to: CGPoint(x: 100, y: 400))
path.close()
UIColor.red.set()
path.lineWidth = 2/zoomScale
path.stroke()
}
private func drawString() {
let font = UIFont.systemFont(ofSize: 30/zoomScale)
let string = NSAttributedString(string: "Test", attributes: [NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: UIColor.red])
string.draw(at: CGPoint(x: 200, y: 200))
}
}
Below are the results
When zoomScale
is 1.0
When zoomScale
is 5.0
When zoomScale
is 5.0
When I zoom, the intended width is maintained, but the elements are pixelated.
Expectation:
When zoomScale
is 5.0
It could be noticed that the current results are pixelated. What would be an ideal way to achieve the expected result which is scaled and sharp?
One option is to use a fixed-size "drawView" and transform your paths and font-sizes.
Here's a basics example:
class BasicScalingView: UIView {
public var zoomScale: CGFloat = 1.0 { didSet { setNeedsDisplay() } }
private var theLinePath: UIBezierPath!
private var theOvalPath: UIBezierPath!
private var theTextPoint: CGPoint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
var someRect: CGRect = .zero
// create a rect path
someRect = .init(x: 4.0, y: 4.0, width: 80.0, height: 50.0)
theLinePath = UIBezierPath()
theLinePath.move(to: .init(x: someRect.maxX, y: someRect.minY))
theLinePath.addLine(to: .init(x: someRect.minX, y: someRect.minY))
theLinePath.addLine(to: .init(x: someRect.minX, y: someRect.maxY))
// create an oval path
someRect = .init(x: 6.0, y: 8.0, width: 50.0, height: 30.0)
theOvalPath = UIBezierPath(ovalIn: someRect)
// this will be the top-left-point of the text-bounds
theTextPoint = .init(x: 8.0, y: 6.0)
}
override func draw(_ rect: CGRect) {
// only draw if we've initialized the paths
guard theLinePath != nil, theOvalPath != nil else { return }
let tr = CGAffineTransform(scaleX: zoomScale, y: zoomScale)
if let path = theLinePath.copy() as? UIBezierPath {
// transform a copy of the rect path
path.apply(tr)
UIColor.green.set()
path.lineWidth = 2.0 * zoomScale
path.stroke()
}
if let path = theOvalPath.copy() as? UIBezierPath {
// transform the path
path.apply(tr)
UIColor.systemBlue.set()
UIColor(white: 0.95, alpha: 1.0).setFill()
path.lineWidth = 2.0 * zoomScale
path.fill()
path.stroke()
}
// scale the font point-size
let font: UIFont = .systemFont(ofSize: 30.0 * zoomScale)
let attribs: [NSAttributedString.Key : Any] = [.font: font, .foregroundColor: UIColor.red]
// transform the point
let trPT: CGPoint = theTextPoint.applying(tr)
// attributed string at zoomed point-size
let string = NSAttributedString(string: "Sample", attributes: attribs)
string.draw(at: trPT)
}
}
That BasicScalingView
is what we'll use as the "drawView." When we set the zoomScale
it will redraw itself, transforming the line path, the oval path, the top-left point for the text and the font size.
We can show that by using a slider to change the zoom scale:
As we see, the lines and curves remain sharp and in position relative to each other.
Now we could use Pinch and Pan gestures, and write a bunch of code to track the zoom scale value and the relative position to allow zooming and panning. We'd also need to use the gestures' .location
, .velocity
, etc properties to implement edge bouncing. With some searching, we could probably find some samples for that.
But... wouldn't it be nice if we could use all of those built-in functions with a scroll view?
Well, we can...
First, we'll use a fairly simple modified "scaling view" that has zoomScale
and contentOffset
properties, which we will update when we get scrollViewDidZoom
and scrollViewDidScroll
.
It draws a rectangle, a novel (inset a bit) and a text string, all centered in the view - looks like this to start:
What we do is put the "drawView" behind a clear scroll view, and we'll use a plain, clear UIView
as the viewForZooming
:
When we zoom / pan the scroll view, we get this:
The empty "clear" view that we use for viewForZooming
can be very big, and can zoom-in to a high zoom scale without memory issues.
Using a "complex" scaling view as our "drawView" -- creating a 32-column x 40-row "grid" of rectangles (alternating rounded and square), ovals, text strings, and a few "SwiftyBird" bezier paths.
Looks like this (scrolled all the way to bottom-right):
and, after some zooming / panning:
Here's the complete code to run these examples... no @IBOutlet
or @IBAction
connections - just assign a fresh view controller to TheBasicsVC
and then SimpleVC
and then ComplexVC
:
class BasicScalingView: UIView {
public var zoomScale: CGFloat = 1.0 { didSet { setNeedsDisplay() } }
private var theLinePath: UIBezierPath!
private var theOvalPath: UIBezierPath!
private var theTextPoint: CGPoint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
var someRect: CGRect = .zero
// create a rect path
someRect = .init(x: 4.0, y: 4.0, width: 80.0, height: 50.0)
theLinePath = UIBezierPath()
theLinePath.move(to: .init(x: someRect.maxX, y: someRect.minY))
theLinePath.addLine(to: .init(x: someRect.minX, y: someRect.minY))
theLinePath.addLine(to: .init(x: someRect.minX, y: someRect.maxY))
// create an oval path
someRect = .init(x: 6.0, y: 8.0, width: 50.0, height: 30.0)
theOvalPath = UIBezierPath(ovalIn: someRect)
// this will be the top-left-point of the text-bounds
theTextPoint = .init(x: 8.0, y: 6.0)
}
override func draw(_ rect: CGRect) {
// only draw if we've initialized the paths
guard theLinePath != nil, theOvalPath != nil else { return }
let tr = CGAffineTransform(scaleX: zoomScale, y: zoomScale)
if let path = theLinePath.copy() as? UIBezierPath {
// transform a copy of the rect path
path.apply(tr)
UIColor.green.set()
path.lineWidth = 2.0 * zoomScale
path.stroke()
}
if let path = theOvalPath.copy() as? UIBezierPath {
// transform the path
path.apply(tr)
UIColor.systemBlue.set()
UIColor(white: 0.95, alpha: 1.0).setFill()
path.lineWidth = 2.0 * zoomScale
path.fill()
path.stroke()
}
// scale the font point-size
let font: UIFont = .systemFont(ofSize: 30.0 * zoomScale)
let attribs: [NSAttributedString.Key : Any] = [.font: font, .foregroundColor: UIColor.red]
// transform the point
let trPT: CGPoint = theTextPoint.applying(tr)
// attributed string at zoomed point-size
let string = NSAttributedString(string: "Sample", attributes: attribs)
string.draw(at: trPT)
}
}
class TheBasicsVC: UIViewController {
let drawView = BasicScalingView()
// a label to put at the top to show the current zoomScale
let infoLabel: UILabel = {
let v = UILabel()
v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
v.textAlignment = .center
v.text = " "
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
let slider = UISlider()
drawView.backgroundColor = .black
[slider, infoLabel, drawView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// slider at the top
slider.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// info label
infoLabel.topAnchor.constraint(equalTo: slider.bottomAnchor, constant: 20.0),
infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
drawView.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 20.0),
drawView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
drawView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
drawView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
slider.minimumValue = 1.0
slider.maximumValue = 20.0
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
updateInfo()
}
func updateInfo() {
infoLabel.text = String(format: "zoomScale: %0.3f", drawView.zoomScale)
}
@objc func sliderChanged(_ sender: UISlider) {
drawView.zoomScale = CGFloat(sender.value)
updateInfo()
}
}
class DrawZoomBaseVC: UIViewController {
let scrollView: UIScrollView = UIScrollView()
// this will be a plain, clear UIView that we will use
// as the viewForZooming
let zoomView = UIView()
// this will be placed *behind* the scrollView
// in our subclasses, we'll set it to either
// Simple or Complex
// and we'll set its zoomScale and contentOffset
// to match the scrollView
var drawView: UIView!
// a label to put at the top to show the current zoomScale
let infoLabel: UILabel = {
let v = UILabel()
v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
v.textAlignment = .center
v.numberOfLines = 0
v.text = "\n\n\n"
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
[infoLabel, drawView, scrollView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
zoomView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(zoomView)
drawView.backgroundColor = .black
scrollView.backgroundColor = .clear
zoomView.backgroundColor = .clear
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// info label at the top
infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
zoomView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
zoomView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
zoomView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
zoomView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),
drawView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0.0),
drawView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 0.0),
drawView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0.0),
drawView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0.0),
])
scrollView.maximumZoomScale = 60.0
scrollView.minimumZoomScale = 0.1
scrollView.zoomScale = 1.0
scrollView.indicatorStyle = .white
scrollView.delegate = self
infoLabel.isHidden = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// if we're using the ComplexDrawScaledView
// we *get* its size that was determined by
// it laying out its elements in its commonInit()
// if we're using the SimpleDrawScaledView
// we set its size to the scroll view's frame size
if let dv = drawView as? SimpleDrawScaledView {
dv.virtualSize = scrollView.frame.size
zoomView.widthAnchor.constraint(equalToConstant: dv.virtualSize.width).isActive = true
zoomView.heightAnchor.constraint(equalToConstant: dv.virtualSize.height).isActive = true
}
else
if let dv = drawView as? ComplexDrawScaledView {
zoomView.widthAnchor.constraint(equalToConstant: dv.virtualSize.width).isActive = true
zoomView.heightAnchor.constraint(equalToConstant: dv.virtualSize.height).isActive = true
}
// let auto-layout size the view before we update the info label
DispatchQueue.main.async {
self.updateInfoLabel()
}
}
func updateInfoLabel() {
infoLabel.text = String(format: "\nzoomView size: (%0.0f, %0.0f)\nzoomScale: %0.3f\n", zoomView.frame.width, zoomView.frame.height, scrollView.zoomScale)
}
}
extension DrawZoomBaseVC: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let dv = drawView as? SimpleDrawScaledView {
dv.contentOffset = scrollView.contentOffset
}
else
if let dv = drawView as? ComplexDrawScaledView {
dv.contentOffset = scrollView.contentOffset
}
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateInfoLabel()
if let dv = drawView as? SimpleDrawScaledView {
dv.zoomScale = scrollView.zoomScale
}
else
if let dv = drawView as? ComplexDrawScaledView {
dv.zoomScale = scrollView.zoomScale
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return zoomView
}
}
class SimpleVC: DrawZoomBaseVC {
override func viewDidLoad() {
drawView = SimpleDrawScaledView()
super.viewDidLoad()
}
}
class ComplexVC: DrawZoomBaseVC {
override func viewDidLoad() {
drawView = ComplexDrawScaledView()
super.viewDidLoad()
}
}
class SimpleDrawScaledView: UIView {
private var _virtualSize: CGSize = .zero
public var virtualSize: CGSize {
set {
_virtualSize = newValue
// let's use a 120x80 rect, centered in the view bounds
var theRect: CGRect = .init(x: 4.0, y: 4.0, width: 120.0, height: 80.0)
theRect.origin = .init(x: (_virtualSize.width - theRect.width) * 0.5, y: (_virtualSize.height - theRect.height) * 0.5)
// create a rect path
theRectPath = UIBezierPath(rect: theRect)
// create an oval path (slightly inset)
theOvalPath = UIBezierPath(ovalIn: theRect.insetBy(dx: 12.0, dy: 12.0))
// we want to center the text in the rects, so
// get the mid-point of the rect
theTextPoint = .init(x: theRect.midX, y: theRect.midY)
setNeedsDisplay()
}
get {
return _virtualSize
}
}
public var zoomScale: CGFloat = 1.0 { didSet { setNeedsDisplay() } }
public var contentOffset: CGPoint = .zero { didSet { setNeedsDisplay() } }
private var theRectPath: UIBezierPath!
private var theOvalPath: UIBezierPath!
private var theTextPoint: CGPoint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
}
override func draw(_ rect: CGRect) {
// only draw if we've initialized the paths
guard theRectPath != nil, theOvalPath != nil else { return }
let tr = CGAffineTransform(translationX: -contentOffset.x, y: -contentOffset.y)
.scaledBy(x: zoomScale, y: zoomScale)
drawRect(insideRect: rect, withTransform: tr)
drawOval(insideRect: rect, withTransform: tr)
drawString(insideRect: rect, withTransform: tr)
}
func drawRect(insideRect: CGRect, withTransform tr: CGAffineTransform) {
if let path = theRectPath.copy() as? UIBezierPath {
// transform a copy of the rect path
path.apply(tr)
// only draw if visible
if path.bounds.intersects(insideRect) {
UIColor.green.set()
path.lineWidth = 2.0 * zoomScale
path.stroke()
}
}
}
func drawOval(insideRect: CGRect, withTransform tr: CGAffineTransform) {
if let path = theOvalPath.copy() as? UIBezierPath {
// transform a copy of the oval path
path.apply(tr)
// only draw if visible
if path.bounds.intersects(insideRect) {
UIColor.systemBlue.set()
UIColor(white: 0.95, alpha: 1.0).setFill()
path.lineWidth = 3.0 * zoomScale
path.fill()
path.stroke()
}
}
}
func drawString(insideRect: CGRect, withTransform tr: CGAffineTransform) {
// scale the font point-size
let font: UIFont = .systemFont(ofSize: 30.0 * zoomScale)
let attribs: [NSAttributedString.Key : Any] = [.font: font, .foregroundColor: UIColor.red]
// transform the point
let trPT: CGPoint = theTextPoint.applying(tr)
// attributed string at zoomed point-size
let string = NSAttributedString(string: "Sample", attributes: attribs)
// calculate the text rect
let sz: CGSize = string.size()
let r: CGRect = .init(x: trPT.x - sz.width * 0.5, y: trPT.y - sz.height * 0.5, width: sz.width, height: sz.height)
// only draw if visible
if r.intersects(insideRect) {
string.draw(at: r.origin)
}
}
}
class ComplexDrawScaledView: UIView {
// this will be set by the "rects" layout in commonInit()
public var virtualSize: CGSize = .zero
public var zoomScale: CGFloat = 1.0 { didSet { setNeedsDisplay() } }
public var contentOffset: CGPoint = .zero { didSet { setNeedsDisplay() } }
private let nCols: Int = 32
private let nRows: Int = 40
private let colWidth: CGFloat = 120.0
private let rowHeight: CGFloat = 80.0
private let colSpacing: CGFloat = 16.0
private let rowSpacing: CGFloat = 16.0
private let rectInset: CGSize = .init(width: 1.0, height: 1.0)
private let ovalInset: CGSize = .init(width: 12.0, height: 12.0)
private var theRectPaths: [UIBezierPath] = []
private var theOvalPaths: [UIBezierPath] = []
private var theTextPoints: [CGPoint] = []
private var theBirdPaths: [UIBezierPath] = []
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// let's create a "grid" of rects
// every rect will be used to create a
// rect path - alternating between rect and roundedRect
// a centered oval path
// and a centered text point
var r: CGRect = .init(x: 0.0, y: 0.0, width: colWidth, height: rowHeight)
for row in 0..<nRows {
for col in 0..<nCols {
let rPath = (row + col) % 2 == 0
? UIBezierPath(roundedRect: r.insetBy(dx: rectInset.width, dy: rectInset.height), cornerRadius: 12.0)
: UIBezierPath(rect: r.insetBy(dx: rectInset.width, dy: rectInset.height))
theRectPaths.append(rPath)
let oPath = UIBezierPath(ovalIn: r.insetBy(dx: ovalInset.width, dy: ovalInset.height))
theOvalPaths.append(oPath)
let pt: CGPoint = .init(x: r.midX, y: r.midY)
theTextPoints.append(pt)
r.origin.x += colWidth + colSpacing
}
r.origin.x = 0.0
r.origin.y += rowHeight + rowSpacing
}
// our "virtual size"
let w: CGFloat = theRectPaths.compactMap( { $0.bounds.maxX }).max()!
let h: CGFloat = theRectPaths.compactMap( { $0.bounds.maxY }).max()!
let sz: CGSize = .init(width: w, height: h)
// let's use 100x100 SwiftyBird paths, arranged:
// - one each at 50-points from the corners
// - one each at 25% from the corners
// - one centered
// so about like this:
// +--------------------+
// | x x |
// | |
// | x x |
// | |
// | x |
// | |
// | x x |
// | |
// | x x |
// +--------------------+
let v: CGFloat = 100.0
r = .init(x: 0.0, y: 0.0, width: v, height: v)
r.origin = .init(x: 50.0, y: 50.0)
theBirdPaths.append(SwiftyBird().path(inRect: r))
r.origin = .init(x: sz.width - (v + 50.0), y: 50.0)
theBirdPaths.append(SwiftyBird().path(inRect: r))
r.origin = .init(x: 50.0, y: sz.height - (v + 50.0))
theBirdPaths.append(SwiftyBird().path(inRect: r))
r.origin = .init(x: sz.width - (v + 50.0), y: sz.height - (v + 50.0))
theBirdPaths.append(SwiftyBird().path(inRect: r))
r.origin = .init(x: sz.width * 0.25 - v * 0.5, y: sz.height * 0.25 - v * 0.5)
theBirdPaths.append(SwiftyBird().path(inRect: r))
r.origin = .init(x: sz.width * 0.75 - v * 0.5, y: sz.height * 0.25 - v * 0.5)
theBirdPaths.append(SwiftyBird().path(inRect: r))
r.origin = .init(x: sz.width * 0.25 - v * 0.5, y: sz.height * 0.75 - v * 0.5)
theBirdPaths.append(SwiftyBird().path(inRect: r))
r.origin = .init(x: sz.width * 0.75 - v * 0.5, y: sz.height * 0.75 - v * 0.5)
theBirdPaths.append(SwiftyBird().path(inRect: r))
r.origin = .init(x: sz.width * 0.5 - v * 0.5, y: sz.height * 0.5 - v * 0.5)
theBirdPaths.append(SwiftyBird().path(inRect: r))
virtualSize = sz
}
override func draw(_ rect: CGRect) {
let tr = CGAffineTransform(translationX: -contentOffset.x, y: -contentOffset.y)
.scaledBy(x: zoomScale, y: zoomScale)
drawRects(insideRect: rect, withTransform: tr)
drawOvals(insideRect: rect, withTransform: tr)
drawStrings(insideRect: rect, withTransform: tr)
drawBirds(insideRect: rect, withTransform: tr)
}
private func drawRects(insideRect: CGRect, withTransform tr: CGAffineTransform) {
UIColor.green.setStroke()
theRectPaths.forEach { pth in
if let path = pth.copy() as? UIBezierPath {
// transform a copy of the path
path.apply(tr)
// only draw if visible
if path.bounds.intersects(insideRect) {
path.lineWidth = 2.0 * zoomScale
path.stroke()
}
}
}
}
private func drawOvals(insideRect: CGRect, withTransform tr: CGAffineTransform) {
UIColor.systemBlue.setStroke()
UIColor(white: 0.95, alpha: 1.0).setFill()
theOvalPaths.forEach { pth in
if let path = pth.copy() as? UIBezierPath {
// transform a copy of the path
path.apply(tr)
// only draw if visible
if path.bounds.intersects(insideRect) {
path.lineWidth = 3.0 * zoomScale
path.fill()
path.stroke()
}
}
}
}
private func drawStrings(insideRect: CGRect, withTransform tr: CGAffineTransform) {
// scale the font point-size
let font: UIFont = .systemFont(ofSize: 30.0 * zoomScale)
let attribs: [NSAttributedString.Key : Any] = [.font: font, .foregroundColor: UIColor.red]
for (i, pt) in theTextPoints.enumerated() {
// transform the point
let trPT: CGPoint = pt.applying(tr)
// attributed string at zoomed point-size
let string = NSAttributedString(string: "\(i+1)", attributes: attribs)
// calculate the text rect
let sz: CGSize = string.size()
let r: CGRect = .init(x: trPT.x - sz.width * 0.5, y: trPT.y - sz.height * 0.5, width: sz.width, height: sz.height)
// only draw if visible
if r.intersects(insideRect) {
string.draw(at: r.origin)
}
}
}
private func drawBirds(insideRect: CGRect, withTransform tr: CGAffineTransform) {
UIColor.yellow.setStroke()
UIColor(red: 1.0, green: 0.6, blue: 0.3, alpha: 0.8).setFill()
theBirdPaths.forEach { pth in
if let path = pth.copy() as? UIBezierPath {
// transform the path
path.apply(tr)
// only draw if visible
if path.bounds.intersects(insideRect) {
path.lineWidth = 2.0 * zoomScale
path.fill()
path.stroke()
}
}
}
}
}
class SwiftyBird: NSObject {
func path(inRect: CGRect) -> UIBezierPath {
let thisShape = UIBezierPath()
thisShape.move(to: CGPoint(x: 0.31, y: 0.94))
thisShape.addCurve(to: CGPoint(x: 0, y: 0.64), controlPoint1: CGPoint(x: 0.18, y: 0.87), controlPoint2: CGPoint(x: 0.07, y: 0.76))
thisShape.addCurve(to: CGPoint(x: 0.12, y: 0.72), controlPoint1: CGPoint(x: 0.03, y: 0.67), controlPoint2: CGPoint(x: 0.07, y: 0.7))
thisShape.addCurve(to: CGPoint(x: 0.57, y: 0.72), controlPoint1: CGPoint(x: 0.28, y: 0.81), controlPoint2: CGPoint(x: 0.45, y: 0.8))
thisShape.addCurve(to: CGPoint(x: 0.57, y: 0.72), controlPoint1: CGPoint(x: 0.57, y: 0.72), controlPoint2: CGPoint(x: 0.57, y: 0.72))
thisShape.addCurve(to: CGPoint(x: 0.15, y: 0.23), controlPoint1: CGPoint(x: 0.4, y: 0.57), controlPoint2: CGPoint(x: 0.26, y: 0.39))
thisShape.addCurve(to: CGPoint(x: 0.1, y: 0.15), controlPoint1: CGPoint(x: 0.13, y: 0.21), controlPoint2: CGPoint(x: 0.11, y: 0.18))
thisShape.addCurve(to: CGPoint(x: 0.5, y: 0.49), controlPoint1: CGPoint(x: 0.22, y: 0.28), controlPoint2: CGPoint(x: 0.43, y: 0.44))
thisShape.addCurve(to: CGPoint(x: 0.22, y: 0.09), controlPoint1: CGPoint(x: 0.35, y: 0.31), controlPoint2: CGPoint(x: 0.21, y: 0.08))
thisShape.addCurve(to: CGPoint(x: 0.69, y: 0.52), controlPoint1: CGPoint(x: 0.46, y: 0.37), controlPoint2: CGPoint(x: 0.69, y: 0.52))
thisShape.addCurve(to: CGPoint(x: 0.71, y: 0.54), controlPoint1: CGPoint(x: 0.7, y: 0.53), controlPoint2: CGPoint(x: 0.7, y: 0.53))
thisShape.addCurve(to: CGPoint(x: 0.61, y: 0), controlPoint1: CGPoint(x: 0.77, y: 0.35), controlPoint2: CGPoint(x: 0.71, y: 0.15))
thisShape.addCurve(to: CGPoint(x: 0.92, y: 0.68), controlPoint1: CGPoint(x: 0.84, y: 0.15), controlPoint2: CGPoint(x: 0.98, y: 0.44))
thisShape.addCurve(to: CGPoint(x: 0.92, y: 0.7), controlPoint1: CGPoint(x: 0.92, y: 0.69), controlPoint2: CGPoint(x: 0.92, y: 0.7))
thisShape.addCurve(to: CGPoint(x: 0.92, y: 0.7), controlPoint1: CGPoint(x: 0.92, y: 0.7), controlPoint2: CGPoint(x: 0.92, y: 0.7))
thisShape.addCurve(to: CGPoint(x: 0.99, y: 1), controlPoint1: CGPoint(x: 1.00, y: 0.86), controlPoint2: CGPoint(x: 1, y: 1.00))
thisShape.addCurve(to: CGPoint(x: 0.75, y: 0.93), controlPoint1: CGPoint(x: 0.92, y: 0.86), controlPoint2: CGPoint(x: 0.81, y: 0.9))
thisShape.addCurve(to: CGPoint(x: 0.31, y: 0.94), controlPoint1: CGPoint(x: 0.64, y: 1.01), controlPoint2: CGPoint(x: 0.47, y: 1.00))
thisShape.close()
let tr = CGAffineTransform(translationX: inRect.minX, y: inRect.minY)
.scaledBy(x: inRect.width, y: inRect.height)
thisShape.apply(tr)
return thisShape
}
}
Edit - I put up a project at https://github.com/DonMag/VirtualZoom showing these examples. Also includes filling the "bird" path with a gradient.