Eduardo
Eduardo

Reputation: 25

Remove extra space from UIImageView

In my UIView I have a method to load an image from a base64 (which comes from an API) and configure it in a UIImageView. As you can imagine, there are different sizes of images returned from the API. I'm using Auto Layout with native Constraints (programmatically) and the UIImageView contentMode = .scaleAspectFit. Some images have extra space in the UIImageView. Here are some screenshots (the background is cyan so I can see the extra spaces I want to remove).

My constraints:

assetImageView.topAnchor.constraint(equalTo: typeOrderLabel.bottomAnchor, constant: 14.7),
assetImageView.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),
 
lazy var assetImageView: UIImageView = {
     let imageView = UIImageView()
     imageView.translatesAutoresizingMaskIntoConstraints = false
     imageView.contentMode = .scaleAspectFit
     imageView.backgroundColor = .cyan
     return imageView
}()

enter image description here

enter image description here enter image description here

Upvotes: 0

Views: 1069

Answers (1)

DonMag
DonMag

Reputation: 77690

If you want your image view to have a max size of 100 x 30, you need to create Width and Height constraints in your cell class:

var cWidth: NSLayoutConstraint!
var cHeight: NSLayoutConstraint!

then initialize them in your cell's init:

// initialize image view Width and Height constraints
cWidth = assetImageView.widthAnchor.constraint(equalToConstant: 0)
cHeight = assetImageView.heightAnchor.constraint(equalToConstant: 0)

NSLayoutConstraint.activate([
        
    // label constraints...

    assetImageView.topAnchor.constraint(equalTo: typeOrderLabel.bottomAnchor, constant: 15.0),
    assetImageView.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),

    // activate image view Width and Height constraints
    cWidth,
    cHeight,
        
])

Then, when you set your image in cellForRowAt, calculate the size based on the size of the image and update the Width and Height constraint constants:

assetImageView.image = img
    
if img.size.height <= maxHeight && img.size.width <= maxWidth {
        
    // image height and width are smaller than max Height and Width
    //  so use actual size
    cWidth.constant = img.size.width
    cHeight.constant = img.size.height
        
} else {
        
    // use standard Aspect Fit calculation
    var f = maxWidth / img.size.width
    var w = img.size.width * f
    var h = img.size.height * f
    if h > maxHeight {
        f = maxHeight / img.size.height
        h = img.size.height * f
        w = img.size.width * f
    }
        
    // update constraints
    cWidth.constant = w
    cHeight.constant = h
        
}

Here is a complete example:

struct EduardoStruct {
    var typeOrder: String = ""
    var other: String = ""
    var asset: String = ""
}

class EduardoCell: UITableViewCell {
    
    let typeOrderLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.font = .systemFont(ofSize: 14, weight: .bold)
        v.textColor = .systemGreen
        // if we want to see the label frame
        //v.backgroundColor = .yellow
        return v
    }()
    let assetImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleToFill
        imageView.backgroundColor = .cyan
        return imageView
    }()
    let otherLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.font = .systemFont(ofSize: 13, weight: .thin)
        v.textColor = .darkGray
        // if we want to see the label frame
        //v.backgroundColor = .yellow
        return v
    }()
    
    var cWidth: NSLayoutConstraint!
    var cHeight: NSLayoutConstraint!
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        // add the subviews
        contentView.addSubview(typeOrderLabel)
        contentView.addSubview(assetImageView)
        contentView.addSubview(otherLabel)
        
        // initialize image view Width and Height constraints
        cWidth = assetImageView.widthAnchor.constraint(equalToConstant: 0)
        cHeight = assetImageView.heightAnchor.constraint(equalToConstant: 0)
        
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            
            typeOrderLabel.topAnchor.constraint(equalTo: g.topAnchor),
            typeOrderLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            typeOrderLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            
            assetImageView.topAnchor.constraint(equalTo: typeOrderLabel.bottomAnchor, constant: 15.0),
            assetImageView.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),
            
            otherLabel.topAnchor.constraint(equalTo: assetImageView.bottomAnchor, constant: 10.0),
            otherLabel.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),
            otherLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            otherLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            
            // activate image view Width and Height constraints
            cWidth,
            cHeight,
            
        ])
    }
    
    func fillData(_ st: EduardoStruct) {
        typeOrderLabel.text = st.typeOrder
        otherLabel.text = st.other
        
        // image max Width and Height
        let maxWidth: CGFloat = 100
        let maxHeight: CGFloat = 30
        
        guard let img = UIImage(named: st.asset) else {
            // if we can't load the image, set the image view
            //  to maxWidth x maxHeight
            cWidth.constant = maxWidth
            cHeight.constant = maxHeight
            assetImageView.image = nil
            return
        }
        
        assetImageView.image = img
        
        if img.size.height <= maxHeight && img.size.width <= maxWidth {
            
            // image height and width are smaller than max Height and Width
            //  so use actual size
            cWidth.constant = img.size.width
            cHeight.constant = img.size.height
            
        } else {
            
            // use standard Aspect Fit calculation
            var f = maxWidth / img.size.width
            var w = img.size.width * f
            var h = img.size.height * f
            if h > maxHeight {
                f = maxHeight / img.size.height
                h = img.size.height * f
                w = img.size.width * f
            }
            
            // update constraints
            cWidth.constant = w
            cHeight.constant = h
            
        }
    }
}

