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

问题描述 投票:0回答:2

我正在使用组合布局加上 Diffable 数据源,使用 PhotoKit 的缓存图像管理器在单列中显示相册中 UICollectionView 中的图像。

我希望图像单元格是集合视图的整个宽度,并且使用 Aspect Fit 内容模式将单元格的高度缩放到图像的高度。

当我创建布局时,我使用项目和组布局对象的估计高度。但每个单元格的初始高度保持在估计高度。一旦我开始滚动,某些单元格实际上会正确调整高度,但并非总是如此。

以下是示例代码(将示例 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
    }
}

由于我在项目和组布局对象中设置估计高度,因此我希望自动计算单元格高度。由于某种原因,高度仅在滚动后计算,但不适用于所有单元格。

ios swift uicollectionview photokit uicollectionviewcompositionallayout
2个回答
0
投票

您可以尝试将其放入视图控制器中:

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

0
投票

根本问题是你不应该直接异步更新

cell
。当检索图像时 (a) 单元已经布局和配置; (b) 更令人不安的是,该单元格可能已滚动到视图之外并被重新用于另一行。诸如使布局无效之类的技巧是代码气味;表明更深层次的问题。

对于可比较的数据源,当异步检索图像时,您应该只更新模型并重新构建快照,然后将更新的快照重新应用到数据源。

此外,在一个不相关的观察中,您正在重建视图层次结构、数据源等,在

viewWillAppear
中。此方法应仅限于更新快照,并且所有配置应仅限于
viewDidLoad

这是一个老问题,所以我怀疑您已经发现了其中许多问题。但如果这个问题确实仍未解决,请告诉我,我可以向您展示如何解决这些问题。

© www.soinside.com 2019 - 2024. All rights reserved.