UICollectionViewCell behavior on different iPhone simulators (iPhone 16/iPhone 16 Pro Max)
00:39 25 Jan 2026

I created a endless carousel based on a UICollectionView and was happy with the result. When testing on an iPhone 16 simulator, I had no issues. On the iPhone 16 Pro Max simulator, I encountered a problem: when opening the carousel screen, the selected cell didn't display a colored glowView. The color only appears after interacting with the carousel.

I found the probable source of the error:

distance < 1

in the func updateSelection(), but I can't fix it.

Has anyone encountered something similar?

My code:

final class AvatarCarouselCell: UICollectionViewCell {
    
    static let reuseId = "AvatarCarouselCell"
    
    private let containerView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .gray90
        view.clipsToBounds = true
        view.isUserInteractionEnabled = false
        return view
    }()
    
    private let glowView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .clear
        view.isUserInteractionEnabled = false
        return view
    }()
    
    private let imageView: UIImageView = {
        let view = UIImageView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.contentMode = .scaleAspectFit
        view.clipsToBounds = true
        view.isUserInteractionEnabled = false
        return view
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        contentView.addSubview(containerView)
        contentView.addSubview(glowView)
        contentView.addSubview(imageView)
        
        NSLayoutConstraint.activate([
            containerView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            containerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
            containerView.widthAnchor.constraint(equalToConstant: 134),
            containerView.heightAnchor.constraint(equalToConstant: 134),
            
            glowView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
            glowView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
            glowView.widthAnchor.constraint(equalTo: containerView.widthAnchor),
            glowView.heightAnchor.constraint(equalTo: containerView.heightAnchor),
            
            imageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
            imageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
            imageView.widthAnchor.constraint(equalToConstant: 88),
            imageView.heightAnchor.constraint(equalToConstant: 88)
        ])
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        imageView.image = nil
        setSelected(false, selectedColor: .clear, defaultColor: .clear)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        glowView.layer.shadowPath = UIBezierPath(
            ovalIn: glowView.bounds
        ).cgPath
    }


    override func didMoveToSuperview() {
        super.didMoveToSuperview()

        let radius = 134 / 2
        containerView.layer.cornerRadius = CGFloat(radius)
        glowView.layer.cornerRadius = CGFloat(radius)
    }

    
    func configure(imageName: String) {
        imageView.image = UIImage(named: imageName)
    }
    
    func setSelected(_ isSelected: Bool,
                     selectedColor: UIColor,
                     defaultColor: UIColor) {
        if isSelected {
            glowView.layer.shadowColor = selectedColor.cgColor
            glowView.layer.shadowRadius = 4
            glowView.layer.shadowOpacity = 0.9
            glowView.layer.shadowOffset = .zero
        } else {
            glowView.layer.shadowOpacity = 0
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

final class AvatarCarouselCollectionView: UIViewController, UICollectionViewDelegate {
    
    private let avatars: [String]
    private let selectedColor: UIColor
    private let defaultColor: UIColor
    private var selectedRealIndex: Int
    private let onIndexChanged: ((Int) -> Void)?
    
    private var didSetInitialSelection = false
    
    private let minScale: CGFloat = 0.7
    private let infiniteItemsCount = 10_000
    
    private lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.itemSize = CGSize(width: 140, height: 140)
        layout.minimumLineSpacing = 4
        
        let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .gray100
        view.showsHorizontalScrollIndicator = false
        view.decelerationRate = .fast
        view.dataSource = self
        view.delegate = self
        view.register(AvatarCarouselCell.self, forCellWithReuseIdentifier: AvatarCarouselCell.reuseId)
        return view
    }()
    
    init(avatars: [String],
         selectedIndex: Int,
         selectedColor: UIColor,
         defaultColor: UIColor,
         onIndexChanged: ((Int) -> Void)?) {
        self.avatars = avatars
        self.selectedRealIndex = max(0, min(selectedIndex, avatars.count - 1))
        self.selectedColor = selectedColor
        self.defaultColor = defaultColor
        self.onIndexChanged = onIndexChanged
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError()
    }
    
    // MARK: - Lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .gray100
        setupConstraints()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        updateSectionInsets()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        scrollToInitialSelectionIfNeeded()
    }
    
    func setSelectedIndex(_ index: Int, animated: Bool) {
        let clamped = clampRealIndex(index)
        guard clamped != selectedRealIndex else { return }
        selectedRealIndex = clamped
        scrollToRealIndex(clamped, animated: animated)
        
        if !animated {
            collectionView.layoutIfNeeded()
            updateSelection()
        }
    }
    
    func currentSelectedIndex() -> Int {
        selectedRealIndex
    }
    
    // MARK: - Helpers
    
    private func setupConstraints() {
        view.addSubview(collectionView)
        NSLayoutConstraint.activate([
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    private func updateSectionInsets() {
        guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
        let inset = (view.bounds.width - layout.itemSize.width) / 2
        layout.sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
    }
    
    private var itemWidth: CGFloat {
        let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
        return layout.itemSize.width + layout.minimumLineSpacing
    }
    
    private func realIndex(forInfiniteIndex index: Int) -> Int {
        guard !avatars.isEmpty else { return 0 }
        return index % avatars.count
    }
    
    private func clampRealIndex(_ index: Int) -> Int {
        guard !avatars.isEmpty else { return 0 }
        return max(0, min(index, avatars.count - 1))
    }
    
    // MARK: - Initial scroll
    
    private func scrollToInitialSelectionIfNeeded() {
        guard !didSetInitialSelection else {
            updateSelection()
            return
        }
        didSetInitialSelection = true
        collectionView.layoutIfNeeded()
        scrollToRealIndex(selectedRealIndex, animated: false)
        collectionView.layoutIfNeeded()
        updateSelection()
    }
    
    private func scrollToRealIndex(_ realIndex: Int, animated: Bool) {
        guard !avatars.isEmpty else { return }
        
        let middle = infiniteItemsCount / 2
        let base = middle - (middle % avatars.count)
        let item = base + realIndex
        
        collectionView.scrollToItem(
            at: IndexPath(item: item, section: 0),
            at: .centeredHorizontally,
            animated: animated
        )
    }
    
    // MARK: - Selection logic
    
    private func updateSelection() {
        guard !avatars.isEmpty else { return }
        
        let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2
        
        for case let cell as AvatarCarouselCell in collectionView.visibleCells {
            let distance = abs(cell.center.x - centerX)
            scaleEffect(to: cell, distance: distance)
            cell.setSelected(distance < 1, selectedColor: selectedColor, defaultColor: defaultColor)
        }
        
        guard
            let cell = centeredCell(),
            let indexPath = collectionView.indexPath(for: cell)
        else { return }
        
        let newReal = realIndex(forInfiniteIndex: indexPath.item)
        updateSelectedIndex(newReal)
    }
    
    private func centeredCell() -> AvatarCarouselCell? {
        let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2
        return collectionView.visibleCells
            .compactMap { $0 as? AvatarCarouselCell }
            .min { abs($0.center.x - centerX) < abs($1.center.x - centerX) }
    }
    
    private func scaleEffect(to cell: AvatarCarouselCell, distance: CGFloat) {
        let scale = max(minScale, 1 - distance / (collectionView.bounds.width / 2))
        cell.transform = CGAffineTransform(scaleX: scale, y: scale)
    }
    
    private func updateSelectedIndex(_ index: Int) {
        guard index != selectedRealIndex else { return }
        selectedRealIndex = index
        onIndexChanged?(index)
    }
}

// MARK: - DataSource

extension AvatarCarouselCollectionView: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        infiniteItemsCount
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(
            withReuseIdentifier: AvatarCarouselCell.reuseId,
            for: indexPath
        ) as! AvatarCarouselCell

        let realIndex = realIndex(forInfiniteIndex: indexPath.item)
        cell.configure(imageName: avatars[realIndex])

        let isSelected = realIndex == selectedRealIndex
        cell.setSelected(isSelected,
                         selectedColor: selectedColor,
                         defaultColor: defaultColor)

        return cell
    }
}

// MARK: - ScrollViewDelegate

extension AvatarCarouselCollectionView: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        updateSelection()
    }
    
    func scrollViewWillEndDragging(_ scrollView: UIScrollView,
                                   withVelocity velocity: CGPoint,
                                   targetContentOffset: UnsafeMutablePointer) {
        let index = round(targetContentOffset.pointee.x / itemWidth)
        targetContentOffset.pointee.x = index * itemWidth
    }
}
uicollectionview ios-simulator uicollectionviewcell