Top Shelf dynamic content help

Hi all,

I've been struggling trying to get my top shelf extension to work with a remote resource. The example app shows how to set it up with a static peice of content. Here's a snippet of what I am trying to do.


    var topShelfItems: [TVContentItem] {
        return sectionedTopShelfItems
    }

    var items: [TVContentItem] = []

    private var sectionedTopShelfItems: [TVContentItem] {
        let jsonUrl = ServiceProvider.ShowcaseURL
     
        let session = NSURLSession.sharedSession()
        let shotsUrl = NSURL(string: jsonUrl)
     
        let task = session.dataTaskWithURL(shotsUrl!) {
            (data, response, error) -> Void in
            do {
                let jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options:NSJSONReadingOptions.MutableContainers ) as! NSDictionary
                self.items = self.mapDataToContentItem( jsonData )
             
             
            } catch _ {
                /
                print("Failed to get JSNO data for top shelf!")
            }
        }
     
        task.resume()
     
        return self.items;
     
    }

Since no logs seem to be output from the console, I can't see what is going on. I think it has to do with the URL request itself being an async call. so when the app gets the JSON from the server it returns an empty list first before it's able to finish processing the data of the URL request. I know it's making the call to get the JSON, I can see the request being made on the server end. Is it possible to get this work using an async call? What am I missing?


--Thanks,

Michael

Accepted Reply

2 things:

• first, presumably you also have a "var topShelfStyle" which is returning .Sectioned, as part of conforming to the protocol;

• second, yes, you need to block execution, after the task.resume() call, until the self.items assignment has been done in dataTaskWithURL()'s completion block, before returning; the empty array you're returning will be ignored by the system.

I'm not sure if the Swift standard library has added any nice things for concurrent or async programming yet. Using dispatch_semaphore_create(0) to create a semaphore, then dispatch_semaphore_signal() and dispatch_semaphore_wait() would be a relatively standard technique on tvOS, like on iOS or OS X. Search around the internet for Swift examples of using those functions (and compare multiple examples). And see docs at https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html

in the section "Using Dispatch Semaphores to...".

Replies

2 things:

• first, presumably you also have a "var topShelfStyle" which is returning .Sectioned, as part of conforming to the protocol;

• second, yes, you need to block execution, after the task.resume() call, until the self.items assignment has been done in dataTaskWithURL()'s completion block, before returning; the empty array you're returning will be ignored by the system.

I'm not sure if the Swift standard library has added any nice things for concurrent or async programming yet. Using dispatch_semaphore_create(0) to create a semaphore, then dispatch_semaphore_signal() and dispatch_semaphore_wait() would be a relatively standard technique on tvOS, like on iOS or OS X. Search around the internet for Swift examples of using those functions (and compare multiple examples). And see docs at https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html

in the section "Using Dispatch Semaphores to...".

So I modified the method to add the semaphores and still no joy.

  private var sectionedTopShelfItems: [TVContentItem] {
        let jsonUrl = ServiceProvider.ShowcaseURL
     
        let session = NSURLSession.sharedSession()
        let shotsUrl = NSURL(string: jsonUrl)
     
        let semaphore = dispatch_semaphore_create(0) 
        let task = session.dataTaskWithURL(shotsUrl!) {
            (data, response, error) -> Void in
            do {
                let jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options:NSJSONReadingOptions.MutableContainers ) as! NSDictionary
                NSLog("JSON Data: %@", jsonData)
             
                self.items = self.mapDataToContentItem( jsonData )
             
                dispatch_semaphore_signal(semaphore) 
             
            } catch _ {
                /
                print("Failed to get JSNO data for top shelf!")
                dispatch_semaphore_signal(semaphore) /
            }
        }
     
        task.resume()
        let timeout = dispatch_time(DISPATCH_TIME_NOW, 10000)
        if dispatch_semaphore_wait(semaphore, timeout) != 0 { /
            NSLog("Timeout occurred!");
        }
     
        return self.items;
     
    }

What happens if you try it without the 10 microsecond timeout? Try to get it to work with no timeout (FOREVER) first.


And really, there's no particular reason to timeout, if you're just going to give blank data to the system. But in this case it looks like you're trying to fall back to giving the previous data. In any case, you're not blocking anything in the system by returning an answer slowly, just not showing anything (or, fresh data) to the user. And the system won't try to get data from your extension again until it hears an answer to a prior request (assuming your extension process remains alive).

I am facing similar issue. Images appear correctly when I set 'imageURL' in 'TVContentItem' to a local resource from main bundle. Image does not work when using remote URL or even locally created file (file is created just before it is set to 'imageURL'). To me this behaviour looks like a bug.

