6

I am working with an Objective-C application that features a scene with a full screen horizontally scrolling UICollectionView. I currently have a function being called every time a new cell appears on screen during scrolling that takes about 3-4 seconds to run and edits ui elements within the newly appeared cell. Because of this, my app lags every time a new cell enters the screen for about 4 seconds and then continues to scroll normally.

Is there a way to edit this function and put it on a background thread so that instead of a a 4 second lag in scrolling, there is seamless, uninterrupted scrolling? I understand that this solution will cause a 4 second delay to the ui elements from appearing but I am fine with that as long as there is a seamless scroll.

EDIT: I didn't think it would be a good idea to post code for this problem since the code in function that does the loading requires a video player api that I purchased, but I will go ahead and post the method below to show what parts of the function edit the UI.

//variables declared in other parts of the file
KolorEyes *myKolorEyesPlayer;
UICollectionView *collectionViewFollwersFeed;
NSIndexPath *lastPath = nil;

//called repeatedly while scrolling
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{


    if(scrollView==_collectionViewFollwersFeed)
    {

        [self tsLoad]; //function call

    }
}


- (void)tsLoad {

    //calculate index path of cell in the center of the screen
    NSIndexPath *centerCellIndexPath =
    [self.collectionViewFollwersFeed indexPathForItemAtPoint:
     [self.view convertPoint:[self.view center] toView:self.collectionViewFollwersFeed]];

    if(centerCellIndexPath != lastPath) //condition is satisfied if a new cell has been scrolled to the center
    {   
        lastPath = centerCellIndexPath;

        UICollectionViewCell *cell; 
        NSString *CellIdentifier;

        //initialize cell from center path
        CellIdentifier = @"FollowersFeed";
        cell = [_collectionViewFollwersFeed cellForItemAtIndexPath:centerCellIndexPath]; 

        //get respective url for video player
        NSMutableDictionary *dict1=[follwerFeed objectAtIndex:centerCellIndexPath.row];
        NSString *tsstring= [dict1 valueForKey:@"video"];
        NSURL *tsurl = [[NSURL alloc] initWithString:tsstring]; 

        //view that the video will be played in
        UIView *tsView = (UIView *)[cell viewWithTag:99]; 

        //API-specific parameters for video player session
        KolorEyesSessionParams *params = [[KolorEyesSessionParams alloc] init];
        params.delegate = self;
        /* other params properties set like above at this point...
           ...
           ...
        */

        //API-specific initialization
        myKolorEyesPlayer = [[KolorEyes alloc] initWithParams:params];

        //API-specific parameters for video player view
        KolorEyesRenderViewParams *fparams = [[KolorEyesRenderViewParams alloc] init];
        fparams.cameraFieldOfView = 90;
        /* other fparams properties set like above at this point...
           ...
           ...
        */

        //API-specific initializations
        id _tsViewHandle = [myKolorEyesPlayer setRenderView: tsView withParams:fparams];
        [myKolorEyesPlayer setCameraControlMode:kKE_CAMERA_MOTION forView:_tsViewHandle];

        __block KolorEyes *player = myKolorEyesPlayer;

        //error checking
        eKEError err = [myKolorEyesPlayer setAssetURL: tsurl
                                withCompletionHandler:^{
                                    // Media is loaded, play now
                                    [player play];
                                }];

        if (err != kKE_ERR_NONE) {
            NSLog(@"Kolor Eyes setAssetURL failed with error");
        }
    } 
}
Roger99
  • 981
  • 2
  • 11
  • 42
  • Will you share your demo, so I can see your issue and try to solve it, if you dont mind – Jitendra Modi Feb 01 '17 at 06:04
  • @JeckyModi I have added a summarized version of the code I am working with. As I mentioned in the edit, it requires a purchased API so you probably won't be able to run it locally but maybe you could see what needs to be done by looking at which parts edit the UI. – Roger99 Feb 02 '17 at 21:52
  • Have you run this through instruments to see exactly which method(s) are causing the delay? – ChrisH Feb 08 '17 at 21:13
  • When reading your code I see several interesting points. One of the main things is that You create a new player every time, I think it will be better, if You have a pool of players and reuse them ( if it is possible to change player items ). IOS 10 has a good prefetching component `UICollectionViewDataSourcePrefetching`, which may help You determine early on, which cell do You need. From there You can try to preload the video on background and display a thumbnail or loading anim in the cell, until everything is ready. If the user scrolls on You can cancel the video loading and so on. – Georgi Boyadzhiev Feb 10 '17 at 09:26

4 Answers4

3

You can define an “NSOperationQueue“ for running background operations and when cell is visible you can load video to cell of UICollectionView.

Also, don’t forget to take advantage of the NSOperationQueue and call “cancelAllOperations” when the collectionView is not needed anymore

Zalak Patel
  • 1,937
  • 3
  • 26
  • 44
0

Is there a way to edit this function and put it on a background thread so that instead of a a 4 second lag in scrolling, there is seamless, uninterrupted scrolling?

You are running an SDK as you mention:

Kolor Eyes is a free 360 video player app rendering spherical videos with pixel accurate projection algorithms. The user can choose different camera viewpoints anytime through touch gestures or device motion controls. More than a player, it's the perfect browser designed for Kolor 360 video hosting website. This documentation concerns Kolor Eyes iOS from version 2.0

It's enough problems running Apples own "AVPlayer" in a UICollectionView/UITableView without lags, and you want to run a Custom made 360 video player without any lags.

Since it is an SDK, you can not access or/and modify any of the code, you need to contact the developers of the SDK to get the guidelines. I don't think that this SDK is developed to be run in a scrollView in mind , since this seems to be heavy task (I am not sure 100% however, you need to ask the developers).

However, you should not modify the UI on a background thread as you are asking, that wont help the lags go away, it will make it worse and even crash or hang your application in most cases, and I am sure (hope so atleast) that the developers do as much threading as possible already with this SDK:

In a Cocoa application, the main thread runs the user interface, that is, all drawing and all events are handled on the main thread. If your application performs any lengthy synchronous operations on that thread, your user interface can become unresponsive and trigger the spinning cursor. To avoid this, you should shorten the amount of time consumed by those operations, defer their execution, or move them to secondary threads.

Source

0

In theory you are right. You should be able to do non-UI related video loading (such as downloading the video) on a background thread, and the dispatch to the main thread when you actually want to display it. In practice I have found this be very hard with many 3rd party libraries. Most video libraries expect to be run entirely on the main thread. You would have to check with your video sdk - but I would be surprised if they supported background loading.

Another solution is to not load the video in the cellForItemAtIndexPath:, but instead load the videos for the visible cell when scrolling stops. You can monitor the delegate callbacks scrollViewDidEndDragging and/or scrollViewDidEndDecelerating.

Jon Rose
  • 8,373
  • 1
  • 30
  • 36
0

Oh I've tried playing videos during scrolling before. I didn't make it back then.. When you've been googling this, you have probably seen my question about it as well, as it's much of the same logic? I ended up starting to play the video when the scrolling stopped - but that was a long time ago, and I'm not working on that anymore.

Now, I might have an idea for you.

Before you dive into this, I'm just gonna say: This does work, but I'm not 100% sure if it will work for video-playing. You'll have to find that out yourself. I'm sorry for the length of this, but it can be quite complex.

Entirely depending on your situation with number of cells etc, you might be interested in disabling the reusing of cells, and simply precompile everything.

I haven't done this in UICollectionViewCell before, and I have noe idea what KolorEyes is, but this would probably work for UITableViewCell and AVPlayer, although I don't have the time or will to test it. I'll try to explain the logic of what I'm thinking.

