Swift5中嵌入UIStackView的UICollectionView消失及图片不加载问题
问题复现
- 开发亚马逊克隆应用的商品详情页,用户点击搜索结果列表的UITableViewCell进入详情页,仅承载轮播图的UICollectionView无法正常加载,其余内容显示正常
- 移除外层UIStackView后所有内容正常显示,将页面内容嵌入UIStackView后,UICollectionViewCell消失,
cellForItemAt方法完全不会被调用 - 后续排查发现cell内UIImageView解包为nil,已在Storyboard完成控件绑定,改为可选链式调用
cell.imagioView?.kf.setImage(with: imageUrl)后,collectionView可横向滚动,但图片始终无法加载,已确认图片URL本身没有问题
相关代码
class ProductViewController: UIViewController, UICollectionViewDelegate { @IBOutlet var stackedView: UIStackView! @IBOutlet weak var detailedView: UIView! @IBOutlet var detailPageView: UIScrollView! @IBOutlet var imagesCarouselCollection: UICollectionView! @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var desc: UILabel! @IBOutlet weak var features: UILabel! @IBOutlet weak var price: UILabel! @IBOutlet weak var starsLabel: UILabel! @IBOutlet weak var noOfReviews: UILabel! @IBOutlet weak var addToCartBtn: UIButton! @IBOutlet weak var buyNowBtn: UIButton! var searchedText: String = "" var asinForSearch: String = "" var resultsManager = ResultsManager() var productDeets: Products? var imagesArray = Array<String>() override func viewDidLoad() { super.viewDidLoad() resultsManager.detailsDelegate = self search() self.setupUI() loadingIndicator.isAnimating = true imagesCarouselCollection.delegate = self imagesCarouselCollection.dataSource = self imagesCarouselCollection.register(ImageCarouselViewCell.self, forCellWithReuseIdentifier: "ImageCarouselCollectionCell") imagesCarouselCollection.frame = imagesCarouselCollection.bounds DispatchQueue.main.asyncAfter(deadline: .now() + 5) { self.populatePage() } } @IBAction func buyNow(_ sender: Any) { } @IBAction func addToCart(_ sender: Any) { } func search(){ resultsManager.getDetails(asinForSearch) } func createFeaturesLabels(featuresArray: [String]) -> String { var newFeatureString = "" for feature in featuresArray { newFeatureString.append(feature + "\n") } return newFeatureString } func populatePage(){ if let productResults = self.productDeets { self.titleLabel.text = productResults.title self.desc.text = productResults.productDescription self.imagesArray = productDeets!.images self.features.text = createFeaturesLabels(featuresArray: productResults.featureBullets) self.price.text = "$" + String(productResults.price.currentPrice) self.starsLabel.text = productResults.reviews.rating self.noOfReviews.text = String(productResults.reviews.totalReviews) self.loadingIndicator.isAnimating = false self.stackedView.isHidden = false } } // MARK: - UI Setup for loading icon private func setupUI() { if #available(iOS 13.0, *) { overrideUserInterfaceStyle = .light } self.stackedView.isHidden = true self.detailPageView.backgroundColor = .white self.detailPageView.addSubview(loadingIndicator) NSLayoutConstraint.activate([ loadingIndicator.centerXAnchor .constraint(equalTo: self.view.centerXAnchor), loadingIndicator.centerYAnchor .constraint(equalTo: self.view.centerYAnchor), loadingIndicator.widthAnchor .constraint(equalToConstant: 50), loadingIndicator.heightAnchor .constraint(equalTo: self.loadingIndicator.widthAnchor) ]) } // MARK: - Properties let loadingIndicator: ProgressView = { let progress = ProgressView(colors: [.red, .systemGreen, .systemBlue], lineWidth: 5) progress.translatesAutoresizingMaskIntoConstraints = false return progress }() } extension ProductViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let width: CGFloat = imagesCarouselCollection.bounds.size.width let height: CGFloat = imagesCarouselCollection.bounds.size.height return CGSize(width: width, height: height) } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCarouselCollectionCell", for: indexPath) as! ImageCarouselViewCell let imageUrl = URL(string: imagesArray[indexPath.item]) cell.imagioView.kf.setImage(with: imageUrl) return cell } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { print(imagesArray.count) return imagesArray.count } func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } } extension ProductViewController: ResultsDetailDelegate { func updateDetails(_ resultsManager: ResultsManager, products: Products) { self.productDeets = products } } class ImageCarouselViewCell: UICollectionViewCell { @IBOutlet weak var imagioView: UIImageView! }
核心原因&修复方案
1. UIStackView下UICollectionView不触发数据源方法
原因:UIStackView会接管内部子视图的布局,你没有给UICollectionView设置明确的宽高/宽高比约束,StackView无法计算它的实际尺寸,最终UICollectionView的渲染尺寸为0,所以不会触发任何数据源相关方法。
修复步骤:
- 删除代码中
imagesCarouselCollection.frame = imagesCarouselCollection.bounds这行手动赋值frame的代码 - 在Storyboard中给UICollectionView添加固定宽高约束,或者设置宽高比约束,保证UIStackView可以正确计算它的布局尺寸
- 检查UIStackView的axis、alignment、distribution属性配置,垂直布局场景下要确保所有子视图都有明确的高度或者可以通过内容自动撑开高度
2. cell中UIImageView为nil、图片无法加载
原因:你的ImageCarouselViewCell是在Storyboard中定义的,已经绑定了@IBOutlet的UIImageView和重用标识符,但你在viewDidLoad中手动调用了imagesCarouselCollection.register(ImageCarouselViewCell.self, forCellWithReuseIdentifier: "ImageCarouselCollectionCell"),这行代码会注册纯代码创建的Cell,直接覆盖Storyboard中绑定的Cell原型,最终出队的Cell是纯代码初始化的,没有加载Storyboard中的控件,所以imagioView为nil。
修复步骤:
直接删除上述手动注册Cell的代码即可。如果Cell是在Storyboard的CollectionView中绑定了重用标识符,系统会自动完成注册,不需要手动调用register方法。
额外优化
你当前使用固定5秒延时调用populatePage刷新页面的逻辑不合理,建议在拿到接口返回数据后主动刷新UI,修改代理方法如下:
extension ProductViewController: ResultsDetailDelegate { func updateDetails(_ resultsManager: ResultsManager, products: Products) { self.productDeets = products DispatchQueue.main.async { self.populatePage() self.imagesCarouselCollection.reloadData() } } }
同时在populatePage方法中给imagesArray赋值后,也需要主动调用imagesCarouselCollection.reloadData()触发collectionView刷新,避免数据源更新后UI不刷新。
内容的提问来源于stack exchange,提问作者zamanada

