Skip to main content
The user profile feature displays user information with an Instagram/TikTok-style interface featuring a stretchy header image and grid of videos.

Architecture

The profile screen uses a UICollectionView with custom layout:
  • ProfileViewController: Main controller managing the collection view
  • ProfileHeaderView: Reusable header with user info and cache clearing
  • ProfileCollectionViewFlowLayout: Custom layout for stretchy header effect
  • ProfileBackgroundImage: Parallax background image

Collection View Setup

The collection view is configured with a custom flow layout:
ProfileViewController.swift:35-57
func setupView(){
    self.view.backgroundColor = .Background
    
    // Collection View
    let collectionViewLayout = ProfileCollectionViewFlowLayout(navBarHeight: getStatusBarHeight())
    collectionViewLayout.minimumLineSpacing = 1;
    collectionViewLayout.minimumInteritemSpacing = 0;
    collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
    collectionView.translatesAutoresizingMaskIntoConstraints = false
    collectionView.backgroundColor = .clear
    collectionView.contentInsetAdjustmentBehavior = .never
    collectionView.alwaysBounceVertical = true
    collectionView.showsVerticalScrollIndicator = false
    collectionView.delegate = self
    collectionView.dataSource = self
    view.addSubview(collectionView)
    collectionView.snp.makeConstraints({make in
        make.edges.equalToSuperview()
    })
    collectionView.register(UINib(nibName: "ProfileHeaderView", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: PROFILE_HEADER_ID)
    collectionView.register(ProfileSlideBarView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SLIDEBAR_ID)
    collectionView.register(ProfileCollectionViewCell.self, forCellWithReuseIdentifier: CELLID)
    collectionView.layoutIfNeeded()
}

Stretchy Header Effect

The profile background image stretches and scales as the user scrolls:

Background Image Setup

ProfileViewController.swift:59-68
// Profile Background Image view
profileBackgroundImgView = UIImageView(image: #imageLiteral(resourceName: "ProfileBackground"))
profileBackgroundImgView.translatesAutoresizingMaskIntoConstraints = false
profileBackgroundImgView.contentMode = .scaleAspectFill
profileBackgroundImgView.alpha = 0.6
self.view.insertSubview(profileBackgroundImgView, belowSubview: collectionView)
profileBackgroundImgView.snp.makeConstraints({ make in
    make.top.left.right.equalToSuperview()
    make.height.equalTo(150)
})

Scroll Transform Logic

The stretchy header effect is implemented in the scroll delegate:
ProfileViewController.swift:147-164
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    /// Y offsets of the scroll view
    let offsetY = scrollView.contentOffset.y
    if offsetY < 0 {
        stretchProfileBackgroundWhenScroll(offsetY: offsetY)
    } else {
        profileBackgroundImgView.transform = CGAffineTransform(translationX: 0, y: -offsetY)
    }
}

// Stretch Profile Background Image when scroll up
func stretchProfileBackgroundWhenScroll(offsetY: CGFloat)  {
    let scaleRatio: CGFloat = abs(offsetY)/500.0
    let scaledHeight: CGFloat = scaleRatio * profileBackgroundImgView.frame.height
    profileBackgroundImgView.transform = CGAffineTransform.init(scaleX: scaleRatio + 1.0, y: scaleRatio + 1.0).concatenating(CGAffineTransform.init(translationX: 0, y: scaledHeight))
}
When offsetY < 0 (scrolling down past the top), the image scales up. When offsetY > 0 (scrolling up), the image translates to create a parallax effect.

Collection View Layout

Two-Section Layout

The profile has two sections:
  1. Section 0: Profile header with user info
  2. Section 1: Slide bar and video grid
ProfileViewController.swift:89-98
func numberOfSections(in collectionView: UICollectionView) -> Int {
    return 2
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    if section == 1 {
        return 20 //TODO: Fetch Data then change this
    }
    return 0
}

Header Sizes

ProfileViewController.swift:126-135
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
    switch section {
    case 0:
        return CGSize.init(width: ScreenSize.Width, height: 420)
    case 1:
        return CGSize.init(width: ScreenSize.Width, height: 42)
    default:
        return .zero
    }
}

Cell Sizing

Video cells are sized in a 3-column grid:
ProfileViewController.swift:137-141
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let itemWidth = (ScreenSize.Width - CGFloat(Int(ScreenSize.Width) % 3)) / 3.0 - 1.0
    let itemHeight =  itemWidth * 1.3
    return CGSize.init(width: itemWidth, height: itemHeight)
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    if indexPath.section == 0 {
        if kind == UICollectionView.elementKindSectionHeader {
            let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: PROFILE_HEADER_ID, for: indexPath) as! ProfileHeaderView
            profileHeader = header
            return header
        }
    }
    
    if indexPath.section == 1{
        if kind == UICollectionView.elementKindSectionHeader{
            let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SLIDEBAR_ID, for: indexPath) as! ProfileSlideBarView
            return header
        }
    }
    return UICollectionReusableView.init()
}

Clear Cache Functionality

The profile header includes a button to clear the video cache:
ProfileHeaderView.swift:38-44
@IBAction func clearCache(_ sender: Any) {
    VideoCacheManager.shared.clearCache(completion: {size in
        let message = "Remove Cache Size: " + size + "MB"
        ProfileViewModel.shared.displayMessage(message: message)
        ProfileViewModel.shared.cleardCache.onNext(true)
    })
}
The cache clearing operation is performed asynchronously and returns the size of cleared data in MB. Always update the UI on the main thread after cache operations.

Reactive UI Updates

The profile uses RxSwift for reactive updates:
ProfileViewController.swift:73-80
func setupBindings(){
    ProfileViewModel.shared.displayAlertMessage
        .asObserver()
        .observeOn(MainScheduler.instance)
        .subscribe(onNext: { message in
            self.showAlert(message)
        }).disposed(by: disposeBag)
}

Key Features

  • Stretchy header: Background image scales when pulled down
  • Parallax effect: Header image moves with scroll
  • Grid layout: Videos displayed in 3-column grid
  • Cache management: One-tap cache clearing with size display
  • Custom layout: ProfileCollectionViewFlowLayout for nav bar handling
  • Reactive bindings: RxSwift for ViewModel communication
  • Rounded profile image: Circular profile picture with border
1
Creating a Similar Profile UI
2
  • Create a UICollectionView with custom flow layout
  • Configure two sections: header and content
  • Add a background image view behind the collection view
  • Implement scrollViewDidScroll for stretchy header effect
  • Calculate cell sizes based on screen width for grid layout
  • Add cache clearing functionality in the header
  • Build docs developers (and LLMs) love