I haven't done any Objective-C in a while so I'll do this in Swift, but I hope you understand what I'm thinking: First, make this class:

class PrecompiledCell{
    var cell:UICollectionViewCell? = nil
    var size:CGSize? = nil
    // constructor and such
}

This class is just a wrapper for your cell. Now you should create one instance of PrecompiledCell for every cell you will display, I.E one for each followerFeed. You should make a function called precompileCells(), and call it whenever you update your dataSource(followerFeed). This function should loop through all the elements in your follwerFeed-array, and create one cell for each. Much like your cellForRowAtIndexPath is probably doing today.

var followerFeed:[[String:Any]] = [] // Your data. Probably an NSArray with NSDictionaries inside?
var precompiledCells:[PrecompiledCell] = [] //Your array of precompiled cells

func precompileCells(){
    var precompiledCells:[PrecompiledCell] = []
    for feed in followerFeed{
        //Create the cell
        let videoCell = UINib(nibName: "MyVideoCell", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! MyVideoCell
        //Each instance of this  cell (MyVideoCell) should already be initialising its very own KolorEyes-player etc., so now you can say this:
        videoCell.kolorEyesPlayer.setAssetURL(feed["video"]) //Of course, convert String to NSURL etc. first..
        let precompiledCell = PrecompiledCell()
        precompiledCell.cell = videoCell
        precompiledCell.size = //Find out what size the cell is. E.g by using videoCell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize), or even easier if you have a constant size.
        precompiledCells.append(precompiledCell)
    }
    self.precompiledCells = precompiledCells
}

Now, it's important to not do this work twice. The entire idea of this answer is that your regular cellForRowAtIndexPath (or scrollViewDidScroll in your case) don't have to do all this work when using the scroll. The reason for the current delay is that the view is initialising so many things while scrolling. With these pre-compiled cells, everything has already been initialised before you can start scrolling. So now, remove the initialisations and all dequeuing from your normal delegate-methods, and do this:

func collectionView(collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    return self.precompiledCells[indexPath.row].cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return self.precompiledCells[indexPath.row].size
}

No more initialising stuff while scrolling, because everything has already been initialised before. The only thing you have to do now, is use your old function, scrollViewDidScroll, do the same as you did (store the current index etc), and simply put [player play];, not the other stuff.

Important thoughts

First off, it's really important to call collectionView.reloadData() after self.precompileCells. So it should happen like this:

self.followerFeed = [/*new data*/]
self.precompileCells()
self.collectionView.reloadData()

Also, this implementation will entirely disable dequeuing, which is actually bad practice, as far as I know. Do not use this if you are loading immense amounts of cells. Then rather try to split it up a bit.

This implementation might cause a delay when actually precompiling, but rather that than delay when scrolling.

If you are loading cells in a paging-style (e.g scrolling to the end will load more cells), then you should really improve my code, as it will re-precompile all the cells. You should then make some sort of insertPrecompiledCells-thingy.

As soon as you have precompiled your cells, each cell can decide on its own if it should start downloading the videos. If they should download the entire video, just a few seconds of the start of the video, or not download it before you request to play it at all. Starting a partial download can both be asynchronous and will have a more seamless start when scrolling over it.

A side-thought I got while writing this is that it may be a bad idea to have KolorEyes-player for each cell. You might want to have one single KolorEyes-player globally, and simply pass it to the correct cell - but you can still precompile the cell before setting the video-player, and you can also start asynchronously download the actual video (without having the player)(depending on what the hell a KolorEyes-player is, but you'll figure that part out..)

Again, this is just an idea that might work. I have used this for tableViews with complex cells to drastically increase the smoothness of scrolling, but in a very controlled way (never more than 30 rows etc.)

I still have a shitload of ideas and thoughts about this, but this post is already way too long, and I have to go now. Good luck!

Community
  • 1
  • 1
Sti
  • 8,275
  • 9
  • 62
  • 124