my project
I have several videos in the table in a row.
The problem is that they are played synchronously.
On screen 1video and 2video play synchronously
How do i can track focus cell on table view and cell.player?.play(). And other cells cell.player?.pause()
my code:
class MyViewController
//don't work
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? ListViewCell {
cell.player?.pause()
}
}
//don't call
func tableView(_ tableView: UITableView, didUpdateFocusIn context: UITableViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
print("table view cell didUpdateFocusIn")
}
class MyTableviewCell
override func prepareForReuse() {
super.prepareForReuse()
player?.pause()
}
I'm making some changes to your project. Check this
https://yadi.sk/d/bQ-eIyMz3VF28V
Decide which video to play or pause when scrolling:
func handleScroll() {
if let indexPathsForVisibleRows = myTableView.indexPathsForVisibleRows, indexPathsForVisibleRows.count > 0 {
var focusCell: ListViewCell?
for indexPath in indexPathsForVisibleRows {
if let cell = myTableView.cellForRow(at: indexPath) as? ListViewCell {
if focusCell == nil {
let rect = myTableView.rectForRow(at: indexPath)
if myTableView.bounds.contains(rect) {
cell.player?.play()
focusCell = cell
} else {
cell.player?.pause()
}
} else {
cell.player?.pause()
}
}
}
}
}
Hope this will help you.
Use this in Your ViewController
extension ViewController {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(UITableView.contentOffset) {
if let playIndexPath = currentPlayIndexPath {
if let cell = tblInstaFeed.cellForRow(at: playIndexPath) {
if player.displayView.isFullScreen { return }
let visibleCells = tblInstaFeed.visibleCells
if visibleCells.contains(cell) {
cell.contentView.addSubview(player.displayView)
player.displayView.snp.remakeConstraints{
$0.edges.equalTo(cell)
}
self.player.play()
} else {
player.displayView.removeFromSuperview()
self.player.pause()
}
}
}
}
}
}
And Call that Like this:
var tableViewContext = 0
func addTableViewObservers() {
let options = NSKeyValueObservingOptions([.new, .initial])
tblInstaFeed?.addObserver(self, forKeyPath: #keyPath(UITableView.contentOffset), options: options, context: &tableViewContext)
}
And Call addTableViewObservers function in viewDidLoad
Hope this will help.
You can use indexPathForRow(at: CGpoint)
Here is Extension to tableView
import Foundation
import UIKit
extension UITableView {
// center point of content size
var centerPoint : CGPoint {
get {
return CGPoint(x: self.center.x + self.contentOffset.x, y: self.center.y + self.contentOffset.y);
}
}
// center indexPath
var centerCellIndexPath: IndexPath? {
if let centerIndexPath: IndexPath = self.indexPathForRow(at: self.centerPoint) {
return centerIndexPath
}
return nil
}
// visible or not
func checkWhichVideoToEnableAtIndexPath() -> IndexPath? {
guard let middleIndexPath = self.centerCellIndexPath else {return nil}
guard let visibleIndexPaths = self.indexPathsForVisibleRows else {return nil}
if visibleIndexPaths.contains(middleIndexPath) {
return middleIndexPath
}
return nil
}
}
Then Use at func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
if let visibleIndex = tableView.checkWhichVideoToEnableAtIndexPath() ,
let cellMiddle = tableView.cellForRow(at: visibleIndex) as? ListViewCell
{
cellMiddle.player?.play()
}
else
{
cell.player?.pause()
}
I have implemented similar functionality like facebook video player.
-- > Auto play video
--> if video pause by user don't auto play it
--> Pause video as soon as it is removed from the screen
This is tested and working in every scenarios
You need to track video status in your datasource array. And I suggest you to create datasource array with class not with struct as it we need reference
var videos:[VideoAlbum] = []
var lastContentOffset:CGFloat = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// print("scrollViewDidScroll")
let visibleCell = self.collView.indexPathsForVisibleItems
if visibleCell.count > 0 {
for indexPath in visibleCell {
if videos[indexPath.row].isPlaying || videos[indexPath.row].pausedByUser {
continue
}
if let cell = self.collView.cellForItem(at: indexPath) as? CustomCell,//cell.video?.mediaType == MediaType.video,
let rect = self.collView.layoutAttributesForItem(at: indexPath)?.center {
//If cell's center is top of bottom of view OR cell's center is on Bottom of top of cell then play
let cellCenterToView = self.collView.convert(rect, to: self.view)
let cellFrameToView = self.collView.convert((self.collView.layoutAttributesForItem(at: indexPath)?.frame)!, to: self.view)
// scrolling up
if scrollView.contentOffset.y > lastContentOffset {
if cellCenterToView.y < self.view.frame.height && (cellFrameToView.height - abs(cellFrameToView.origin.y)) > self.view.frame.size.height / 2 {
self.pauseVideo(notFor: indexPath)
self.playVideoForCell(cell: cell,at: indexPath)
} else {
self.pauseVideoFor(indexPath: indexPath)
print("ELSE on SCROLL UP")
}
} else {
if cellCenterToView.y > 0 && (cellFrameToView.height - abs(cellFrameToView.origin.y)) > self.view.frame.size.height / 2 {
print(self.view.frame.intersection(cellFrameToView).size.height)
self.pauseVideo(notFor: indexPath)
self.playVideoForCell(cell: cell,at: indexPath)
} else {
self.pauseVideoFor(indexPath: indexPath)
print("ELSE on SCROLL DOwn \((self.view.frame.intersection(cellFrameToView).size.height + 64))")
}
}
}
}
}
lastContentOffset = scrollView.contentOffset.y
}
And Here is function for playing and pausing video
//--------------------------------------------------------------------------------
private func pauseVideo(notFor autoPlayIndexPath:IndexPath) {
let visibleCell = self.collView.indexPathsForVisibleItems
if visibleCell.count > 0 {
for indexPath in visibleCell {
if videos[indexPath.row].isPlaying && indexPath.row != autoPlayIndexPath.row {
guard let cellToHide = self.collView.cellForItem(at: indexPath) as? CustomCell/*,cellToHide.video?.mediaType == MediaType.video */ else {continue}
cellToHide.player?.pause()
cellToHide.player?.removeTimeObserver(cellToHide.video.timeObserver)
cellToHide.video.currentTime = cellToHide.player?.currentTime() ?? kCMTimeZero
cellToHide.video.isPlaying = false
NotificationCenter.default.removeObserver(cellToHide.player?.currentItem, name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
// if cellToHide.video.timeObserver != nil {
// cellToHide.player?.removeTimeObserver(cellToHide.video.timeObserver)
// }
}
}
}
}
//--------------------------------------------------------------------------------
private func pauseVideoFor(indexPath:IndexPath) {
if videos[indexPath.row].isPlaying {
guard let cellToHide = self.collView.cellForItem(at: indexPath) as? CustomCell/*,cellToHide.video?.mediaType == MediaType.video */ else {return}
cellToHide.player?.pause()
cellToHide.player?.removeTimeObserver(cellToHide.video.timeObserver)
cellToHide.video.currentTime = cellToHide.player?.currentTime() ?? kCMTimeZero
cellToHide.video.isPlaying = false
NotificationCenter.default.removeObserver(cellToHide.player?.currentItem, name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
// if cellToHide.video.timeObserver != nil {
// cellToHide.player?.removeTimeObserver(cellToHide.video.timeObserver)
// }
}
}
And here is model that is loaded in collection view
class VideoAlbum:Codable {
let id, image,video: String?
let mediaType: JSONNull?
let type, deleted, createdOn: String?
let modifiedOn: JSONNull?
var isPlaying:Bool = false
var currentTime:CMTime = kCMTimeZero
var timeObserver:Any? = nil
var pausedByUser:Bool = false
var hidePlayingControls = false
enum CodingKeys: String, CodingKey {
case id, image, video
case mediaType = "media_type"
case type, deleted
case createdOn = "created_on"
case modifiedOn = "modified_on"
}
}
Hope it is helpful to you
Related
For some reason, when the cell that's being animated goes off-screen and comes back, the animation speed changes. Upon tapping the cell, a new view controller is opened. After I returned from the view controller to the initial view, the animation stopped altogether.
So far, I've tried to start the animation in cellForRowAt, but that didn't seem to work either.
Link to video for the problem: https://drive.google.com/file/d/1jt5IM1Ya4gIfzb1ok-NTmS2QTnrSTNoG/view?usp=sharing
Below is the code for willDisplay cell and the functions for animating my ui view inside my table view cell.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let cell = cell as? DirectionSummaryTableViewCell {
if let isLive = cell.isLive {
if isLive {
cell.animateBusStatusBackground()
} else {
cell.removeAnimation()
}
}
}
}
func animateBusStatusBackground() {
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.repeat, .autoreverse, .curveEaseInOut], animations: { [weak self] in
if self?.busStatusView.backgroundColor == .red {
self?.busStatusView.backgroundColor = .grey6
} else {
self?.busStatusView.backgroundColor = .red
}
}, completion: nil)
}
func removeAnimation() {
self.busStatusView.layer.removeAllAnimations()
self.layer.removeAllAnimations()
self.layoutIfNeeded()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: DirectionSummaryTableViewCell.identifier, for: indexPath) as? DirectionSummaryTableViewCell else { return UITableViewCell() }
cell.configure(with: viewModel.placeToPlacePossibleDirections[indexPath.section][indexPath.row].directions,
tripDuration: viewModel.placeToPlacePossibleDirections[indexPath.section][indexPath.row].tripTime,
destinationArrivalTime:viewModel.placeToPlacePossibleDirections[indexPath.section][indexPath.row].reachBy,
busDepartureTime: viewModel.placeToPlacePossibleDirections[indexPath.section][indexPath.row].directions.routes[0].departureTime,
startLocation: viewModel.placeToPlacePossibleDirections[indexPath.section][indexPath.row].directions.routes[0].stops[0].name, addFullLabel: true,
isLive: viewModel.placeToPlacePossibleDirections[indexPath.section][indexPath.row].responseType == "realtime")
return cell
}
func configure(with directions: PlaceToPlaceBusDirections, tripDuration: Double, destinationArrivalTime: String, busDepartureTime: String, startLocation: String, addFullLabel: Bool, isLive: Bool) {
self.directions = directions
self.isLive = isLive
self.collectionView.reloadData()
self.collectionView.layoutIfNeeded()
var formattedTripDuration = ""
if tripDuration > 60 {
let hrs = Int(tripDuration / 60)
formattedTripDuration += String(hrs) + " hr"
if hrs > 1 { formattedTripDuration += "s " } else { formattedTripDuration += " " }
}
formattedTripDuration += String(Int(tripDuration) % 60)
self.tripDurationLabel.text = formattedTripDuration + " mins"
self.destinationArrivalTimeLabel.text = Date.dateStringFromString(dateString: destinationArrivalTime)
if addFullLabel {
self.busDeparturePlaceAndTimeLabel.text = ("Leaves at " + Date.dateStringFromString(dateString: busDepartureTime) + " from " + startLocation).maxLength(length: 40)
} else {
self.busDeparturePlaceAndTimeLabel.text = ("Leaves at " + Date.dateStringFromString(dateString: busDepartureTime)).maxLength(length: 40)
}
if !isLive {
busStatusText.text = "Live"
busStatusText.textColor = .white
} else {
busStatusText.text = "Scheduled"
busStatusText.textColor = .grey2
busStatusView.backgroundColor = .grey6
}
removeAnimation()
}
How do I fix this so that the animation is the same all the time?
To animate cells when you return from a different screen you can use something like below:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let visibleCells = tableView.visibleCells as? [DirectionSummaryTableViewCell] {
visibleCells.forEach {
if let isLive = $0.isLive {
if isLive {
$0.animateBusStatusBackground()
} else {
$0.removeAnimation()
}
}
}
}
}
I have a collection view that shows a group of videos of whatever data object I pass it. When you press on one of the videos in the content view then a new view (DetailViewController) get's presented and the video gets shown in a more detailed view. In that DetailViewController there's a back button that dismisses the view and puts you back to the main page with the collection view - which is when the crash happens, when I go from that 'DetailViewController' back the main view controller.
Everything works perfectly when the collection view's 'scroll direction' is set to vertical, but when I set it to horizontal (which is what I want), it crashes like explained above.
The crash is:
Unexpectedly found nil while unwrapping an Optional value
and the source is:
private func findCurrentCell(path: IndexPath) -> UICollectionViewCell {
return playerCollect.cellForItem(at: path)!
}
I'm guessing the path or IndexPath isn't getting updated when the user scrolls so the index isn't getting updated?
Any ideas? If need be I can provide a video or some extra code.
EDIT (extra code):
import UIKit
import AVFoundation
import MMPlayerView
class ViewController: UIViewController {
var offsetObservation: NSKeyValueObservation?
lazy var mmPlayerLayer: MMPlayerLayer = {
let l = MMPlayerLayer()
l.cacheType = .memory(count: 5)
l.coverFitType = .fitToPlayerView
l.videoGravity = AVLayerVideoGravity.resizeAspect
l.replace(cover: CoverA.instantiateFromNib())
l.repeatWhenEnd = true
return l
}()
#IBOutlet weak var playerCollect: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
// remove previous download fails file
MMPlayerDownloader.cleanTmpFile()
self.navigationController?.mmPlayerTransition.push.pass(setting: { (_) in
})
offsetObservation = playerCollect.observe(\.contentOffset, options: [.new]) { [weak self] (_, value) in
guard let self = self, self.presentedViewController == nil else {return}
NSObject.cancelPreviousPerformRequests(withTarget: self)
self.perform(#selector(self.startLoading), with: nil, afterDelay: 0.2)
}
playerCollect.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 200, right:0)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.updateByContentOffset()
self?.startLoading()
}
mmPlayerLayer.getStatusBlock { [weak self] (status) in
switch status {
case .failed(let err):
let alert = UIAlertController(title: "err", message: err.description, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
case .ready:
print("Ready to Play")
case .playing:
print("Playing")
case .pause:
print("Pause")
case .end:
print("End")
default: break
}
}
mmPlayerLayer.getOrientationChange { (status) in
print("Player OrientationChange \(status)")
}
}
deinit {
offsetObservation?.invalidate()
offsetObservation = nil
print("ViewController deinit")
}
}
func backReplaceSuperView(original: UIView?) -> UIView? {
guard let path = self.findCurrentPath() else {
return original
}
let cell = self.findCurrentCell(path: path) as! PlayerCell
return cell.imgView
}
// add layer to temp view and pass to another controller
var passPlayer: MMPlayerLayer {
return self.mmPlayerLayer
}
func transitionWillStart() {
}
// show cell.image
func transitionCompleted() {
self.updateByContentOffset()
self.startLoading()
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let m = min(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height)
return CGSize(width: m, height: m*0.75)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
DispatchQueue.main.async { [unowned self] in
if self.presentedViewController != nil || self.mmPlayerLayer.isShrink == true {
//self.playerCollect.scrollToItem(at: indexPath, at: .centeredVertically, animated: true)
//self.updateDetail(at: indexPath)
} else {
self.presentDetail(at: indexPath)
}
}
}
fileprivate func updateByContentOffset() {
if mmPlayerLayer.isShrink {
return
}
if let path = findCurrentPath(),
self.presentedViewController == nil {
self.updateCell(at: path)
//Demo SubTitle
if path.row == 0, self.mmPlayerLayer.subtitleSetting.subtitleType == nil {
let subtitleStr = Bundle.main.path(forResource: "srtDemo", ofType: "srt")!
if let str = try? String.init(contentsOfFile: subtitleStr) {
self.mmPlayerLayer.subtitleSetting.subtitleType = .srt(info: str)
self.mmPlayerLayer.subtitleSetting.defaultTextColor = .red
self.mmPlayerLayer.subtitleSetting.defaultFont = UIFont.boldSystemFont(ofSize: 20)
}
}
}
}
fileprivate func presentDetail(at indexPath: IndexPath) {
self.updateCell(at: indexPath)
mmPlayerLayer.resume()
if let vc = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "DetailViewController") as? DetailViewController {
vc.data = DemoSource.shared.demoData[indexPath.row]
self.present(vc, animated: true, completion: nil)
}
}
fileprivate func updateCell(at indexPath: IndexPath) {
if let cell = playerCollect.cellForItem(at: indexPath) as? PlayerCell, let playURL = cell.data?.play_Url {
// this thumb use when transition start and your video dosent start
mmPlayerLayer.thumbImageView.image = cell.imgView.image
// set video where to play
mmPlayerLayer.playView = cell.imgView
mmPlayerLayer.set(url: playURL)
}
}
#objc fileprivate func startLoading() {
self.updateByContentOffset()
if self.presentedViewController != nil {
return
}
// start loading video
mmPlayerLayer.resume()
}
private func findCurrentPath() -> IndexPath? {
let p = CGPoint(x: playerCollect.frame.width/2, y: playerCollect.contentOffset.y + playerCollect.frame.width/2)
return playerCollect.indexPathForItem(at: p)
}
private func findCurrentCell(path: IndexPath) -> UICollectionViewCell {
return playerCollect.cellForItem(at: path)!
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return DemoSource.shared.demoData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PlayerCell", for: indexPath) as? PlayerCell {
cell.data = DemoSource.shared.demoData[indexPath.row]
return cell
}
return UICollectionViewCell()
}
}
It is bad practice to use the forced optional operator ! as it will often lead the error message you described. Your findCurrentPath calculation has not been updated for horizontal movement. First, change your findCurrentPath method to (what I believe you are trying to achieve):
private func findCurrentPath() -> IndexPath? {
let p = CGPoint(x: playerCollect.contentOffset.x + playerCollect.frame.width/2,
y: playerCollect.frame.height/2)
return playerCollect.indexPathForItem(at: p)
}
To prevent crashing, even if it doesn't do what you want exactly, I cleaned up only two other methods, because there are way too many forced optional operators to address them all; change your findCurrentCell method to the following:
private func findCurrentCell(path: IndexPath) -> UICollectionViewCell? {
return playerCollect.cellForItem(at: path)
}
and finally your backReplaceSuperView method to:
func backReplaceSuperView(original: UIView?) -> UIView? {
guard let path = self.findCurrentPath(),
let cell = self.findCurrentCell(path: path) as? PlayerCell else {
return original
}
return cell.imgView
}
I'm trying to implement an infinite scroll/pagination feature to a table view. I use scrollViewDidScroll to measure when the user reaches the bottom of the page, which then triggers a function to fetch the next batch of data. However I think the measurements are off because my fetchMoreEvents function is being triggered upon the launch of the app.
This is the pagination code (scrollViewDidScroll and fetchMoreEvents):
func fetchMoreEvents() {
fetchingMore = true
var page = 1
page += 1
let seatGeekApiUrl = URL(string: "https://api.seatgeek.com/2/events?venue.state=NY&page=\(page)&client_id=MTM5OTE0OTd8MTU0MjU2NTQ4MC4z")!
fetchData(url: seatGeekApiUrl) { (result: FetchResult<Welcome>) -> (Void) in
switch result {
case .success(let object): self.eventData.append(contentsOf: object.events)
print("\neventData: \n\n\(self.eventData)")
case .failure(let error):
print("\nError decoding JSON: \n\n\(error)")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
print("\nFetching next batch of events: (Page \(page))\n")
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.height {
if !fetchingMore {
fetchMoreEvents()
}
}
}
Once fetchMoreEvents is triggered, I have it append my eventData array with the next page of results and reload the table view. My print statement confirms that it fetches page 2 of the data, but like I said that happens immediately instead of when I scroll down the page. Also, it never gets triggered again.
Is this an issue with the measurements in scrollViewDidScroll, or am I going wrong somewhere else?
These are the table view methods if they're applicable here:
override func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return eventData.count
} else if section == 1 && fetchingMore {
return 1
}
return 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "eventsCell", for: indexPath) as! EventsTableViewCell
let event = eventData[indexPath.row]
// Labels
cell.eventNameLabel.text = event.title
cell.eventVenueLabel.text = event.venue.nameV2
cell.eventAddressLabel.text = event.venue.address
cell.eventTimeLabel.text = dateFormatter.string(from: event.dateTimeLocal)
// Image
if let urlString = event.performers[0].image, let imageURL = URL(string: urlString) {
ImageService.getImage(url: imageURL) { (image) in
cell.eventImageView.image = image
}
}
else {
cell.eventImageView.image = UIImage(named: "noImageFound")
}
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "loadingCell", for: indexPath) as! LoadingCell
cell.spinner.startAnimating()
return cell
}
}
Declare an Bool as property
var isAllRowSeeked: Bool = false
Put this logic in your scrollViewDidScroll
let height = scrollView.frame.size.height
let contentYoffset = scrollView.contentOffset.y
let distanceFromBottom = scrollView.contentSize.height - contentYoffset
if distanceFromBottom < height,
self.isAllRowSeeked == true {
// you've reached the end, you are now ready to load more data
self.isAllRowSeeked = false
}
Now in cellForRowAtIndexPath
if fetchingMore == true,
isLastSectionRow(indexPath: indexPath) {// it's the last row of this section
state.isAllRowSeeked = true
return paginatorUI?.getPaginatedLoadMoreCell()
} else {
return nil
}
Add the following method
public func isLastSectionRow(indexPath: IndexPath) -> Bool {
let lastSection = tableview.dataSource?.numberOfSections?(in: tableview)
?? 1
let lastRow = tableview.dataSource?.tableView(tableview,
numberOfRowsInSection: indexPath.section)
?? 0
return lastSection == (indexPath.section+1) && lastRow == (indexPath.row+1)
}
Actually this logic is borrowed from one of my pod, which you can use. Complete code for this pagination can be found here
Edit: I think problem is somehow connected with cell reuse ability, but still can't figure out how to fix
I'm building an application that has a tableView with a posts with an URL to a music track. When post becomes fully visible, my player starts playback of posts track. During playback I should display progress of playback on a waveform view in a post.
The problem is that observation correctly works only with 3 first cells, for the next cells waveformPlot does not get updated and seeking stops working.
I have no idea what is causing this problem. Any help is appreciated.
Here is a layout:
In my viewController's viewDidLoad I init AVPlayer observer with addPeriodicTimeObserver and assign its return type to a variable playerObserver.
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
timeLinePostsArray = User.sharedUser.requestTimelineData()
playerObserver = StreamMusicPlayer.shared.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2),
queue: DispatchQueue.main,
using: { (progress) in
if StreamMusicPlayer.shared.isPlaying {
self.cellToObserve?.updatePlot(with: progress)
}
})
}
Below is the code I use to detect fully visible cell. When fully visible cell is detected, I assign it to variable cellToObserve for observer and begin playback of cell's music track.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for item in tableView.indexPathsForVisibleRows!{
if tableView.bounds.contains(tableView.rectForRow(at: item)){
let fullyVisibleCell = tableView.cellForRow(at: item) as! HomeControllerTableViewCell
if StreamMusicPlayer.currentItemURL != fullyVisibleCell.containedMusicTrack?.trackURL.absoluteString {
//Play music track and observe the playback progress
self.cellToObserve = fullyVisibleCell
self.numberOfCellToObserve = tableView.indexPath(for: fullyVisibleCell)?.row
fullyVisibleCell.playButton.isEnabled = true
fullyVisibleCell.plot.isUserInteractionEnabled = true
StreamMusicPlayer.playItem(musicTrack: fullyVisibleCell.containedMusicTrack!)
}
} else {
let _cell = tableView.cellForRow(at: item) as! HomeControllerTableViewCell
_cell.plot.isUserInteractionEnabled = false
_cell.playButton.isEnabled = false
}
}
}
In the viewController's deinit method I attempt to remove player's observer.
func attemptRemoveObservation() {
if self.playerObserver != nil {
StreamMusicPlayer.shared.removeTimeObserver(self.playerObserver)
self.playerObserver = nil
}
}
deinit {
self.attemptRemoveObservation()
}
This is my player's implementation:
class StreamMusicPlayer: AVPlayer {
private override init(){
super.init()
}
static var currentItemURL: String?
static var shared = AVPlayer()
static func playItem(musicTrack: MusicTrack) {
let item = AVPlayerItem(url: musicTrack.trackURL)
StreamMusicPlayer.shared.replaceCurrentItem(with: item)
StreamMusicPlayer.currentItemURL = musicTrack.trackURL.absoluteString
StreamMusicPlayer.shared.play()
}
}
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}
If needed, here is full viewController's source code:
//
// HomeViewController.swift
// CheckMyTrack
//
// Created by Alexey Savchenko on 09.03.17.
// Copyright © 2017 Alexey Savchenko. All rights reserved.
//
import UIKit
import CoreMedia
import Foundation
class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
//MARK: Vars
var playerObserver: Any!
var timeLinePostsArray: [TimeLinePost] = []
var cellToObserve: HomeControllerTableViewCell?
var numberOfCellToObserve: Int?
//MARK: Outlets
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
timeLinePostsArray = User.sharedUser.requestTimelineData()
playerObserver = StreamMusicPlayer.shared.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2),
queue: DispatchQueue.main,
using: { (progress) in
if StreamMusicPlayer.shared.isPlaying {
self.cellToObserve?.updatePlot(with: progress)
}
})
}
func attemptRemoveObservation(){
if self.playerObserver != nil{
StreamMusicPlayer.shared.removeTimeObserver(self.playerObserver)
self.playerObserver = nil
}
}
//MARK: TableView delegate methods
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return timeLinePostsArray.count
}
//TODO: Implement correct change of observing cell plot
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for item in tableView.indexPathsForVisibleRows!{
if tableView.bounds.contains(tableView.rectForRow(at: item)){
let fullyVisibleCell = tableView.cellForRow(at: item) as! HomeControllerTableViewCell
if StreamMusicPlayer.currentItemURL != fullyVisibleCell.containedMusicTrack?.trackURL.absoluteString {
//Play music track and observe the playback progress
self.cellToObserve = fullyVisibleCell
self.numberOfCellToObserve = tableView.indexPath(for: fullyVisibleCell)?.row
fullyVisibleCell.playButton.isEnabled = true
fullyVisibleCell.plot.isUserInteractionEnabled = true
StreamMusicPlayer.playItem(musicTrack: fullyVisibleCell.containedMusicTrack!)
}
} else {
let _cell = tableView.cellForRow(at: item) as! HomeControllerTableViewCell
_cell.plot.isUserInteractionEnabled = false
_cell.playButton.isEnabled = false
}
}
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// if let _cell = cell as? HomeControllerTableViewCell{
// if self.playerToken != nil{
// StreamMusicPlayer.shared.removeTimeObserver(self.playerToken)
// self.playerToken = nil
// }
// }
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HomeViewControllerCell", for: indexPath) as? HomeControllerTableViewCell
let track = timeLinePostsArray[indexPath.row].musicTrack
cell?.containedMusicTrack = track
cell?.authorNameLabel.text = timeLinePostsArray[indexPath.row].authorName
cell?.authorPicture.image = timeLinePostsArray[indexPath.row].authorPicture
cell?.trackName.text = track.trackName
cell?.plot.populateWithData(from: track.trackWaveformData)
cell?.plot.normalColor = UIColor.gray
cell?.plot.progressColor = UIColor.orange
cell?.playAction = { (cell) in
if StreamMusicPlayer.shared.isPlaying {
if StreamMusicPlayer.currentItemURL == cell.containedMusicTrack?.trackURL.absoluteString {
cell.playButton.setImage(UIImage(named: "play"), for: .normal)
StreamMusicPlayer.shared.pause()
}
} else {
StreamMusicPlayer.shared.play()
cell.playButton.setImage(UIImage(named: "pause"), for: .normal)
}
}
cell?.likeAction = { (cell) in
}
return cell!
}
deinit {
self.attemptRemoveObservation()
}
}
I have found the origin of problem.
It is necessary to implement prepareForReuse method for a custom cell class I use and perform a clean-up of a cell's waveformPlot.
WaveformPlot's waveforms property requires to be explicitly emptied.
My implementation is below:
override func prepareForReuse() {
plot.clearPlot()
plot.waveforms = []
}
func clearPlot(){
for item in self.subviews{
item.removeFromSuperview()
}
guard let layers = self.layer.sublayers else { return }
for _item in layers{
_item.removeFromSuperlayer()
}
}
I have a cell which has an image in it that is retrieved from Amazon S3. The height of this image will affect the height of the cell but unfortunately the image is retrieved in a completion handler after the cell height is determined. If I scroll down and up the cell reloads properly.
Before scrolling (incorrect cell height):
After scrolling (correct cell height):
Cell configuration (some unnecessary stuff redacted for readability):
func setCompletionCell(completion: CompletionPublic, contentType: String, classType: String){
self.completionPub = completion
self.selectionStyle = UITableViewCellSelectionStyle.None //Disables clicking
//Setting image or video
if (contentType == "image"){
nwa.fetchSignedUrl("image/accepted/" + completion.mediaId + ".png") { (result, err) in
self.nwa.fetchImage(result) { (image, err) in
if image != nil{
let screenSize: CGRect = UIScreen.mainScreen().bounds
var multiplyNum = screenSize.width / image.size.width
//if image height is going to be more than 60% of the screen, resize width and height to ensure that it isn't greater than 60% while keeping the aspect ratio correct
if ((image.size.height*multiplyNum) > (screenSize.height*0.6)){
multiplyNum = screenSize.height*0.6 / image.size.height
self.imageViewWidthConstraint.constant = (multiplyNum*image.size.width)
self.imageViewHeightConstraint.constant = screenSize.height*0.6
}
else{
self.imageViewWidthConstraint.constant = screenSize.width
self.imageViewHeightConstraint.constant = (multiplyNum*image.size.height)
}
self.imgView.image = image
}
else{
//no image returned
}
}
}
}
else if (contentType == "video"){
ytplayer.loadWithVideoId(completion.mediaId)
}
}
TableView delegate methods:
func callNWT(tableView: UITableView, completionHandler: () -> ()) {
switch trendingToggle {
case 0:
nwt.getTrendingBounties(0) { (bountyArr, err) in
//#TODO: change pos
if bountyArr == nil {
self.bountyArr = []
}
else {
self.bountyArr = bountyArr as [BountyPublic]
}
if self.bountyArr.count == 0 {
completionHandler()
}
self.reloadTableViewContent(tableView)
}
case 1:
nwt.getTrendingCompletions(0) { (compArr, err) in
if compArr == nil {
self.compArr = []
}
else {
self.compArr = compArr as [CompletionPublic]
}
if self.compArr.count == 0 {
completionHandler()
}
self.reloadTableViewContent(tableView)
}
case 2:
nwt.getTrendingPeople(0) { (peopleArr, err) in
if peopleArr == nil {
self.peopleArr = []
}
else {
self.peopleArr = peopleArr as [Person]
}
if self.peopleArr.count == 0 {
completionHandler()
}
self.reloadTableViewContent(tableView)
}
default:
break
}
}
func configureTableView(tableView: UITableView){
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 500.0
tableView.allowsSelection = false; //disables selection highlighting of cells
tableView.tableFooterView = UIView()
tableView.dataSource = self
}
func reloadTableViewContent(tableView: UITableView) {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
tableView.reloadData()
print("reloading table view content")
tableView.scrollRectToVisible(CGRectMake(0, 0, 1, 1), animated: false)
})
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if trendingToggle == 0 {
return bountyArr.count
}
else if trendingToggle == 1 {
return compArr.count
}
else {
return peopleArr.count
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if trendingToggle == 0 {
return bountyCellAtIndexPath(tableView, indexPath: indexPath)
}
else if trendingToggle == 1 {
return completedCellAtIndexPath(tableView, indexPath:indexPath)
}
else {
return personCellAtIndexPath(tableView, indexPath: indexPath)
}
}
func completedCellAtIndexPath(tableView: UITableView, indexPath:NSIndexPath) -> CompletedCell{
var cell: CompletedCell
if compArr[indexPath.row].contentType == "image" {
cell = tableView.dequeueReusableCellWithIdentifier(completedImgCellIdentifier) as! CompletedCell
let comp = compArr[indexPath.row]
cell.setCompletionCell(comp, contentType: "image", classType: "trending")
}
else { //video
cell = tableView.dequeueReusableCellWithIdentifier(completedVidCellIdentifier) as! CompletedCell
let comp = compArr[indexPath.row]
cell.setCompletionCell(comp, contentType: "video", classType: "trending")
}
return cell
}
How do I ensure the cell height is correctly calculated the first time? Is there a way I can delay the code executing until the image is retrieved? Or is that not a good idea?
Reload the tableview in completion handler.
tableView.reloadData()
In the heightForRowAtIndex method for the datasource/delegate protocols recalculate the height using the image and return the appropriate value for each individual cell.
When you call reloadData() all the datasource/delegate methods are called again, so return the correct height will allow you to resize the cell as needed.