Reloading tableview row without interrupting the vide playing inside - ios

I implemented AVQueuePlayer inside of each tableview cell and those cells showing a video.
When I reload any of these rows, the video inside gets interrupted and plays from the beginning.
My question is that how can I reload the row without touching the video at all.
UIView.performWithoutAnimation {
self.tableView.reloadRows(at: [indexPath], with: .none)
}
CellForRowAt
let cell: PostsWithVideoCustom = tableView.dequeueReusableCell(withIdentifier: "CellWithVideo", for: indexPath) as! PostsWithVideoCustom
let scale = UIScreen.main.bounds.width / CGFloat(release.photoWidth)
let bestHeight = CGFloat(release.photoHeight) * scale
cell.videoViewHeight.constant = bestHeight
cell.videoView.frame.size.height = bestHeight
let soundMuteTap = UITapGestureRecognizer(target: self, action: #selector(videoSoundMute))
cell.videoView.addGestureRecognizer(soundMuteTap)
cell.videoView.layer.sublayers?
.filter { $0 is AVPlayerLayer }
.forEach { $0.removeFromSuperlayer() }
CacheManager.shared.getFileWith(stringUrl: release.photoFilename) { result in
switch result {
case let .success(url):
let playerItem = AVPlayerItem(url: url)
cell.videoView.player = AVQueuePlayer(playerItem: playerItem)
cell.videoView.looper = AVPlayerLooper(player: cell.videoView.player!, templateItem: playerItem)
var layer = AVPlayerLayer()
layer = AVPlayerLayer(player: cell.videoView.player)
layer.backgroundColor = UIColor.white.cgColor
layer.frame = cell.videoView.bounds
layer.videoGravity = .resizeAspectFill
cell.videoView.layer.addSublayer(layer)
if self.userDefaults.string(forKey: "videoSound") == "1" {
cell.videoView.player?.isMuted = false
} else {
cell.videoView.player?.isMuted = true
}
case let .failure(error):
print(error)
}
}
return cell

Assuming you only play one video at a time, you could programatically create an AVQueuePlayer and give it desired frame and position instead of putting it in your cells. Doing this have two merits:
It achieves your goal.
Less memory usage.
You would also have to implement UIScrollViewDelegate to update AVPlayer's current position & playing state.

Related

How to reuse UICollectionViewCell with nested UIView for video

I am using a UICollectionViewController to display cells with content loaded from an external JSON api. To be specific, these cells include UILabels for the user that posted the video, the title of the video, etc.
In addition to the text labels, there is a large UIView in the middle of the cell to play video.
The following is the code for my UICollectionViewCell.
class PostCell : UICollectionViewCell {
var videoCell: VideoPlayerView
let storyTitleLabel: UILabel = {
let storyTitleLabel = UILabel()
storyTitleLabel.translatesAutoresizingMaskIntoConstraints = false
return storyTitleLabel
}()
let usernameLabel: UILabel = {
let usernameLabel = UILabel()
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
return usernameLabel
}()
func prepare(post: ApiPost) {
self.usernameLabel.text = post.user?.username
self.postTitleLabel.text = post.title!
self.storyTitleLabel.text = post.story?.title
self.videoCell.prepare(post: post)
}
func focus() {
self.videoCell.focus()
}
func unfocus() {
self.videoCell.unfocus()
}
}
When a user is scrolling through the UICollectionView and the middle of the screen is over the visible UICollectionViewCell, the focus method is called.
This loads up the video corresponding to the ApiPost (api data wrapper) for that UICollectionViewCell.
I have also programmed my own subclass of UIView for the videoCell as you can see. The following is the source code of that class.
class VideoPlayerView: UIView {
func prepare(post: ApiPost) {
self.post = post
self.videoUrl = sharedConfig.apiUrl + "/stream/" + post.video!.uuid! + ".mp4"
if let url = URL(string: self.videoUrl!) {
let avPlayerItem = AVPlayerItem(url: url)
if (self.hasReceivedApiData) {
DispatchQueue.global(qos: .background).async {
print("inserting...")
self.player?.replaceCurrentItem(with: avPlayerItem)
print("done")
}
} else {
self.hasReceivedApiData = true
self.player = AVQueuePlayer.init()
self.player?.automaticallyWaitsToMinimizeStalling = false
self.player?.insert(avPlayerItem, after: nil)
self.player?.volume = 1
let playerLayer = AVPlayerLayer(player: self.player)
playerLayer.frame = CGRect(x: 0, y: -70, width: self.frame.width - 32, height: self.frame.height)
self.layer.addSublayer(playerLayer)
}
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem, queue: nil, using: { (_) in
DispatchQueue.main.async {
self.player?.seek(to: kCMTimeZero)
self.player?.play()
}
})
}
}
func focus() {
self.player?.volume = 1.0
self.player?.play()
}
func unfocus() {
self.player?.seek(to: kCMTimeZero)
self.player?.pause()
}
}
The problem that I am having is that when I scroll over the UICollectionView, it lags really badly when prepare() is called on a reused UICollectionViewCell. It seems that calling self.player?.replaceCurrentItem(with: avPlayerItem) is slowing the UI down on scroll. I have verified this by putting the print calls between. It seems this is a slow operation that is lagging the UI, as you can see, I even tried putting that operation on a different thread, but no luck.
Am I displaying video properly in a UICollectionViewCell? Should I be doing this using some other method? The only way I could figure out to clear the old video is by using an AVQueuePlayer to clear out old AVPlayerItems, and insert the new AVPlayerItem when the cell is reused and prepared for the next video.
Any help on this would be greatly appreciated! Thanks in advance.

