Here you can find a good class for that.
//
// ResizeableImageView.swift
//
// Created by Amir Daliri on 8.08.2023.
// Copyright © 2023 Amir Daliri. All rights reserved.
//
import UIKit
/// Resizeable Image View that takes a max height and max width
/// Will resize the imageView to best fit for the aspect ratio of the image,
/// With the given space provided.
public class ResizeableImageView: UIImageView {
private var widthConstraint: NSLayoutConstraint?
private var heightConstraint: NSLayoutConstraint?
// MARK: - INITIALIZERS:
public override init(image: UIImage?) {
super.init(image: image)
}
/// Given the max width and height, resizes the imageView to fit the image.
/// - IMPORTANT: This subclass adds a height and width constraint.
/// - Parameters:
/// - image: (UIImage?) The image to add to the imageView.
/// - maxWidth: (CGFloat) The max width you would like the imageView to grow to.
/// - maxHeight: (CGFloat) The max height you would like the imageView to grow to.
convenience init(image: UIImage?, maxWidth: CGFloat, maxHeight: CGFloat) {
self.init(image: image)
widthConstraint = constrain(width: maxWidth)
heightConstraint = constrain(height: maxHeight)
}
/// Required initializer for creating a view from the nib or storyboard.
///
/// This initializer is essential when you want to instantiate the custom view
/// from a storyboard or nib file. Failing to implement this initializer will
/// result in a runtime crash when the system tries to load the view.
///
/// - Parameter aDecoder: An abstract class that serves as the basis for objects that enable
/// archiving and distribution of other objects.
/// - Returns: An optional instance of the class. It returns nil if the object can't be
/// initialized.
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
/// Additional setup after the view is loaded.
///
/// Use this function to make any additional configurations or settings after the view has been
/// instantiated, especially if it has been loaded from a storyboard or nib file. This can
/// include setting default values, adding subviews, setting constraints, or any other kind
/// of initial setup.
private func setup() {
// Any additional setup after loading the view, typically from a nib or storyboard.
}
// MARK: - VARIABLES:
/// The maximum width that you want this imageView to grow to.
private var maxWidth: CGFloat {
get { widthConstraint?.constant ?? 0 }
set { widthConstraint?.constant = newValue }
}
/// The maximum height that you want this imageView to grow to.
private var maxHeight: CGFloat {
get { heightConstraint?.constant ?? 0 }
set { heightConstraint?.constant = newValue }
}
private var maxAspectRatio: CGFloat { maxWidth / maxHeight }
override public var intrinsicContentSize: CGSize {
guard let classImage = self.image else { return frame.size }
let imageWidth = classImage.size.width
let imageHeight = classImage.size.height
let aspectRatio = imageWidth / imageHeight
// Width is greater than height, return max width image and new height.
if imageWidth > imageHeight {
let newHeight = maxWidth/aspectRatio
self.widthConstraint?.constant = maxWidth
self.heightConstraint?.constant = newHeight
return CGSize(width: maxWidth, height: newHeight)
}
// Height is greater than width, return max height and new width.
if imageHeight > imageWidth {
// If the aspect ratio is larger than our max ratio, then using max width
// will be hit before max height.
if aspectRatio > maxAspectRatio {
let newHeight = maxWidth/aspectRatio
self.widthConstraint?.constant = maxWidth
self.heightConstraint?.constant = newHeight
return CGSize(width: maxWidth, height: newHeight)
}
let newWidth = maxHeight * aspectRatio
self.widthConstraint?.constant = newWidth
self.heightConstraint?.constant = maxHeight
return CGSize(width: newWidth, height: maxHeight)
}
// Square image, return the lesser of max width and height.
let squareMinimumValue = min(maxWidth, maxHeight)
self.widthConstraint?.constant = squareMinimumValue
self.heightConstraint?.constant = squareMinimumValue
return CGSize(width: squareMinimumValue, height: squareMinimumValue)
}
}
// MARK: - Helper
private extension ResizeableImageView {
/// Creates a width constraint for the view and activates it.
///
/// - Parameter width: The width for the constraint.
/// - Returns: The created width constraint.
private func constrain(width: CGFloat) -> NSLayoutConstraint {
let constraint = self.widthAnchor.constraint(equalToConstant: width)
constraint.isActive = true
return constraint
}
/// Creates a height constraint for the view and activates it.
///
/// - Parameter height: The height for the constraint.
/// - Returns: The created height constraint.
private func constrain(height: CGFloat) -> NSLayoutConstraint {
let constraint = self.heightAnchor.constraint(equalToConstant: height)
constraint.isActive = true
return constraint
}
}
// MARK: - Update
extension ResizeableImageView {
/// Updates the imageView with a new image and dimensions, resizing it accordingly.
///
/// - Parameters:
/// - image: (UIImage?) The new image to add to the imageView.
/// - maxWidth: (CGFloat) The new max width you'd like the imageView to have.
/// - maxHeight: (CGFloat) The new max height you'd like the imageView to have.
public func updateImageView(with image: UIImage?, maxWidth: CGFloat, maxHeight: CGFloat) {
self.image = image
// Remove existing constraints if any
if let widthC = widthConstraint {
self.removeConstraint(widthC)
}
if let heightC = heightConstraint {
self.removeConstraint(heightC)
}
// Apply new constraints
widthConstraint = constrain(width: maxWidth)
heightConstraint = constrain(height: maxHeight)
// Request layout update
self.layoutIfNeeded()
}
}
// MARK: - Public API:
extension ResizeableImageView {
/// Sets the image view's image from a given URL string and resizes it based on provided max dimensions.
///
/// - Parameters:
/// - urlString: The string representation of the image URL. If nil or invalid, the function will call the completion handler with nil.
/// - placeholder: An optional placeholder image to display while the image is being fetched.
/// - maxWidth: The max width you would like the imageView to grow to.
/// - maxHeight: The max height you would like the imageView to grow to.
/// - completion: An optional completion handler that gets called when the image fetch completes.
func setImage(from urlString: String?, placeholder: UIImage? = nil, maxWidth: CGFloat, maxHeight: CGFloat, completion: ((UIImage?) -> Void)? = nil) {
// Remove existing constraints if any
if let widthC = widthConstraint {
self.removeConstraint(widthC)
}
if let heightC = heightConstraint {
self.removeConstraint(heightC)
}
// Apply new constraints
widthConstraint = constrain(width: maxWidth)
heightConstraint = constrain(height: maxHeight)
// Check if the provided urlString is non-nil and can be converted into a valid URL.
guard let urlString = urlString, let url = URL(string: urlString) else {
completion?(nil) // Call the completion with nil if URL is invalid.
return
}
// Start an URLSession data task to fetch the image from the URL.
URLSession.shared.dataTask(with: url) { (data, response, error) in
// Ensure there are no errors, that data is non-nil, and the data can be converted into an image.
guard error == nil, let data = data, let downloadedImage = UIImage(data: data) else {
DispatchQueue.main.async {
// If the image fetch fails, set the imageView to display the placeholder image.
self.image = placeholder
completion?(nil) // Call the completion with nil since fetching failed.
}
return
}
DispatchQueue.main.async {
// Set the downloaded image to the imageView.
self.image = downloadedImage
completion?(downloadedImage) // Call the completion with the downloaded image.
}
}.resume() // Start the URLSession data task.
}
}