I have a UIImageView
and the user can set the image so the width/height
varies. I would like add a "x"- button
to the upper right corner but NOT from the UIImageView
but from the actual image
.
Here is how it looks right now. The whole picture is the UIImageView
, on the left you can see the image
and on the right top corner you can see the button
.
This is how I constrain it at the moment:
theStackView.addArrangedSubview(self.imageView)
imageContainerView.addSubview(wishImageView)
imageContainerView.addSubview(deleteImageButton)
imageContainerView.heightAnchor.constraint(equalToConstant: 60).isActive = true
imageContainerView.isHidden = true
wishImageView.leadingAnchor.constraint(equalTo: imageContainerView.leadingAnchor, constant: 20).isActive = true
wishImageView.topAnchor.constraint(equalTo: imageContainerView.topAnchor, constant: 3).isActive = true
wishImageView.bottomAnchor.constraint(equalTo: imageContainerView.bottomAnchor, constant: 3).isActive = true
wishImageView.trailingAnchor.constraint(equalTo: imageContainerView.trailingAnchor, constant: -20).isActive = true
deleteImageButton.widthAnchor.constraint(equalToConstant: 10).isActive = true
deleteImageButton.heightAnchor.constraint(equalToConstant: 10).isActive = true
deleteImageButton.topAnchor.constraint(equalTo: wishImageView.topAnchor, constant: 5).isActive = true
deleteImageButton.trailingAnchor.constraint(equalTo: wishImageView.trailingAnchor, constant: -5).isActive = true
This is obviously wrong but is there a way to constrain it to the actual image
?
I put together a sample project that the repo is here.
Basically you need to do a few things:
UIImageView
frame size along with the frame of the UIMage
being displayed in scaledAspectFit
.For the first, you need to remember that the frames may not really be set until viewDidLayoutSubviews
. I create a UIImageView
extension that easily computes where the UIImage
frame really is. (It's old but working code. I'm sure it can be improved.)
extension UIImageView {
public var scaleFactor:CGFloat {
guard let image = self.image, self.frame != CGRect.zero else {
return 0.0
}
let frame = self.frame
let extent = image.size
let heightFactor = frame.height/extent.height
let widthFactor = frame.width/extent.width
if extent.height > frame.height || extent.width > frame.width {
if heightFactor < 1 && widthFactor < 1 {
if heightFactor > widthFactor {
return widthFactor
} else {
return heightFactor
}
} else if extent.height > frame.height {
return heightFactor
} else {
return widthFactor
}
} else if extent.height < frame.height && extent.width < frame.width {
if heightFactor < widthFactor {
return heightFactor
} else {
return widthFactor
}
} else {
return 1
}
}
public var imageSize:CGSize {
if self.image == nil {
return CGSize.zero
} else {
return CGSize(width: (self.image?.size.width)!, height: (self.image?.size.height)!)
}
}
public var scaledSize:CGSize {
guard let image = self.image, self.frame != CGRect.zero else {
return CGSize.zero
}
let factor = self.scaleFactor
return CGSize(width: image.size.width * factor, height: image.size.height * factor)
}
}
For the second bullet you need to create two variables of type NSConstraint
. I adapted my answer from two years ago for this:
var btnTop:NSLayoutConstraint!
var btnTrailing:NSLayoutConstraint!
And in `viewDidLoad:
button.heightAnchor.constraint(equalToConstant: 20).isActive = true
button.widthAnchor.constraint(equalToConstant: 20).isActive = true
btnTop = button.topAnchor.constraint(equalTo: imageView.topAnchor, constant: 10)
btnTop.isActive = true
btnTrailing = button.trailingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: -10)
btnTrailing.isActive = true
Note that you need to code two lines for each constraint! I never figured out why, but if you try to add the isActive
property with the actual constraint the compiler doesn't know the correct type of the variable.
Now, you tie all this together in viewDidLayoutSubviews
:
let scaledSize = imageView.scaledSize
var imageFrame = CGRect(origin: CGPoint.zero, size: scaledSize)
if scaledSize.width == imageView.frame.width {
// image fills view along width, calculate Y constant
imageFrame.origin.y = (imageView.frame.height - scaledSize.height) / 2
} else {
// image fills view along height, calculate X constant
imageFrame.origin.x = (imageView.frame.width - scaledSize.width) / 2
}
//btnTop.constant = imageFrame.width - 30
btnTop.constant = imageFrame.origin.y + 10
btnTrailing.constant = ((imageView.frame.width - imageFrame.width - imageFrame.origin.x) * -1) - 10
Placing the button in the top left is much simpler - it took me a good 20 minutes to get the correct calculation to make it top right instead!
In my test project I encapsulated this code in repositionCloseButton()
, which would be called anytime the app displays a new image. This should work in both portrait and landscape orientation, and both portrait and landscape images - positioning a 20x20 close button 10 points away from the top right of an image.