sangam pokharel
sangam pokharel

Reputation: 39

How to Implement Sticky CustomNavbar and TabView (XLPagerTabStrip) in UIScrollView like Twitter Profile Page?

https://s5.ezgif.com/tmp/ezgif-5-417f58acf0.gif

I’m trying to implement a scrolling layout similar to the profile screen of the iOS Twitter/X app. In my layout, I have the following components:

Current Issue: I am unable to get the product header and XLPagerTabStrip tab view to "stick" below the custom navigation bar while scrolling down. The product header scrolls out of view along with the content, and the tabs do not stick to the top as expected.

StoreViewController - > MainViewController


import UIKit
import XLPagerTabStrip

class StoreViewController: EKViewController {
    
    private let scrollView = UIScrollView()
    private let containerView = UIView()
    private let bannerImageView = UIImageView()
    private let logoImageView = UIImageView()
    private let divider = UIView()
    private let storeDetailsView = StoreDetailsView()
    private let customNavigationBar = CustomNavigationBar()
    private let navBarBackgroundView = UIView()
    private let productHeaderView = ProductHeaderView()
    private let tabView = UIView()
    let overlayView = UIView()
    let basketView = BasketView()
    private var productHeaderTopConstraint: NSLayoutConstraint!
    private var navBarHeight: CGFloat = 44
    private var statusBarHeight: CGFloat = 0.0
    private var goingUp: Bool?
    private var childScrollingDownDueToParent = false
    private var pagerTabStripVC:MyPagerTabStripViewController? = nil
    let vendorFetcherS = VendorFetcherS()
    var cart:Cart? = nil
    var hasFilterFetched = false
    var categories:[Categories] = []
    var filterData:FilterResponse? = nil
    var dismissParentVC:(()->())?
    @objc var vendor : Vendor? {
        didSet {
            setData()
        }
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(true, animated: animated)
       
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setStatusBarHeight()
        setupScrollView()
        setupBannerAndDetails()
        setUpDivider()
        setUpProductHeaderView()
        // addTemporaryContent()
        view.addSubview(customNavigationBar)
        let bottom = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
        if bottom > 0{
            coverSafeArea()
        }
        setUpCustomNavBar()
        setUpProductHeaderView()
        setupTabBarView()
        setUpViewBasket()
        fetchCategories()
        setUpNotificationCenter()
        setData()
    }
    
    private func setUpNotificationCenter() {
        NotificationCenter.default.addObserver(self, selector: #selector(self.refreshCheckOutBar), name: NSNotification.Name(rawValue: NOTIFICATION_CART_UPDATED), object: nil)
    }
    
    private func fetchCategories(sortingOption: String = "REL", showSoldOutItemsAtBottom: Bool = true, selectedBrandIds: [Int] = []) {
        showHUD(true)
        vendorFetcherS.fetchCategories(vendor?.id,sortBy: sortingOption, showSoldOutItemsAtBottom: showSoldOutItemsAtBottom, brandIds: selectedBrandIds) { data in
            DispatchQueue.main.async {
                self.categories = data ?? []
                self.setupPagerTabStrip()
                self.pagerTabStripVC?.categories = data ?? []
                self.showHUD(false)
                self.tabView.isHidden = false
            }
            
        } failure: { error in
            DispatchQueue.main.async {
                self.alert(message: "Failed to call api VendorFetcher \(error)")
                self.showHUD(false)
            }
        }
    }
    
    private func setData(){
        storeDetailsView.vendor = vendor
        customNavigationBar.vendor = vendor
        let bannerUrl = Utility.makeURL(vendor?.cover)
        if (bannerUrl != nil){
            bannerImageView.setImageWith(bannerUrl!, placeholderImage: .vendorBackgroundPlaceholder, withAnimation: true)
        } else {
            bannerImageView.image = .vendorBackgroundPlaceholder
        }
        if let logoUrl = Utility.makeURL(vendor?.logo){
            logoImageView.setImageWith(logoUrl, placeholderImage: .placeholderIcon, withAnimation: true)
        } else {
            logoImageView.image = .placeholderIcon
        }
    }
    
    private func setStatusBarHeight(){
        if let statusBarFrame = UIApplication.shared.windows.first?.windowScene?.statusBarManager?.statusBarFrame {
            statusBarHeight = statusBarFrame.height
        } else {
        }
    }
    
    // This method covers the safe area for nav bar when user scrolls
    private func coverSafeArea(){
        view.addSubview(navBarBackgroundView)
        navBarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
        navBarBackgroundView.backgroundColor = .clear
        
        NSLayoutConstraint.activate([
            navBarBackgroundView.topAnchor.constraint(equalTo: view.topAnchor),
            navBarBackgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            navBarBackgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            navBarBackgroundView.bottomAnchor.constraint(equalTo: customNavigationBar.topAnchor)
        ])
    }
    
    private func setUpCustomNavBar(){
        customNavigationBar.backgroundColor = .clear
        customNavigationBar.translatesAutoresizingMaskIntoConstraints = false
        let bottom = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
        
        if bottom > 0 {
            // has notch
            customNavigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
            customNavigationBar.heightAnchor.constraint(equalToConstant: navBarHeight).isActive = true
        } else {
            self.navBarHeight = 54
            customNavigationBar.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
            customNavigationBar.heightAnchor.constraint(equalToConstant: navBarHeight + statusBarHeight).isActive = true
        }
        
        NSLayoutConstraint.activate([
            customNavigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            customNavigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        ])
        
        customNavigationBar.backTapped = { [weak self] in
            self?.dismiss(animated: true,completion: {
                self?.dismissParentVC?()
            })
        }
        
        customNavigationBar.searchTapped = { [weak self] in
            let search = FoodmanduFreshSearchViewController()
            search.vendorId = self?.vendor?.id
            let ekNav = EKNavigationController(rootViewController: search)
            ekNav.modalPresentationStyle = .fullScreen
            self?.present(ekNav, animated: true)
        }
        
        customNavigationBar.heartTapped = { [weak self] in
            if UserFetcher.isLoggedIn() {
                if let vendor = self?.vendor {
                    self?.vendorFetcherS.doFavoriteAction(vendor.id, favorite: !vendor.isFavorite, success: { success in}, failure: {failure in})
                    let fvendor = FVendor(forPrimaryKey: vendor.id)
                    if(fvendor != nil){
                        fvendor?.setFavorite(!vendor.isFavorite)
                    }
                    vendor.setFavorite(!vendor.isFavorite)
                    let userInfo: [AnyHashable : Any] = [
                        "vendor": fvendor
                    ]
                    self?.customNavigationBar.isLiked = vendor.isFavorite
                    NotificationCenter.default.post(name: !vendor.isFavorite ? NSNotification.Name(rawValue: NOTIFICATION_VENDOR_REMOVED_FROM_FAVORITE) : NSNotification.Name(rawValue: NOTIFICATION_VENDOR_ADDED_TO_FAVORITE), object: nil, userInfo: userInfo)
                 
                }
                
                
            }else{
                let alertStyle: UIAlertController.Style = UIDevice.current.userInterfaceIdiom == .phone ? .actionSheet : .alert
                let alert = UIAlertController(title: nil, message: "You must be logged in order to add item. Would you like to login?", preferredStyle: alertStyle)
                let loginAction = UIAlertAction(title: "Log In", style: .default) { _ in
                    let controller = Initializer.controller("SignInController") as! SignInController
                    controller.redirectionMode = Int(DISSMISS_TO_PRESENTING)
                    controller.isDisplayedThroughPresenting = true
                    let ekNavController = EKNavigationController(rootViewController: controller)
                    self?.present(ekNavController, animated: true, completion: nil)
                    
                }
                alert.addAction(loginAction)
                let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
                alert.addAction(cancelAction)
                self?.present(alert, animated: true, completion: nil)
            }
        }
        
        
        
    }
    
    private func setupScrollView() {
        view.addSubview(scrollView)
        scrollView.delegate = self
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(containerView)
        scrollView.bounces = false
        containerView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.contentInsetAdjustmentBehavior = .never
        
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            containerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
            
        ])
    }
    
    private func setupBannerAndDetails() {
        containerView.addSubview(bannerImageView)
        containerView.addSubview(overlayView)
        containerView.addSubview(logoImageView)
        
        bannerImageView.image = UIImage(named: "")
        bannerImageView.contentMode = .scaleAspectFill
        bannerImageView.clipsToBounds = true
        bannerImageView.translatesAutoresizingMaskIntoConstraints = false
        overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
        overlayView.translatesAutoresizingMaskIntoConstraints = false
        
        logoImageView.image = UIImage(named: "")
        logoImageView.contentMode = .scaleAspectFill
        logoImageView.translatesAutoresizingMaskIntoConstraints = false
        logoImageView.layer.cornerRadius = 28
        logoImageView.clipsToBounds = true
        
        containerView.addSubview(storeDetailsView)
        storeDetailsView.translatesAutoresizingMaskIntoConstraints = false
        
        // Constraints for Banner and Details
        NSLayoutConstraint.activate([
            bannerImageView.topAnchor.constraint(equalTo: containerView.topAnchor),
            bannerImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            bannerImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            bannerImageView.heightAnchor.constraint(equalToConstant: 280),
            
            overlayView.leadingAnchor.constraint(equalTo: bannerImageView.leadingAnchor),
            overlayView.trailingAnchor.constraint(equalTo: bannerImageView.trailingAnchor),
            overlayView.topAnchor.constraint(equalTo: bannerImageView.topAnchor),
            overlayView.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor),
            
            logoImageView.topAnchor.constraint(equalTo: bannerImageView.bottomAnchor, constant: -28),
            logoImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
            logoImageView.widthAnchor.constraint(equalToConstant: 56),
            logoImageView.heightAnchor.constraint(equalToConstant: 56),
            
            storeDetailsView.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 8),
            storeDetailsView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            storeDetailsView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
        ])
        
        storeDetailsView.reviewTapped = { [weak self] in
            self?.presentAllReviewsAndFeedbackPage()
        }
        
        storeDetailsView.infoTapped = { [weak self] in
            self?.fetchOpeningHours()
        }
    }
    
    public func fetchOpeningHours(){
       if let id = vendor?.id {
            let fetcher = VendorFetcherS()
            self.showHUD(true)
            fetcher.fetchUpdatedVendorOpeningHours(id.intValue, success: { (array) in
                DispatchQueue.main.async {
                    
                    //TODO open another view controller
                    let daysTimingViewController = DaysTimingViewController()
                    daysTimingViewController.daysTiming = array ?? []
                    if #available(iOS 15.0, *) {
                        if let sheet = daysTimingViewController.sheetPresentationController {
                            sheet.detents = [.medium()]
                            sheet.prefersGrabberVisible = true
                         }
                    } else {
                        daysTimingViewController.modalPresentationStyle = .automatic
                    }
                    self.showHUD( false)
                  
                    self.present(daysTimingViewController,animated: true)
                    
                }
              
                //       self.fetchMenuList(showBy: MENULIST_SHOW_BY_CATEGORY)
            }) { (error) in
                DispatchQueue.main.async {
                    self.showHUD(false)
                    self.alert(message: error)
                }
            }
        }
       
    }
    
    private func setUpDivider() {
        containerView.addSubview(divider)
        divider.translatesAutoresizingMaskIntoConstraints = false
        divider.backgroundColor = .systemGray6
        NSLayoutConstraint.activate([
            divider.topAnchor.constraint(equalTo:storeDetailsView.bottomAnchor, constant: 16),
            divider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor,constant: 0),
            divider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor,constant: -0),
            divider.heightAnchor.constraint(equalToConstant: 4)
        ])
        
    }
    
    private func setUpProductHeaderView() {
        containerView.addSubview(productHeaderView)
        productHeaderView.translatesAutoresizingMaskIntoConstraints = false
        productHeaderTopConstraint = productHeaderView.topAnchor.constraint(equalTo: divider.bottomAnchor, constant: 16)
        productHeaderTopConstraint.isActive = true
        productHeaderView.filterBtnTapped = { [weak self] in
            guard let self else {return }
            if (self.hasFilterFetched) {
                self.openFilterViewController()
            }else{
                showHUD(true)
                vendorFetcherS.getFilters(success: { [weak self] filterResponse in
                    guard let self = self else { return }
                    DispatchQueue.main.async {
                        self.showHUD(false)
                        self.filterData = filterResponse
                        
                        if var sortingOptions = self.filterData?.sortingOptions {
                            sortingOptions = sortingOptions.map { option in
                                var updatedOption = option
                                if updatedOption.title?.lowercased() == "relevance" {
                                    updatedOption.isSelected = true
                                }
                                return updatedOption
                            }
                            self.filterData?.sortingOptions = sortingOptions
                        }
                        
                        self.hasFilterFetched = true
                        self.openFilterViewController()
                    }
                }, failure: { [weak self] errorMessage in
                    DispatchQueue.main.async {
                        self?.showHUD(false)
                        self?.hasFilterFetched = false
                        self?.alert(message: errorMessage)
                    }
                })

            }
            
        }
        productHeaderView.quickNavigationTapped = { [weak self] in
            
            let quickNavigationVC = QuickNavigationViewController()
            
            if #available(iOS 15.0, *) {
                if let sheet = quickNavigationVC.sheetPresentationController {
                    sheet.detents = [.medium(),.large()]
                    sheet.prefersGrabberVisible = true
                 }
            } else {
                quickNavigationVC.modalPresentationStyle = .automatic
            }
            quickNavigationVC.categories = self?.categories ?? []
            quickNavigationVC.getSelectedIds = { catId,subId in
                guard let pager = self?.pagerTabStripVC else {return }
                pager.moveToCategory(withId: catId, withSubId: subId)
              }
            self?.present(quickNavigationVC, animated: true)
        }
        
        NSLayoutConstraint.activate([
            productHeaderView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            productHeaderView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            productHeaderView.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
    
    private func openFilterViewController() {
        let filterVC = FilterViewController()
        if #available(iOS 15.0, *) {
            if let sheet = filterVC.sheetPresentationController {
                sheet.detents = [.medium(), .large()]
                sheet.prefersGrabberVisible = true
             }
        } else {
            filterVC.modalPresentationStyle = .automatic
        }
        filterVC.filterData = self.filterData
        filterVC.onDismiss = { [weak self] filterCount, sortedList, brandList, isOn in
            guard let self else {return}
            let selectedSortingOption = sortedList.first(where: { $0.isSelected })?.shortCode ?? "REL"
            let selectedBrandIds = brandList.filter { $0.isCompleted }.compactMap { $0.id }
            productHeaderView.badgeCount = filterCount
            self.filterData?.brandList = brandList
            self.filterData?.sortingOptions = sortedList
            self.filterData?.showSoldOutItemsAtBottom = isOn
            self.fetchCategories(sortingOption: selectedSortingOption,showSoldOutItemsAtBottom: isOn,selectedBrandIds: selectedBrandIds)
        }
        
        self.present(filterVC, animated: true)
    }
    
    func setupTabBarView() {
        containerView.addSubview(tabView)
        tabView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            tabView.topAnchor.constraint(equalTo: productHeaderView.bottomAnchor),
            tabView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            tabView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            tabView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
            //            tabView.heightAnchor.constraint(equalToConstant:720)
            tabView.heightAnchor.constraint(equalToConstant:600)
        ])
    }
    
    // Add temporary content to test scrolling
    private func addTemporaryContent() {
        let tempView = UIView()
        tempView.translatesAutoresizingMaskIntoConstraints = false
        tempView.backgroundColor = .lightGray
        containerView.addSubview(tempView)
        NSLayoutConstraint.activate([
            tempView.topAnchor.constraint(equalTo: divider.bottomAnchor, constant: 16),
            tempView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            tempView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            tempView.heightAnchor.constraint(equalToConstant: 800), // Arbitrary height to make it scrollable
            tempView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) // Make sure containerView height adjusts
        ])
    }
    
    private func setupPagerTabStrip() {
        pagerTabStripVC = MyPagerTabStripViewController()
        pagerTabStripVC?.vendorId = vendor?.id
        pagerTabStripVC?.filterData = self.filterData
        if let pagerTabStripVC {
            addChild(pagerTabStripVC)
            tabView.addSubview(pagerTabStripVC.view)
            tabView.backgroundColor = .white
            tabView.isHidden = true
            pagerTabStripVC.didMove(toParent: self)
            
            pagerTabStripVC.view.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                pagerTabStripVC.view.topAnchor.constraint(equalTo: productHeaderView.bottomAnchor),
                pagerTabStripVC.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
                pagerTabStripVC.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
                pagerTabStripVC.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
            ])
            pagerTabStripVC.collectionViewDidScroll = { [weak self] (scrollView, collectionView) in
                guard let self = self else { return }
                
                
                let goingUp = scrollView.panGestureRecognizer.translation(in: scrollView).y < 0
                
                let parentViewMaxContentYOffset = self.scrollView.contentSize.height - self.scrollView.frame.height
                
                UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut], animations: {
                    if goingUp {
                        if scrollView == collectionView {
                            if self.scrollView.contentOffset.y < parentViewMaxContentYOffset && !self.childScrollingDownDueToParent {
                                self.scrollView.contentOffset.y = max(min(self.scrollView.contentOffset.y + collectionView.contentOffset.y, parentViewMaxContentYOffset), 0)
                                collectionView.contentOffset.y = 0
                            }
                        }
                    } else {
                        if scrollView == collectionView {
                            if collectionView.contentOffset.y <= 0 && self.scrollView.contentOffset.y >= 0 {
                                self.scrollView.contentOffset.y = max(self.scrollView.contentOffset.y - abs(collectionView.contentOffset.y), 0)
                                //self.scrollView.contentOffset.y = 0
                            }
                        }
                        if scrollView == self.scrollView {
                            
                            if collectionView.contentOffset.y >= 0 && self.scrollView.contentOffset.y <= parentViewMaxContentYOffset {
                                self.childScrollingDownDueToParent = true
                                collectionView.contentOffset.y = max(collectionView.contentOffset.y - (parentViewMaxContentYOffset - self.scrollView.contentOffset.y), 0)
                                self.scrollView.contentOffset.y = parentViewMaxContentYOffset
                                self.childScrollingDownDueToParent = false
                            }
                        }
                    }
                }, completion: nil)
            }
        }
        
    }
    
    private func setUpViewBasket() {
        view.addSubview(basketView)
        basketView.translatesAutoresizingMaskIntoConstraints = false
        basketView.isHidden = true
        basketView.viewBasketTapped = { [weak self] in
            guard let self else {return}
            if(self.cart != nil){
                let controller = Initializer.controller("ShoppingCartController") as! ShoppingCartController
                controller.vendorId = cart?.vendorId
                controller.modalPresentationStyle = .overFullScreen
                present(EKNavigationController(rootViewController: controller), animated: true, completion: nil)
            }
        }
        NSLayoutConstraint.activate([
            basketView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            basketView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            basketView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
        ])
        refreshCheckOutBar()
    }
    
    @objc func refreshCheckOutBar(){
       guard let vendorId = vendor?.id else {return}
        cart = Cart(vendorId: vendorId)
        if(cart != nil){
            basketView.isHidden = false
            basketView.cart = cart
            
        }else{
            basketView.isHidden = true
        }
    }
    
    private func presentAllReviewsAndFeedbackPage() {
        let sb = UIStoryboard(name: "ViewAllReviewsAndFeedback", bundle: nil)
        if let vc = sb.instantiateViewController(withIdentifier: "ViewAllReviewsAndFeedbackViewController") as? ViewAllReviewsAndFeedbackViewController {
            vc.vendorId = self.vendor?.id?.intValue
            vc.modalPresentationStyle = .fullScreen
            present(vc, animated: true)
        }
    }
}

extension StoreViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let bottom = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
        
        let offset = scrollView.contentOffset.y
        print("OFFSET \(offset)")
        if offset > 100 { // Adjust this threshold as needed
            UIView.animate(withDuration: 0.3) {
                self.customNavigationBar.backgroundColor = .white
                if bottom > 0{
                    self.navBarBackgroundView.backgroundColor = .white
                }
                
            }
            
        } else {
            UIView.animate(withDuration: 0.3) {
                self.customNavigationBar.backgroundColor = .clear
                if bottom > 0{
                    self.navBarBackgroundView.backgroundColor = .clear
                }
            }
        }
        
        
    }
}

Upvotes: 0

Views: 32

Answers (0)

Related Questions