简体   繁体   English

Swift - CompositionalLayout - 集合视图单元格高度未根据图像高度计算

[英]Swift - CompositionalLayout - collection view cell height not calculated based on Image height

I am using Compositional Layout plus Diffable Data Source to display images in a UICollectionView from Photo Album using PhotoKit's Cached Image Manager in a single column.我正在使用合成布局加可区分数据源在单列中使用 PhotoKit 的缓存图像管理器显示相册中 UICollectionView 中的图像。

I would like the image cell to be the entire width of the collection view and the height of the cell to be scaled to the height of the image using Aspect Fit content mode.我希望图像单元格是集合视图的整个宽度,并且使用纵横比适合内容模式将单元格的高度缩放到图像的高度。

When I create the layout I use estimated height for both item and group layout objects.当我创建布局时,我使用项目和组布局对象的估计高度。 But the initial height of each cell stays at the estimated height.但是每个单元格的初始高度保持在估计的高度。 As soon as I start scrolling, some of the cells do actually resize the height correctly, but not always.一旦我开始滚动,一些单元格确实会正确调整高度,但并非总是如此。

Here is the sample code (replace the default ViewController logic in a sample iOS project with the following code):下面是示例代码(将示例 iOS 项目中的默认 ViewController 逻辑替换为以下代码):

import UIKit
import Photos
import PhotosUI

class ViewController: UIViewController, UICollectionViewDelegate {
    
    enum Section: CaseIterable {
        case main
    }
    
    var fetchResult: PHFetchResult<PHAsset>!
    var dataSource: UICollectionViewDiffableDataSource<Section, PHAsset>!
    var collectionView: UICollectionView!
    var emptyAlbumMessageView : UIView! = nil
    let imageManager = PHCachingImageManager()
    
    var selectedAssets: [PHAsset] {
        
        var pAssets = [PHAsset]()
        fetchResult.enumerateObjects { (asset, index, stop) in
            pAssets.append(asset)
        }
        
        return pAssets
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        
        createEmptyAlbumMessageView()
        
        configurePhotoData()
        configureHierarchy()
        configureDataSource()
        
        displayPhotos(fetchResult!, title: "All Photos")
    }
    
