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()
}
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)
})
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:
- Section 0: Profile header with user info
- 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
}
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
Creating a Similar Profile UI
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