I am also having issues with displaying an image that's created locally just before being set in imageURL. Nothing shows up.

I too struggled with this for a while, but I've got it working now. The critical piece for me was that if you return a topShelfStyle of TVTopShelfContentStyleSectioned, the array of TVContentItems that you return MUST have "child" content items in order to display anything. Check out the UIKitCatalog sample project where this is done.


TVContentIdentifier *sectionID = [[TVContentIdentifier alloc] initWithIdentifier: @"some_container_id" container: nil];

TVContentItem *sectionItem = [[TVContentItem alloc] initWithContentIdentifier: sectionID];

sectionItem.title = @"Section Title";

sectionItem.topShelfItems = <array of TVContentItems, each of which has an imageURL, title, etc.>

Without that last line, you will get nothing in the top shelf for the TVTopShelfContentStyleSectioned type. And no relevant error message in the console either. Hope this helps!

Early on it was because I mapped the data incorrectly from the json to my model to the TVContentItem objects. Once I figured that out it all worked.


The property for topShelfItems needs to return an array of TVContentItems. Each item is composed of topShelfItems which are basically the elements you want to display for that section.


var topShelfItems: [TVContentItem] {

return sectionedTopShelfItems

}

It's a little cumbersome to set it all up in swift vs the TVJS (no longer supported) but it works. no bugs found so far.


Just out of curiosity, what URL scheme are you using for the linkouts? I'm curious to see what other people are doing for the playUrl and DispalyUrls and how they deep link into the application.

Since the valid character set for a URL scheme is a superset of the valid characters for your app's bundle ID, you can use your app's bundle ID as the URL scheme. This will then avoid possibility of collisions with other apps.


Then implement the new iOS 9 UIApplication delegate method in your delegate's class:

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*, id> *)options NS_AVAILABLE_IOS(9_0);


and declare the URL in your app's Info.plist.


The sections on custom URL handling also apply to tvOS: https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html

Thanks for starting this thread mnguyen-1ms. I was also able to get static content to appear in my Top Shelf, but when I tried to request data from a URL in the sectionedTopShelfItems method (following your pattern), I never saw a request post to my server. Did you have to do anything beyond your code snippet to get the URL request to fire? Thanks!

Got remote URL working. With iOS 9 and tvOS App Transport Security is enabled by default, so I had to change HTTP-request to HTTPS. Still no luck with local files though.

I have been able to load and display locally generated images.

You need to use the Caches directory, the Documents one will only work on the simulator.

@superg,

I don't remember having to do anything special to get the URL request to fire other than what I already posted. My issue mainly came from the fact I mapped my information incorrectly to the Swift object causing the display to never happen.

Still no luck with local images. Here's my top shelf extension code:


- (TVTopShelfContentStyle)topShelfStyle
{
    return TVTopShelfContentStyleSectioned;
}
- (NSArray *)topShelfItems
{
    TVContentIdentifier *sectionid = [[TVContentIdentifier alloc] initWithIdentifier:@"sectionId" container:NULL];
    TVContentItem *sectionitem = [[TVContentItem alloc] initWithContentIdentifier:sectionid];
    NSMutableArray *gameList = [[NSMutableArray alloc] init];
    sectionitem.title = @"SectionTitle";
    TVContentItem *item;
   
    for (int i = 0; i < 5; i++)
    {
        NSString *fileName = [NSString stringWithFormat:@"file%d.jpg", i];
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        NSString *filePath = [[paths objectAtIndex:0] stringByAppendingPathComponent:fileName];
       
        UIImage *tmpimg = [UIImage imageNamed:@"icon.jpg"];
        [UIImagePNGRepresentation(tmpimg) writeToFile:filePath atomically:YES];
       
        TVContentIdentifier *contentid = [[TVContentIdentifier alloc] initWithIdentifier:[NSString stringWithFormat:@"%d", i] container:NULL];
        item = [[TVContentItem alloc] initWithContentIdentifier:contentid];
        item.imageURL = [NSURL URLWithString: filePath];
        item.title = @"title";
        item.imageShape = TVContentItemImageShapeSquare;
        item.displayURL = [NSURL URLWithString:@"NA"];
        item.playURL = [NSURL URLWithString:@"NA"];
        [gameList addObject: item];
    }
    sectionitem.topShelfItems = [gameList copy];
    return [NSArray arrayWithObject:sectionitem];
}

Use for item.imageURL:

        filePath = [NSString stringWithFormat:@"file:/
        NSLog(@"filePath = %@", filePath);
        NSURL *url = [NSURL URLWithString:[filePath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
      
        item.imageURL = url; //[NSURL URLWithString: filePath];