scaling buttons grid with CGAffineTransform goes wrong

62 views Asked by At

I am trying to make buttons grid that will be scale up and down with pinching the scrollview , I am inspired by Scale UIView elements and the answer by donmag. the problem is that the buttons comes out of the scrollview, its showing on the main view as well but I want it inside the scrollview and when I zoom in and out they do not stay together like the grid , they separate or go together. dose anyone knows how to fix these two problem.i know I should ask just one question at time but these are some how in the same category ,sorry and thanks for helping. .you can run the code , just give ComplexVC to a view controller.

enter image description here

enter image description here

enter image description here

import UIKit






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 = UIColor.yellow
        
        [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 = false
    }
    
    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 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? ComplexDrawScaledView {
                dv.contentOffset = scrollView.contentOffset
        }
    }
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        updateInfoLabel()
      
            if let dv = drawView as? ComplexDrawScaledView {
                dv.zoomScale = scrollView.zoomScale
        }
    }
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return zoomView
    }
}



class ComplexVC: DrawZoomBaseVC {
    
    override func viewDidLoad() {
        drawView = ComplexDrawScaledView()
        super.viewDidLoad()
    }
    
}



class ComplexDrawScaledView: UIView {
    
    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 = 128
    private let colWidth: CGFloat = 120.0
    private let rowHeight: CGFloat = 80.0
    private let colSpacing: CGFloat = -2.0
    private let rowSpacing: CGFloat = -2.0
    
    private let buttonInset: CGSize = .init(width: 1.0, height: 1.0)
    
    private var buttons: [UIButton] = []
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    private func commonInit() {
        var r: CGRect = .init(x: 0.0, y: 0.0, width: colWidth, height: rowHeight)
        for _ in 0..<nRows {
            for _ in 0..<nCols {
                let button = UIButton(type: .system)
                button.frame = r.insetBy(dx: buttonInset.width, dy: buttonInset.height)
                button.setTitle("Button", for: .normal)
                button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
                // Set button border properties
                button.layer.borderWidth = 1.0
                button.layer.borderColor = UIColor.red.cgColor

                buttons.append(button)
                addSubview(button)
                r.origin.x += colWidth + colSpacing
            }
            r.origin.x = 0.0
            r.origin.y += rowHeight + rowSpacing
            
        }
        
        // Set virtual size based on button layout
        let w: CGFloat = buttons.compactMap({ $0.frame.maxX }).max() ?? 0
        let h: CGFloat = buttons.compactMap({ $0.frame.maxY }).max() ?? 0
        virtualSize = CGSize(width: w, height: h)
    }
    
    override func draw(_ rect: CGRect) {
        let tr = CGAffineTransform(translationX: -contentOffset.x, y: -contentOffset.y)
            .scaledBy(x: zoomScale, y: zoomScale)
        
        buttons.forEach { button in
            button.transform = CGAffineTransform.identity
            button.transform = tr
        }
    }
    
    @objc private func buttonTapped(_ sender: UIButton) {
        print("Button tapped")
        // Handle button tap if needed
    }
}
0

There are 0 answers