如何在保持纵横比的同时缩放 UIView 的内容以适合目标矩形?

How To Scale The Contents Of A UIView To Fit A Destination Rectangle Whilst Maintaining The Aspect Ratio?

我正在尝试解决一个没有成功的问题,希望有人能提供帮助。

我找过类似的帖子,但没能找到解决我问题的任何内容。

我的场景如下: 我有一个 UIView,可以在上面放置许多其他 UIView。这些可以使用手势识别器移动、缩放和旋转(这里没有问题)。 用户能够更改主视图的纵横比(Canvas),我的问题是试图缩放 Canvas 的内容以适应新的目标大小。

有许多主题相似的帖子,例如:

calculate new size and location on a CGRect

但是这些并没有解决比率多次变化的问题。

我的方法:

当我更改 canvas 的纵横比时,我利用 AVFoundation 计算 canvas 的子视图应适合的纵横比拟合矩形:

let sourceRectangleSize = canvas.frame.size

canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
view.layoutIfNeeded()

let destinationRectangleSize = canvas.frame.size

let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
ratioVisualizer.frame = aspectFittedFrame

红色框只是为了可视化Aspect Fitted Rectangle。正如您所看到的,虽然纵横比拟合矩形是正确的,但对象的缩放不起作用。当我将缩放和旋转应用到子视图 (CanvasElement) 时尤其如此。

我缩放对象的逻辑显然是错误的:

@objc
private func setRatio(_ control: UISegmentedControl) {
  guard let aspect = Aspect(rawValue: control.selectedSegmentIndex) else { return }
  
  let sourceRectangleSize = canvas.frame.size
 
  canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
  view.layoutIfNeeded()
 
  let destinationRectangleSize = canvas.frame.size
  
  let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
  ratioVisualizer.frame = aspectFittedFrame
  
  let scale = min(aspectFittedFrame.size.width/canvas.frame.width, aspectFittedFrame.size.height/canvas.frame.height)
  
  for case let canvasElement as CanvasElement in canvas.subviews {
  
    canvasElement.frame.size = CGSize(
      width: canvasElement.baseFrame.width * scale,
      height: canvasElement.baseFrame.height * scale
    )
    canvasElement.frame.origin = CGPoint(
      x: aspectFittedFrame.origin.x + canvasElement.baseFrame.origin.x * scale,
      y:  aspectFittedFrame.origin.y + canvasElement.baseFrame.origin.y * scale
    )
  }
}

如果这有帮助,我也附上 Canvas 元素 Class:

final class CanvasElement: UIView {
  
  var rotation: CGFloat = 0
  var baseFrame: CGRect = .zero

  var id: String = UUID().uuidString
  
  // MARK: - Initialization
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    storeState()
    setupGesture()
  }
  
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
  
  // MARK: - Gesture Setup
  
  private func setupGesture() {
    let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:)))
    let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinchGesture(_:)))
    let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(rotateGesture(_:)))
    addGestureRecognizer(panGestureRecognizer)
    addGestureRecognizer(pinchGestureRecognizer)
    addGestureRecognizer(rotateGestureRecognizer)
  }
  
  // MARK: - Touches
  
  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesBegan(touches, with: event)
    moveToFront()
  }
  
  //MARK: - Gestures
  
  @objc
  private func panGesture(_ sender: UIPanGestureRecognizer) {
    let move = sender.translation(in: self)
    transform = transform.concatenating(.init(translationX: move.x, y: move.y))
    sender.setTranslation(CGPoint.zero, in: self)
    storeState()
  }
  
  @objc
  private func pinchGesture(_ sender: UIPinchGestureRecognizer) {
    transform = transform.scaledBy(x: sender.scale, y: sender.scale)
    sender.scale = 1
    storeState()
  }
  
  @objc
  private func rotateGesture(_ sender: UIRotationGestureRecognizer) {
    rotation += sender.rotation
    transform = transform.rotated(by: sender.rotation)
    sender.rotation = 0
    storeState()
  }
  
  // MARK: - Miscelaneous
  
  func moveToFront() {
    superview?.bringSubviewToFront(self)
  }
  
  public func rotated(by degrees: CGFloat) {
    transform = transform.rotated(by: degrees)
    rotation += degrees
  }
  
  func storeState() {
    print("""
    Element Frame = \(frame)
    Element Bounds = \(bounds)
    Element Center = \(center)
    """)
    baseFrame = frame
  }
}

