Search code examples
iosuitableviewuiimageviewuikit

Custom UIImageView is not circle inside custom UITableViewCell


I have custom UITableViewCell done in code, but I have trouble with circular UIImageView with SfSymbol inside. Sometimes it is working well as you can see on the screenshot, but sometimes it has some strange shape. If I don't set any SfSymbol shape is good.

I think that I tried anything, which I can, but still it's not working. This is my custom cell code:

import UIKit

class ListsTableViewCell: UITableViewCell {
    
    // MARK: - Properties
    
    let configuration = UIImage.SymbolConfiguration(pointSize: 16, weight: .medium)
    
    var list: List? {
        didSet {
            guard let list = list else { return }
            
            iconView.backgroundColor = list.color
            titleLabel.text = list.name
        }
    }
    
    // MARK: - Layout properties
    
    var iconView: CircularImageView!
    var titleLabel: UILabel!
    
    // MARK: - Initialization
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        iconView = CircularImageView()
        iconView.translatesAutoresizingMaskIntoConstraints = false
        iconView.tintColor = .white
        iconView.contentMode = .center
        
        titleLabel = UILabel()
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        
        contentView.addSubview(iconView)
        contentView.addSubview(titleLabel)
        
        NSLayoutConstraint.activate([
            iconView.heightAnchor.constraint(equalToConstant: 34),
            iconView.widthAnchor.constraint(equalToConstant: 34),
            iconView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
            iconView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
            iconView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            iconView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
            
            titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 12),
            titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

This is table view cell for row function:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let list = listsToDisplay![indexPath.row]
    
    let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "list", for: indexPath) as! ListsTableViewCell
    tableViewCell.iconView.image = UIImage(systemName: list.icon, withConfiguration: tableViewCell.configuration)
    tableViewCell.list = list
    tableViewCell.accessoryType = .disclosureIndicator
   
    return tableViewCell
}

And this is custom subclass of UIImageView

import UIKit

class CircularImageView: UIImageView {
    override func layoutSubviews() {
        self.layer.masksToBounds = true
        self.clipsToBounds = true
        self.layer.cornerRadius = self.frame.size.width / 2
    }
}

Simulator screen


