0

enter image description here

I have this scene of a neon cassette as you see. In the interface builder I have created 3 UIImageViews, one for the background of the cassette and 2 for the left and right spindles. The idea is that these spindles spin backwards or forwards depending on the state (rewind, ffw or play). I can do this fine in the case that I run this solely on, say, an iPhone 8. However, when I decide I want to run this on an iPhone 11 Pro, the following happens:

enter image description here

No matter what bizarre mixture of constraints I attempt to concoct, I am unable to have the spindles follow their correct position/size across devices. I will have to spend some time finding and reading guides on the beast that is Auto Layout, but until that mountain is climbed, I'd appreciate any help!

EDIT:

Here is a programmatic, so more descriptive, version of what I have so far:

override func viewDidLoad() {
    super.viewDidLoad()

    // Code to position/size cassette
    let cassette = UIImageView(image: UIImage(named: "cassette"))
    cassette.translatesAutoresizingMaskIntoConstraints = false
    cassette.contentMode = .scaleAspectFit
    view.addSubview(cassette)

    view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: cassette.bottomAnchor, constant: 44).isActive = true
    view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: cassette.trailingAnchor, constant: 20).isActive = true
    cassette.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20).isActive = true
    cassette.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 44).isActive = true

    // Code to position/size spindles
    let spindleLeft = UIImageView(image: UIImage(named: "spindle"))
    spindleLeft.translatesAutoresizingMaskIntoConstraints = false
    spindleLeft.widthAnchor.constraint(equalToConstant: 74).isActive = true
    spindleLeft.heightAnchor.constraint(equalToConstant: 74).isActive = true
    cassette.addSubview(spindleLeft)

    spindleLeft.centerXAnchor.constraint(equalTo: cassette.centerXAnchor, constant: -130.0).isActive = true
    spindleLeft.centerYAnchor.constraint(equalTo: cassette.centerYAnchor, constant: -14.0).isActive = true

}

Here are the 2 assets: enter image description here enter image description here

I'm currently stuck trying to figure out if there's some way to use the aspectRatio of the device and what UIImageView Content Mode (Aspect Fit, Aspect Fill) I should use. Perhaps I've dug too deep a whole and there's a far simpler way to deal with this that I'm not seeing...

ev350
  • 429
  • 3
  • 16
  • @matt the cassette image is resized by auto layout: Safe Area.bottom = cassette.bottom + 44, Safe Area.trailing = cassette.trailing + 20, cassette.leading, Safe Area.leading + 20, cassette.top = Safe Area.top + 44. So, from my current understanding, the image should resize according to these constraints, no? The View's Content Mode is "Aspect Fit". In other words, the cassette should always remain 44 points from top/bottom and 20 points from trail/lead. – ev350 Jan 12 '20 at 18:39
  • Aspect fit. So you must make the spindles behave like part of an aspect fit image. You must simulate the mathematics of image resizing. – matt Jan 12 '20 at 18:42
  • @matt I would have thought that Aspect Fit would keep the spindles as spheres. Regardless, I suppose I should be changing the multiplier of certain constraints with some calculated value in order to achieve the desired effect? – ev350 Jan 12 '20 at 18:56
  • @matt I assume it's not possible to achieve this in the Interface Builder then? i.e. can only be achieved programmatically? – ev350 Jan 12 '20 at 19:02
  • Anything you can do in IB you can do programmatically. IB is just a GUI for doing certain things (not all) in iOS development, like adding and positioning subviews. I think you will have a much easier (and quicker) time doing it programmatically. – trndjc Jan 12 '20 at 19:57
  • If I were you, I'd make two images, the left side of the cassette and the right side, and scale them using `.scaleAspectFit`. This way, the cassette itself looks the same on all devices; any adjustments leave transparent white space, which I think is more desirable to stretching or squeezing the image or clipping. This also allows you to very easily center the spindle in the center-x and center-y of each side of the cassette. – trndjc Jan 12 '20 at 20:05
  • Personally, since I have already worked out what the aspect content mode does to an image, I would just position the spindles in code, without autolayout. – matt Jan 12 '20 at 20:08
  • @matt I've added the image assets and some code I used to be more declarative so you can see what I've been doing. I know that it still uses constraints here, but I have been experimenting without in order to set the positions programmatically but I've yet to find an equation, or the values, to use. I've now placed the spindles as subviews to the cassette as I thought this might help, but still not quite there. – ev350 Jan 12 '20 at 23:21
  • I'm quite certain that mere autolayout is not going to do this. You have to measure where the spindle would be if it were part of the _original_ image, and then mathematically _put_ the spindle there based on the actual size of the image view and where it that puts that frame via its content mode. – matt Jan 13 '20 at 00:39

