Swift 当 imageView 图像设置为 ViewController 时自定义 UICollectionViewCell 子视图消失

Swift custom UICollectionViewCell subViews disappear when imageView image set at ViewController

我在处理 UICollectionView

时遇到了奇怪的问题

我创建了简单的自定义 UICollectionViewCell,它只有一个 imageViewLabel:

Cell 的 imageView 有默认占位符图像,并从 collectionView(_:cellForItemAt:) 更新 imageView.image。但是当设置图像时,单元格的所有子视图都会消失:

(单元格不会同时消失,因为下载和设置图像是异步的)

注意:我使用的示例数据没有错(相同的数据适用于同一应用程序中的 TableView)

为什么会发生这种情况,我该如何解决?

这是我使用的示例数据:

    let movies = [
        MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955),
        MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955),
        MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955),
        MovieFront(title: "Spider-Man: No Way Home", posterPath: "1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", genre: "Genre", releaseDate: "2021-12-15", ratingScore: 8.4, ratingCount: 3955)
    ]

这是我的部分 ViewController:


    lazy var collectionView = { () -> UICollectionView in
        
        // FlowLayout
        var flowLayout = UICollectionViewFlowLayout()
        flowLayout.headerReferenceSize = CGSize(width: self.preferredContentSize.width, height: 180)
        flowLayout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
        flowLayout.minimumInteritemSpacing = 20
        flowLayout.minimumLineSpacing = 20

        // Collection View
        var collectionView =  UICollectionView(frame: self.view.frame, collectionViewLayout: flowLayout)
        collectionView.register(DiscoverCollectionViewCell.self, forCellWithReuseIdentifier: identifiers.discover_collection_cell)
        collectionView.register(DiscoverCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: identifiers.discover_collection_header)
        collectionView.backgroundColor = UIColor(named: Colors.background)
        
        return collectionView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.title = "Discover"
        
        collectionView.dataSource = self
        collectionView.delegate = self
        
        self.view.backgroundColor = UIColor(named: Colors.background)
    
        self.view.addSubview(collectionView)
        collectionView.snp.makeConstraints { [=13=].edges.equalTo(self.view.safeAreaLayoutGuide) }
        
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        // Sample Cell
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifiers.discover_collection_cell, for: indexPath) as? DiscoverCollectionViewCell else { return DiscoverCollectionViewCell() }
        
        let movie = movies[indexPath.row]

        cell.movieTitle.text = movie.title
    
        DispatchQueue.global().async {
            guard let imageURL = URL(string: "https://image.tmdb.org/t/p/original/\(movie.posterPath)") else { return }
            guard let imageData = try? Data(contentsOf: imageURL) else { return }
            
            DispatchQueue.main.sync {
                cell.posterImage.image = UIImage(data: imageData)
            }
        }
        return cell
    }

这是我的习惯 CollectionViewCell,我使用了 Snapkit, Then 库:

class DiscoverCollectionViewCell: UICollectionViewCell {
    
    //MARK: Create properties
    lazy var posterImage = UIImageView().then {
        [=14=].image = UIImage(named: "img_placeholder")
        [=14=].contentMode = .scaleAspectFit
    }
    
    lazy var movieTitle = UILabel().then {
        [=14=].font = UIFont.systemFont(ofSize: 15)
        [=14=].textColor = .white
        [=14=].numberOfLines = 2
        [=14=].minimumScaleFactor = 10
    }
        
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        // add to view
        self.addSubview(posterImage)
        self.addSubview(movieTitle)
        
        //MARK: Add Constraints
        posterImage.snp.makeConstraints { make in
            make.top.left.right.equalToSuperview()
        }
        
        movieTitle.snp.makeConstraints { make in
            make.top.equalTo(posterImage.snp.bottom).offset(5)
            make.bottom.greaterThanOrEqualToSuperview()
            make.leading.equalTo(posterImage.snp.leading)
            make.trailing.equalTo(posterImage.snp.trailing)
        }
        