Solution

  • Ah - I've seen this before.

    I don't know why, but using a SF Symbol in a UIImageView will change the image view's height!!!

    You can easily confirm this:

    • add a view controller in Storyboard
    • add a UIImageView
    • set content mode to Center (doesn't really matter)
    • constrain Width: 60 Height: 60 centerX and centerY
    • Check the Size Inspector - it will show 60 x 60

    Now:

    • use the drop-down to set the image to "globe"
    • Check the Size Inspector - it will show 60 x 59

    Now:

    • use the drop-down to set the image to "envelope"
    • Check the Size Inspector - it will show 60 x 56.5

    No apparent constraint conflicts... no apparent reason for this.

    As far as I can tell (unless this changes with iOS 14), we need to embed the image view in a UIView ... constrain it centerX & centerY ... set the background color and corner-radius properties of that view to make it round.


    Edit -- just as an exercise...

    Two horizontal stack views:

    • Alignment: Fill
    • Distribution: FillEqually
    • Spacing: 0
    • each constrained to Width: 300 and Height: 100
    • each filled with 3 UIImageViews

    Gives us 3 100 x 100 square image views. For the second stack, set each image view to a system image, with UIImage.SymbolConfiguration(pointSize: 60, weight: .regular). Should still give us 3 100 x 100 squares.

    Instead, this is the result:

    enter image description here

    Here's the code to produce that:

    class SystemImageTestViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let stack1 = UIStackView()
            stack1.axis = .horizontal
            stack1.alignment = .fill
            stack1.distribution = .fillEqually
            
            let stack2 = UIStackView()
            stack2.axis = .horizontal
            stack2.alignment = .fill
            stack2.distribution = .fillEqually
            
            // add stack views to the view
            [stack1, stack2].forEach {
                $0.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview($0)
            }
            
            let g = view.safeAreaLayoutGuide
            
            NSLayoutConstraint.activate([
                
                stack1.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                stack1.widthAnchor.constraint(equalToConstant: 300.0),
                stack1.heightAnchor.constraint(equalToConstant: 100.0),
                stack1.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                
                stack2.topAnchor.constraint(equalTo: stack1.bottomAnchor, constant: 20.0),
                stack2.widthAnchor.constraint(equalToConstant: 300.0),
                stack2.heightAnchor.constraint(equalToConstant: 100.0),
                stack2.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                
            ])
            
            let colors: [UIColor] = [
                UIColor(red: 0.75, green: 0.00, blue: 0.00, alpha: 1.0),
                UIColor(red: 0.00, green: 0.75, blue: 0.00, alpha: 1.0),
                UIColor(red: 0.00, green: 0.00, blue: 1.00, alpha: 1.0),
            ]
            
            for c in colors {
                let v = UIImageView()
                v.backgroundColor = c
                stack1.addArrangedSubview(v)
            }
            
            let configuration = UIImage.SymbolConfiguration(pointSize: 60, weight: .regular)
            
            let names: [String] = [
                "globe",
                "bandage",
                "envelope",
            ]
    
            for (c, n) in zip(colors, names) {
                let v = UIImageView()
                v.backgroundColor = c
                v.contentMode = .center
                v.tintColor = .white
                let img = UIImage(systemName: n, withConfiguration: configuration)
                v.image = img
                stack2.addArrangedSubview(v)
            }
            
        }
    }
    

    Edit 2 - just for the heck of it...

    Implementing a couple classes for a SystemImageView and CircularSystemImageView:

    class SystemImageView: UIView {
    
        override var contentMode: UIView.ContentMode {
            didSet {
                imageView.contentMode = contentMode
            }
        }
        override var tintColor: UIColor! {
            didSet {
                imageView.tintColor = tintColor
            }
        }
        var image: UIImage = UIImage() {
            didSet {
                imageView.image = image
            }
        }
        let imageView = UIImageView()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            clipsToBounds = true
            addSubview(imageView)
            imageView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                imageView.topAnchor.constraint(equalTo: topAnchor),
                imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
                imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
                imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
            ])
        }
    }
    
    class CircularSystemImageView: SystemImageView {
        override func layoutSubviews() {
            super.layoutSubviews()
            self.layer.masksToBounds = true
            self.clipsToBounds = true
            self.layer.cornerRadius = bounds.size.width * 0.5
        }
    }
    
    class SystemImageTestViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let stack1 = UIStackView()
            stack1.axis = .horizontal
            stack1.alignment = .fill
            stack1.distribution = .fillEqually
            
            let stack2 = UIStackView()
            stack2.axis = .horizontal
            stack2.alignment = .fill
            stack2.distribution = .fillEqually
            
            let stack3 = UIStackView()
            stack3.axis = .horizontal
            stack3.alignment = .fill
            stack3.distribution = .fillEqually
            
            let stack4 = UIStackView()
            stack4.axis = .horizontal
            stack4.alignment = .fill
            stack4.distribution = .fillEqually
            
            // add stack views to the view
            [stack1, stack2, stack3, stack4].forEach {
                $0.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview($0)
            }
            
            let g = view.safeAreaLayoutGuide
            
            NSLayoutConstraint.activate([
                
                stack1.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                stack1.widthAnchor.constraint(equalToConstant: 300.0),
                stack1.heightAnchor.constraint(equalToConstant: 100.0),
                stack1.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                
                stack2.topAnchor.constraint(equalTo: stack1.bottomAnchor, constant: 20.0),
                stack2.widthAnchor.constraint(equalToConstant: 300.0),
                stack2.heightAnchor.constraint(equalToConstant: 100.0),
                stack2.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                
                stack3.topAnchor.constraint(equalTo: stack2.bottomAnchor, constant: 20.0),
                stack3.widthAnchor.constraint(equalToConstant: 300.0),
                stack3.heightAnchor.constraint(equalToConstant: 100.0),
                stack3.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                
                stack4.topAnchor.constraint(equalTo: stack3.bottomAnchor, constant: 20.0),
                stack4.widthAnchor.constraint(equalToConstant: 300.0),
                stack4.heightAnchor.constraint(equalToConstant: 100.0),
                stack4.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                
            ])
            
            let colors: [UIColor] = [
                UIColor(red: 0.75, green: 0.00, blue: 0.00, alpha: 1.0),
                UIColor(red: 0.00, green: 0.75, blue: 0.00, alpha: 1.0),
                UIColor(red: 0.00, green: 0.00, blue: 1.00, alpha: 1.0),
            ]
            
            for c in colors {
                let v = UIImageView()
                v.backgroundColor = c
                stack1.addArrangedSubview(v)
            }
            
            let configuration = UIImage.SymbolConfiguration(pointSize: 60, weight: .regular)
            
            let names: [String] = [
                "globe",
                "bandage",
                "envelope",
            ]
    
            for (c, n) in zip(colors, names) {
                let v = UIImageView()
                v.backgroundColor = c
                v.contentMode = .center
                v.tintColor = .white
                if let img = UIImage(systemName: n, withConfiguration: configuration) {
                    v.image = img
                }
                stack2.addArrangedSubview(v)
            }
            
            for (c, n) in zip(colors, names) {
                let v = SystemImageView()
                v.backgroundColor = c
                v.contentMode = .center
                v.tintColor = .white
                if let img = UIImage(systemName: n, withConfiguration: configuration) {
                    v.image = img
                }
                stack3.addArrangedSubview(v)
            }
            
            for (c, n) in zip(colors, names) {
                let v = CircularSystemImageView()
                v.backgroundColor = c
                v.contentMode = .center
                v.tintColor = .white
                if let img = UIImage(systemName: n, withConfiguration: configuration) {
                    v.image = img
                }
                stack4.addArrangedSubview(v)
            }
            
        }
    }
    

    Results:

    enter image description here

    or, with a little more practical let configuration = UIImage.SymbolConfiguration(pointSize: 40, weight: .regular) to show the sizes are not being determined by the SF Symbol size:

    enter image description here