    func createEmptyAlbumMessageView() {
        
        emptyAlbumMessageView = UIView()
        emptyAlbumMessageView.backgroundColor = .black
        view.addSubview(emptyAlbumMessageView)
        emptyAlbumMessageView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            emptyAlbumMessageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            emptyAlbumMessageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            emptyAlbumMessageView.topAnchor.constraint(equalTo: view.topAnchor),
            emptyAlbumMessageView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        // Title Label
        let titleLabel : UILabel = UILabel()
        titleLabel.text = "Empty Album"
        titleLabel.textAlignment = .center
        titleLabel.font = UIFont.boldSystemFont(ofSize: 21.0)
        emptyAlbumMessageView.addSubview(titleLabel)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            titleLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 100),
            titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
            titleLabel.centerXAnchor.constraint(equalTo: emptyAlbumMessageView.centerXAnchor, constant: -30),
            titleLabel.centerYAnchor.constraint(equalTo: emptyAlbumMessageView.centerYAnchor)])
        
        // Message Label
        let messageLabel : UILabel = UILabel(frame: CGRect(x: 290, y: 394, width: 294, height: 80))
        messageLabel.text = "This album is empty. Add some photos to it in the Photos app and they will appear here automatically."
        messageLabel.font = UIFont.systemFont(ofSize: 17.0)
        messageLabel.numberOfLines = 3
        messageLabel.textAlignment = .center
        messageLabel.lineBreakMode = .byWordWrapping
        
        emptyAlbumMessageView.addSubview(messageLabel)
        messageLabel.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            messageLabel.widthAnchor.constraint(equalToConstant: 294),
            messageLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 80),
            messageLabel.centerXAnchor.constraint(equalTo: titleLabel.centerXAnchor),
            messageLabel.firstBaselineAnchor.constraint(equalTo: titleLabel.lastBaselineAnchor, constant: 10)
        ])
        
        self.view.bringSubviewToFront(emptyAlbumMessageView)
        self.emptyAlbumMessageView.isHidden = true
    }
    
    func configurePhotoData() {
        let allPhotosOptions = PHFetchOptions()
        allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
        allPhotosOptions.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue)
        
        fetchResult = PHAsset.fetchAssets(with: allPhotosOptions)
    }
    
    func configureHierarchy() {
        
        let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
        collectionView.delegate = self
        
        view.addSubview(collectionView)
        
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        
        if #available(iOS 11.0, *) {
            let safeArea = self.view.safeAreaLayoutGuide
            collectionView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 0).isActive = true
        } else {
            let topGuide = self.topLayoutGuide
            collectionView.topAnchor.constraint(equalTo: topGuide.bottomAnchor, constant: 0).isActive = true
        }
        
        collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        
        self.collectionView = collectionView
        
        self.collectionView.scrollsToTop = false
    }
    
    func configureDataSource() {
        
        let cellRegistration = UICollectionView.CellRegistration
        <PhotoThumbnailCollectionViewCell, PHAsset> { [weak self] cell, indexPath, asset in
            
            guard let self = self else { return }
            
            let scale = UIScreen.main.scale
            
            cell.contentMode = .scaleAspectFit
            
            let imageViewFrameWidth = self.collectionView.frame.width
            let imageViewFrameHeight = (Double(asset.pixelHeight)/scale) / (Double(asset.pixelWidth)/scale) * imageViewFrameWidth
            
            let thumbnailSize = CGSize(width: imageViewFrameWidth * scale, height: imageViewFrameHeight * scale)
            
            cell.representedAssetIdentifier = asset.localIdentifier
            
            self.imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: cell.contentMode == .scaleAspectFit ? .aspectFit : .aspectFill, options: nil, resultHandler: { image, _ in
                
                if cell.representedAssetIdentifier == asset.localIdentifier {
                    cell.image = image
                }
            })
            
            cell.layoutIfNeeded()
        }
        
        dataSource = UICollectionViewDiffableDataSource<Section, PHAsset>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, asset: PHAsset) -> UICollectionViewCell? in
            
            let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: asset)
            
            return cell
        }
    }
    
    func createLayout() -> UICollectionViewLayout {
        
        let layout = UICollectionViewCompositionalLayout { 
            (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection in
            
            let estimateHeight : CGFloat = 200
                        
            let itemWidthDimension = NSCollectionLayoutDimension.fractionalWidth(1.0)
            let itemHeightDimension = NSCollectionLayoutDimension.estimated(estimateHeight)

            let itemSize = NSCollectionLayoutSize(widthDimension: itemWidthDimension,
                                                  heightDimension: itemHeightDimension)
            
            let itemLayout = NSCollectionLayoutItem(layoutSize: itemSize)
                        
            let groupWidthDimension = NSCollectionLayoutDimension.fractionalWidth(1.0)
            let groupHeightDimension = NSCollectionLayoutDimension.estimated(estimateHeight)

            let groupSize = NSCollectionLayoutSize(widthDimension: groupWidthDimension,
                                                   heightDimension: groupHeightDimension )
            
            let groupLayout = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [itemLayout])

            let sectionLayout = NSCollectionLayoutSection(group: groupLayout)
            
            return sectionLayout
        }
        return layout
    }
    
    public func displayPhotos(_ fetchResult: PHFetchResult<PHAsset>, title: String?) {
        
        self.fetchResult = fetchResult
        self.title = title
        
        updateSnapshot(animate: false)
        scrollToBottom()
    }
    
    func updateSnapshot(animate: Bool = false, reload: Bool = true) {
        
        self.emptyAlbumMessageView.isHidden = !(0 == fetchResult.count)
        
        var snapshot = NSDiffableDataSourceSnapshot<Section, PHAsset>()
        snapshot.appendSections([.main])
        let selectedAssets = selectedAssets
        snapshot.appendItems(selectedAssets)
        
        if true == reload {
            snapshot.reloadItems(selectedAssets)
        } else {
            snapshot.reconfigureItems(selectedAssets)
        }
        
        dataSource.apply(snapshot, animatingDifferences: animate)
    }
    
    public func scrollToBottom() {
        collectionView.layoutIfNeeded()
        DispatchQueue.main.async { [self] in
            self.collectionView!.scrollToItem(at: IndexPath(row: fetchResult.count-1, section: 0), at: .bottom, animated: false)
        }
    }
}

class PhotoThumbnailCollectionViewCell: UICollectionViewCell {
    
    var image: UIImage? {
        didSet {
            setNeedsUpdateConfiguration()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    override func updateConfiguration(using state: UICellConfigurationState) {
        var config = PhotoThumbnailCellConfiguration().updated(for: state)
        config.image = image
        config.contentMode = self.contentMode
        contentConfiguration = config
    }
    
    var representedAssetIdentifier: String!
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

struct PhotoThumbnailCellConfiguration : UIContentConfiguration {
    var text : String? = nil
    var image: UIImage? = nil
    var contentMode : UIView.ContentMode = .scaleAspectFit
    
    func makeContentView() -> UIView & UIContentView {
        return PhotoThumbnailContentView(self)
    }
    
    func updated(for state: UIConfigurationState) -> PhotoThumbnailCellConfiguration {
        return self
    }
}

class PhotoThumbnailContentView : UIView, UIContentView {
    var configuration: UIContentConfiguration {
        didSet {
            self.configure(configuration: configuration)
        }
    }
    
    let imageView = UIImageView()
    
    override var intrinsicContentSize: CGSize {
        CGSize(width: 0, height: 200)
    }
    
    init(_ configuration: UIContentConfiguration) {
        
        self.configuration = configuration
        super.init(frame:.zero)
        
        self.addSubview(self.imageView)
        imageView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0),
            imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0),
            imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0),
            imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0),
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(configuration: UIContentConfiguration) {
        guard let configuration = configuration as? PhotoThumbnailCellConfiguration else { return }
        imageView.image = configuration.image
        imageView.contentMode = configuration.contentMode
    }
}

Since I am setting the estimated height in both the item and group layout objects, I would expect the cell height to get calculated automatically.由于我在项目和组布局对象中都设置了估计高度,因此我希望单元格高度能够自动计算。 For some reason, the height only gets calculated after scrolling, but not for ALL cells.出于某种原因,高度只在滚动后计算,而不是所有单元格。

You can try put this in your view controller:您可以尝试将其放入您的视图 controller 中:

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    DispatchQueue.main.async {
        self.collectionView.collectionViewLayout.invalidateLayout()
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM