Reputation: 39
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.
When the user scrolls up, the productHeaderView and TabView should stick below the custom navigation bar.
When the user scrolls down, the productHeaderView and TabView should behave normally, i.e., scrolling back down as the content moves.
This should replicate the scrolling effect found on the profile screen of the Twitter/X iOS app, where the profile info sticks below the navigation bar, and tabs stick to the top.
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