There are a couple ways to do this...
If we use this as the original image:

it has a transparent bounding box... so it looks like this in an image editing program:

If you are only dealing with how it looks at run-time, you can use a UIImageView
with Content Mode: Scale to Fill
and then adjust the width of the image view.
If you want to actually scale the UIImage
, this simple extension will do the job:
extension UIImage {
func squeeze(w: CGFloat, h: CGFloat) -> UIImage {
let newWidth: CGFloat = self.width * w
let newHeight: CGFloat = self.height * h
let sz: CGSize = CGSize(width: newWidth, height: newHeight)
let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(size: sz)
let img = renderer.image { _ in
self.draw(in: CGRect(origin: .zero, size: sz))
}
return img
}
}

(the image views have a green border, so we can see the frames).
Here is some example code you can play with:
class ViewController: UIViewController, UITextFieldDelegate {
var origIMG: UIImage!
let imgViewA = UIImageView()
let imgViewB = UIImageView()
let imgViewC = UIImageView()
let st = UIStackView()
let statusLabel = UILabel()
let slider = UISlider()
var bWidth: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
guard let img = UIImage(named: "car1"),
let bkgImg = UIImage(named: "carsBKG")
else { return }
print(img.size)
origIMG = img
// add a background image view
let vBKG = UIImageView(image: bkgImg)
st.axis = .vertical
st.alignment = .center
st.spacing = 2
[imgViewA, imgViewB, imgViewC].forEach { v in
v.layer.borderWidth = 2
v.layer.borderColor = UIColor.green.cgColor
v.backgroundColor = .clear
v.image = origIMG
}
// for A and B, we let the image view frame scale the image
imgViewA.contentMode = .scaleToFill
imgViewB.contentMode = .scaleToFill
// for C we keep the image aspect ratio, because
// we're going to modify the UIImage size
imgViewC.contentMode = .scaleAspectFit
var label: UILabel!
label = UILabel()
label.text = "Original"
st.addArrangedSubview(label)
st.addArrangedSubview(imgViewA)
st.setCustomSpacing(20.0, after: imgViewA)
label = UILabel()
label.text = "Modify Image View Frame"
st.addArrangedSubview(label)
st.addArrangedSubview(imgViewB)
st.setCustomSpacing(20.0, after: imgViewB)
label = UILabel()
label.text = "Modify UIImage"
st.addArrangedSubview(label)
st.addArrangedSubview(imgViewC)
st.setCustomSpacing(20.0, after: imgViewC)
statusLabel.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
statusLabel.textAlignment = .center
st.addArrangedSubview(statusLabel)
slider.backgroundColor = .white.withAlphaComponent(0.6)
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
st.addArrangedSubview(slider)
st.setCustomSpacing(20.0, after: slider)
vBKG.translatesAutoresizingMaskIntoConstraints = false
st.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(vBKG)
view.addSubview(st)
// we're going to modify the 2nd image view width
bWidth = imgViewB.widthAnchor.constraint(equalTo: st.widthAnchor)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
st.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
st.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
st.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// background image view same size/position as stack view
// plus 20-points on each side
vBKG.topAnchor.constraint(equalTo: st.topAnchor, constant: -20.0),
vBKG.leadingAnchor.constraint(equalTo: st.leadingAnchor, constant: -20.0),
vBKG.trailingAnchor.constraint(equalTo: st.trailingAnchor, constant: 20.0),
vBKG.bottomAnchor.constraint(equalTo: st.bottomAnchor, constant: 20.0),
// top image (original) full width, proportional height
imgViewA.widthAnchor.constraint(equalTo: st.widthAnchor),
imgViewA.heightAnchor.constraint(equalTo: imgViewA.widthAnchor, multiplier: origIMG.height / origIMG.width),
// 2nd image starts at full width, proportional height
// we'll change the image VIEW width
bWidth,
imgViewB.heightAnchor.constraint(equalTo: imgViewA.widthAnchor, multiplier: origIMG.height / origIMG.width),
// 3rd image full width, proportional height
// we'll change the actual IMAGE
imgViewC.widthAnchor.constraint(equalTo: st.widthAnchor),
imgViewC.heightAnchor.constraint(equalTo: imgViewA.widthAnchor, multiplier: origIMG.height / origIMG.width),
slider.widthAnchor.constraint(equalTo: st.widthAnchor),
])
// start slider at 100%
slider.value = 1.0
statusLabel.text = "100%"
// let's make the labels readable
st.arrangedSubviews.forEach { vSub in
if let v = vSub as? UILabel {
v.textColor = .white
v.backgroundColor = .black.withAlphaComponent(0.4)
v.textAlignment = .center
v.widthAnchor.constraint(equalTo: st.widthAnchor).isActive = true
}
}
}
@objc func sliderChanged(_ sender: UISlider) {
let pct = CGFloat(sender.value)
// let's use a minimum scale of 0.5%
if pct < 0.05 { return }
// update imgViewB's width anchor as a percentage
bWidth.isActive = false
bWidth = imgViewB.widthAnchor.constraint(equalTo: st.widthAnchor, multiplier: pct)
bWidth.isActive = true
// set imgViewC's image to a NEW rendered UIImage
// we're scaling the width, keeping the original height
imgViewC.image = origIMG.squeeze(w: pct, h: 1.0)
statusLabel.text = "\(Int(pct * 100))%"
}
}
extension UIImage {
func squeeze(w: CGFloat, h: CGFloat) -> UIImage {
let newWidth: CGFloat = self.width * w
let newHeight: CGFloat = self.height * h
let sz: CGSize = CGSize(width: newWidth, height: newHeight)
let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(size: sz)
let img = renderer.image { _ in
self.draw(in: CGRect(origin: .zero, size: sz))
}
return img
}
}
When running, it looks like this - dragging the slider sets the Widths to a percentage of the original:


