Search code examples
iosuicollectionviewuicollectionviewcelluilistcontentconfiguration

How to get default system size of symbol image in UIListContentConfiguration?


When using UIListContentConfiguration, if you provide an SF Symbol image, the system decides how to size it based on your dynamic type text size. reservedLayoutSize documentation states: "Symbol images will be centered inside a predefined reservedLayoutSize that is scaled with the content size category." Also note with Mac Catalyst the image size is determined by your setting in System Preferences > General > Sidebar icon size.

In my app I want to create a sidebar with cells like the Photos app - an album cell has a photo for the image rather than a symbol. To do this, I need to know what size to make the image to ensure it's the same size as symbols in other cells, since the size of the cell's image view is determined by the size of the image you provide. "Non-symbol images will use a reservedLayoutSize equal to the actual size of the displayed image."

How do you get the predefined system reserved layout size?

UIListContentConfiguration.ImageProperties.standardDimension sounded promising but this is just a constant that informs the system to use its default size, rather than exposing that size - its value is -1.7976931348623157e+308.

contentConfiguration.imageProperties.reservedLayoutSize is CGSize.zero by default which means the default size is used. So how do we get that size?


Solution

  • Normal usage as per docs:

    You cannot get the dimension, but you can use it to set the reservedLayoutSize height OR width. You then have to size your image based on the current text size. This takes into account the text size options and accessibility options. This is the normal way to use it:

    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    
    let font = UIFont.preferredFont(forTextStyle: .body)
    let currentFontHeight = font.lineHeight
    
    let originalImage = UIImage(named: "ImageNameInAsset")!
    let scaleFactor = currentFontHeight / originalImage.size.height
    
    let scaledImage = originalImage.resized(scaleFactor: scaleFactor)
    
    var config = cell.defaultContentConfiguration()
    
    config.image = scaledImage
    config.text = "Text"
    
    config.imageProperties.reservedLayoutSize.width = UIListContentConfiguration.ImageProperties.standardDimension
    config.imageProperties.reservedLayoutSize.height = UIListContentConfiguration.ImageProperties.standardDimension
    config.imageProperties.cornerRadius = 3
    
    cell.contentConfiguration = config
    
    return cell
    

    Retrieving the actual width and height:

    If you want to retrieve the actual width and height of an image and work from there, you have to work-around the strange behaviour of the UIListContentConfiguration.ImageProperties.standardDimension, I created this class to layout a custom icon image in the same way as an SF Symbol is layed out.

    The trick is to set a "clear" SF Symbol as the image in the contentConfiguration and then setting a custom image. The custom image then conforms to the constraints provided by the imageLayoutGuide inside the UIListContentView to match the layout. It centers the custom image.

    Maybe it's also possible to create an own contentConfiguration class, but this works for me now. I hope it's elegant enough to work around your problem. It even scales with different text sizes or accessibility options.

    Here's how to use it:

    // In the tableview setup:
    tableView.register(CustomImageViewCell.self, forCellReuseIdentifier: "customImageViewCell")
    
    // When dequeuing a cell
    let cell = tableView.dequeueReusableCell(withIdentifier: "customImageViewCell", for: indexPath) as! CustomImageViewCell
    
    var config = cell.defaultContentConfiguration()
    
    let imageConfig = UIImage.SymbolConfiguration.init(hierarchicalColor: .clear)
    let image = UIImage(systemName: "calendar")?.withConfiguration(imageConfig)
    config.image = image
    
    config.text = "Label"
    
    cell.contentConfiguration = config
                        
    // This image will get overlayed in the custom class later on
    cell.setCustomImage(UIImage(named: "YourAssetName"))
    
    return cell
    

    Here's the custom class:

    class CustomImageViewCell: UITableViewCell {
        private let customImageView: UIImageView = {
            let imageView = UIImageView()
            imageView.translatesAutoresizingMaskIntoConstraints = false
            imageView.layer.cornerRadius = 3
            imageView.clipsToBounds = true
            return imageView
        }()
        
        private var didSetupConstraints = false
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)        
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        override func willMove(toSuperview newSuperview: UIView?) {
            super.willMove(toSuperview: newSuperview)
            
            if !didSetupConstraints {
                if let contentView = contentView as? UIListContentView,
                   let imageLayoutGuide = contentView.imageLayoutGuide {
                    
                    contentView.addSubview(customImageView)
                    
                    NSLayoutConstraint.activate([
                        customImageView.topAnchor.constraint(equalTo: imageLayoutGuide.topAnchor),
                        customImageView.bottomAnchor.constraint(equalTo: imageLayoutGuide.bottomAnchor),
                        customImageView.centerXAnchor.constraint(equalTo: imageLayoutGuide.centerXAnchor),
                        customImageView.widthAnchor.constraint(equalTo: imageLayoutGuide.heightAnchor)
                    ])
                    
                    didSetupConstraints = true
                }
            }
        }
        
        func setCustomImage(_ image: UIImage?) {
            customImageView.image = image
        }
    }