任何帮助或建议、方法以及一些实际示例都会很棒。我不指望任何人提供完整的源代码,但我可以作为基础的东西。

感谢您花时间阅读我的问题。

以下是玩这个游戏时的一些想法和发现

1.是否使用了正确的比例因子?

您使用的比例有点自定义,无法直接与只有 1 个比例因子(如 2 或 3)的示例进行比较。但是,您的比例因子有 2 个维度,但我看到您对此进行了补偿以获得宽度和高度缩放的最小值:

let scale = min(aspectFittedFrame.size.width / canvas.frame.width,
                aspectFittedFrame.size.height / canvas.frame.height)

在我看来,我认为这不是正确的比例因子。对我来说,这是将新 aspectFittedFrame 与新 canvas frame

进行比较

实际上我认为正确的比例因子是将新的 aspectFittedFrame 与之前的 canvas frame

进行比较
let scale
    = min(aspectFittedFrame.size.width / sourceRectangleSize.width,
          aspectFittedFrame.size.height / sourceRectangleSize.height)

2。比例是否应用于正确的值?

如果您注意到,从 1:1 到 16:9 的第一个顺序效果很好。但是在那之后它似乎不起作用,我相信问题出在这里:

for case let canvasElement as CanvasElement in strongSelf.canvas.subviews
{
    canvasElement.frame.size = CGSize(
        width: canvasElement.baseFrame.width * scale,
        height: canvasElement.baseFrame.height * scale
    )
    
    canvasElement.frame.origin = CGPoint(
        x: aspectFittedFrame.origin.x
            + canvasElement.baseFrame.origin.x * scale,
        
        y:  aspectFittedFrame.origin.y
            + canvasElement.baseFrame.origin.y * scale
    )
}

第一次,缩放效果很好,因为 canvas 和 canvas 元素正在同步缩放或正确映射:

但是,如果超出此范围,因为您总是根据基值进行缩放,所以您的纵横比框架和 canvas 元素不同步

所以在 1:1 -> 16:9 -> 3:2

的例子中
  • 您的视口已缩放两次 1:1 -> 16:9 和 16:9 -> 3:2
  • 虽然您的元素每次都缩放一次,但 1:1 -> 16:9 和 1:1 -> 3:2 因为您总是从基值缩放

所以我觉得要看到红色视口内的值,您需要基于前一个视图而不是基础视图应用相同的连续缩放。

为了立即快速修复,我在每次更改 canvas 大小后通过调用 canvasElement.storeState():

更新 canvas 元素的基值
for case let canvasElement as CanvasElement in strongSelf.canvas.subviews
{
    canvasElement.frame.size = CGSize(
        width: canvasElement.baseFrame.width * scale,
        height: canvasElement.baseFrame.height * scale
    )
    
    canvasElement.frame.origin = CGPoint(
        x: aspectFittedFrame.origin.x
            + canvasElement.baseFrame.origin.x * scale,
        
        y:  aspectFittedFrame.origin.y
            + canvasElement.baseFrame.origin.y * scale
    )
    
    // I added this
    canvasElement.storeState()
}

结果可能更接近您想要的?

最后的想法

虽然这 可能 解决您的问题,但您会注意到不可能返回到原始状态,因为在每一步都应用了转换。

一个解决方案可能是存储映射到特定视口纵横比的当前值,并为其他值计算正确的大小,这样如果您需要返回到原始值,您可以这样做。

也许你可以在一个视图中制作三个矩形。然后你可以保留视图的 aspect-ratio。

如果您正在使用自动布局和 Snapkit。约束可能是这样的:

view.snp.makeConstraints { make in
  make.width.height.lessThanOrEqualToSuperview()
  make.centerX.centerY.equalToSuperview()
  make.width.equalTo(view.snp.height)
  make.width.height.equalToSuperview().priority(.high)
}

所以这个视图将在超级视图中 aspect-fit。

在此视图中返回 children。如果你想在视图的框架改变时每隔 child 缩放一次,你也应该添加约束。或者你可以使用autoresizingMask,它可能更简单。

如果您不想使用自动布局。也许你可以试试transform。当你转换一些视图时,这个视图中的 children 也会改变。

// The scale depends on the aspect-ratio of superview.
view.transform = CGAffineTransformMakeScale(0.5, 0.5);

几个建议...

首先,在使用 CanvasElement 时,如果视图已旋转,则平移无法正常工作。

因此,不要使用平移转换来移动视图,而是更改 .center 本身。此外,平移时,我们希望在 superview 中使用翻译,而不是在视图本身中使用:

@objc
func panGesture(_ gest: UIPanGestureRecognizer) {
    // change the view's .center instead of applying translate transform
    //  use translation in superview, not in self
    guard let superV = superview else { return }
    let translation = gest.translation(in: superV)
    center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
    gest.setTranslation(CGPoint.zero, in: superV)
}

现在,当我们想在“Canvas”改变大小时缩放子视图时,我们可以这样做...

我们将跟踪“以前的”边界并使用“新边界”来计算比例:

let newBounds: CGRect = bounds
    
let scW: CGFloat = newBounds.size.width / prevBounds.size.width
let scH: CGFloat = newBounds.size.height / prevBounds.size.height
    
for case let v as CanvasElement in subviews {
    // reset transform before scaling / positioning
    let tr = v.transform
    v.transform = .identity

    let w = v.frame.width * scW
    let h = v.frame.height * scH
    let cx = v.center.x * scW
    let cy = v.center.y * scH

    v.frame.size = CGSize(width: w, height: h)
    v.center = CGPoint(x: cx, y: cy)

    // re-apply transform
    v.transform = tr
}

prevBounds = newBounds

这是一个完整的示例实现。请注意:这只是 示例代码!!! 它不打算“生产就绪”。

import UIKit

// MARK: enum to provide strings and aspect ratio values
enum Aspect: Int, Codable, CaseIterable {
    case a1to1
    case a16to9
    case a3to2
    case a4to3
    case a9to16
    var stringValue: String {
        switch self {
        case .a1to1:
            return "1:1"
        case .a16to9:
            return "16:9"
        case .a3to2:
            return "3:2"
        case .a4to3:
            return "4:3"
        case .a9to16:
            return "9:16"
        }
    }
    var aspect: CGFloat {
        switch self {
        case .a1to1:
            return 1
        case .a16to9:
            return 9.0 / 16.0
        case .a3to2:
            return 2.0 / 3.0
        case .a4to3:
            return 3.0 / 4.0
        case .a9to16:
            return 16.0 / 9.0
        }
    }
}

class EditorView: UIView {
    // no code -
    //  just makes it easier to identify
    //  this view when debugging
}

// CanvasElement views will be added as subviews
//  this handles the scaling / positioning when the bounds changes
//  also (optionally) draws a grid (for use during development)
class CanvasView: UIView {
    
    public var showGrid: Bool = true

    private let gridLayer: CAShapeLayer = CAShapeLayer()
    
    private var prevBounds: CGRect = .zero
    