Animate cell.imageview on async load

Im trying to do a table where the imageView on the cell changes alpha from 0 to 1 when the image is done loading (async).
What ever I do it seem that the image is just shown at one and not fading in. I'm sure it's some kind of race condition but I am new to animations in iOS and have no idea how to solve this. Any input would be great.
Here is my code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
//Configure the cell...
let episode = episodes[indexPath.row]
cell.textLabel?.text = episode.title
cell.detailTextLabel?.text = episode.content
let logoUrl = URL(string: episode.logoUrl!)
if (episode.logoImage == nil){
episode.logoImage = UIImage()
DispatchQueue.global().async {
let data = try? Data(contentsOf: logoUrl!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
DispatchQueue.main.async {
episode.logoImage = UIImage(data: data!)
cell.imageView?.image = episode.logoImage
self.episodesTable.reloadData()
cell.imageView?.alpha = 0
UIView.animate(withDuration: 1, animations: {
cell.imageView?.alpha = 1
})
}
}
} else{
cell.imageView?.image = episode.logoImage
}
return cell
}
You need to set alpha to 0 first before animating to 1.
cell.imageView?.alpha = 0
UIView.animate(withDuration: 1, animations: {
cell.imageView?.alpha = 1
})
Also, you dont need to reload table. Remove self.episodesTable.reloadData().
You are spanning a background thread and loading the image from url inside that thread. What if, in between user has scrolled the cell. You would be left with a wrong image on a wrong cell(because of cell reuse, that is).
My advice is to use SDWebImageCache, and use its completion block to animate the alpha.
// Changing animation duration to 0.2 seconds from 1 second
if(cacheType == SDImageCacheTypeNone) {
cell.imageView?.alpha = 0
[UIView animateWithDuration:0.2 animations:^{
cell.imageView?.alpha = 1;
}];
}
reloadData() call is causing reloading of all the cells including the one you are trying to animate. My advice is to mark your cell with it's index path. After async call check if it is still presenting the right data and animate it without reloading the whole table view.
// ...
cell.tag = indexPath.item
DispatchQueue.global().async {
// async load
DispatchQueue.main.async {
guard cell.tag == indexPath.item else { return }
cell.imageView?.alpha = 0.0
cell.imageView?.image = image
// animate
}
}
// ...

Using VideoNode in CollectionView instead of AVPlayer

I have a UICollectionView which displays photos and videos. I was using an AVPlayer to display videos, however this resulted in very choppy scrolling. To combat this I am using the VideoNode from the AsyncDisplayKit. Currently in my cellForItemAt method I do the following:
cell.viewWithTag(300)?.removeFromSuperview()
if (image) {
//show image
}
else if (video) {
let mainNode = ASDisplayNode()
let videoNode = ASVideoNode()
DispatchQueue.background {
videoNode.frame = CGRect(x: 0.0,y:0.0,width: cell.bounds.width,height: cell.bounds.height)
videoNode.gravity = AVLayerVideoGravityResizeAspectFill
videoNode.shouldAutoplay = true
videoNode.shouldAutorepeat = true
videoNode.muted = true
videoNode.asset = AVAsset(url: cached_url)
}
DispatchQueue.main.async {
mainNode.addSubnode(videoNode)
mainNode.view.tag = 300
cell.addSubview(mainNode.view)
cell.sendSubview(toBack: mainNode.view)
videoNode.play()
cell.backgroundImageView.alpha = 0
cell.gradientOverlay.alpha = 1
}
}
However, with fast scrolling this is still a bit choppy, and the cells containing videos are briefly white before the video shows. Is there a way I could improve this code to further improve scrolling performance and make it as smooth as possible?

Custom pop animation to a UICollectionViewController doesn't work

