The app uses several custom view components to create the TikTok-like user interface with specialized interactions and animations.
Full-screen table view cell displaying a single video with interaction controls.
Location: Modules/Home/HomeTableViewCell.swift:18
Key Features
- Embedded video player with looping
- Like animation with heart gesture
- Pause/play tap gesture
- Comment pop-up integration
- Profile navigation
- Marquee scrolling music label
Properties
Custom video player view managing AVPlayer
User profile image with tap gesture for navigation
Button displaying author username
Video caption/description text
Scrolling label showing background music name
Like button with heart icon
Opens comment pop-up view
Current playback state (read-only)
Whether user has liked the video
Configuration
override func awakeFromNib() {
super.awakeFromNib()
selectionStyle = .none
playerView = VideoPlayerView(frame: self.contentView.frame)
musicLbl.holdScrolling = true
musicLbl.animationDelay = 0
contentView.addSubview(playerView)
contentView.sendSubviewToBack(playerView)
let pauseGesture = UITapGestureRecognizer(target: self,
action: #selector(handlePause))
self.contentView.addGestureRecognizer(pauseGesture)
let likeDoubleTapGesture = UITapGestureRecognizer(target: self,
action: #selector(handleLikeGesture(sender:)))
likeDoubleTapGesture.numberOfTapsRequired = 2
self.contentView.addGestureRecognizer(likeDoubleTapGesture)
pauseGesture.require(toFail: likeDoubleTapGesture)
}
func configure(post: Post) {
self.post = post
nameBtn.setTitle("@" + post.autherName, for: .normal)
nameBtn.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold)
musicLbl.text = post.music + " " + post.music + " " + post.music
captionLbl.text = post.caption
likeCountLbl.text = post.likeCount.shorten()
shareCountLbl.text = post.shareCount.shorten()
playerView.configure(url: post.videoURL,
fileExtension: post.videoFileExtension,
size: (post.videoWidth, post.videoHeight))
}
Playback Control
func play() {
if !isPlaying {
playerView.play()
musicLbl.holdScrolling = false
isPlaying = true
}
}
func pause() {
if isPlaying {
playerView.pause()
musicLbl.holdScrolling = true
isPlaying = false
}
}
func replay() {
if !isPlaying {
playerView.replay()
play()
}
}
Like Animation
Double-tap gesture creates an animated heart at tap location:
@objc func handleLikeGesture(sender: UITapGestureRecognizer) {
let location = sender.location(in: self)
let heartView = UIImageView(image: UIImage(systemName: "heart.fill"))
heartView.tintColor = .red
let width: CGFloat = 110
heartView.contentMode = .scaleAspectFit
heartView.frame = CGRect(x: location.x - width / 2,
y: location.y - width / 2,
width: width, height: width)
heartView.transform = CGAffineTransform(rotationAngle:
CGFloat.random(in: -CGFloat.pi * 0.2...CGFloat.pi * 0.2))
self.contentView.addSubview(heartView)
UIView.animate(withDuration: 0.3, delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 3,
options: [.curveEaseInOut],
animations: {
heartView.transform = heartView.transform.scaledBy(x: 0.85, y: 0.85)
}, completion: { _ in
UIView.animate(withDuration: 0.4, delay: 0.1,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 3,
options: [.curveEaseInOut],
animations: {
heartView.transform = heartView.transform.scaledBy(x: 2.3, y: 2.3)
heartView.alpha = 0
}, completion: { _ in
heartView.removeFromSuperview()
})
})
likeVideo()
}
Pause Toggle Animation
@objc func handlePause() {
if isPlaying {
UIView.animate(withDuration: 0.075, delay: 0,
options: .curveEaseIn, animations: { [weak self] in
guard let self = self else { return }
self.pauseImgView.alpha = 0.35
self.pauseImgView.transform = CGAffineTransform(scaleX: 0.45, y: 0.45)
}, completion: { [weak self] _ in
self?.pause()
})
} else {
UIView.animate(withDuration: 0.075, delay: 0,
options: .curveEaseInOut, animations: { [weak self] in
guard let self = self else { return }
self.pauseImgView.alpha = 0
}, completion: { [weak self] _ in
self?.play()
self?.pauseImgView.transform = .identity
})
}
}
Modal bottom sheet displaying video comments with drag-to-dismiss gesture.
Location: Modules/Home/CommentPopUpView.swift:12
Key Features
- Bottom sheet modal presentation
- Pan gesture for dismissal
- Blur effect background
- Comment table view
- Rounded top corners
Properties
Dimmed background that dismisses popup when tapped
Main container with rounded corners and blur effect
Table view displaying comment cells
Tracks pan gesture distance for dismissal logic
Implementation
Initialization
Presentation
Pan Gesture
init() {
super.init(frame: CGRect(x: 0, y: ScreenSize.Height,
width: ScreenSize.Width,
height: ScreenSize.Height))
setupView()
}
func setupView() {
panGesture = UIPanGestureRecognizer(target: self,
action: #selector(animatePopUpView(sender:)))
// Background
backgroundView = UIView(frame: self.bounds)
backgroundView.backgroundColor = .clear
backgroundView.isUserInteractionEnabled = true
addSubview(backgroundView)
let tapGesture = UITapGestureRecognizer(target: self,
action: #selector(handleDismiss(sender:)))
backgroundView.addGestureRecognizer(tapGesture)
// Pop Up View
popUpView = UIView(frame: CGRect(x: 0, y: ScreenSize.Height * 0.25,
width: ScreenSize.Width,
height: ScreenSize.Height * 0.75))
popUpView.backgroundColor = UIColor.black.withAlphaComponent(0.9)
addSubview(popUpView)
popUpView.addGestureRecognizer(panGesture)
}
@objc func show() {
guard let windowScene = UIApplication.shared.connectedScenes.first
as? UIWindowScene,
let sceneDelegate = windowScene.delegate as? SceneDelegate
else {
return
}
sceneDelegate.window?.addSubview(self)
UIView.animate(withDuration: 0.25, delay: 0.0,
options: .curveEaseOut, animations: {
self.frame.origin.y = 0
})
}
@objc func dismiss() {
UIView.animate(withDuration: 0.25, delay: 0.0,
options: .curveEaseIn, animations: {
self.frame.origin.y = ScreenSize.Height
}) { finished in
self.removeFromSuperview()
}
}
@objc func animatePopUpView(sender: UIPanGestureRecognizer) {
let transition = sender.translation(in: popUpView)
switch sender.state {
case .began, .changed:
if totalSlidingDistance <= 0 && transition.y < 0 { return }
if self.frame.origin.y + transition.y >= 0 {
self.frame.origin.y += transition.y
sender.setTranslation(.zero, in: popUpView)
totalSlidingDistance += transition.y
}
case .ended:
if sender.velocity(in: popUpView).y > 300 {
dismiss()
} else if totalSlidingDistance >= 0 {
UIView.animate(withDuration: 0.2, delay: 0,
options: [.curveEaseOut], animations: {
self.frame.origin.y -= self.totalSlidingDistance
self.layoutIfNeeded()
})
}
commentTableView.isUserInteractionEnabled = true
totalSlidingDistance = 0
default:
UIView.animate(withDuration: 0.2, delay: 0,
options: [.curveEaseOut], animations: {
self.frame.origin.y -= self.totalSlidingDistance
self.layoutIfNeeded()
})
commentTableView.isUserInteractionEnabled = true
totalSlidingDistance = 0
}
}
Custom animated recording button with visual feedback.
Location: Modules/Media/Views/RecordButton.swift:11
Key Features
- Two-layer animation system
- Transforms from circle to rounded square
- Color and scale animations
- Movable during recording
Properties
Outer border ring that scales and changes color
Inner fill layer that transforms to rounded square
Base radius for circular shape (default: 45)
Duration of animations (default: 0.35)
Implementation
Setup
Start Recording
Stop Recording
override func awakeFromNib() {
super.awakeFromNib()
originalCenter = self.center
outerLayer = CAShapeLayer()
outerLayer.frame = self.bounds
outerLayer.cornerRadius = originalRadius
outerLayer.backgroundColor = UIColor.clear.cgColor
outerLayer.borderColor = UIColor.Red.withAlphaComponent(0.5).cgColor
outerLayer.borderWidth = outerLineWidth
self.layer.addSublayer(outerLayer)
innerLayer = CAShapeLayer()
let offset = outerLineWidth + spacing
innerLayer.frame = CGRect(x: offset, y: offset,
width: (originalRadius - offset) * 2,
height: (originalRadius - offset) * 2)
innerLayer.cornerRadius = originalRadius - offset
innerLayer.backgroundColor = UIColor.Red.cgColor
self.layer.addSublayer(innerLayer)
}
func startRecordingAnimation() {
removeAnimations()
let outerBorderColorAnimation = RecordAnimation(keyPath: "borderColor")
outerBorderColorAnimation.fromValue = outerLayer.borderColor
outerBorderColorAnimation.toValue = UIColor.Red.cgColor
let outerScaleAnimation = RecordAnimation(keyPath: "transform.scale")
outerScaleAnimation.fromValue = 1
outerScaleAnimation.toValue = 1.5
outerLayer.add(outerBorderColorAnimation,
forKey: "outerBackgroundColorAnimation")
outerLayer.add(outerScaleAnimation, forKey: "outerScaleAnimation")
let innerCornerRadiusAnimation = RecordAnimation(keyPath: "cornerRadius")
innerCornerRadiusAnimation.fromValue = innerLayer.cornerRadius
innerCornerRadiusAnimation.toValue = CGFloat(5)
let innerScaleAnimation = RecordAnimation(keyPath: "transform.scale")
innerScaleAnimation.fromValue = 1
innerScaleAnimation.toValue = 0.5
innerLayer.add(innerCornerRadiusAnimation,
forKey: "innerCornerRadiusAnimation")
innerLayer.add(innerScaleAnimation, forKey: "innerScaleAnimation")
}
func stopRecodingAnimation() {
let outerBorderColorAnimation2 = RecordAnimation(keyPath: "borderColor")
outerBorderColorAnimation2.fromValue = UIColor.Red.cgColor
outerBorderColorAnimation2.toValue = UIColor.Red.withAlphaComponent(0.5).cgColor
let outerScaleAnimation2 = RecordAnimation(keyPath: "transform.scale")
outerScaleAnimation2.fromValue = 1.5
outerScaleAnimation2.toValue = 1
outerLayer.add(outerBorderColorAnimation2,
forKey: "outerBackgroundColorAnimation2")
outerLayer.add(outerScaleAnimation2, forKey: "outerScaleAnimation2")
let innerCornerRadiusAnimation2 = RecordAnimation(keyPath: "cornerRadius")
innerCornerRadiusAnimation2.fromValue = CGFloat(5)
innerCornerRadiusAnimation2.toValue = CGFloat(originalRadius - outerLineWidth - spacing)
let innerScaleAnimation2 = RecordAnimation(keyPath: "transform.scale")
innerScaleAnimation2.fromValue = 0.5
innerScaleAnimation2.toValue = 1
innerLayer.add(innerCornerRadiusAnimation2,
forKey: "innerCornerRadiusAnimation2")
innerLayer.add(innerScaleAnimation2, forKey: "innerScaleAnimation2")
UIView.animate(withDuration: TimeInterval(animationDuration), animations: {
self.center = self.originalCenter!
})
}
func locationChanged(location: CGPoint) {
self.center = location
}
RecordAnimation Helper
class RecordAnimation: CABasicAnimation {
override init() {
super.init()
duration = 0.7
fillMode = .forwards
isRemovedOnCompletion = false
}
}
VideoPlayerView
Custom video player with caching and streaming capabilities.
Location: Modules/Home/VideoPlayerView.swift:13
Key Features
- AVQueuePlayer with looping
- Video caching system
- Progressive streaming
- Custom resource loading
- Aspect ratio handling
Properties
AVFoundation player for video playback
Layer displaying video content
Handles seamless video looping
URL of the video (local or streaming)
Configuration
func configure(url: URL?, fileExtension: String?, size: (Int, Int)) {
// Adjust aspect ratio based on video dimensions
avPlayerLayer.videoGravity = (size.0 < size.1) ? .resizeAspectFill : .resizeAspect
guard let url = url else { return }
self.fileExtension = fileExtension
VideoCacheManager.shared.queryURLFromCache(key: url.absoluteString,
fileExtension: fileExtension) { [weak self] data in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if let path = data as? String {
self.videoURL = URL(fileURLWithPath: path)
} else {
guard let redirectUrl = url.convertToRedirectURL(scheme: "streaming") else {
return
}
self.videoURL = redirectUrl
}
self.originalURL = url
self.asset = AVURLAsset(url: self.videoURL!)
self.asset!.resourceLoader.setDelegate(self, queue: .main)
self.playerItem = AVPlayerItem(asset: self.asset!)
self.addObserverToPlayerItem()
if let queuePlayer = self.queuePlayer {
queuePlayer.replaceCurrentItem(with: self.playerItem)
} else {
self.queuePlayer = AVQueuePlayer(playerItem: self.playerItem)
}
self.playerLooper = AVPlayerLooper(player: self.queuePlayer!,
templateItem: self.queuePlayer!.currentItem!)
self.avPlayerLayer.player = self.queuePlayer
}
}
}
AccessPermissionView
Permission request UI for camera and microphone access.
Location: Modules/Media/Views/AccessPermissionView.swift:14
Properties
Button to request camera permission
Button to request microphone permission
Close button to dismiss view
State Management
func cameraAccessPermitted() {
cameraAccessBtn.setTitleColor(.gray, for: .normal)
cameraAccessBtn.isEnabled = false
}
func microphoneAccessPermitted() {
microphoneAccessBtn.setTitleColor(.gray, for: .normal)
microphoneAccessBtn.isEnabled = false
}
Profile header displaying user information and stats.
Location: Modules/Profile/ProfileHeaderView.swift:12
Properties
Circular profile picture with border
Button to clear video cache
Cache Management
@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)
})
}