    // MARK: init
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {

        gridLayer.fillColor = UIColor.clear.cgColor
        gridLayer.strokeColor = UIColor.red.cgColor
        gridLayer.lineWidth = 1
        
        layer.addSublayer(gridLayer)
        
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        
        // MARK: 10 x 10 grid
        if showGrid {
            // draw a grid on the inside of the bounds
            //  so the edges are not 1/2 point width
            let gridBounds: CGRect = bounds.insetBy(dx: 0.5, dy: 0.5)
            
            let path: UIBezierPath = UIBezierPath()
            
            let w: CGFloat = gridBounds.width / 10.0
            let h: CGFloat = gridBounds.height / 10.0
            
            var p: CGPoint = .zero
            
            p = CGPoint(x: gridBounds.minX, y: gridBounds.minY)
            for _ in 0...10 {
                path.move(to: p)
                path.addLine(to: CGPoint(x: p.x, y: gridBounds.maxY))
                p.x += w
            }
            
            p = CGPoint(x: gridBounds.minX, y: gridBounds.minY)
            for _ in 0...10 {
                path.move(to: p)
                path.addLine(to: CGPoint(x: gridBounds.maxX, y: p.y))
                p.y += h
            }
            
            gridLayer.path = path.cgPath
        }
        
        // MARK: update subviews
        // we only want to move/scale the subviews if
        //  the bounds has > 0 width and height and
        //  prevBounds has > 0 width and height and
        //  the bounds has changed

        guard bounds != prevBounds,
              bounds.width > 0, prevBounds.width > 0,
              bounds.height > 0, prevBounds.height > 0
        else { return }

        let newBounds: CGRect = bounds
        
        let scW: CGFloat = newBounds.size.width / prevBounds.size.width
        let scH: CGFloat = newBounds.size.height / prevBounds.size.height
        
        for case let v as CanvasElement in subviews {
            // reset transform before scaling / positioning
            let tr = v.transform
            v.transform = .identity

            let w = v.frame.width * scW
            let h = v.frame.height * scH
            let cx = v.center.x * scW
            let cy = v.center.y * scH

            v.frame.size = CGSize(width: w, height: h)
            v.center = CGPoint(x: cx, y: cy)

            // re-apply transform
            v.transform = tr
        }

        prevBounds = newBounds
    }
    
    override var bounds: CGRect {
        willSet {
            prevBounds = bounds
        }
    }
}

// self-contained Pan/Pinch/Rotate view
//  set allowSimultaneous to TRUE to enable
//  simultaneous gestures
class CanvasElement: UIView, UIGestureRecognizerDelegate {
    
    public var allowSimultaneous: Bool = false
    
    // MARK: init
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        
        isUserInteractionEnabled = true
        isMultipleTouchEnabled = true
        
        let panG = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:)))
        let pinchG = UIPinchGestureRecognizer(target: self, action: #selector(pinchGesture(_:)))
        let rotateG = UIRotationGestureRecognizer(target: self, action: #selector(rotateGesture(_:)))
        
        [panG, pinchG, rotateG].forEach { g in
            g.delegate = self
            addGestureRecognizer(g)
        }
        
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        // unwrap optional superview
        guard let superV = superview else { return }
        superV.bringSubviewToFront(self)
    }
    
    // MARK: UIGestureRecognizer Methods
    
    @objc
    func panGesture(_ gest: UIPanGestureRecognizer) {
        // change the view's .center instead of applying translate transform
        //  use translation in superview, not in self
        guard let superV = superview else { return }
        let translation = gest.translation(in: superV)
        center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
        gest.setTranslation(CGPoint.zero, in: superV)
    }
    
    @objc
    func pinchGesture(_ gest: UIPinchGestureRecognizer) {
        // apply scale transform
        transform = transform.scaledBy(x: gest.scale, y: gest.scale)
        gest.scale = 1
    }
    
    @objc
    func rotateGesture(_ gest : UIRotationGestureRecognizer) {
        // apply rotate transform
        transform = transform.rotated(by: gest.rotation)
        gest.rotation = 0
    }
    
    // MARK: UIGestureRecognizerDelegate Methods
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return allowSimultaneous
    }
    
}

// example view controller
//  Aspect Ratio segmented control
//      changes the Aspect Ratio of the Editor View
//  includes triple-tap gesture to cycle through
//      3 "starting subview" layouts
class ViewController: UIViewController, UIGestureRecognizerDelegate {
    