I have a collection view called ProfileCollectionViewController for collection of images.
When clicked on an image it presents a HorizontalProfileViewController which displays images in full screen.
When back button is pressed on HorizontalProfileViewController I want the full screen image to animate back to a thumbnail in ProfileViewController. I pass the selected index path from ProfileViewController as initialIndexPath to HorizontalProfileViewController so that the position of thumbnail is known. Below is my transition animation code
import UIKit
class SEHorizontalFeedToProfile: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.2
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
if let profileVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as? SEProfileGridViewController, horizontalFeedVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as? SEProfileHorizontalViewController, containerView = transitionContext.containerView() {
let duration = transitionDuration(transitionContext)
profileVC.collectionView?.reloadData()
if let indexPath = horizontalFeedVC.initialIndexPath {
let cell = profileVC.collectionView?.cellForItemAtIndexPath(indexPath)
print(indexPath)
let imageSnapshot = horizontalFeedVC.view.snapshotViewAfterScreenUpdates(false)
let snapshotFrame = containerView.convertRect(horizontalFeedVC.view.frame, fromView: horizontalFeedVC.view)
imageSnapshot.frame = snapshotFrame
horizontalFeedVC.view.hidden = true
profileVC.view.frame = transitionContext.finalFrameForViewController(profileVC)
containerView.insertSubview(profileVC.view, belowSubview: horizontalFeedVC.view)
containerView.addSubview(imageSnapshot)
UIView.animateWithDuration(duration, animations: {
var cellFrame = CGRectMake(0, 0, 0, 0)
if let theFrame = cell?.frame {
cellFrame = theFrame
}
let frame = containerView.convertRect(cellFrame, fromView: profileVC.collectionView)
imageSnapshot.frame = frame
}, completion: { (succeed) in
if succeed {
horizontalFeedVC.view.hidden = false
// cell.contentView.hidden = false
imageSnapshot.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
})
}
}
}
}
I put breakpoints and found out that in the code
let cell = profileVC.collectionView?.cellForItemAtIndexPath(indexPath)
the cell is nil. I don't understand why it would be nil. Please help me. I thank you in advance.
The profileVC is a subclass of UICollectionViewController
PS: Please check out the following project that does exactly the same thing without any issues. I tried to mimic it but it doesn't work on mine.
https://github.com/PeteC/InteractiveViewControllerTransitions
As was pointed out in the comments on the question, the cell is set to nil because the cell is not visible. Since your HorizontalProfileViewController allows scrolling to other pictures it is possible to scroll to an image that has not yet had a UICollectionViewCell created for it in your ProfileCollectionViewController.
You can remedy this by scrolling to where the current image should be on the UICollectionView. You say that initialIndexPath is initially the indexPath of the image that was first selected. Assuming you update that as the user scrolls to different images so that it still accurately represents the on screen image's indexPath, we can use that to force the UICollectionView to scroll to the current cell if necessary.
With a few tweaks to your code this should give you the desired effect:
import UIKit
class SEHorizontalFeedToProfile: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.2
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
if let profileVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as? SEProfileGridViewController, horizontalFeedVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as? SEProfileHorizontalViewController, containerView = transitionContext.containerView() {
let duration = transitionDuration(transitionContext)
//profileVC.collectionView?.reloadData() //Remove this
if let indexPath = horizontalFeedVC.initialIndexPath {
var cell = profileVC.collectionView?.cellForItemAtIndexPath(indexPath) //make cell a var so it can be reassigned if necessary
print(indexPath)
if cell == nil{ //This if block is added. Again it is crucial that initialIndexPath was updated as the user scrolled.
profileVC.collectionView?.scrollToItemAtIndexPath(horizontalFeedVC.initialIndexPath, atScrollPosition: UICollectionViewScrollPosition.CenteredVertically, animated: false)
profileVC.collectionView?.layoutIfNeeded() //This is necessary to force the collectionView to layout any necessary new cells after scrolling
cell = profileVC.collectionView?.cellForItemAtIndexPath(indexPath)
}
let imageSnapshot = horizontalFeedVC.view.snapshotViewAfterScreenUpdates(false)
let snapshotFrame = containerView.convertRect(horizontalFeedVC.view.frame, fromView: horizontalFeedVC.view)
imageSnapshot.frame = snapshotFrame
horizontalFeedVC.view.hidden = true
profileVC.view.frame = transitionContext.finalFrameForViewController(profileVC)
containerView.insertSubview(profileVC.view, belowSubview: horizontalFeedVC.view)
containerView.addSubview(imageSnapshot)
UIView.animateWithDuration(duration, animations: {
var cellFrame = CGRectMake(0, 0, 0, 0)
if let theFrame = cell?.frame {
cellFrame = theFrame
}
let frame = containerView.convertRect(cellFrame, fromView: profileVC.collectionView)
imageSnapshot.frame = frame
}, completion: { (succeed) in
if succeed {
horizontalFeedVC.view.hidden = false
// cell.contentView.hidden = false
imageSnapshot.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
})
}
}
}
Now if the current UICollectionViewCell is not yet created it:
Scrolls so the indexPath of the cell is centered on the view
Forces the UICollectionView to layout its subviews which creates any UITableViewCells that are necessary
Asks for the UITableViewCell again, but this time it should not be nil

