缩放 UIView 元素

问题描述 投票:0回答:1

我有一个

UIScrollView
,我在它的
contentView
类型的
DrawView
中画了一条线和一个字符串。我想保持绘制元素相对于
zoomScale
的宽度。下面是我的代码。

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()
    }
}

这是我的

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))
    }
}

以下是结果

zoomScale
1.0

zoomScale
5.0

zoomScale
5.0

当我缩放时,预期的宽度保持不变,但元素被像素化。

期望:

zoomScale
5.0

可以注意到当前结果是像素化的。实现规模化且清晰的预期结果的理想方法是什么?

ios swift uiview uiscrollview uibezierpath
1个回答
1
投票

一种选择是使用固定大小的“drawView”并转换路径和字体大小。

这是一个基本示例:

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)
        
    }
    
}

那个

BasicScalingView
就是我们将用作“drawView”的东西。当我们设置
zoomScale
时,它会重新绘制自身,从而变换线条路径、椭圆形路径、文本的左上角点和字体大小。

我们可以通过使用滑块来更改缩放比例来展示这一点:

正如我们所看到的,直线和曲线保持锐利并且相对于彼此就位。

现在我们可以使用捏合和平移手势,并编写一堆代码来跟踪缩放比例值和相对位置,以允许缩放和平移。我们还需要使用手势的

.location
.velocity
等属性来实现边缘弹跳。通过一些搜索,我们可能会找到一些样本。

但是...如果我们可以在滚动视图中使用所有这些内置函数,那不是很好吗?

好吧,我们可以...

首先,我们将使用一个相当简单的修改后的“缩放视图”,它具有

zoomScale
contentOffset
属性,当我们获得
scrollViewDidZoom
scrollViewDidScroll
时,我们将更新该属性。

它绘制了一个矩形、一本小说(插入了一点)和一个文本字符串,所有这些都位于视图的中心 - 看起来像这样开始:

我们所做的是将“drawView”放在清晰的滚动视图后面,我们将使用简单、清晰的

UIView
作为
viewForZooming

当我们缩放/平移滚动视图时,我们得到:

我们用于

viewForZooming
的空“清晰”视图可以是非常大,并且可以放大到高缩放比例而不会出现内存问题。

使用“复杂”缩放视图作为我们的“drawView”——创建一个由矩形(圆形和方形交替)、椭圆形、文本字符串和一些“SwiftyBird”贝塞尔曲线路径组成的 32 列 x 40 行“网格”。

看起来像这样(一直滚动到右下角):

并且经过一些缩放/平移后:

这是运行这些示例的完整代码...没有

@IBOutlet
@IBAction
连接 - 只需将新的视图控制器分配给
TheBasicsVC
,然后是
SimpleVC
,然后是
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
    }
}

编辑 - 我在 https://github.com/DonMag/VirtualZoom 上发布了一个项目,展示了这些示例。还包括用渐变填充“鸟”路径。

© www.soinside.com 2019 - 2024. All rights reserved.