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.