- Handled inline play play pause.

- Created a enum to handle the play pause resume functionality
- Added tap handler with animation for cells.
- Made logic if one audio is playing and then other audio plays , it ill deinit the first audio and play second.
- Added deinit to the view controller, if the view dismiss the play will stop and denitialize
This commit is contained in:
2024-06-11 19:49:10 +05:30
parent 5a7750d012
commit 5164f2fe10
7 changed files with 388 additions and 43 deletions

View File

@@ -73,6 +73,7 @@
527AC6FA2C17387300434FB7 /* SongBlogDM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527AC6F92C17387300434FB7 /* SongBlogDM.swift */; };
527AC6FD2C173A5100434FB7 /* SongListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527AC6FB2C173A5100434FB7 /* SongListCell.swift */; };
527AC6FE2C173A5100434FB7 /* SongListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 527AC6FC2C173A5100434FB7 /* SongListCell.xib */; };
527AC7012C182DCE00434FB7 /* TimeStringToSeconds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527AC7002C182DCE00434FB7 /* TimeStringToSeconds.swift */; };
529B0DD42C06156B00CFC54B /* LoginNavVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529B0DD32C06156B00CFC54B /* LoginNavVC.swift */; };
529B0DD62C070C0F00CFC54B /* GuestDataDM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529B0DD52C070C0F00CFC54B /* GuestDataDM.swift */; };
52A3F6A52BECBA8D0000BB0B /* LinkedChildDM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A3F6A42BECBA8D0000BB0B /* LinkedChildDM.swift */; };
@@ -274,6 +275,7 @@
527AC6F92C17387300434FB7 /* SongBlogDM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SongBlogDM.swift; sourceTree = "<group>"; };
527AC6FB2C173A5100434FB7 /* SongListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SongListCell.swift; sourceTree = "<group>"; };
527AC6FC2C173A5100434FB7 /* SongListCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SongListCell.xib; sourceTree = "<group>"; };
527AC7002C182DCE00434FB7 /* TimeStringToSeconds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeStringToSeconds.swift; sourceTree = "<group>"; };
529B0DD32C06156B00CFC54B /* LoginNavVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginNavVC.swift; sourceTree = "<group>"; };
529B0DD52C070C0F00CFC54B /* GuestDataDM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestDataDM.swift; sourceTree = "<group>"; };
52A3F6A42BECBA8D0000BB0B /* LinkedChildDM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedChildDM.swift; sourceTree = "<group>"; };
@@ -770,6 +772,14 @@
path = Alerts;
sourceTree = "<group>";
};
527AC6FF2C182D1700434FB7 /* Timer */ = {
isa = PBXGroup;
children = (
527AC7002C182DCE00434FB7 /* TimeStringToSeconds.swift */,
);
path = Timer;
sourceTree = "<group>";
};
52C1A4DF2C05B670007BAA50 /* Delegate */ = {
isa = PBXGroup;
children = (
@@ -792,6 +802,7 @@
52C8B0512BDA4B51003B51D0 /* Helpers */ = {
isa = PBXGroup;
children = (
527AC6FF2C182D1700434FB7 /* Timer */,
9CBE1B3E2C0F37B200CA6E61 /* DropDown */,
9C535DB62C0089A700DA6DCD /* Animation */,
525953D22BE8B2CD00191286 /* UIApplication */,
@@ -1422,6 +1433,7 @@
9C535DB52C005A6D00DA6DCD /* KeyWindowFix.swift in Sources */,
9C9BEEC72BEE1BBF004ECC2F /* CollectionViewCenteredFlowLayout.swift in Sources */,
5222426A2BFC7AFC0085C632 /* SideMenuVC.swift in Sources */,
527AC7012C182DCE00434FB7 /* TimeStringToSeconds.swift in Sources */,
9CBCB29D2BE4D6BB007D7934 /* LoginVM.swift in Sources */,
524C42312C0499560016A11C /* NotificationCenterReloads.swift in Sources */,
9C8C4FAE2C1315410017DD3B /* WebViewVC.swift in Sources */,

View File

@@ -0,0 +1,48 @@
//
// TimeStringToSeconds.swift
// WOKA
//
// Created by MacBook Pro on 11/06/24.
//
import UIKit
extension String {
func timeStringToSeconds() -> Int? {
let components = self.split(separator: ":")
switch components.count {
case 2:
// MM:SS format
let minutes = Int(components[0]) ?? 0
let seconds = Int(components[1]) ?? 0
return (minutes * 60) + seconds
case 3:
// HH:MM:SS format
let hours = Int(components[0]) ?? 0
let minutes = Int(components[1]) ?? 0
let seconds = Int(components[2]) ?? 0
return (hours * 3600) + (minutes * 60) + seconds
default:
// Invalid format
return nil
}
}
func checkHourZero() -> String{
// Function to format the time
// Split the time string by ":"
let components = self.split(separator: ":")
// Check if the time string starts with "00"
if components.count == 3 && components[0] == "00" {
// Remove the initial "00:"
return "\(components[1]):\(components[2])"
} else {
// Return the original time string
return self
}
}
}

View File

@@ -757,7 +757,7 @@
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemGreenColor">
<color red="0.20392156862745098" green="0.7803921568627451" blue="0.34901960784313724" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.20392156859999999" green="0.78039215689999997" blue="0.34901960780000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@@ -16,14 +16,16 @@ class MoreVC: UIViewController {
@IBOutlet weak var songTableView: UITableView!
var vm = MoreVM()
override func viewDidLoad() {
super.viewDidLoad()
vm.vc = self
vm.initView()
}
override func viewWillDisappear(_ animated: Bool) {
vm.player.pause()
}
}
// MARK: - TableView DataSource , Delegates
@@ -36,17 +38,104 @@ extension MoreVC : TableViewSRC{
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: K.CellIdentifier.Home.songListCell) as! SongListCell
let data = vm.songData[indexPath.row]
cell.setData(data: data)
cell.setData(data: data,playerStatus: vm.playerStatus)
// if let playingIndex = vm.currentIndexPlayingSong {
// if indexPath.row == playingIndex{
// if vm.onGoingTime != nil{ // if index is nil that means its just paused
// cell.setData(data: data,playerStatus: vm.playerStatus,onGoingTime: vm.onGoingTime)
// }else{ // if index is there it means song is playing and needs to be paused
// cell.setData(data: data,playerStatus: .resume,onGoingTime: vm.onGoingTime)
// }
// }else{
// cell.setData(data: data,playerStatus: vm.playerStatus,onGoingTime: nil)
// }
// }else{
// cell.setData(data: data,playerStatus: .stopped ,onGoingTime: nil)
// }
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let data = vm.songData[indexPath.row]
if let url = data.contentMoreDetails?.first?.url{
vm.player = AVPlayer()
vm.playSong(urlString: url)
func getFormattedTime(from player: AVPlayer) -> String {
let currentTime = player.currentTime()
let seconds = CMTimeGetSeconds(currentTime)
if !seconds.isFinite {
return "00:00:00"
}
let hours = Int(seconds) / 3600
let minutes = (Int(seconds) % 3600) / 60
let secs = Int(seconds) % 60
return String(format: "%02d:%02d:%02d", hours, minutes, secs)
}
func handleTap(indexPath : IndexPath){
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) else { return }
addAnimation(cell: cell) { [weak self] in
guard let self else{return}
/*
this will declare if the song is playing or not
*/
if let playingIndex = vm.currentIndexPlayingSong {
if indexPath.row == playingIndex{ // if same row is selected pause the row
print(vm.playerStatus)
switch vm.playerStatus{
case .play:
print("Player is playing")
vm.player.pause()
case .pause:
print("Player is pause")
case .loading:
print("Player is loading")
case .resume:
print("Player is resume")
default:
break
}
}else{
/*
this means other cell was playing now stop it and play this new cell
first reload the playing cell and pause the audio
*/
print("Other cell playing")
}
}else{
// if there is no playing audio before
vm.playerStatus = .play
vm.currentIndexPlayingSong = indexPath.row
tableView.reloadRows(at: [indexPath], with: .none)
let data = vm.songData[indexPath.row]
playSong(url: data.contentMoreDetails?.first?.url)
print("First Play")
}
}
}
func addAnimation(cell : UITableViewCell, completion : @escaping () -> Void){
UIView.animate(withDuration: 0.1,animations: {
cell.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
},
completion: { _ in
UIView.animate(withDuration: 0.1) {
cell.transform = CGAffineTransform.identity
completion()
}
})
}
func playSong(url : String?){
if let url {
vm.player = AVPlayer(url: URL(string: url)!)
vm.observePlayer()
vm.player.play()
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
@@ -95,8 +184,48 @@ extension MoreVC : UICollectionViewDelegateFlowLayout{
let widthPerItem = availableWidth / itemsPerRow
return CGSize(width: widthPerItem, height: widthPerItem - 20)
}
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
// return UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)
// }
}
class VideoAVPlayer {
var player: AVPlayer?
var playerObserver: NSKeyValueObservation?
var status = PlayerStatus.loading
init(url: URL) {
self.player = AVPlayer(url: url)
observePlayer()
}
private func observePlayer() {
playerObserver = player?.observe(\.timeControlStatus, options: [.new, .old], changeHandler: { [weak self] player, change in
guard let self = self else { return }
switch player.timeControlStatus {
case .playing:
print("Player is playing")
self.status = .play
case .paused:
print("Player is paused")
self.status = .pause
case .waitingToPlayAtSpecifiedRate:
print("Player is waiting to play at specified rate")
self.status = .loading
@unknown default:
print("Unknown player status")
}
})
}
deinit {
playerObserver?.invalidate()
}
func play() {
player?.play()
}
func pause() {
player?.pause()
}
}

View File

@@ -2,7 +2,7 @@
// SongListCell.swift
// WOKA
//
// Created by MacBook Pro on 10/06/24.
// Created by Bilal Khan on 10/06/24.
//
import UIKit
@@ -12,22 +12,129 @@ class SongListCell: UITableViewCell {
@IBOutlet weak var ongoingTimeLabel: UILabel!
@IBOutlet weak var totalTimeLabel: UILabel!
@IBOutlet weak var songTitle: UILabel!
@IBOutlet weak var outerStack: UIStackView!
@IBOutlet weak var playPauseImage: UIImageView!
@IBOutlet weak var indicatorView: UIView!
// typealias btnTappedBlock = (String?) -> Void
// var btnTapped : btnTappedBlock!
var timer: Timer?
private var currentTime: Int = 0
var totalTime: Int = 0
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
// self.addTapGesture { [weak self] in
// guard let self else{return}
// UIView.animate(withDuration: 0.1, animations: {
// self.outerStack.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
// }) { _ in
// UIView.animate(withDuration: 0.1) {
// self.outerStack.transform = .identity
// self.btnTapped(self.ongoingTimeLabel.text)
// }
// }
// }
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func setData(data : SongBlogDM.PaintDatum){
self.totalTimeLabel.text = "/" + (data.songDuration ?? "0:00")
self.ongoingTimeLabel.text = "0:00"
self.songTitle.text = data.title!
override func prepareForReuse() {
super.prepareForReuse()
// Invalidate the timer when the cell is reused
timer?.invalidate()
timer = nil
ongoingTimeLabel.text = "0:00"
}
func setData(data: SongBlogDM.PaintDatum, playerStatus: PlayerStatus) {
ongoingTimeLabel.text = "00:00"
totalTime = 0
currentTime = 0
switch playerStatus{
case .stopped:
playPauseImage.image = UIImage(systemName: "play.fill")
case .pause:
playPauseImage.image = UIImage(systemName: "play.fill")
case .play:
playPauseImage.image = UIImage(systemName: "pause.fill")
totalTime = data.songDuration?.timeStringToSeconds() ?? 0
// if let onGoingTime{
// currentTime = onGoingTime.timeStringToSeconds() ?? 0
// ongoingTimeLabel.text = onGoingTime
// }else{
// currentTime = 0
// }
startCountdown()
case .resume:
playPauseImage.image = UIImage(systemName: "pause.fill")
totalTime = data.songDuration?.timeStringToSeconds() ?? 0
// if let onGoingTime{
// currentTime = onGoingTime.timeStringToSeconds() ?? 0
// ongoingTimeLabel.text = onGoingTime
// }else{
// currentTime = 0
// }
startCountdown()
case .loading:
break
}
// Set total time label and song title
if let totalDuration = data.songDuration{
//Check of the hour is zero removing it
let totalTimeFiltered = totalDuration.checkHourZero()
totalTimeLabel.text = "/" + totalTimeFiltered
}else{
totalTimeLabel.text = "/" + (data.songDuration ?? "00:00")
}
songTitle.text = data.title ?? "Unknown Title"
}
private func startCountdown() {
// Invalidate any existing timer
timer?.invalidate()
// Schedule a new timer
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateCountdown), userInfo: nil, repeats: true)
}
@objc private func updateCountdown() {
// Update the timer label
if currentTime < totalTime {
currentTime += 1
ongoingTimeLabel.text = formatTime(currentTime)
} else {
timer?.invalidate()
timer = nil
}
}
// @objc private func updateCountdown() {
// // Update countdown label
// if totalTime > 0 {
// totalTime -= 1
// ongoingTimeLabel.text = formatTime(totalTime)
// } else {
// timer?.invalidate()
// timer = nil
// ongoingTimeLabel.text = "0:00"
// }
// }
private func formatTime(_ seconds: Int) -> String {
// Format time as MM:SS
let minutes = seconds / 60
let seconds = seconds % 60
return String(format: "%02d:%02d", minutes, seconds)
}
}

View File

@@ -28,13 +28,25 @@
<stackView opaque="NO" contentMode="scaleToFill" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="ngc-D8-N88">
<rect key="frame" x="0.0" y="0.0" width="96.666666666666671" height="59"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="play.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="MdW-yq-Mld">
<rect key="frame" x="0.0" y="2" width="59" height="55"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" secondItem="MdW-yq-Mld" secondAttribute="height" multiplier="1:1" id="XHF-ha-GMe"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rtY-dp-opx">
<rect key="frame" x="0.0" y="0.0" width="59" height="59"/>
<subviews>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kPE-0t-owy">
<rect key="frame" x="-59" y="0.0" width="59" height="59"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" secondItem="kPE-0t-owy" secondAttribute="height" multiplier="1:1" id="z5t-lK-vIu"/>
</constraints>
</view>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="play.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="MdW-yq-Mld">
<rect key="frame" x="0.0" y="2" width="59" height="55"/>
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" secondItem="MdW-yq-Mld" secondAttribute="height" multiplier="1:1" id="XHF-ha-GMe"/>
</constraints>
</imageView>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Text" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QJ2-HR-46k">
<rect key="frame" x="64" y="0.0" width="32.666666666666657" height="59"/>
<fontDescription key="fontDescription" name="Exo2-Bold" family="Exo 2" pointSize="16"/>
@@ -43,8 +55,8 @@
</label>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="eI6-es-65E">
<rect key="frame" x="287.33333333333331" y="0.0" width="48.666666666666686" height="59"/>
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eI6-es-65E">
<rect key="frame" x="258" y="0.0" width="78" height="59"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0:00" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mcL-xd-aWa">
<rect key="frame" x="0.0" y="0.0" width="34.666666666666664" height="59"/>
@@ -52,8 +64,8 @@
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="/" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5XU-52-i0j">
<rect key="frame" x="39.666666666666686" y="0.0" width="9" height="59"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="/0:00" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5XU-52-i0j">
<rect key="frame" x="34.666666666666686" y="0.0" width="43.333333333333343" height="59"/>
<fontDescription key="fontDescription" name="Exo2-Bold" family="Exo 2" pointSize="16"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -81,7 +93,10 @@
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<outlet property="indicatorView" destination="kPE-0t-owy" id="zk4-72-spv"/>
<outlet property="ongoingTimeLabel" destination="mcL-xd-aWa" id="RAG-Kd-oGz"/>
<outlet property="outerStack" destination="era-Az-5kr" id="doP-ZG-AnD"/>
<outlet property="playPauseImage" destination="MdW-yq-Mld" id="QWN-o8-gMJ"/>
<outlet property="songTitle" destination="QJ2-HR-46k" id="de9-lx-Wmg"/>
<outlet property="totalTimeLabel" destination="5XU-52-i0j" id="2BY-NO-kXO"/>
</connections>

View File

@@ -8,6 +8,13 @@
import UIKit
import AVFoundation
enum PlayerStatus{
case play
case loading
case pause
case resume
case stopped
}
class MoreVM{
weak var vc : MoreVC!
@@ -15,27 +22,54 @@ class MoreVM{
var blogData = [BlogDM.Blog]()
var songData = [SongBlogDM.PaintDatum]()
var player: AVPlayer?
var player = AVPlayer()
var playerObserver: NSKeyValueObservation?
var playerStatus = PlayerStatus.loading
var currentIndexPlayingSong : Int?
func initView(){
getBLogs()
getSong()
setupCell()
vc.songTableView.showsVerticalScrollIndicator = false
vc.songTableView.showsHorizontalScrollIndicator = false
vc.homeBtn.addTapGesture {
self.vc.dismiss(animated: true)
}
}
func playSong(urlString: String) {
guard let url = URL(string: urlString) else {
print("Invalid URL string.")
return
}
player = AVPlayer(url: url)
player?.play()
func observePlayer() {
playerObserver = self.player.observe(\.timeControlStatus, options: [.new, .old], changeHandler: { [weak self] player, change in
guard let self = self else { return }
switch player.timeControlStatus {
case .playing:
print("Player is playing")
self.playerStatus = .play
case .paused:
print("Player is paused")
self.playerStatus = .pause
case .waitingToPlayAtSpecifiedRate:
print("Player is waiting to play at specified rate")
self.playerStatus = .loading
@unknown default:
print("Unknown player status")
}
})
}
//
// func playSong(urlString: String) {
// guard let url = URL(string: urlString) else {
// print("Invalid URL string.")
// return
// }
//
// player = AVPlayer(url: url)
// player?.play()
// }
func setupCell(){
vc.blogsCollectionView.register(UINib(nibName: K.CellIdentifier.Home.blogsCell, bundle: nil), forCellWithReuseIdentifier: K.CellIdentifier.Home.blogsCell)