2 Answers2

1

Mere autolayout is not going to do this. You have to measure where the spindle would be if it were part of the original image, and then mathematically put the spindle there based on the actual size of the image view and where it that puts the image via its content mode.

Let's do that for one spindle (the left spindle). The cassette image you posted is 4824x2230. We discover by careful inspection that if the cassette image were shown at full size, the center of the spindle should be at approximately (1294,1000) and it should be about 500 pixels on a side.

That is the same as saying that we want the image of the spindle to occupy a square whose frame (with respect to the original cassette image) is (1045,750,500,500).

Okay, but that’s just the image. We have to position the spindle relative to the image view that portrays it.

So now I'll show the cassette in an image view that is sized by autolayout to some smaller size. (I can discover what size it is in viewDidLayoutSubviews, which runs after autolayout has done its work.) And let this image view have a content mode of Aspect Fit. The image will be resized, and also its position will probably not be at the image view’s origin.

So the problem you have to solve (in code) is: now where is that spindle frame with respect to the image view once the image has been resized and repositioned? You then just place the spindle image view there.

Here is the code from viewDidLayoutSubviews; cassette is the cassette image view, spindle is the spindle image view:

    let cassettebounds = cassette.bounds
    let cw = cassettebounds.width
    let ch = cassettebounds.height
    let w = 4824 as CGFloat
    let h = 2230 as CGFloat
    var scale = cw/w
    var imw = cw
    var imh = h*scale
    var imx = 0 as CGFloat
    var imy = (ch-imh)/2
    if imh > ch { // we got it backwards
        scale = ch/h
        imh = ch
        imw = w*scale
        imy = 0 as CGFloat
        imx = (cw-imw)/2
    }
    cassette.addSubview(spindle)
    spindle.frame.size.width = 500*scale
    spindle.frame.size.height = 500*scale
    spindle.frame.origin.x = imx + (1045*scale)
    spindle.frame.origin.y = imy + (750*scale)

Here's the result on a 6s and an 11 Pro:

enter image description here

enter image description here

Looks darned good if I do say so myself. The right spindle is left as an exercise for the reader.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Brilliant! This is just what I was looking for. I'll mark this correct but will also add my finished code for both spindles. Thanks for you help; it's much appreciated! – ev350 Jan 13 '20 at 10:44
0

Thank you to @matt for his superb answer. I definitely was ascribing too much power to auto layout when what was needed was a more rudimentary approach. I've marked his answer correct and will provide my new code that works and supports screens of all sizes:

override func viewDidLoad() {
    super.viewDidLoad()

    // Code to position/size cassette
    cassette = UIImageView(image: UIImage(named: "cassette"))
    cassette.translatesAutoresizingMaskIntoConstraints = false
    cassette.isUserInteractionEnabled = true
    cassette.contentMode = .scaleAspectFit
    view.addSubview(cassette)

    view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: cassette.bottomAnchor, constant: 44).isActive = true
    view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: cassette.trailingAnchor, constant: 20).isActive = true
    cassette.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20).isActive = true
    cassette.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 44).isActive = true
}

override func viewDidLayoutSubviews() {
    spindleLeft = addSpindle(to: cassette, posX: 505, posY: 350)
    spindleRight = addSpindle(to: cassette, posX: 1610, posY: 350)
}

func addSpindle(to cassette: UIImageView, posX: CGFloat, posY: CGFloat) -> UIImageView {
    let cassetteBounds = cassette.bounds
    let cassetteWidth = cassetteBounds.width
    let cassetteHeight = cassetteBounds.height

    let cassetteImageWidth =  cassette.image!.size.width
    let cassetteImageHeight = cassette.image!.size.height

    var scale = cassetteWidth / cassetteImageWidth

    var spindleWidth = cassetteWidth
    var spindleHeight = cassetteImageHeight*scale
    var spindleX = 0 as CGFloat
    var spindleY = (cassetteHeight - spindleHeight)/2

    if spindleHeight > cassetteHeight {
        scale = cassetteHeight / cassetteImageHeight
        spindleHeight = cassetteHeight
        spindleWidth = cassetteImageWidth*scale
        spindleY = 0 as CGFloat
        spindleX = (cassetteWidth - spindleWidth)/2
    }

    let spindle = UIImageView(image: UIImage(named: "spindle"))
    cassette.addSubview(spindle)

    spindle.frame.origin.x = spindleX + (posX*scale)
    spindle.frame.origin.y = spindleY + (posY*scale)
    spindle.frame.size.width = spindle.image!.size.width*scale //299.0
    spindle.frame.size.height = spindle.image!.size.height*scale //299.0

    return spindle
}
ev350
  • 429
  • 3
  • 16