class EduardoExampleTableViewController: UITableViewController {
    
    var myData: [EduardoStruct] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let imageNames: [String] = [
            "img100x30", "img80x20", "img120x50", "img80x80", "img150x30", "img120x120",
        ]
        
        for (i, str) in imageNames.enumerated() {
            var st: EduardoStruct = EduardoStruct()
            st.typeOrder = "TYPE \(i)"
            st.other = "OTHER \(i)"
            st.asset = str
            myData.append(st)
        }
        
        tableView.register(EduardoCell.self, forCellReuseIdentifier: "cell")
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! EduardoCell
        cell.fillData(myData[indexPath.row])
        return cell
    }
    
}

With that code, using these "asset images":

enter image description here enter image description here enter image description here enter image description here enter image description here enter image description here

We get this result:

enter image description here

As you see, the images are all "aspect fit" scaled to a max size of 100 x 30, with no "extra space."


Edit

After clarification from the OP, the images will be 160 x 55 pixels, and the goal is to:

  • extract only the "useful" part of the image (the non-alpha portion)
  • display that portion at a max size of 100 x 30 while maintaining aspect ratio.

So, with a new set of images, each being 160 x 55 with transparent "backgrounds" (download these images to see):

enter image description here enter image description here enter image description here enter image description here enter image description here

We can use this UIImage extension to "clip out" the non-transparent portion:

extension UIImage {
    
    func clipAlpha(_ tolerancePercent: Double) -> UIImage {
    
        guard let imageRef = self.cgImage else {
            return self
        }
        
        let columns = imageRef.width
        let rows = imageRef.height
        
        let bytesPerPixel = 4
        let bytesPerRow = bytesPerPixel * columns
        let bitmapByteCount = bytesPerRow * rows
        
        // allocate memory
        let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmapByteCount)
        // initialize buffer to Zeroes
        rawData.initialize(repeating: 0, count: bitmapByteCount)
        defer {
            rawData.deallocate()
        }
        
        guard let colorSpace = CGColorSpace(name: CGColorSpace.genericRGBLinear) else {
            return self
        }
        
        guard let context = CGContext(
            data: rawData,
            width: columns,
            height: rows,
            bitsPerComponent: 8,
            bytesPerRow: bytesPerRow,
            space: colorSpace,
            bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
            | CGBitmapInfo.byteOrder32Big.rawValue
        ) else {
            return self
        }

        var l: Int = -1
        var r: Int = -1
        var t: Int = -1
        var b: Int = -1
        
        // for debugging...
        //  used to count the number of iterations needed
        //  to find the non-alpha bounding box
        //var c: Int = 0
        
        var colOffset: Int = 0
        var rowOffset: Int = 0
        
        // Draw source image on created context.
        let rc = CGRect(x: 0, y: 0, width: columns, height: rows)
        context.draw(imageRef, in: rc)
        
        let tolerance: Int = Int(255.0 * tolerancePercent)

        // find the left-most non-alpha pixel
        for col in 0..<columns {
            colOffset = col * bytesPerPixel
            for row in 0..<rows {
                // debugging
                //c += 1
                rowOffset = row * bytesPerRow
                // Get alpha of current pixel
                let alpha = CGFloat(rawData[colOffset + rowOffset + 3])
                if alpha > CGFloat(tolerance) {
                    l = col
                    break
                }
            }
            if l > -1 {
                break
            }
        }
        
        // find the right-most non-alpha pixel
        for col in stride(from: columns - 1, to: l, by: -1) {
            colOffset = col * bytesPerPixel
            for row in 0..<rows {
                // debugging
                //c += 1
                rowOffset = row * bytesPerRow
                // Get alpha of current pixel
                let alpha = CGFloat(rawData[colOffset + rowOffset + 3])
                if alpha > CGFloat(tolerance) {
                    r = col
                    break
                }
            }
            if r > -1 {
                break
            }
        }
        
        // find the top-most non-alpha pixel
        for row in 0..<rows {
            rowOffset = row * bytesPerRow
            for col in l..<r {
                // debugging
                //c += 1
                colOffset = col * bytesPerPixel
                // Get alpha of current pixel
                let alpha = CGFloat(rawData[colOffset + rowOffset + 3])
                if alpha > CGFloat(tolerance) {
                    t = row
                    break
                }
            }
            if t > -1 {
                break
            }
        }
        
        // find the bottom-most non-alpha pixel
        for row in stride(from: rows - 1, to: t, by: -1) {
            rowOffset = row * bytesPerRow
            for col in l..<r {
                // debugging
                //c += 1
                colOffset = col * bytesPerPixel
                // Get alpha of current pixel
                let alpha = CGFloat(rawData[colOffset + rowOffset + 3])
                if alpha > CGFloat(tolerance) {
                    b = row
                    break
                }
            }
            if b > -1 {
                break
            }
        }
        
        // debugging
        //print(c, l, t, r, b)

        // define a rectangle for the non-alpha pixels
        let targetRect = CGRect(x: l, y: t, width: r - l + 1, height: b - t + 1)
        let size = targetRect.size
        let renderer = UIGraphicsImageRenderer(size: size)
        
        let renderedImage = renderer.image { _ in
            // render the non-alpha portion
            self.draw(at: CGPoint(x: -targetRect.origin.x, y: -targetRect.origin.y))
        }
        
        return renderedImage

    }

}

Then, instead of using the image directly, we'll first "clip" it and then apply the previous aspect-sizing code.

Here is the complete example (only slightly modified from the original posting):

struct EduardoStruct {
    var typeOrder: String = ""
    var other: String = ""
    var asset: String = ""
}

class EduardoCell: UITableViewCell {
    
    let typeOrderLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.font = .systemFont(ofSize: 14, weight: .bold)
        v.textColor = .systemGreen
        // if we want to see the label frame
        //v.backgroundColor = .yellow
        return v
    }()
    let assetImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleToFill
        imageView.backgroundColor = .cyan
        return imageView
    }()
    let otherLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.font = .systemFont(ofSize: 13, weight: .thin)
        v.textColor = .darkGray
        // if we want to see the label frame
        //v.backgroundColor = .yellow
        return v
    }()
    
    var cWidth: NSLayoutConstraint!
    var cHeight: NSLayoutConstraint!
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        // add the subviews
        contentView.addSubview(typeOrderLabel)
        contentView.addSubview(assetImageView)
        contentView.addSubview(otherLabel)
        
        // initialize image view Width and Height constraints
        cWidth = assetImageView.widthAnchor.constraint(equalToConstant: 0)
        cHeight = assetImageView.heightAnchor.constraint(equalToConstant: 0)
        
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            
            typeOrderLabel.topAnchor.constraint(equalTo: g.topAnchor),
            typeOrderLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            typeOrderLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            
            assetImageView.topAnchor.constraint(equalTo: typeOrderLabel.bottomAnchor, constant: 15.0),
            assetImageView.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),
            
            otherLabel.topAnchor.constraint(equalTo: assetImageView.bottomAnchor, constant: 10.0),
            otherLabel.leadingAnchor.constraint(equalTo: typeOrderLabel.leadingAnchor),
            otherLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            otherLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            
            // activate image view Width and Height constraints
            cWidth,
            cHeight,
            
        ])
    }
    
    func fillData(_ st: EduardoStruct, showBKG: Bool) {
        typeOrderLabel.text = st.typeOrder
        otherLabel.text = st.other
        
        // image max Width and Height
        let maxWidth: CGFloat = 100
        let maxHeight: CGFloat = 30
        
        assetImageView.backgroundColor = showBKG ? .cyan : .clear
        
        guard let origImg = UIImage(named: st.asset) else {
            // if we can't load the image, set the image view
            //  to maxWidth x maxHeight
            cWidth.constant = maxWidth
            cHeight.constant = maxHeight
            assetImageView.image = nil
            return
        }
        
        let img = origImg.clipAlpha(0.0)
        
        assetImageView.image = img
        
        if img.size.height <= maxHeight && img.size.width <= maxWidth {
            
            // image height and width are smaller than max Height and Width
            //  so use actual size
            cWidth.constant = img.size.width
            cHeight.constant = img.size.height
            
        } else {
            
            // use standard Aspect Fit calculation
            var f = maxWidth / img.size.width
            var w = img.size.width * f
            var h = img.size.height * f
            if h > maxHeight {
                f = maxHeight / img.size.height
                h = img.size.height * f
                w = img.size.width * f
            }
            
            // update constraints
            cWidth.constant = w
            cHeight.constant = h
            
        }
    }
}

class TestSizingCellTableViewController: UITableViewController {
    
    var myData: [EduardoStruct] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let imageNames: [String] = [
            "img1", "img2", "img3", "img4", "img5",
        ]
        
        for (i, str) in imageNames.enumerated() {
            var st: EduardoStruct = EduardoStruct()
            st.typeOrder = "TYPE \(i)"
            st.other = "OTHER \(i)"
            st.asset = str
            myData.append(st)
        }
        
        tableView.register(EduardoCell.self, forCellReuseIdentifier: "cell")
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! EduardoCell
        
        // set cell's imageView background to cyan or clear
        let showBKG = false
        
        cell.fillData(myData[indexPath.row], showBKG: showBKG)
        
        return cell
    }
    
}

And the output - first with the imageView background set to .cyan (so we can see the actual frames):

enter image description here

and with the imageView background set to .clear:

enter image description here

Upvotes: 1

Related Questions