How to smoothly transition a UICollectionView into a UISearchBar when scrolling in Swift?

I'm working on an iOS app in Swift where I have a UICollectionView displaying ads at the top of my screen and another UICollectionView displaying products below it. I want to implement a smooth transition where, as I scroll up, the ad UICollectionView gradually transforms into a UISearchBar. Here are my specific requirements:

  1. When scrolling up, the ad UICollectionView should move up together with the product UICollectionView.
  2. As the ad UICollectionView scrolls up, it should gradually transform into a UISearchBar.
  3. When scrolling back down, the UISearchBar should transform back into the ad UICollectionView.
  4. The UISearchBar should stick at the top, just below the safe area, with a 5-point offset, and the product UICollectionView should not overlap it.

Problems I am facing:

I've tried implementing this with a UIScrollView containing both UICollectionViews and using animations, but I haven't been able to get it to work smoothly. Here’s my current code:

import UIKit

class MainView: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UIScrollViewDelegate {

    // Temporary data
    var adArr = ["ad1", "ad2", "ad3"]
    var extendedAdArr: [String] = []
    private let cosmetics = [
        Cosmetic(imageName: "sary", name: "Ma:Nyo Pure Cleansing Oil", discountedPrice: "14", originalPrice: "27"),
        Cosmetic(imageName: "zhasyl", name: "Ma:Nyo Bifida Cica Herb Toner", discountedPrice: "17", originalPrice: "23"),
        Cosmetic(imageName: "koz", name: "Ma:Nyo 4GF Eyelash Ampoule", discountedPrice: "16", originalPrice: "25"),
        Cosmetic(imageName: "the", name: "The Ordinary Caffeine Solution", discountedPrice: "13", originalPrice: "20")
    ]

    // MARK: - Properties
    private var adCollectionViewHeightConstraint: NSLayoutConstraint!
    private var adCollectionViewTopConstraint: NSLayoutConstraint!
    
    private var scrollView: UIScrollView = {
        let scrollView = UIScrollView()
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        return scrollView
    }()
    
    private var contentView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    private var adCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.itemSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 0.3)
        layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        layout.minimumLineSpacing = 0

        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.showsVerticalScrollIndicator = false
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.backgroundColor = .white
        collectionView.isPagingEnabled = true

        return collectionView
    }()
    
    private var pageControl: UIPageControl = {
        let pageControl = UIPageControl()
        pageControl.translatesAutoresizingMaskIntoConstraints = false
        pageControl.currentPageIndicatorTintColor = .purple
        pageControl.pageIndicatorTintColor = .white
        return pageControl
    }()
    
    private var searchBarView: SearchBarView = {
        let view = SearchBarView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.alpha = 0 // Initially hidden
        return view
    }()
    
    private var productsCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        layout.itemSize = CGSize(width: UIScreen.main.bounds.width / 2 - 10, height: UIScreen.main.bounds.height * 0.3)
        layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        layout.minimumLineSpacing = 10
        layout.minimumInteritemSpacing = 10

        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.showsVerticalScrollIndicator = false
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.backgroundColor = .white

        return collectionView
    }()
    
    // Initialization
    override init(frame: CGRect) {
        super.init(frame: frame)
        setBackgroudColor()
        setupScrollView()
        setupAdCollectionView()
        setupPageControl()
        setupSearchBarView()
        extendAdArray()
        setupProductCollectionView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        adjustInitialScrollPosition()
    }
    
    // MARK: - Private and Public Methods
    func setBackgroudColor() {
        backgroundColor = UIColor(white: 0.97, alpha: 1)
    }
    
    private func setupScrollView() {
        addSubview(scrollView)
        scrollView.addSubview(contentView)
        
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
            
            contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
        ])
    }
    
    private func setupAdCollectionView() {
        contentView.addSubview(adCollectionView)
        adCollectionView.dataSource = self
        adCollectionView.delegate = self
        adCollectionView.register(AdCollectionViewCell.self, forCellWithReuseIdentifier: AdCollectionViewCell.identifier)
        adCollectionViewTopConstraint = adCollectionView.topAnchor.constraint(equalTo: contentView.topAnchor)
        adCollectionViewHeightConstraint = adCollectionView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height * 0.3)
        NSLayoutConstraint.activate([
            adCollectionViewTopConstraint,
            adCollectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            adCollectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            adCollectionViewHeightConstraint
        ])
    }
    
    private func setupProductCollectionView() {
        contentView.addSubview(productsCollectionView)
        productsCollectionView.dataSource = self
        productsCollectionView.delegate = self
        productsCollectionView.register(CosmeticCollectionViewCell.self, forCellWithReuseIdentifier: CosmeticCollectionViewCell.identifier)
        NSLayoutConstraint.activate([
            productsCollectionView.topAnchor.constraint(equalTo: adCollectionView.bottomAnchor),
            productsCollectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            productsCollectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            productsCollectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
    }
    
    private func setupPageControl() {
        contentView.addSubview(pageControl)
        NSLayoutConstraint.activate([
            pageControl.bottomAnchor.constraint(equalTo: adCollectionView.bottomAnchor, constant: -8),
            pageControl.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
        ])
    }
    
    private func setupSearchBarView() {
        addSubview(searchBarView)
        NSLayoutConstraint.activate([
            searchBarView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 5),
            searchBarView.leadingAnchor.constraint(equalTo: leadingAnchor),
            searchBarView.trailingAnchor.constraint(equalTo: trailingAnchor),
            searchBarView.heightAnchor.constraint(equalToConstant: 56)
        ])
    }
    
    private func extendAdArray() {
        extendedAdArr = adArr + adArr + adArr // Duplicate the array 3 times
        adCollectionView.reloadData()
    }
    
    private func adjustInitialScrollPosition() {
        let middleIndexPath = IndexPath(item: adArr.count, section: 0)
        adCollectionView.scrollToItem(at: middleIndexPath, at: .centeredHorizontally, animated: false)
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = scrollView.contentOffset.y
        let maxOffsetY = UIScreen.main.bounds.height * 0.3 - 56 // Height difference between adCollectionView and searchBar
        let safeAreaOffset = safeAreaInsets.top + 5

        if offsetY <= maxOffsetY {
            adCollectionViewTopConstraint.constant = -offsetY
            adCollectionViewHeightConstraint.constant = UIScreen.main.bounds.height * 0.3 - offsetY
            searchBarView.alpha = offsetY / maxOffsetY
            searchBarView.transform = .identity
        } else {
            adCollectionViewTopConstraint.constant = -maxOffsetY
            adCollectionViewHeightConstraint.constant = 56
            searchBarView.alpha = 1
            searchBarView.transform = CGAffineTransform(translationX: 0, y: safeAreaOffset)
            scrollView.contentOffset.y = maxOffsetY
        }
    }
    
    // MARK: - CollectionView DataSource and Delegate
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        if collectionView == adCollectionView {
            return extendedAdArr.count
        } else if collectionView == productsCollectionView {
            return cosmetics.count
        }
        return 0
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 4
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if collectionView == adCollectionView {
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AdCollectionViewCell.identifier, for: indexPath) as? AdCollectionViewCell else {
                fatalError("unable to dequeue the AdCollectionViewCell")
            }
            let imageName = extendedAdArr[indexPath.row]
            if let image = UIImage(named: imageName) {
                cell.configure(with: image)
            }
            return cell
        } else if collectionView == productsCollectionView {
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CosmeticCollectionViewCell.identifier, for: indexPath) as? CosmeticCollectionViewCell else {
                fatalError("unable to dequeue the CosmeticCollectionViewCell")
            }
            let cosmetic = cosmetics[indexPath.row]
            cell.configure(with: cosmetic)
            return cell
        }
        return UICollectionViewCell()
    }
}

struct Cosmetic {
    let imageName: String
    let name: String
    let discountedPrice: String
    let originalPrice: String
}

extension MainView: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        if collectionView == productsCollectionView {
            let padding: CGFloat = 10
            let width = (collectionView.frame.size.width - padding * 3) / 2
            return CGSize(width: width, height: width * 1.6)
        }
        return CGSize(width: collectionView.frame.size.width, height: collectionView.frame.size.height)
    }
}


class SearchBarView: UIView {

    lazy var searchBar: UISearchBar = {
        let searchBar = UISearchBar()
        searchBar.translatesAutoresizingMaskIntoConstraints = false
        searchBar.placeholder = "Search"
        return searchBar
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupView() {
        addSubview(searchBar)
        NSLayoutConstraint.activate([
            searchBar.topAnchor.constraint(equalTo: topAnchor),
            searchBar.leadingAnchor.constraint(equalTo: leadingAnchor),
            searchBar.trailingAnchor.constraint(equalTo: trailingAnchor),
            searchBar.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
}

in previous section i wrote down everything i am facing:(

Upvotes: 0

Views: 25

Answers (0)

Related Questions