You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

UIKit:UITableViewCell内UIStackView实现图片重叠排列求助

UITableViewCell实现用户图片水平堆叠重叠效果问题

需求

使用UIKit开发应用,需在UITableViewCell中展示一组用户图片,要求图片以水平堆叠且互相重叠的形式呈现。

效果对比

期望效果:
期望效果示意图

当前效果:
当前效果示意图

问题代码

import UIKit
import SDWebImage

class CountryTableCell: UITableViewCell {
    static let reuseIdentifier = "CountryTableCell"

    //MARK: - Propeties
    var viewModel: CountryViewModel? {
        didSet {
            viewModel?.delegate = self
            setUpSpace(viewModel)
        }
    }
    
    //MARK: - SubViews
    private let container: UIView = {
        let view = UIView()
        view.withSize(CGSize(width: Dimensions.maxSafeWidth, height: 150))
        view.backgroundColor = .secondary_background
        view.layer.cornerRadius = 20
        return view
    }()
    
    private let adminsContainer: UIView = {
        let view = UIView()
        view.withSize(CGSize(width: (Dimensions.maxSafeWidth - 32), height: Dimensions.image.mediumHeigthWithOverlay))
        view.backgroundColor = .secondary_background
        return view
    }()
    
    private let adminsStack: UIStackView = {
        let stack = UIStackView()
        stack.axis = .horizontal
        stack.alignment = .leading
        stack.spacing = -10
        stack.sizeToFit()
        stack.distribution = .fillEqually
        stack.withSize(CGSize(width: Dimensions.image.mediumHeigthWithOverlay, height: Dimensions.image.mediumHeigthWithOverlay))
        return stack
    }()
    
    private let nameLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: UIFont.preferredFont(forTextStyle: .title2).pointSize, weight: .bold)
        label.textColor = .primary_label
        label.textAlignment = .left
        label.sizeToFit()
        label.numberOfLines = 0
        return label
    }()
    
    var headerView: UIImageView = {
        let iv = UIImageView()
        iv.contentMode = .scaleAspectFill
        iv.clipsToBounds = true
        iv.withSize(CGSize(width: Dimensions.image.headerSizeForCell.width, height: ((Dimensions.image.mediumHeigth/2) + Padding.horizontal)))
        iv.backgroundColor = .secondary_background
        iv.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
        iv.layer.cornerRadius = 20
        return iv
    }()
    
    //MARK: - Init
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        addSubview(container)
        container.center(inView: self)
        
        addSubview(headerView)
        headerView.anchor(top: container.topAnchor, left: container.leftAnchor, right: container.rightAnchor)
        
        addSubview(nameLabel)
        nameLabel.anchor(top: headerView.bottomAnchor, left: container.leftAnchor, right: container.rightAnchor, paddingLeft: Padding.horizontal, paddingRight: Padding.horizontal)
        
        addSubview(adminsContainer)
        adminsContainer.anchor(left: container.leftAnchor, bottom: headerView.bottomAnchor, right: container.rightAnchor ,paddingLeft: Padding.horizontal, paddingBottom: -(Dimensions.image.mediumHeigth/2), paddingRight: Padding.horizontal)
        bringSubviewToFront(adminsContainer)
        adminsContainer.backgroundColor = .clear.withAlphaComponent(0.0)
        adminsContainer.addSubview(adminsStack)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    //MARK: - Lifecycle
    override func willRemoveSubview(_ subview: UIView) {
        
    }
    
    //MARK: - Helpers
    private func setUpSpace(_ viewModel: CountryViewModel?) {
        guard let viewModel = viewModel else {return}
        
        if let url = viewModel.country.headerImage?.url {
            headerView.sd_setImage(with: URL(string: url))
        }
        
        nameLabel.text = viewModel.space.name
    }
}

// MARK: Extension
extension SpaceTableCell: CountryViewModelDelegate {
    func didFetchAdmin(_ admin: User) {
        if let admins = viewModel?.admins.count {
            if admins == 1 {
                adminsStack.withSize(CGSize(width: Dimensions.image.mediumHeigthWithOverlay, height: Dimensions.image.mediumHeigthWithOverlay))
            } else if admins == 2 {
                adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*2) - 10), height: Dimensions.image.mediumHeigthWithOverlay))
            } else if admins == 3 {
                adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*3) - 20), height: Dimensions.image.mediumHeigthWithOverlay))
            } else if admins == 4 {
                adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*4) - 30), height: Dimensions.image.mediumHeigthWithOverlay))
            } else if admins == 5 {
                adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*5) - 40), height: Dimensions.image.mediumHeigthWithOverlay))
            }
        }
        
        let image = UserImageView(height: Dimensions.image.mediumHeigth)
        image.sd_setImage(with: admin.profileImageURL)
        adminsStack.addArrangedSubview(image)
    }
}


class UserImageView: UIImageView {
    
    //MARK: - Propeties
    let selectedHeigth: CGFloat
    
    init(height: CGFloat) {
        self.selectedHeigth = height
        super.init(frame: CGRect(x: 0, y: 0, width: selectedHeigth, height: selectedHeigth))
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        contentMode = .scaleAspectFill
        clipsToBounds = true
        backgroundColor = .secondary_background
        layer.cornerRadius = Dimensions.userImageCornerRadious(selectedHeigth)
    }
}

解决方案

1. 修正StackView核心配置

fillEqually会强制拉伸子视图导致图片变形,固定宽度也会限制堆叠效果,修改adminsStack初始化代码:

private let adminsStack: UIStackView = {
    let stack = UIStackView()
    stack.axis = .horizontal
    stack.alignment = .leading
    stack.spacing = -10 // 保留负间距实现重叠
    stack.distribution = .fill // 改为fill,让子视图保持原尺寸
    return stack
}()

2. 完善StackView布局约束

在init方法中给adminsStack添加明确约束,确保它在容器内靠左对齐、高度匹配:

adminsContainer.addSubview(adminsStack)
// 添加以下约束
adminsStack.anchor(top: adminsContainer.topAnchor, left: adminsContainer.leftAnchor, bottom: adminsContainer.bottomAnchor)

3. 移除冗余宽度计算逻辑

删除didFetchAdmin中手动计算StackView宽度的代码块,StackView会根据子视图尺寸和负间距自动调整宽度。

4. 增加Cell复用清理

在setUpSpace方法开头添加清理逻辑,避免Cell复用时图片重复叠加:

private func setUpSpace(_ viewModel: CountryViewModel?) {
    guard let viewModel = viewModel else {return}
    // 清理旧的图片视图
    adminsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
    
    if let url = viewModel.country.headerImage?.url {
        headerView.sd_setImage(with: URL(string: url))
    }
    
    nameLabel.text = viewModel.space.name
}

5. 优化后的didFetchAdmin方法

func didFetchAdmin(_ admin: User) {
    let image = UserImageView(height: Dimensions.image.mediumHeigth)
    image.sd_setImage(with: admin.profileImageURL)
    adminsStack.addArrangedSubview(image)
}

以上修改完成后,图片会以水平堆叠、互相重叠的形式正常显示,符合期望效果。

内容的提问来源于stack exchange,提问作者SamP

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 05:50:11