I have a UICollectionView with multiple cells (FeedCell). Those cells can also contain a UICollectionView which can also have multiple cells, up to five (MediaSliderCell). Basically, the set up is the same as Instagram: you have a post, and one post can have multiple images or videos.
The problem I am now facing is, sometimes the wrong video is shown in the wrong cell. I use an imageView to show a placeholder when the video is not played yet, this placeholder is hidden when the video starts playing (play button is tapped).
I figured everything goes well when the videos are not playing, but the problem arises when I do play videos. The cells get switched up, meaning the video of MediaSliderCell indexPath.item 5 is shown in MediaSLiderCell indexPath.item 2, for example.
At first, I thought the problem was in the cells, which don't get reused well, but this would also mean the photos could get switched up, which never happens. So I feel the problem is in my AVPlayer, which then uses the wrong reference or wrong URL. Let me demonstrate my code:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "mediaSliderCell", for: indexPath) as! MediaSliderCell
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
if(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia!.count > 0) {
if(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaType == .photo) {
let mediaURL = URL(string: "https://myfileserver.com/media/\(HomeControllerID.userFeed.userFeed[feedCellID!].feedID!)/\(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaURL)")
cell.photoView.kf.setImage(with: mediaURL, options: [.targetCache(mediaCache)])
cell.photoView.isHidden = false
cell.videoView.isHidden = true
} else if(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaType == .video) {
let mediaThumbURL = URL(string: "https://myfileserver.com/media/\(HomeControllerID.userFeed.userFeed[feedCellID!].feedID!)/\(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaThumbURL!)")
let mediaURL = URL(string: "https://myfileserver.com/media/\(HomeControllerID.userFeed.userFeed[feedCellID!].feedID!)/\(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaURL)")!
cell.videoView.placeholderView.kf.setImage(with: mediaThumbURL, options: [.targetCache(mediaCache)])
cell.videoView.mediaURL = mediaURL
cell.photoView.isHidden = true
cell.videoView.isHidden = false
}
}
return cell
}
MediaSLiderCell is some pretty basic UICollectionViewCell stuff:
class MediaSliderCell: UICollectionViewCell {
override func prepareForReuse() {
super.prepareForReuse()
photoView.isHidden = false
videoView.isHidden = true
videoView.mediaURL = nil
videoView.placeholderView.kf.cancelDownloadTask()
videoView.placeholderView.image = UIImage()
photoView.image = UIImage()
}
var photoView: UIImageView = {
let photoView = UIImageView()
photoView.translatesAutoresizingMaskIntoConstraints = false
photoView.backgroundColor = .black
photoView.isHidden = true
return photoView
}()
var videoView: VideoView = {
let videoView = VideoView()
videoView.translatesAutoresizingMaskIntoConstraints = false
videoView.backgroundColor = .black
return videoView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
addSubview(photoView)
addSubview(videoView)
photoView.topAnchor.constraint(equalTo: topAnchor).isActive = true
photoView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
photoView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
photoView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
videoView.topAnchor.constraint(equalTo: topAnchor).isActive = true
videoView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
videoView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
videoView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I think all of the above is fine. But then, for the VideoView, I use a custom UIView in which I set up a placeholder (UIImageView). It's a lot of code below, but most if it is controls and user interface stuff. What's important to know is, I use two libraries: Cache and CachingPlayerItem. This is to avoid downloading the same video over and over again, so when the video is not found in cache, I download the item, and when the video is downloaded, I save it in cache to reuse later. All makes sense I guess. I feel the problem lies somewhere in there, or in the AVPlayer itself. Take a look at the code:
class VideoView: UIView, CachingPlayerItemDelegate {
var playerItem: CachingPlayerItem?
func playerItem(_ playerItem: CachingPlayerItem, didFinishDownloadingData data: Data) {
// A track is downloaded. Saving it to the cache asynchronously.
print("Saving video to cache on device")
storage?.async.setObject(data, forKey: mediaURL.absoluteString, completion: { _ in} )
}
var playerLooper: NSObject?
var playerLayer: AVPlayerLayer!
var queuePlayer: AVQueuePlayer?
var mediaURL: URL!
var placeholderView: UIImageView = {
let placeholderView = UIImageView()
placeholderView.translatesAutoresizingMaskIntoConstraints = false
placeholderView.backgroundColor = .black
return placeholderView
}()
var playerView: UIView = {
let playerView = UIView()
playerView.translatesAutoresizingMaskIntoConstraints = false
playerView.backgroundColor = .clear
playerView.isHidden = true
return playerView
}()
var playButton: UIImageView = {
let playButton = UIImageView()
playButton.image = UIImage(named: "playButton")
playButton.translatesAutoresizingMaskIntoConstraints = false
playButton.isUserInteractionEnabled = true
playButton.backgroundColor = .clear
return playButton
}()
var pauseButton: UIImageView = {
let pauseButton = UIImageView()
pauseButton.image = UIImage(named: "pauseButton")
pauseButton.translatesAutoresizingMaskIntoConstraints = false
pauseButton.backgroundColor = .clear
pauseButton.isUserInteractionEnabled = true
pauseButton.alpha = 0
return pauseButton
}()
var volumeOnButton: UIImageView = {
let volumeOnButton = UIImageView()
volumeOnButton.image = UIImage(named: "volumeOn")
volumeOnButton.translatesAutoresizingMaskIntoConstraints = false
volumeOnButton.backgroundColor = .clear
volumeOnButton.isUserInteractionEnabled = true
volumeOnButton.alpha = 0
return volumeOnButton
}()
var volumeOffButton: UIImageView = {
let volumeOffButton = UIImageView()
volumeOffButton.image = UIImage(named: "volumeOff")
volumeOffButton.translatesAutoresizingMaskIntoConstraints = false
volumeOffButton.backgroundColor = .clear
volumeOffButton.isUserInteractionEnabled = true
volumeOffButton.alpha = 0
return volumeOffButton
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.setupViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupViews()
}
func setupViews() {
addSubview(placeholderView)
placeholderView.topAnchor.constraint(equalTo: topAnchor).isActive = true
placeholderView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
placeholderView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
placeholderView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
addSubview(playerView)
playerView.topAnchor.constraint(equalTo: topAnchor).isActive = true
playerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
playerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
playerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
let tapView = UITapGestureRecognizer(target: self, action: #selector(showControls))
playerView.addGestureRecognizer(tapView)
addSubview(playButton)
playButton.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
playButton.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
playButton.heightAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
playButton.widthAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
let tapPlayButton = UITapGestureRecognizer(target: self, action: #selector(playVideo))
playButton.addGestureRecognizer(tapPlayButton)
addSubview(pauseButton)
pauseButton.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
pauseButton.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
pauseButton.heightAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
pauseButton.widthAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
let tapPauseButton = UITapGestureRecognizer(target: self, action: #selector(pauseVideo))
pauseButton.addGestureRecognizer(tapPauseButton)
addSubview(volumeOnButton)
volumeOnButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -(normalSpacing + 2)).isActive = true
volumeOnButton.topAnchor.constraint(equalTo: topAnchor, constant: normalSpacing + 2).isActive = true
volumeOnButton.heightAnchor.constraint(equalToConstant: 32).isActive = true
volumeOnButton.widthAnchor.constraint(equalToConstant: 32).isActive = true
let tapVolumeOnButton = UITapGestureRecognizer(target: self, action: #selector(volumeAction))
volumeOnButton.addGestureRecognizer(tapVolumeOnButton)
addSubview(volumeOffButton)
volumeOffButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -(normalSpacing + 2)).isActive = true
volumeOffButton.topAnchor.constraint(equalTo: topAnchor, constant: normalSpacing + 2).isActive = true
volumeOffButton.heightAnchor.constraint(equalToConstant: 32).isActive = true
volumeOffButton.widthAnchor.constraint(equalToConstant: 32).isActive = true
let tapVolumeOffButton = UITapGestureRecognizer(target: self, action: #selector(volumeAction))
volumeOffButton.addGestureRecognizer(tapVolumeOffButton)
}
#objc func volumeAction() {
buttonTimer?.invalidate()
volumeTimer?.invalidate()
if UserDefaults.exists(key: "volumeOn") {
if(UserDefaults.standard.bool(forKey: "volumeOn") == false) {
self.queuePlayer?.isMuted = false
UserDefaults.standard.set(true, forKey: "volumeOn")
self.volumeOnButton.alpha = 1
self.volumeOffButton.alpha = 0
} else {
self.queuePlayer?.isMuted = true
UserDefaults.standard.set(false, forKey: "volumeOn")
self.volumeOnButton.alpha = 0
self.volumeOffButton.alpha = 1
}
} else {
self.queuePlayer?.isMuted = false
UserDefaults.standard.set(true, forKey: "volumeOn")
self.volumeOnButton.alpha = 1
self.volumeOffButton.alpha = 0
}
volumeTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadeVolumeButton), userInfo: nil, repeats: false)
buttonTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadePauseButton), userInfo: nil, repeats: false)
}
#objc func checkVolume() {
if UserDefaults.exists(key: "volumeOn") {
if(UserDefaults.standard.bool(forKey: "volumeOn") == false) {
self.queuePlayer?.isMuted = true
} else {
self.queuePlayer?.isMuted = false
}
} else {
self.queuePlayer?.isMuted = true
}
}
#objc func showControls() {
buttonTimer?.invalidate()
volumeTimer?.invalidate()
if(self.volumeOnButton.alpha > 0 || self.volumeOffButton.alpha > 0) {
UIView.animate(withDuration: 0.2) {
self.volumeOnButton.alpha = 0
self.volumeOffButton.alpha = 0
}
} else {
if UserDefaults.exists(key: "volumeOn") {
if(UserDefaults.standard.bool(forKey: "volumeOn") == false) {
UIView.animate(withDuration: 0.2) {
self.volumeOffButton.alpha = 1
self.volumeOnButton.alpha = 0
}
} else {
UIView.animate(withDuration: 0.2) {
self.volumeOffButton.alpha = 0
self.volumeOnButton.alpha = 1
}
}
} else {
UIView.animate(withDuration: 0.2) {
self.volumeOffButton.alpha = 1
self.volumeOnButton.alpha = 0
}
}
volumeTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadeVolumeButton), userInfo: nil, repeats: false)
}
if(self.queuePlayer?.timeControlStatus == .playing) {
if(self.pauseButton.alpha > 0) {
UIView.animate(withDuration: 0.2) { self.pauseButton.alpha = 0 }
} else {
UIView.animate(withDuration: 0.2) { self.pauseButton.alpha = 1 }
buttonTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadePauseButton), userInfo: nil, repeats: false)
}
} else if(self.queuePlayer?.timeControlStatus == .paused) {
if(self.playButton.alpha > 0) {
UIView.animate(withDuration: 0.2) { self.pauseButton.alpha = 0 }
} else {
UIView.animate(withDuration: 0.2) { self.playButton.alpha = 1 }
}
}
}
lazy var storage: Cache.Storage? = {
return try? Storage(diskConfig: diskConfig, memoryConfig: memoryConfig, transformer: TransformerFactory.forData())
}()
func loadVideo() {
// Trying to retrieve a track from cache asynchronously.
storage?.async.entry(forKey: mediaURL.absoluteString, completion: { result in
switch result {
case .error:
// The track is not cached.
print("Downloading from network")
self.playerItem = CachingPlayerItem(url: self.mediaURL)
case .value(let entry):
// The track is cached.
print("Downloading from cached library on device")
self.playerItem = CachingPlayerItem(data: entry.object, mimeType: "video/mp4", fileExtension: "mp4")
}
self.playerItem?.delegate = self
DispatchQueue.main.async {
if let playerItem = self.playerItem {
self.queuePlayer = AVQueuePlayer(items: [playerItem])
self.queuePlayer?.automaticallyWaitsToMinimizeStalling = false
self.playerLayer = AVPlayerLayer(player: self.queuePlayer)
self.playerLooper = AVPlayerLooper(player: self.queuePlayer!, templateItem: playerItem)
self.playerView.layer.addSublayer(self.playerLayer!)
self.playerLayer?.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 1.25)
self.checkVolume()
do {
try AVAudioSession.sharedInstance().setCategory(.playback)
} catch(let error) {
print(error.localizedDescription)
}
self.queuePlayer?.play()
self.queuePlayer?.addObserver(self, forKeyPath: "timeControlStatus", options: .initial, context:nil)
}
}
})
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if(keyPath == "timeControlStatus") {
if(self.playerView.isHidden && self.queuePlayer?.timeControlStatus == .playing) {
self.playerView.isHidden = false
}
}
}
var buttonTimer: Timer?
var volumeTimer: Timer?
#objc func playVideo() {
buttonTimer?.invalidate()
volumeTimer?.invalidate()
if(self.queuePlayer?.currentItem == nil) {
self.loadVideo()
}
if(self.queuePlayer?.timeControlStatus == .paused) {
do {
try AVAudioSession.sharedInstance().setCategory(.playback)
} catch(let error) {
print(error.localizedDescription)
}
self.queuePlayer?.play()
}
self.playButton.alpha = 0
buttonTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(fadePauseButton), userInfo: nil, repeats: false)
volumeTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(fadeVolumeButton), userInfo: nil, repeats: false)
}
#objc func pauseVideo() {
buttonTimer?.invalidate()
self.queuePlayer?.pause()
self.playButton.alpha = 1
self.pauseButton.alpha = 0
self.volumeOnButton.alpha = 0
self.volumeOffButton.alpha = 0
}
#objc func fadePauseButton() {
UIView.animate(withDuration: 0.8) {
self.pauseButton.alpha = 0
}
}
#objc func fadeVolumeButton() {
UIView.animate(withDuration: 0.8) {
self.volumeOnButton.alpha = 0
self.volumeOffButton.alpha = 0
}
}
}
In my opinion, the problem should be somewhere above in loadVideo(). However, I provide all my code of the custom UIView VideoView to avoid any mistakes or other important parts. Anybody who can help me? Would help me out a big deal, because I've been looking into this for weeks now. I have tried every suggestion for dequeuing, I have tried everything I could for reusing cells, ... and it all didn't work. So I guess the solution should be found in the AVPlayer, the Cache library or the CachingPlayerItem library. Would appreciate any help or suggestions. Thank you in advance.
For the record, something I forgot but should be clear from the code: the AVPlayer is not shown and thus loadVideo() is not called initially. Only when the user taps the play button (playVideo()). Everything goes well if I don't play video too. Photos are shown correctly, the placeholders are shown correctly, but the videos get mixed up after I start playing one or more videos (tap the play button, which calls playVideo(), which calls loadVideo()).
EDIT: Ok so I figured something out. It seems like the video itself is not changed when the cell gets reused. What I mean is, in loadVideo(), the AVQueuePlayer() gets set, and this is what changes the video shown and played afterwards. I feel like I need to set this block:
// Trying to retrieve a track from cache asynchronously.
storage?.async.entry(forKey: mediaURL.absoluteString, completion: { result in
switch result {
case .error:
// The track is not cached.
print("Downloading from network")
DispatchQueue.main.async {
self.playerItem = CachingPlayerItem(url: self.mediaURL)
}
case .value(let entry):
// The track is cached.
print("Downloading from cached library on device")
DispatchQueue.main.async {
self.playerItem = CachingPlayerItem(data: entry.object, mimeType: "video/mp4", fileExtension: "mp4")
}
}
self.playerItem?.delegate = self
DispatchQueue.main.async {
if let playerItem = self.playerItem {
self.queuePlayer = AVQueuePlayer(items: [playerItem])
self.queuePlayer?.automaticallyWaitsToMinimizeStalling = false
self.playerLayer = AVPlayerLayer(player: self.queuePlayer)
self.playerLooper = AVPlayerLooper(player: self.queuePlayer!, templateItem: playerItem)
self.playerView.layer.addSublayer(self.playerLayer!)
self.playerLayer?.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 1.25)
self.checkVolume()
do {
try AVAudioSession.sharedInstance().setCategory(.playback)
} catch(let error) {
print(error.localizedDescription)
}
self.queuePlayer?.addObserver(self, forKeyPath: "timeControlStatus", options: .initial, context:nil)
}
}
})
somewhere else, before the video gets played at all. Because this only gets called when the play button is tapped, but then the wrong video is shown. Have tried calling loadVideo() directly in the cell, but doesn't seem to fix the problem. I feel like I am getting closer tho. Any thoughts?
Don't store AVQueuePlayer inside tableView cells. Remember that you have upper limit on the amount of AVPlayer instances you can keep.
Move players it-selfs, their configurations, starting, pausing, whatever else action you have into UIViewController what store your UITableView.
When User will press play on UITableViewCell, you will tell the UIViewController thru delegate to start playing and UIViewController will do something like that:
Cancel previous playing
Populate AVQueuePlayer with player items
Insert AVQueuePlayer into UITableViewCell
Start playing
Don't forget to remove AVQueuePlayer from UITableViewCell when you reuse cell
Related
I have a collection view and a button in my view controller I want to change the image in my collection view by tapping on my button, but it is proving quite difficult.I have made it so when i click on my collection view my button image changes now i want to do vice versa. Any help would be appreciated
import UIKit
import SwiftAudioPlayer
import Soundable
import AVFoundation
import AVKit
import MediaPlayer
import Kingfisher
protocol poorBtn: AnyObject {
func poorBtnTapped(sender: UIButton!)
}
class SongsViewController: BaseViewController {
#IBOutlet weak var forYouView: UIView!
#IBOutlet weak var noSongsLbl: UILabel!
#IBOutlet weak var navView: UIView!
#IBOutlet weak var albumCollection: UICollectionView!
#IBOutlet weak var songsCollection: UICollectionView!
#IBOutlet weak var playBtn: UIButton!
#IBOutlet weak var playImg: UIImageView!
#IBOutlet weak var songProgress: UIProgressView!
#IBOutlet weak var startTime: UILabel!
#IBOutlet weak var endTime: UILabel!
#IBOutlet weak var songDetailView: UIView!
#IBOutlet weak var sliderProgress: UISlider!
#IBOutlet weak var forwardBtn: UIButton!
weak var delegate: poorBtn?
var play = false
var isplaying = false
let Parent = UIViewController()
var selectedIndex : Int?
let playList: NSMutableArray = NSMutableArray()
var albums : [String] = ["manHat", "manHat","manHat", "manHat"]
var albumLbl : [String] = ["George Harrison", "George Harrison","George Harrison",
"George Harrison"]
var songs : [String] = ["Geazy", "manHat","Geazy", "manHat"]
var playImage : [String] = ["wPause","wPause","wPause","wPause","wPause"]
var timer: Timer?
var index: Int = Int()
var avPlayer: AVPlayer!
var isPaused: Bool!
var didPlayed: Bool!
var isTapped: Bool!
var audio: AVAudioPlayer!
var playerItem: AVPlayerItem!
var music : [String] = ["https://p.scdn.co/mp3-
preview/6f9999d909b017eabef97234dd7a206355720d9d?
cid=d8a5ed958d274c2e8ee717e6a4b0971d","https://p.scdn.co/mp3-
preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?
cid=d8a5ed958d274c2e8ee717e6a4b0971d"]
var counter: Timer?
var currentCellIndex = 0
var getSender: Int = Int()
var pop = false
var ploy = true
var newSender = 0
var isPlaying: Int?
var realSender: Int = Int()
var isPlay: Int?
var forPrev: Int?
var mainPlay: Int?
var playingIndex: Int?
lazy var viewModel:ForYouSongsViewModel = {
let viewModel = ForYouSongsViewModel()
viewModel.delegate = self
return viewModel
}()
var forSongs = [String]()
var currentIndex : Int?
var forImages = [ForYou]()
var albumImages = [Album]()
var albumId: String?
override func viewDidLoad() {
super.viewDidLoad()
sliderProgress.setValue(0, animated: true)
self.startLoading()
albumCollection.delegate = self
albumCollection.dataSource = self
songsCollection.delegate = self
songsCollection.dataSource = self
isPaused = false
songDetailView.layer.mask = maskLayer
viewModel.getForYouSongs()
// counter = Timer.scheduledTimer(timeInterval: 2, target: self, selector:
#selector(slider), userInfo: nil, repeats: true)
}
override func viewDidLayoutSubviews() {
self.setUpNavView()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
//test
#IBAction func backTap(_ sender: Any) {
self.dismiss(animated: true) {
// self.avPlayer = nil
self.timer?.invalidate()
}
}
#objc func slider() {
if forImages.isEmpty == true {
print("Sadly it's empty")
forYouView.isHidden = true
songsCollection.isHidden = true
noSongsLbl.text = "No Songs"
} else {
noSongsLbl.isHidden = true
if currentCellIndex < forSongs.count - 1 {
currentCellIndex+=1
}
else {
currentCellIndex = 0
}
songsCollection.scrollToItem(at: IndexPath(item: currentCellIndex, section: 0),
at: .right, animated: true)
}
}
#IBAction func playBtn(_ sender: UIButton) {
//delegate?.poorBtnTapped(sender: sender)
isPlay = 1
mainPlay = playingIndex
print(mainPlay)
if play == false {
self.startLoading()
guard let songUrlString = forSongs[self.index] as? String else {return}
guard let songUrl = URL(string:songUrlString) else{return}
print(songUrlString)
print(songUrl)
self.playSong(url: songUrl)
//NotificationCenter.default.post(name: Notification.Name("playSong"),
object: nil)
playImg.image = UIImage(named: "pause")
tick()
play = true
didPlayed = false
}
else if isplaying == true {
// playSaveSound()
avPlayer?.play()
playImg.image = UIImage(named: "pause")
// NotificationCenter.default.post(name: Notification.Name("playSong"),
object: nil)
isplaying = false
tick()
}
else {
avPlayer?.pause()
playImg.image = UIImage(named: "play")
isplaying = true
}
self.setupTimer()
NotificationCenter.default.post(name: Notification.Name("songPlay"), object: nil)
songsCollection.reloadData()
}
#IBAction func forwardBtn(_ sender: Any) {
self.startLoading()
self.nextTrack()
}
#IBAction func backBtn(_ sender: Any) {
self.startLoading()
self.prevTrack()
}
func playSong(url:URL) {
self.playerItem = AVPlayerItem(url: url)
self.avPlayer = AVPlayer(playerItem: playerItem)
self.avPlayer.automaticallyWaitsToMinimizeStalling = false
avPlayer!.volume = 1.0
avPlayer.play()
setupNowPlaying()
setupRemoteCommandCenter()
self.stopLoading()
}
func setupTimer(){
NotificationCenter.default.addObserver(self, selector:
#selector(self.didPlayToEnd), name: .AVPlayerItemDidPlayToEndTime, object: nil)
timer = Timer(timeInterval: 0.001, target: self, selector:
#selector(SongsViewController.tick), userInfo: nil, repeats: true)
RunLoop.current.add(timer!, forMode: RunLoop.Mode.common)
}
#objc func didPlayToEnd() {
self.startLoading()
self.nextTrack()
}
#IBAction func sliderTapped(_ sender: UISlider) {
let seconds : Int64 = Int64(sender.value)
let targetTime:CMTime = CMTimeMake(value: seconds, timescale: 1)
avPlayer?.seek(to: targetTime)
}
#objc func tick() {
//print(avPlayer)
if((avPlayer.currentItem?.asset.duration) != nil){
if let _ = avPlayer.currentItem?.asset.duration{}else{return}
if let _ = avPlayer.currentItem?.currentTime(){}else{return}
let currentTime1 : CMTime = (avPlayer.currentItem?.asset.duration)!
let seconds1 : Float64 = CMTimeGetSeconds(currentTime1)
let time1 : Float = Float(seconds1)
sliderProgress.minimumValue = 0
sliderProgress.maximumValue = time1
let currentTime : CMTime = (self.avPlayer?.currentTime())!
let seconds : Float64 = CMTimeGetSeconds(currentTime)
guard !(seconds.isNaN || seconds.isInfinite) else {
return
}
let time : Float = Float(seconds)
self.sliderProgress.value = time
endTime.text = self.formatTimeFromSeconds(totalSeconds:
[enter image description here]
[1]Int32(Float(Float64(CMTimeGetSeconds((self.avPlayer?.currentItem?.asset.duration)!
)))). )
startTime.text = self.formatTimeFromSeconds(totalSeconds:
Int32(Float(Float64(CMTimeGetSeconds((self.avPlayer?.currentItem?.currentTime())!))
)))
}else{
if let duration = (self.avPlayer?.currentItem?.duration) {
let secs = CMTimeGetSeconds(duration)
guard !(secs.isNaN || secs.isInfinite) else {
return
}
let secondsText = Int(secs.truncatingRemainder(dividingBy:
60.0))
let minutesText = Int(secs / 60)
self.endTime.text = "\(minutesText):\(secondsText)"
}
}
}
func nextTrack(){
if(index < forSongs.count-1){
index = index + 1
isPaused = false
playImg.image = UIImage(named: "pause")
self.playSong(url: URL(string:(forSongs[self.index] ))!)
}else{
index = 0
isPaused = false
playImg.image = UIImage(named: "pause")
self.playSong(url: URL(string:(forSongs[self.index] ))!)
}
}
func prevTrack(){
if(index > 0){
index = index - 1
isPaused = false
playImg.image = UIImage(named: "pause")
self.playSong(url: URL(string:(forSongs[self.index] as! String))!)
}
}
func formatTimeFromSeconds(totalSeconds: Int32) -> String {
let seconds: Int32 = totalSeconds%60
let minutes: Int32 = (totalSeconds/60)%60
// let hours: Int32 = totalSeconds/3600
return String(format: "%02d:%02d", minutes,seconds)
}
private func setUpNavView(){
let btnGradientLayer = CAGradientLayer()
let colorTop = UIColor(red: 0.0/255.0, green: 0.0/255.0, blue: 0.0/255.0, alpha:
1.0).cgColor
let colorBottom = UIColor(red: 6.0/255.0, green: 47.0/255.0, blue: 79.0/255.0,
alpha: 1.0).cgColor
let startPoint = CGPoint(x: 0.0, y: 0.1)
let endPoint = CGPoint(x: 0.0, y: 1.0)
let frame = self.navView.bounds
btnGradientLayer.colors = [colorTop, colorBottom]
btnGradientLayer.startPoint = startPoint
btnGradientLayer.endPoint = endPoint
btnGradientLayer.frame = frame
navView.backgroundColor = .red
self.navView.layer.insertSublayer(btnGradientLayer, at: 0)
self.navView.clipsToBounds = false
}
#IBAction func playPauseButton(_ sender: UIButton!) {
self.selectedIndex = sender.tag
self.isTapped = true
}
//FOR NOTIFICATION
func setupNowPlaying() {
// Define Now Playing Info
var nowPlayingInfo = [String : Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = forImages[0].title
nowPlayingInfo[MPMediaItemPropertyArtist] = "G-Eazy"
if let movieArt = UIImage(named: "Geazy") {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize:
movieArt.size, requestHandler: {
imageSize in return movieArt })
}
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = false
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] =
playerItem.currentTime().seconds
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] =
playerItem.asset.duration.seconds
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = avPlayer.rate
// Set the metadata
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
MPNowPlayingInfoCenter.default().playbackState = .playing
}
func setupRemoteCommandCenter() {
let commandCenter = MPRemoteCommandCenter.shared();
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget {event in
self.avPlayer.play()
self.playImg.image = UIImage(named: "pause")
self.setupTimer()
return .success
}
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget {event in
self.avPlayer.pause()
self.playImg.image = UIImage(named: "play")
// self.tick()
self.setupTimer()
return .success
}
}
}
extension SongsViewController: UICollectionViewDelegate, UICollectionViewDataSource,
UICollectionViewDelegateFlowLayout {
//FOR ALBUM COLLECTION
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection
section: Int) -> Int {
if collectionView == self.albumCollection {
return albumImages.count
}
return forSongs.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath:
IndexPath) -> UICollectionViewCell {
if collectionView == self.albumCollection {
let cellA = collectionView.dequeueReusableCell(withReuseIdentifier: "album", for:
indexPath) as! AlbumsCollectionViewCell
if albumImages.isEmpty != true {
let albumUrl = URL(string: albumImages[indexPath.row].image)
cellA.albumImg.kf.setImage(with: albumUrl)
cellA.albumLbl.text = albumImages[indexPath.row].name
}
return cellA
}
else {
let cellB = collectionView.dequeueReusableCell(withReuseIdentifier: "songs",
for: indexPath) as! ForYouCollectionViewCell
let songUrl = URL(string: forImages[indexPath.row].image)
cellB.forImg.kf.setImage(with: songUrl)
cellB.forButton.setImage(UIImage(named: playImage[indexPath.row]), for:
.normal)
cellB.songTitle.text = forImages[indexPath.row].title
cellB.forButton.tag = indexPath.row
if indexPath.row == isPlaying {
cellB.forButton.setImage(UIImage(named: "wPlay"), for: .normal)
} else {
cellB.forButton.setImage(UIImage(named: "wPause"), for: .normal)
}
// if isPlay == nil {
// print("I'm a noob")
//
// } else {
// if playImg.image == UIImage(named: "play") {
// cellB.forButton.setImage(UIImage(named: "wPause"), for:
.normal)
// } else {
// cellB.forButton.setImage(UIImage(named: "wPlay"), for: .normal)
// isPlay = nil
// }
//
}
if mainPlay != nil {
if cellB.forButton.image(for: .normal) == UIImage(named: "wPause") {
cellB.forButton.setImage(UIImage(named: "wPlay"), for: .normal)
} else {
cellB.forButton.setImage(UIImage(named: "wPause"), for: .normal)
mainPlay = nil
}
}
cellB.delegate = self
cellB.btnTapped
return cellB
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout:
UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 10
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath:
IndexPath) {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "songs", for:
indexPath) as! ForYouCollectionViewCell
currentIndex = indexPath.row
if collectionView == self.albumCollection {
if didPlayed == false {
avPlayer.pause()
playImg.image = UIImage(named: "play")
//avPlayer = nil
timer?.invalidate()
self.presentAlbumCollectionController(fromController: self, withAId:
forImages[indexPath.row].album_id)
} else {
self.presentAlbumCollectionController(fromController: self, withAId:
forImages[indexPath.row].album_id)
}
}
isPlaying = indexPath.row
songsCollection.reloadData()
guard let songUrlString = forSongs[indexPath.row] as? String else {return}
guard let songUrl = URL(string:songUrlString) else{return}
print(songUrlString)
print(songUrl)
print(indexPath.row)
print(playList.count)
if play == false && getSender == indexPath.row {
self.playSong(url: songUrl)
tick()
playImg.image = UIImage(named: "pause")
play = true
getSender = indexPath.row
pop = false
ploy = true
} else if getSender == indexPath.row{
avPlayer?.pause()
playImg.image = UIImage(named: "play")
play = false
getSender = indexPath.row
isPlaying = nil
} else if newSender == indexPath.row && ploy == false {
playImg.image = UIImage(named: "pause")
self.playSong(url: songUrl)
tick()
play = false
pop = true
ploy = true
}
else if pop == true {
avPlayer?.pause()
playImg.image = UIImage(named: "play")
play = false
pop = false
isPlaying = nil
} else {
playImg.image = UIImage(named: "pause")
self.playSong(url: songUrl)
tick()
if realSender == getSender {
pop = false
}
ploy = true
getSender = indexPath.row
play = true
}
// if mainPlay != nil {
// if cell.forButton.image(for: .normal) == UIImage(named: "wPause") {
// cell.forButton.setImage(UIImage(named: "wPlay"), for: .normal)
// } else {
// cell.forButton.setImage(UIImage(named: "wPause"), for: .normal)
// }
// }
playingIndex = indexPath.row
self.setupTimer()
//
songsCollection.reloadData()
}
func collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath) {
cell.alpha = 0
UIView.animate(withDuration: 0.8) {
cell.alpha = 1
}
}
/* func conditions() {
if play == false {
playImg.image = UIImage(named: "pause")
tick()
// SongTime()
play = true
}
else if isplaying == true {
// playSaveSound()
avPlayer?.play()
playImg.image = UIImage(named: "pause")
isplaying = false
tick()
//SongTime()
}
else {
avPlayer?.pause()
playImg.image = UIImage(named: "play")
isplaying = true
}
} */
}
extension SongsViewController:SongDelegate {
func songBtnTapped(sender: UIButton!) {
isPlaying = sender.tag
songsCollection.reloadData()
guard let songUrlString = forSongs[sender.tag] as? String else {return}
guard let songUrl = URL(string:songUrlString) else{return}
print(songUrlString)
print(songUrl)
print(sender.tag)
print(playList.count)
if play == false && getSender == sender.tag {
self.playSong(url: songUrl)
tick()
playImg.image = UIImage(named: "pause")
play = true
getSender = sender.tag
pop = false
ploy = true
} else if getSender == sender.tag{
avPlayer?.pause()
playImg.image = UIImage(named: "play")
play = false
getSender = sender.tag
isPlaying = nil
} else if newSender == sender.tag && ploy == false {
playImg.image = UIImage(named: "pause")
self.playSong(url: songUrl)
tick()
play = false
pop = true
ploy = true
}
else if pop == true {
avPlayer?.pause()
playImg.image = UIImage(named: "play")
play = false
pop = false
isPlaying = nil
} else {
playImg.image = UIImage(named: "pause")
self.playSong(url: songUrl)
tick()
if realSender == getSender {
pop = false
}
ploy = true
getSender = sender.tag
play = true
}
self.setupTimer()
}
}
I have a UICollectionView setup that has cells of video posts from my database. Right now, when the collection view is loaded, all of the videos in the different cells start playing. I want the videos to not play in any cells except the selected cell so that the video audios don't play over each other. How can I do this? Here is the code...
The view controller:
import UIKit
import Photos
struct VideoModel {
let username: String
let videoFileURL: String
}
class BetaClipsViewController: UIViewController, UICollectionViewDelegate {
private var collectionView: UICollectionView?
private var data = [VideoModel]()
/// Notification observer
private var observer: NSObjectProtocol?
/// All post models
private var allClips: [(clip: Clip, owner: String)] = []
private var viewModels = [[ClipFeedCellType]]()
override func viewDidLoad() {
super.viewDidLoad()
title = ""
// for _ in 0..<10 {
// let model = VideoModel(username: "#CJMJM",
// videoFileURL: "https://firebasestorage.googleapis.com:443/v0/b/globe-e8b7f.appspot.com/o/clipvideos%2F1637024382.mp4?alt=media&token=c12d0481-f834-4a17-8eee-30595bdf0e8b")
// data.append(model)
// }
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: view.frame.size.width,
height: view.frame.size.height)
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView?.register(ClipsCollectionViewCell.self,
forCellWithReuseIdentifier: ClipsCollectionViewCell.identifier)
collectionView?.isPagingEnabled = true
collectionView?.delegate = self
collectionView?.dataSource = self
view.addSubview(collectionView!)
fetchClips()
observer = NotificationCenter.default.addObserver(
forName: .didPostNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.viewModels.removeAll()
self?.fetchClips()
}
self.collectionView?.reloadData()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView?.frame = view.bounds
}
private func fetchClips() {
// guard let username = UserDefaults.standard.string(forKey: "username") else {
// return
// }
let userGroup = DispatchGroup()
userGroup.enter()
var allClips: [(clip: Clip, owner: String)] = []
DatabaseManager.shared.clips() { result in
DispatchQueue.main.async {
defer {
userGroup.leave()
}
switch result {
case .success(let clips):
allClips.append(contentsOf: clips.compactMap({
(clip: $0, owner: $0.owner)
}))
case .failure:
break
}
}
}
userGroup.notify(queue: .main) {
let group = DispatchGroup()
self.allClips = allClips
allClips.forEach { model in
group.enter()
self.createViewModel(
model: model.clip,
username: model.owner,
completion: { success in
defer {
group.leave()
}
if !success {
print("failed to create VM")
}
}
)
}
group.notify(queue: .main) {
self.sortData()
self.collectionView?.reloadData()
}
}
}
private func sortData() {
allClips = allClips.shuffled()
viewModels = viewModels.shuffled()
}
}
extension BetaClipsViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return viewModels.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModels[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cellType = viewModels[indexPath.section][indexPath.row]
switch cellType {
case .clip(let viewModel):
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ClipsCollectionViewCell.identifier,
for: indexPath)
as? ClipsCollectionViewCell else {
fatalError()
}
cell.delegate = self
cell.configure(with: viewModel)
return cell
}
}
}
extension BetaClipsViewController: ClipsCollectionViewCellDelegate {
func didTapProfile(with model: VideoModel) {
print("profile tapped")
let owner = model.username
DatabaseManager.shared.findUser(username: owner) { [weak self] user in
DispatchQueue.main.async {
guard let user = user else {
return
}
let vc = ProfileViewController(user: user)
self?.navigationController?.pushViewController(vc, animated: true)
}
}
}
func didTapShare(with model: VideoModel) {
print("tapped share")
}
func didTapNewClip(with model: VideoModel) {
let vc = RecordViewController()
navigationController?.pushViewController(vc, animated: true)
}
}
extension BetaClipsViewController {
func createViewModel(
model: Clip,
username: String,
completion: #escaping (Bool) -> Void
) {
// StorageManager.shared.profilePictureURL(for: username) { [weak self] profilePictureURL in
// guard let clipURL = URL(string: model.clipUrlString),
// let profilePhotoUrl = profilePictureURL else {
// return
// }
let clipData: [ClipFeedCellType] = [
.clip(viewModel: VideoModel(username: username,
videoFileURL: model.clipUrlString))
]
self.viewModels.append(clipData)
completion(true)
// }
}
}
The cell:
import UIKit
import AVFoundation
protocol ClipsCollectionViewCellDelegate: AnyObject {
func didTapProfile(with model: VideoModel)
func didTapShare(with model: VideoModel)
func didTapNewClip(with model: VideoModel)
}
class ClipsCollectionViewCell: UICollectionViewCell {
static let identifier = "ClipsCollectionViewCell"
var playerLooper: NSObject?
// Labels
private let usernameLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = UIColor.systemPink.withAlphaComponent(0.5)
label.backgroundColor = UIColor.systemPink.withAlphaComponent(0.1)
label.clipsToBounds = true
label.layer.cornerRadius = 8
return label
}()
// Buttons
private let profileButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "person.circle"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 32
button.isUserInteractionEnabled = true
return button
}()
private let shareButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "square.and.arrow.down"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 4
button.isUserInteractionEnabled = true
return button
}()
private let newClipButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "plus"), for: .normal)
button.tintColor = .systemOrange
button.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 25
button.isUserInteractionEnabled = true
return button
}()
private let videoContainer = UIView()
// Delegate
weak var delegate: ClipsCollectionViewCellDelegate?
// Subviews
var player: AVPlayer?
private var model: VideoModel?
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .black
contentView.clipsToBounds = true
addSubviews()
}
private func addSubviews() {
contentView.addSubview(videoContainer)
contentView.addSubview(usernameLabel)
contentView.addSubview(profileButton)
contentView.addSubview(shareButton)
contentView.addSubview(newClipButton)
// Add actions
profileButton.addTarget(self, action: #selector(didTapProfileButton), for: .touchUpInside)
shareButton.addTarget(self, action: #selector(didTapShareButton), for: .touchUpInside)
newClipButton.addTarget(self, action: #selector(didTapNewClipButton), for: .touchUpInside)
videoContainer.clipsToBounds = true
contentView.sendSubviewToBack(videoContainer)
}
#objc private func didTapProfileButton() {
guard let model = model else {
return
}
delegate?.didTapProfile(with: model)
}
#objc private func didTapShareButton() {
guard let model = model else {
return
}
delegate?.didTapShare(with: model)
}
#objc private func didTapNewClipButton() {
guard let model = model else {
return
}
delegate?.didTapNewClip(with: model)
}
override func layoutSubviews() {
super.layoutSubviews()
videoContainer.frame = contentView.bounds
let size = contentView.frame.size.width/7
let width = contentView.frame.size.width
let height = contentView.frame.size.height
// Labels
usernameLabel.frame = CGRect(x: (width-(size*3))/2, y: height-880-(size/2), width: size*3, height: size)
// Buttons
profileButton.frame = CGRect(x: width-(size*7), y: height-850-size, width: size, height: size)
shareButton.frame = CGRect(x: width-size, y: height-850-size, width: size, height: size)
newClipButton.frame = CGRect(x: width-size-10, y: height-175-size, width: size/1.25, height: size/1.25)
}
override func prepareForReuse() {
super.prepareForReuse()
usernameLabel.text = nil
player?.pause()
player?.seek(to: CMTime.zero)
}
public func configure(with model: VideoModel) {
self.model = model
configureVideo()
// Labels
usernameLabel.text = "#" + model.username
}
private func configureVideo() {
guard let model = model else {
return
}
guard let url = URL(string: model.videoFileURL) else { return }
player = AVPlayer(url: url)
let playerView = AVPlayerLayer()
playerView.player = player
playerView.frame = contentView.bounds
playerView.videoGravity = .resizeAspectFill
videoContainer.layer.addSublayer(playerView)
player?.volume = 5
player?.play()
player?.actionAtItemEnd = .none
NotificationCenter.default.addObserver(self,
selector: #selector(playerItemDidReachEnd(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: player?.currentItem)
}
#objc func playerItemDidReachEnd(notification: Notification) {
if let playerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: .zero, completionHandler: nil)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If anyone wants to try this code you can just c+p the file and it will run
I actually have 2 problems in the code below.
1- I have a Timer and a CABasicAnimation that both run when a longPressGesture is triggered. The timer is 15 secs and I decided to use it to just time the animation once I noticed the issue. What's happening is the animation finishes before the timer does. The animation will close/reach its endpoint around 1 sec before the timer finishes AND before CATransaction.setCompletionBlock() and animationDidStop(_:finished) are called. Basically the animation finishes too early.
2- If I take my finger off of the button, the longPressGesture's .cancelled/.ended are called and I pause the timer in invalidateTimer via pauseShapeLayerAnimation(). That was the only way I found to actually stop the animation. When I long press the button again, I restart the timer and animation from the beginning. The issue is because pauseShapeLayerAnimation() is also called when the timer stops (goes to 15secs) CATransaction.setCompletionBlock() are never animationDidStop(_:finished) called. They are only called once I put my finger back on the button.
UPDATE I fixed the second issue by just checking if seconds are != 0 in the invalidateTimer function
import UIKit
class ViewController: UIViewController {
//MARK:- UIElements
fileprivate lazy var roundButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = UIColor.blue
return button
}()
fileprivate lazy var timerLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.monospacedDigitSystemFont(ofSize: 22, weight: .medium)
label.textColor = UIColor.black
label.text = initialStrForTimerLabel
label.textAlignment = .center
return label
}()
fileprivate lazy var box: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .brown
return view
}()
//MARK:- Properties
fileprivate let shapeLayer = CAShapeLayer()
fileprivate let bgShapeLayer = CAShapeLayer()
fileprivate var basicAnimation: CABasicAnimation!
fileprivate var maxTimeInSecs = 15
fileprivate lazy var seconds = maxTimeInSecs
fileprivate var milliseconds = 0
fileprivate lazy var timerStr = initialStrForTimerLabel
fileprivate lazy var initialStrForTimerLabel = "\(maxTimeInSecs).0"
fileprivate weak var timer: Timer?
//MARK:- View Controller Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setAnchors()
setGestures()
}
fileprivate var wereCAShapeLayersAdded = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !wereCAShapeLayersAdded {
wereCAShapeLayersAdded = true
roundButton.layer.cornerRadius = roundButton.frame.width / 2
addBothCAShapeLayersToRoundButton()
}
}
//MARK:- Animation Methods
fileprivate func addBothCAShapeLayersToRoundButton() {
bgShapeLayer.frame = box.bounds
bgShapeLayer.path = UIBezierPath(rect: box.bounds).cgPath
bgShapeLayer.strokeColor = UIColor.lightGray.cgColor
bgShapeLayer.fillColor = UIColor.clear.cgColor
bgShapeLayer.lineWidth = 6
box.layer.addSublayer(bgShapeLayer)
box.layer.insertSublayer(bgShapeLayer, at: 0)
shapeLayer.frame = box.bounds
shapeLayer.path = UIBezierPath(rect: box.bounds).cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 6
shapeLayer.lineCap = .round
shapeLayer.strokeEnd = 0
box.layer.addSublayer(shapeLayer)
}
fileprivate var isBasicAnimationAnimating = false
fileprivate func addProgressAnimation() {
CATransaction.begin()
basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
removeAnimation()
if shapeLayer.timeOffset > 0.0 {
shapeLayer.speed = 1.0
shapeLayer.timeOffset = 0.0
}
basicAnimation.delegate = self
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
basicAnimation.fromValue = 0
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(seconds)
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
basicAnimation.isRemovedOnCompletion = false
CATransaction.setCompletionBlock {
print("CATransaction completion called\n")
}
shapeLayer.add(basicAnimation, forKey: "myAnimation")
CATransaction.commit()
}
fileprivate func removeAnimation() {
shapeLayer.removeAnimation(forKey: "myAnimation")
}
fileprivate func pauseShapeLayerAnimation() {
let pausedTime = shapeLayer.convertTime(CACurrentMediaTime(), from: nil)
shapeLayer.speed = 0.0
shapeLayer.timeOffset = pausedTime
print("animation has paused/stopped\n")
}
//MARK:- Anchors
fileprivate func setAnchors() {
view.addSubview(box)
view.addSubview(roundButton)
view.addSubview(timerLabel)
box.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 3).isActive = true
box.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 3).isActive = true
box.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -3).isActive = true
box.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -3).isActive = true
roundButton.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true
roundButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
roundButton.widthAnchor.constraint(equalToConstant: 75).isActive = true
roundButton.heightAnchor.constraint(equalToConstant: 75).isActive = true
timerLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10).isActive = true
timerLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
timerLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
}
}
//MARK:- CAAnimationDelegate
extension ViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print("***** animation done *****\n")
}
}
//MARK:- Timer Methods
extension ViewController {
fileprivate func startTimer() {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] _ in
self?.timerIsRunning()
})
}
#objc fileprivate func timerIsRunning() {
updateTimerLabel()
if !isBasicAnimationAnimating {
isBasicAnimationAnimating = true
addProgressAnimation()
}
milliseconds -= 1
if milliseconds < 0 {
milliseconds = 9
if seconds != 0 {
seconds -= 1
} else {
invalidateTimer()
print("timer done\n")
}
}
if milliseconds == 0 {
milliseconds = 0
}
}
fileprivate func updateTimerLabel() {
let millisecStr = "\(milliseconds)"
let secondsStr = seconds > 9 ? "\(seconds)" : "0\(seconds)"
timerLabel.text = "\(secondsStr).\(millisecStr)"
}
fileprivate func resetTimerSecsAndLabel() {
milliseconds = 0
seconds = maxTimeInSecs
timerLabel.text = initialStrForTimerLabel
}
fileprivate func invalidateTimer() {
if isBasicAnimationAnimating {
isBasicAnimationAnimating = false
if seconds != 0 {
pauseShapeLayerAnimation()
}
}
timer?.invalidate()
}
}
//MARK:- Gestures
extension ViewController {
fileprivate func setGestures() {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGesture))
roundButton.addGestureRecognizer(tapRecognizer)
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressGesture))
roundButton.addGestureRecognizer(longPressRecognizer)
}
#objc private func tapGesture(recognizer: UITapGestureRecognizer) {
print("tap\n")
}
#objc private func longPressGesture(recognizer: UILongPressGestureRecognizer) {
switch recognizer.state {
case .began:
resetTimerSecsAndLabel()
startTimer()
print("long gesture began\n")
case .ended, .cancelled:
invalidateTimer()
print("long gesture ended or cancelled\n")
case .failed:
print("long gesture failed\n")
default:
break
}
}
}
I think the animation finishing early is an illusion caused by three factors:
You are using CAMediaTimingFunctionName.easeInEaseOut which means the drawing starts slow and ends slow making it hard to judge the real end of drawing.
The drawing finishes by drawing over the start of the line which also makes it hard to see exactly when drawing stops.
Your timer should be subtracting 0.1 from the time before updating the label, because 0.1 has already passed when the timer first updates.
When I changed the timing function to CAMediaTimingFunctionName.linear and fixed the timer, it seemed to always hit 0 when the drawing finished.
I am working on a chatbot where the different type of response comes from the server and I display the response using UICollectionView cells in chat screen. Different type of cells presents according to server response. when server response with playing video, I am presenting the cell that contains youtube player. I am using https://github.com/kieuquangloc147/YouTubePlayer-Swift. The issue is when I scroll chat screen (collectionView) youtube player is opening again and again. Sometimes it is blocking all the UI element and stop scrolling. I tried different methods but can't able to resolve it. Here is the code:
PlayerView:
import UIKit
class PlayerView: UIView, YouTubePlayerDelegate {
override init(frame: CGRect) {
super.init(frame: frame)
addYotubePlayer()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// youtube player
lazy var youtubePlayer: YouTubePlayerView = {
let viewFrame = UIScreen.main.bounds
let player = YouTubePlayerView(frame: CGRect(x: 0, y: 0, width: viewFrame.width - 16, height: viewFrame.height * 1/3))
player.delegate = self
return player
}()
// used as an overlay to dismiss the youtube player
let blackView = UIView()
// youtube player loader
lazy var playerIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView()
indicator.activityIndicatorViewStyle = .whiteLarge
indicator.hidesWhenStopped = true
return indicator
}()
// shows youtube player
func addYotubePlayer() {
if let window = UIApplication.shared.keyWindow {
blackView.frame = window.frame
self.addSubview(blackView)
blackView.backgroundColor = UIColor(white: 0, alpha: 0.5)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleDismiss))
tap.numberOfTapsRequired = 1
tap.cancelsTouchesInView = false
blackView.addGestureRecognizer(tap)
let centerX = UIScreen.main.bounds.size.width / 2
let centerY = UIScreen.main.bounds.size.height / 2
blackView.addSubview(playerIndicator)
playerIndicator.center = CGPoint(x: centerX, y: centerY)
playerIndicator.startAnimating()
blackView.addSubview(youtubePlayer)
youtubePlayer.center = CGPoint(x: centerX, y: centerY)
blackView.alpha = 0
youtubePlayer.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.blackView.alpha = 1
self.youtubePlayer.alpha = 1
}, completion: nil)
}
}
func play(_ videoID: String) {
youtubePlayer.loadVideoID(videoID)
}
#objc func handleDismiss() {
blackView.removeFromSuperview()
UIApplication.shared.keyWindow?.viewWithTag(24)?.removeFromSuperview()
UIApplication.shared.keyWindow?.removeFromSuperview()
}
func playerReady(_ videoPlayer: YouTubePlayerView) {
self.playerIndicator.stopAnimating()
}
func playerStateChanged(_ videoPlayer: YouTubePlayerView, playerState: YouTubePlayerState) {
}
func playerQualityChanged(_ videoPlayer: YouTubePlayerView, playbackQuality: YouTubePlaybackQuality) {
}
}
YouTubePlayerCell (Which I present in collectionView wthe hen server responds for video):
import UIKit
class YouTubePlayerCell: ChatMessageCell {
var player: PlayerView = PlayerView(frame: UIScreen.main.bounds)
override func setupViews() {
super.setupViews()
setupCell()
}
func setupCell() {
messageTextView.frame = CGRect.zero
textBubbleView.frame = CGRect.zero
}
func loadVideo(with videoID: String) {
player.tag = 24
UIApplication.shared.keyWindow?.addSubview(player)
player.play(videoID)
}
override func prepareForReuse() {
super.prepareForReuse()
player.removeFromSuperview()
UIApplication.shared.keyWindow?.viewWithTag(24)?.removeFromSuperview()
}
}
Here is how I am presenting the YouTubePlayerCell in cellForItemAt method of UICollectionView
let message = messages[indexPath.row]
if message.actionType == ActionType.video_play.rawValue {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ControllerConstants.youtubePlayerCell, for: indexPath) as? YouTubePlayerCell {
self.resignResponders()
if let videoId = message.videoData?.identifier {
cell.loadVideo(with: videoId)
}
return cell
}
}
Full Source Code can be found here: https://github.com/imjog/susi_iOS/tree/ytplayer
I can see that in the below code
if let videoId = message.videoData?.identifier {
cell.loadVideo(with: videoId)
}
you are calling loadVideo method, which is responsible for showing the player.
So while scrolling you are reusing the cell and it calls loadVideo method and present the player. so the solution is don't start playing the video by default on displaying the cell, provide a play/pause button on the cell video overlay and on clicking the the button start playing the video.
If my analysis is wrong please let me know, what exact issue you have.
Why do you add the player as a subView each time you have to play the video ? My suggestion would be, as you are adding the player view on the whole screen, you can have just one instance of the view and add it just once(may be at the beginning) and keep it hidden. To play the video just unhide the player and load the video.
Instead best practice would be to have a View controller for Youtube Player and present it with the video id each time you need to play and then dismissing when done.
Thanks for your answers. I solve this by this way:
Rather than presenting Player on setting on the cell, I am now adding a thumbnail to the cell and a button on thumbnail view so that whenever the user clicks play button, it opens a new controller (Previously I was presenting in UIWindow) and presenting it as modalPresentationStyle of overFullScreen by using protocol because cell cannot present a ViewController.
Protocol: (In YouTubePlayerCell class)
protocol PresentControllerDelegate: class {
func loadNewScreen(controller: UIViewController) -> Void
}
Final YouTubePlayer.swift:
import UIKit
import Kingfisher
protocol PresentControllerDelegate: class {
func loadNewScreen(controller: UIViewController) -> Void
}
class YouTubePlayerCell: ChatMessageCell {
weak var delegate: PresentControllerDelegate?
var message: Message? {
didSet {
addThumbnail()
}
}
lazy var thumbnailView: UIImageView = {
let imageView = UIImageView()
imageView.image = ControllerConstants.Images.placeholder
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 15
imageView.isUserInteractionEnabled = true
return imageView
}()
lazy var playButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(ControllerConstants.Images.youtubePlayButton, for: .normal)
button.addTarget(self, action: #selector(playVideo), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func setupViews() {
super.setupViews()
setupCell()
prepareForReuse()
}
func setupCell() {
messageTextView.frame = CGRect.zero
textBubbleView.frame = CGRect(x: 8, y: 0, width: 208, height: 158)
textBubbleView.layer.borderWidth = 0.2
textBubbleView.backgroundColor = .white
}
override func prepareForReuse() {
super.prepareForReuse()
thumbnailView.image = nil
}
func addThumbnail() {
textBubbleView.addSubview(thumbnailView)
textBubbleView.addConstraintsWithFormat(format: "H:|-4-[v0]-4-|", views: thumbnailView)
textBubbleView.addConstraintsWithFormat(format: "V:|-4-[v0]-4-|", views: thumbnailView)
self.downloadThumbnail()
self.addPlayButton()
}
func addPlayButton() {
thumbnailView.addSubview(playButton)
playButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
playButton.widthAnchor.constraint(equalToConstant: 44).isActive = true
playButton.centerXAnchor.constraint(equalTo: thumbnailView.centerXAnchor).isActive = true
playButton.centerYAnchor.constraint(equalTo: thumbnailView.centerYAnchor).isActive = true
}
func downloadThumbnail() {
if let videoID = message?.videoData?.identifier {
let thumbnailURLString = "https://img.youtube.com/vi/\(videoID)/default.jpg"
let thumbnailURL = URL(string: thumbnailURLString)
thumbnailView.kf.setImage(with: thumbnailURL, placeholder: ControllerConstants.Images.placeholder, options: nil, progressBlock: nil, completionHandler: nil)
}
}
#objc func playVideo() {
if let videoID = message?.videoData?.identifier {
let playerVC = PlayerViewController(videoID: videoID)
playerVC.modalPresentationStyle = .overFullScreen
delegate?.loadNewScreen(controller: playerVC)
}
}
}
Delegate implementation in CollectionViewController:
extension ChatViewController: PresentControllerDelegate {
func loadNewScreen(controller: UIViewController) {
self.present(controller, animated: true, completion: nil)
}
}
Final source code can be found here: https://github.com/fossasia/susi_iOS/pull/372
I have a collectionViewCell that either plays a video or displays and image.Now the elements in the cell are generated programatically. I added tap gesture to toggle the sound when video play. The gesture recognizer wasn't getting called. I tried to place a button in story and get its action, that also didn't recieve a call. Then, I tried to place a view inside the cell, that also didn't display.
Here is my code with tap gesture:
import UIKit
import AVKit
import AVFoundation
#IBDesignable class CHCollectionImageCell: UICollectionViewCell {
// MARK: Properties
var imgView: UIImageView! = UIImageView()
var screenWidth:CGFloat = 0
var screenHeight:CGFloat = 0
var playerLayer: AVPlayerLayer!
let tapOnCell = UITapGestureRecognizer(target: self, action: #selector (CHCollectionImageCell.changeMuteRegimeVideo))
// MARK: Functions
override func awakeFromNib() {
super.awakeFromNib()
}
func configureCell(insight: InsightModel) {
imgView.removeFromSuperview()
if playerLayer != nil {
playerLayer.removeFromSuperlayer()
playerLayer = nil
}
self.removeGestureRecognizer(tapOnCell)
if insight.isVideo {
guard let unwrappedVideoURLString = insight.videoURL,
let unwrappedVideoURL = URL(string: unwrappedVideoURLString) else {
return
}
let playerItem = AVPlayerItem(url: unwrappedVideoURL)
let player = AVPlayer(playerItem: playerItem)
playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.bounds
player.isMuted = false
layer.addSublayer(playerLayer)
addGestureRecognizer(self.tapOnCell)
} else {
imgView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.width)
imgView.image = UIImage(named: "stone")
imgView.contentMode = UIViewContentMode.scaleAspectFill
imgView.clipsToBounds = true
clipsToBounds = true
addSubview(self.imgView)
}
}
/*
#IBAction func tapToTurnOfSound(_ sender: Any) {
if isInsightViedo{
if let unwrappedPlayer = playerLayer.player {
unwrappedPlayer.isMuted = !unwrappedPlayer.isMuted
}
}
//Even Tried adding view as below in the cell
//let tapView = UIView()
//tapView.backgroundColor = ColorCodes.appThemeColor
//self.addSubview(tapView)
//self.bringSubview(toFront: tapView)
//tapView.addGestureRecognizer(tapOnCell)
}
*/
func configureCell() {
imgView.removeFromSuperview()
if playerLayer != nil {
playerLayer.removeFromSuperlayer()
playerLayer = nil
}
self.removeGestureRecognizer(tapOnCell)
imgView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.width)
imgView.image = UIImage(named: "stone")
imgView.contentMode = UIViewContentMode.scaleAspectFill
imgView.clipsToBounds = true
clipsToBounds = true
addSubview(self.imgView)
}
func changeMuteRegimeVideo() {
if let unwrappedPlayer = playerLayer.player {
unwrappedPlayer.isMuted = !unwrappedPlayer.isMuted
}
}
}
Iam doing the same thing in my application by using the following code :
let longPressGesture:UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(viewController.longPress(_:)))
longPressGesture.minimumPressDuration = 0.8
longPressGesture.delegate = self
collectionView.addGestureRecognizer(longPressGesture)
and then call the function:
func longPress(_ longPressGestureRecognizer: UILongPressGestureRecognizer) {
if longPressGestureRecognizer.state == UIGestureRecognizerState.began {
let touchPoint = longPressGestureRecognizer.location(in: collectionView)
if eventsTableView.indexPathForRow(at: touchPoint) != nil {
let index = eventsTableView.indexPathForRow(at: touchPoint)//do whatever you want to do with this index
}}}
you can do whatever you want to do in this function. In my case i used this to enlarge the image in the collection view