SWIFT 2 - UICollectionView - slow scrolling

I have setup a uicollectionview in my project that get data from a JSON file. Everything works good however, the scrolling is very slow and when the view is scrolling the coming cell, for few moments shows the content of the cell before.
I have tried using dispatch_async but it still very slow and jumpy.
any Idea what am I doing wrong?
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let videoCell = collectionView.dequeueReusableCellWithReuseIdentifier("VideoCell", forIndexPath: indexPath) as UICollectionViewCell
let communityViewController = storyboard?.instantiateViewControllerWithIdentifier("community_id")
videoCell.frame.size.width = (communityViewController?.view.frame.size.width)!
videoCell.center.x = (communityViewController?.view.center.x)!
videoCell.layer.borderColor = UIColor.lightGrayColor().CGColor
videoCell.layer.borderWidth = 2
let fileURL = NSURL(string:self.UserVideosInfo[indexPath.row][2])
let asset = AVAsset(URL: fileURL!)
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
assetImgGenerate.appliesPreferredTrackTransform = true
let time = CMTimeMake(asset.duration.value / 3, asset.duration.timescale)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
//self.showIndicator()
let NameLabelString = self.UserVideosInfo[indexPath.row][0]
let CommentLabelString = self.UserVideosInfo[indexPath.row][1]
let DateLabelString = self.UserVideosInfo[indexPath.row][3]
let buttonPlayUserVideo = videoCell.viewWithTag(1) as! UIButton
let nameLabel = videoCell.viewWithTag(2) as! UILabel
let commentUserVideo = videoCell.viewWithTag(3) as! UILabel
let dateUserVideo = videoCell.viewWithTag(4) as! UILabel
let thumbUserVideo = videoCell.viewWithTag(5) as! UIImageView
let deleteUserVideo = videoCell.viewWithTag(6) as! UIButton
buttonPlayUserVideo.layer.setValue(indexPath.row, forKey: "indexPlayBtn")
deleteUserVideo.layer.setValue(indexPath.row, forKey: "indexDeleteBtn")
dispatch_async(dispatch_get_main_queue()) {
nameLabel.text = NameLabelString
commentUserVideo.text = CommentLabelString
dateUserVideo.text = DateLabelString
self.shadowText(nameLabel)
self.shadowText(commentUserVideo)
self.shadowText(dateUserVideo)
if let cgImage = try? assetImgGenerate.copyCGImageAtTime(time, actualTime: nil) {
thumbUserVideo.image = UIImage(CGImage: cgImage)
}
}
}
//THIS IS VERY IMPORTANT
videoCell.layer.shouldRasterize = true
videoCell.layer.rasterizationScale = UIScreen.mainScreen().scale
return videoCell
}
At first - you are working with UI objects from global queue and seems like without any purpose. That is forbidden - or behavior will be undefined.
Secondary, the mostly heavy operation is creation of thumbnail which you perform on main queue.
Consider using of the AVAssetImageGenerator's method
public func generateCGImagesAsynchronouslyForTimes(requestedTimes: [NSValue], completionHandler handler: AVAssetImageGeneratorCompletionHandler)
instead of your own asyncs.
At third, viewWithTag is pretty heavy operation causing enumeration on subviews. Consider to declare properties in the cell for views which you need.
UPD: to declare properties in a cell, create subclass of UICollectionViewCell with appropriate properties as IBOutlets. Then, in your view controller viewDidLoad implementation, call
collecionView.registerClass(<YourCellSubclass>.dynamicType, forCellWithReuseIdentifier:"VideoCell")
Or, if your collection view cell is configured in the Storyboard, specify the class of the cell and connect its subviews to class' outlets directly in the cell's settings window in Interface Builder.
At fourth, your cells are being reused by a collection view. Each time your cell is going out of visible area, it is removed from collection view and is put to reuse queue. When you scroll back to the cell, your view controller is asked again to provide a cell. And you're fetching the thumbnail for the video again for each newly appeared cell. Consider caching of already fetched thumbnails by storing them in some array by collectionView's indexPath.item index.

Resources