    let editorView: EditorView = {
        let v = EditorView()
        v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    let canvasView: CanvasView = {
        let v = CanvasView()
        v.backgroundColor = .yellow
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    // segmented control for selecting Aspect Ratio
    let aspectRatioSeg: UISegmentedControl = {
        let v = UISegmentedControl()
        v.setContentCompressionResistancePriority(.required, for: .vertical)
        v.setContentHuggingPriority(.required, for: .vertical)
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    // this will be changed by the Aspect Ratio segmented control
    var evAspectConstraint: NSLayoutConstraint!
    
    // used to cycle through intitial subviews layout
    var layoutMode: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = UIColor(white: 0.99, alpha: 1.0)
        
        // container view for laying out editor view
        let containerView: UIView = {
            let v = UIView()
            v.backgroundColor = .cyan
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()

        // setup the aspect ratio segmented control
        for (idx, m) in Aspect.allCases.enumerated() {
            aspectRatioSeg.insertSegment(withTitle: m.stringValue, at: idx, animated: false)
        }
        
        // add canvas view to editor view
        editorView.addSubview(canvasView)
        
        // add editor view to container view
        containerView.addSubview(editorView)
        
        // add container view to self's view
        view.addSubview(containerView)
        
        // add UI Aspect Ratio segmented control to self's view
        view.addSubview(aspectRatioSeg)
        
        // always respect the safe area
        let safeG = view.safeAreaLayoutGuide
        
        // editor view inset from container view sides
        let evInset: CGFloat = 0
        
        // canvas view inset from editor view sides
        let cvInset: CGFloat = 0
        
        // these sets of constraints will make the Editor View and the Canvas View
        //  as large as their superviews (with "Inset Edge Padding" if set above)
        //  while maintaining aspect ratios and centering
        let evMaxW = editorView.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor, constant: -evInset)
        let evMaxH = editorView.heightAnchor.constraint(lessThanOrEqualTo: containerView.heightAnchor, constant: -evInset)
        
        let evW = editorView.widthAnchor.constraint(equalTo: containerView.widthAnchor)
        let evH = editorView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
        evW.priority = .required - 1
        evH.priority = .required - 1
        
        let cvMaxW = canvasView.widthAnchor.constraint(lessThanOrEqualTo: editorView.widthAnchor, constant: -cvInset)
        let cvMaxH = canvasView.heightAnchor.constraint(lessThanOrEqualTo: editorView.heightAnchor, constant: -cvInset)
        
        let cvW = canvasView.widthAnchor.constraint(equalTo: editorView.widthAnchor)
        let cvH = canvasView.heightAnchor.constraint(equalTo: editorView.heightAnchor)
        cvW.priority = .required - 1
        cvH.priority = .required - 1
        
        // editor view starting aspect ratio
        //  this is changed by the segmented control
        let editorAspect: Aspect = .a1to1
        aspectRatioSeg.selectedSegmentIndex = editorAspect.rawValue
        evAspectConstraint = editorView.heightAnchor.constraint(equalTo: editorView.widthAnchor, multiplier: editorAspect.aspect)

        // we can set the Aspect Ratio of the CanvasView here
        //  it will maintain its Aspect Ratio independent of
        //  the Editor View's Aspect Ratio
        let canvasAspect: Aspect = .a1to1

        NSLayoutConstraint.activate([
            containerView.topAnchor.constraint(equalTo: safeG.topAnchor),
            containerView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
            
            editorView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
            editorView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
            evMaxW, evMaxH,
            evW, evH,
            evAspectConstraint,
            
            canvasView.centerXAnchor.constraint(equalTo: editorView.centerXAnchor),
            canvasView.centerYAnchor.constraint(equalTo: editorView.centerYAnchor),
            cvMaxW, cvMaxH,
            cvW, cvH,
            canvasView.heightAnchor.constraint(equalTo: canvasView.widthAnchor, multiplier: canvasAspect.aspect),

            aspectRatioSeg.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8.0),
            aspectRatioSeg.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -8.0),
            aspectRatioSeg.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
            aspectRatioSeg.widthAnchor.constraint(greaterThanOrEqualTo: safeG.widthAnchor, multiplier: 0.5),
            aspectRatioSeg.widthAnchor.constraint(lessThanOrEqualTo: safeG.widthAnchor),
        ])
        
        aspectRatioSeg.addTarget(self, action: #selector(aspectRatioSegmentChanged(_:)), for: .valueChanged)
        
        // triple-tap anywhere to "reset" the 3 subviews
        //  cycling between starting sizes/positions
        let tt = UITapGestureRecognizer(target: self, action: #selector(resetCanvas))
        tt.numberOfTapsRequired = 3
        tt.delaysTouchesEnded = false
        view.addGestureRecognizer(tt)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // we don't have the frames in viewDidLoad,
        //  so wait until now to add the CanvasElement views
        resetCanvas()
    }
    
    @objc func resetCanvas() {
        
        canvasView.subviews.forEach { v in
            v.removeFromSuperview()
        }

        // add 3 views to the canvas

        let v1 = CanvasElement()
        v1.backgroundColor = .systemYellow

        let v2 = CanvasElement()
        v2.backgroundColor = .systemGreen

        let v3 = CanvasElement()
        v3.backgroundColor = .systemBlue

        // default size of subviews is 2/10ths the width of the canvas
        let w: CGFloat = canvasView.bounds.width * 0.2
        
        [v1, v2, v3].forEach { v in
            v.frame = CGRect(x: 0, y: 0, width: w, height: w)
            canvasView.addSubview(v)
            // if we want to allow simultaneous gestures
            //  i.e. pan/scale/rotate all at the same time
            //v.allowSimultaneous = true
        }
        
        switch (layoutMode % 3) {
        case 1:
            //  top-left corner
            //  center at 1.5 times the size
            //  bottom-right corner
            v1.frame.origin = CGPoint(x: 0, y: 0)
            v2.frame.size = CGSize(width: w * 1.5, height: w * 1.5)
            v2.center = CGPoint(x: canvasView.bounds.midX, y: canvasView.bounds.midY)
            v3.center = CGPoint(x: canvasView.bounds.maxX - w * 0.5, y: canvasView.bounds.maxY - w * 0.5)
            ()
        case 2:
            // different sized views
            v1.frame = CGRect(x: 0, y: 0, width: w * 0.5, height: w)
            v2.frame.size = CGSize(width: w, height: w)
            v2.center = CGPoint(x: canvasView.bounds.midX, y: canvasView.bounds.midY)
            v3.frame.size = CGSize(width: w, height: w * 0.5)
            v3.center = CGPoint(x: canvasView.bounds.maxX - v3.frame.width * 0.5, y: canvasView.bounds.maxY - v3.frame.height * 0.5)
            ()
        default:
            //  on a "diagonal"
            //  starting at top-left corner
            v1.frame.origin = CGPoint(x: 0, y: 0)
            v2.frame.origin = CGPoint(x: w, y: w)
            v3.frame.origin = CGPoint(x: w * 2, y: w * 2)
            ()
        }
        
        layoutMode += 1
    }

    @objc func aspectRatioSegmentChanged(_ sender: Any?) {
        if let seg = sender as? UISegmentedControl,
           let r = Aspect.init(rawValue: seg.selectedSegmentIndex)
        {
            evAspectConstraint.isActive = false
            evAspectConstraint = editorView.heightAnchor.constraint(equalTo: editorView.widthAnchor, multiplier: r.aspect)
            evAspectConstraint.isActive = true
        }
    }

}

一些示例屏幕截图...

  • 黄色是 Canvas 视图...带有可选的红色 10x10 网格
  • 灰色是编辑器视图...这是改变纵横比的视图
  • 青色是“容器”视图....编辑器视图fits/centers本身

请注意,Canvas 视图可以设置为方形(1:1 比例)以外的其他视图。例如,此处它设置为 9:16 比率——并保持其纵横比独立于编辑器视图纵横比:

使用此示例控制器,triple-tap 可在任何地方循环 3 个“起始布局”: