Skip to main content

Overview

ProfileViewController displays a user’s profile with a sticky header showing profile information, a slide bar for content filtering, and a grid collection view of the user’s posted videos. It features a parallax stretching effect on the background image when scrolling. Location: KD Tiktok-Clone/Modules/Profile/ProfileViewController.swift

Key Components

UI Components

collectionView
UICollectionView
Main collection view displaying user videos in a 3-column grid layout with custom flow layout for header parallax effects.
profileHeader
ProfileHeaderView
Header view containing profile picture, follower counts, bio, and action buttons (Follow, Message, etc.).
profileBackgroundImgView
UIImageView
Background image view positioned behind the collection view with stretchy parallax behavior on scroll.

Properties

CELLID
String
Reuse identifier for profile collection view cells (“ProfileCell”).
PROFILE_HEADER_ID
String
Reuse identifier for profile header view (“ProfileHeader”).
SLIDEBAR_ID
String
Reuse identifier for slide bar header view (“ProfileSlideBar”).
disposeBag
DisposeBag
RxSwift dispose bag for managing observable subscriptions.

Core Methods

setupView()

Configures the collection view with custom layout and background image.
func setupView() {
    self.view.backgroundColor = .Background
    
    // Collection View with custom layout
    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()
    })
    
    // Register cells and headers
    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
    )
    
    // Profile Background Image
    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)
    })
}
The custom ProfileCollectionViewFlowLayout enables parallax effects and sticky header behavior.

setupBindings()

Establishes RxSwift bindings to the ProfileViewModel for alert messages.
func setupBindings() {
    ProfileViewModel.shared.displayAlertMessage
        .asObserver()
        .observeOn(MainScheduler.instance)
        .subscribe(onNext: { message in
            self.showAlert(message)
        }).disposed(by: disposeBag)
}

Collection View Implementation

UICollectionViewDataSource

numberOfSections(in:)
func
Returns 2 sections: section 0 for profile header, section 1 for video grid.
func numberOfSections(in collectionView: UICollectionView) -> Int {
    return 2
}
numberOfItemsInSection
func
Returns 0 items for header section, 20 for video grid (placeholder count).
func collectionView(_ collectionView: UICollectionView, 
                    numberOfItemsInSection section: Int) -> Int {
    if section == 1 {
        return 20 // TODO: Fetch Data then change this
    }
    return 0
}
cellForItemAt
func
Dequeues and returns profile collection view cells.
func collectionView(_ collectionView: UICollectionView,
                    cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(
        withReuseIdentifier: CELLID,
        for: indexPath
    )
    return cell
}

Supplementary Views

viewForSupplementaryElementOfKind
func
Provides header views for both sections.
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()
}

UICollectionViewDelegateFlowLayout

referenceSizeForHeaderInSection

Defines header sizes for each section.
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
    }
}
Section 0 header is 420pt tall for profile information, section 1 is 42pt for the slide bar.

sizeForItemAt

Calculates cell size for the 3-column grid layout.
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)
}
Cells maintain a 1:1.3 aspect ratio (width:height) typical of vertical videos.

Scroll Effects

scrollViewDidScroll(_:)

Implements parallax and stretching effects on the background image.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let offsetY = scrollView.contentOffset.y
    
    if offsetY < 0 {
        // Stretch when pulling down
        stretchProfileBackgroundWhenScroll(offsetY: offsetY)
    } else {
        // Parallax when scrolling up
        profileBackgroundImgView.transform = CGAffineTransform(
            translationX: 0,
            y: -offsetY
        )
    }
}

stretchProfileBackgroundWhenScroll(offsetY:)

Creates the stretchy header effect when scrolling down.
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))
}
The stretchy effect uses a scale ratio of abs(offsetY) / 500.0 for smooth expansion.

Parallax Effect Breakdown

Scrolling Up (offsetY > 0)

Background image translates upward in sync with scroll, creating a parallax effect:
profileBackgroundImgView.transform = CGAffineTransform(translationX: 0, y: -offsetY)

Scrolling Down (offsetY < 0)

Background image scales and stretches, creating a “rubber band” effect:
// Calculate scale based on pull distance
let scaleRatio = abs(offsetY) / 500.0

// Apply scale transform
let scaleTransform = CGAffineTransform(scaleX: scaleRatio + 1.0, y: scaleRatio + 1.0)

// Add translation to keep it centered
let scaledHeight = scaleRatio * profileBackgroundImgView.frame.height
let translationTransform = CGAffineTransform(translationX: 0, y: scaledHeight)

// Combine transforms
profileBackgroundImgView.transform = scaleTransform.concatenating(translationTransform)

Layout Structure

Section 0: Profile Header

  • Height: 420pt
  • Content: Profile picture, username, follower/following counts, bio, action buttons
  • Behavior: Scrolls with content

Section 1: Content Grid

  • Header Height: 42pt (slide bar for filtering: Posts, Likes, Private)
  • Cell Layout: 3 columns with 1pt spacing
  • Cell Aspect Ratio: 1:1.3 (width:height)
  • Content: User’s posted videos in thumbnail grid

Usage Example

// Initialize ProfileViewController
let profileVC = ProfileViewController()

// Push to navigation stack
navigationController?.pushViewController(profileVC, animated: true)

// Handle scroll effects
extension ProfileViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetY = scrollView.contentOffset.y
        
        if offsetY < 0 {
            // Stretch effect when pulling down
            stretchProfileBackgroundWhenScroll(offsetY: offsetY)
        } else {
            // Parallax effect when scrolling up
            profileBackgroundImgView.transform = CGAffineTransform(
                translationX: 0,
                y: -offsetY
            )
        }
    }
}

Custom Layout

ProfileCollectionViewFlowLayout

Custom UICollectionViewFlowLayout subclass that handles:
  • Sticky header behavior for the slide bar
  • Parallax scrolling for the profile header
  • Navigation bar height compensation
let collectionViewLayout = ProfileCollectionViewFlowLayout(navBarHeight: getStatusBarHeight())
collectionViewLayout.minimumLineSpacing = 1
collectionViewLayout.minimumInteritemSpacing = 0

Dependencies

  • UIKit: Core UI framework
  • SnapKit: Auto Layout DSL
  • RxSwift: Reactive data binding
  • ProfileHeaderView: Custom header with profile information
  • ProfileSlideBarView: Tab bar for filtering content
  • ProfileCollectionViewCell: Grid cell for video thumbnails
  • ProfileCollectionViewFlowLayout: Custom layout with parallax support
  • ProfileViewModel: Shared view model managing profile data

Build docs developers (and LLMs) love