-
-
Save algal/03e661630a6869c883d9d915d5ecccbc to your computer and use it in GitHub Desktop.
import UIKit | |
// known-good: Xcode 8.2.1 | |
/** | |
UIImageView subclass which works with Auto Layout to try | |
to maintain the same aspect ratio as the image it displays. | |
This is unlike the usual behavior of UIImageView, where the | |
scaleAspectFit content mode only affects what the view displays | |
and not the size it prefers, and so it does not play | |
well with AL constraints. In particular, UIImageView.intrinsicContentSize | |
always returns each of the intrinsic size dimensions of the image | |
itself, not a size that adjusts to reflect constraints on the | |
view. So if you constrain the width of a UIImageView, for example, | |
the view's intrinsic content size still declares a preferred | |
height based on the image's intrinsic height, rather than the | |
displayed height produced by the scaleAspectFit content mode. | |
In contrast, this subclass has a few notable properties: | |
- If you externally constraint one dimension, its internal constraints | |
will then adjust the other dimension so it holds the image's aspect | |
ratio. | |
- Uses a low layout priority to do this. So if you externally | |
require it to have an incorrect aspect ratio, you do not get conflicts. | |
- Still uses the scaleAspectFit content mode internally, so if a | |
client requires an incorrect aspect, you still get scaleAspectFit | |
behavior to determining what is displayed within whatever | |
dimensionsare finally used. | |
- It is a subclass of UIImageView and supports all of UIImageView's | |
initializers, so it is a drop-in substitute. | |
*/ | |
public class ScaleAspectFitImageView : UIImageView | |
{ | |
/// constraint to maintain same aspect ratio as the image | |
private var aspectRatioConstraint:NSLayoutConstraint? = nil | |
required public init?(coder aDecoder: NSCoder) | |
{ | |
super.init(coder:aDecoder) | |
self.setup() | |
} | |
public override init(frame:CGRect) | |
{ | |
super.init(frame:frame) | |
self.setup() | |
} | |
public override init(image: UIImage!) | |
{ | |
super.init(image:image) | |
self.setup() | |
} | |
public override init(image: UIImage!, highlightedImage: UIImage?) | |
{ | |
super.init(image:image,highlightedImage:highlightedImage) | |
self.setup() | |
} | |
override public var image: UIImage? { | |
didSet { | |
self.updateAspectRatioConstraint() | |
} | |
} | |
private func setup() | |
{ | |
self.contentMode = .scaleAspectFit | |
self.updateAspectRatioConstraint() | |
} | |
/// Removes any pre-existing aspect ratio constraint, and adds a new one based on the current image | |
private func updateAspectRatioConstraint() | |
{ | |
// remove any existing aspect ratio constraint | |
if let c = self.aspectRatioConstraint { | |
self.removeConstraint(c) | |
} | |
self.aspectRatioConstraint = nil | |
if let imageSize = image?.size, imageSize.height != 0 | |
{ | |
let aspectRatio = imageSize.width / imageSize.height | |
let c = NSLayoutConstraint(item: self, attribute: .width, | |
relatedBy: .equal, | |
toItem: self, attribute: .height, | |
multiplier: aspectRatio, constant: 0) | |
// a priority above fitting size level and below low | |
c.priority = (UILayoutPriorityDefaultLow + UILayoutPriorityFittingSizeLevel) / 2.0 | |
self.addConstraint(c) | |
self.aspectRatioConstraint = c | |
} | |
} | |
} |
@ratulSharker your extension-based solution does avoid subclassing but in doing so it loses access to override the didSet of the UIImageView’s image property, which is done here to reset the aspect ratio anytime the view’s image is changed. If you are loading static image assets you are probably loading the UIImageView’s image property at init (because why not), and then immediately, explicitly calling addAspectRatioConstraint(...). However if you need to download remote image data or update the value of the view’s image property multiple times, you’ll have to remember to call at least the first of your two methods manually every time the image property is updated. For a single engineer that is easy to stay on top of. On a team of any size >2, I’d be concerned about adding an extra step to something as common as initializing an ImageView, a step which must take place manually or let the UI immediately break. There seems like a lot of possibility in that environment for some future coder down the line to not get the memo and either get confused by unexpected “extra” UIImageView behavior, or spend time to recreate the same solution you already reached. I guess the question is whether or not avoiding the hassle of subclassing is worth adding to a shared codebase another “remember this or everything breaks” tripwire. The answer, I’m sure, is highly project-specific.
I suggest the following changes to the logic:
- also remove constraints if a nil image is given
- make sure
image.size.height > 0
to prevent division by zero
func addAspectRatioConstraint(image: UIImage?) {
self.removeAspectRatioConstraint()
if let image = image, image.size.height > 0 {
let aspectRatio = image.size.width / image.size.height
let constraint = NSLayoutConstraint(item: self, attribute: .width,
relatedBy: .equal,
toItem: self, attribute: .height,
multiplier: aspectRatio, constant: 0.0)
addConstraint(constraint)
}
}
This solution is robust, developer don't have to take overhead of nothing, but the most painful part seems the subclassing. I use several subclass of
UIImageView
in my project. Which are provided bycocoapod
. So incorporating this seems a daunting task.A simple bare extension, which is doing the same task would be appreciable