        self.backgroundColor = .blue
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

虽然我不确定,因为集合视图单元格正在被重用,单元格的初始化只会在第一次调用,而不是在从服务器加载图像数据时调用。

尝试在单元格的不同方法中移动与布局相关的代码(特别是添加子视图和约束它们),并在每次加载图像时调用它。

您的单元格布局有两个问题...

    // add to view
    self.addSubview(posterImage)
    self.addSubview(movieTitle)
    
    //MARK: Add Constraints
    posterImage.snp.makeConstraints { make in
        make.top.left.right.equalToSuperview()
    }
  1. 您应该始终将 UI 元素添加到单元格的 .contentView,而不是单元格本身。

  2. 您没有约束图像视图的底部

     // add to ContentView!
     self.contentView.addSubview(posterImage)
     self.contentView.addSubview(movieTitle)
    
     //MARK: Add Constraints
     posterImage.snp.makeConstraints { make in
         make.top.left.right.bottom.equalToSuperview()
     }
    

编辑

您的 post 中遗漏了一些内容(包括您如何设置单元格/项目大小),因此在进行上述更改的同时 do 修复了图片根本没有显示,这不是您想要的。

我假设你在某处设置流布局 .itemSize,所以你的原始约束 - 没有将 .bottom. 添加到图像视图约束 - 是 close...

当您将图像添加到 UIImageView 时,intrinsicContentSize 成为图像的大小。您的约束正在控制宽度,但是...

您标签上的限制条件:

make.bottom.greaterThanOrEqualToSuperview()

表示"将标签的底部放在其父视图的底部或更靠下!"

加载图像时,它会将图像视图高度设置为其自身的高度,并将标签向下推到单元格底部之外。

那一行需要是:

make.bottom.equalToSuperview()

这将防止标签底部移动。

接下来,您需要告诉auto-layout “不要垂直压缩或拉伸标签”:

// prevent label from stretching vertically
movieTitle.setContentHuggingPriority(.required, for: .vertical)
    
// prevent label from compressing vertically
movieTitle.setContentCompressionResistancePriority(.required, for: .vertical)
    

否则,标签将被压缩到零高度。

我发现添加评论非常有帮助,所以我知道我期待发生什么:

override init(frame: CGRect) {
    super.init(frame: frame)
    
    // add to ContentView
    self.contentView.addSubview(posterImage)
    self.contentView.addSubview(movieTitle)
    
    //MARK: Add Constraints
    posterImage.snp.makeConstraints { make in
        // constrain image view to
        //  Top / Left / Right of contentView
        make.top.left.right.equalToSuperview()
    }
    
    // prevent label from stretching vertically
    movieTitle.setContentHuggingPriority(.required, for: .vertical)
    
    // prevent label from compressing vertically
    movieTitle.setContentCompressionResistancePriority(.required, for: .vertical)
    
    movieTitle.snp.makeConstraints { make in
        // constrain Top of label to Bottom of image view
        //  because we've set Hugging and Compression Resistance on the label,
        //  this will "pull down" the bottom of the image view
        make.top.equalTo(posterImage.snp.bottom).offset(5)

        // constrain Bottom of label to Bottom of contentView
        //  must be EQUAL TO
        //make.bottom.greaterThanOrEqualToSuperview()
        make.bottom.equalToSuperview()
        
        //  Leading / Trailing equal to image view
        make.leading.equalTo(posterImage.snp.leading)
        make.trailing.equalTo(posterImage.snp.trailing)
    }
    
    self.backgroundColor = .blue
}

现在我们得到这个结果:

图片下载后:

最后一件事 - 尽管您可能已经做了一些事情来解决这个问题...

如您在这些屏幕截图中所见,在标签上设置 .numberOfLines = 2 不会 强制 2 行高度...它仅 限制 它到 2 行。如果电影标题 ,标签高度将更短,如第二个单元格中所示。

解决这个问题的一种方法是将标签高度限制为 2.5 行,方法是将此添加到您的初始化中:

    if let font = movieTitle.font {
        movieTitle.snp.makeConstraints { make in
            make.height.equalTo(font.lineHeight * 2.5)
        }
    }

这将给出这个输出: