Reputation: 25
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
}()
Upvotes: 0
Views: 1069
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":
We get this result:
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:
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):
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):
and with the imageView background set to .clear
:
Upvotes: 1