Building an Electronic Programme Guide [part 3]

This is my third and final write-up on the development of an electronic programme guide app. As of part 2, the main scrolling view with the programme details, scaled to length, are displayed.

Next up I wanted to have the ability to display programme details. This seemed like it was going to be pretty straightforward: when building the per-programme view, I included a button, which is transparent, and has the same dimensions as the view itself. The first problem is associating the programme details with the button itself. There is plenty of discussion on StackOverflow about how to do this in the least offensive way. I went with a category and then object association. This allowed me to set the GUID as a property for the button.

I rebuilt the app, hit the button and… entered into a fortnight of debugging an EXC_BAD_ACCESS error. I knew what the problem was: the ARC memory management was dereferencing the button object once it was set. I tried lots of different options, such as adding the buttons to an array, set with various properties, and passing the array back to the main view controller. Nothing worked until I did more reading around the property attributes, and ended up redefining the Interface Builder defaults for the scrolling contentView to:

@property (nonatomic, strong) IBOutlet UIView *contentView;

That ‘strong’ means that everything in the view is held in memory. It has to be said that the app is very heavy on memory – as a direct consequence of that view object retention. It routinely occupies 63Mb in my testing.

Next up is the popup that is rendered. So finding the programme itself is pretty easy, using an NSPredicate based on the GUID. What proved a bit harder to deal with is if the main view [the ‘contentView’ for the scrollView] is zoomed. As you have to add the popup view to the zoomed parent, the former is going to inherit the zoom setting. I couldn’t think of an elegant way around this so I worked around it in stages. First off, the popup sits on a blurred view of the current background:

// This is quite neat: make a CGRect of the currently visible part of the scrollview:
CGRect visibleRect = [scrollView convertRect:scrollView.bounds toView:contentView];
visualEffectView = [[UIVisualEffectView alloc] initWithFrame:visibleRect];
visualEffectView.effect = blurEffect;
visualEffectView.frame = contentView.bounds;
[contentView addSubview:visualEffectView];

Next, I register the scrollView offset in a property:

scrollOffSet = scrollView.contentOffset;

…set the zoomScale to 1, and disable the ability to zoom:

[scrollView setZoomScale:1.0];
scrollView.scrollEnabled = NO;
scrollView.maximumZoomScale = 1.0;
scrollView.minimumZoomScale = 1.0;

Placing the programme details subview is then relative to the currently visible rectangle:

float xForLittleView = visibleRect.origin.x + 30 ;
float yForLittleView = visibleRect.origin.y + 100;

CGRect progViewRect = CGRectMake(xForLittleView, yForLittleView, 350, 500);

I then have to undo the various view settings when the button to dismiss the view is touched:

[visualEffectView removeFromSuperview];
[littleView removeFromSuperview];
scrollView.scrollEnabled = YES;
scrollView.maximumZoomScale = 2.0;
scrollView.minimumZoomScale = 0.8;
[scrollView setZoomScale:zoomScale];
[scrollView setContentOffset:scrollOffSet];

It’s all a bit clunky, but it works. I imagine that this sort of interface plumbing actually happens quite a lot behind the scenes. That said, I may have missed a trick to do it in an easier way.

I’ll call out two more details that I wrestled with. The first is a search facility on the programme title. I wanted the NSPredicate to support as many search terms as the user entered. My initial idea was to split the UITextField input on spaces, and then loop through the resulting array, appending to a stringWithFormat, where all but the first element would be in the form:

AND (title CONTAINS[c][/c] %@)

Having experimented with this, it appears that predicateWithFormat has to have the actual string passed to it, as opposed to a variable containing the string. Which I have to say strikes me as a little odd. The functional upshot of this is that I couldn’t support a variable number of search terms. I support up to three, and construct a separate predicateWithFormat for each possibility.

One final problem that I couldn’t find a fix for was implementing a UITableView’s delegates in a class that I pass the view into as a parameter. I couldn’t find a way of getting the cellForRowAtIndexPath delegate method to be called. The conclusion I came to with this was that it was setting the delegate to self, when ‘self’ was the custom object, rather than the view. It was largely a cosmetic thing [I’ve noticed that for complicated apps, I have a tendency to pile way to much code into the main viewController] so it was easily solved.

Here’s what may be the final version of the app looks like, showing a search result in the popup view, and the to/from dates for the EPG coverage:

Search Results

Search Results

The other buttons that I haven’t talked about explicitly are an ability to switch between days, and initiate a download of EPG data – but which are pretty straightforward. What’s still either ugly or hasn’t been fully implemented is the download progress indicator, and also the what’s-on-now quick look on the Apple Watch, as I want to have a mess around with something completely unrelated to this app: the motion detection capability.

I did add a quick fix to ‘justify’ the right hand side of the ‘table’ of programmes. Formerly they were falling off the contentView. I simply check if the rightmost width of the cell is going to be greater than the width of the contentView. If so, I set it to be the same as the width.

So that’s it. I have a pretty serviceable EPG app, which I use myself over the ad-funded variant I had before, which I guess is a fair indicator of utility. Main lesson learned: not knowing what those property attributes meant tripped me up really badly!

Building an Electronic Programme Guide [Part 2]

A little later than intended, here’s the follow-up to my first posting on building an EPG.

So, having marshalled the data into a structure that can be displayed, on to the guts of this app, which is the main UIView for guide itself. The pre-requisite of that main view is configuring the scrollView: I’m not going to dwell on this too long, as it’s a well documented feature.

For the formatting of the content in that classic programme guide UI, with rows of variable width cells, I initially started with looking at collectionViews, but couldn’t find a way of doing it – or certainly an amenable way. I also briefly looked at customising a tableView, which I could add multiple elements to, but rejected it for the same reason.

I finally settled on a custom implementation, with each programme being represented by its own UIView, with:

  • the width: the duration in minutes divided by the number of minutes in the day, then multiplied by the width of the scrollview.
  • the height: this is a constant for all the cells, just based on experimenting with the scrollView.

For what it’s worth, my scrollView is 4000, and the row height is 60. [Actually it’s the width of the contentView the scrollView contains, but I’ll refer to the scrollView from here for purposes of readability.]

Before I go on to calculating the [X,Y] coordinates of the top left corner of the subview, I need to recap on how the programme information is written to Core Data and then read back. At some point in the implementation as I envisage it, it will be possible for the user to both download more programme data, as only a couple of weeks worth come down at a time, and also to configure which channels to do the download for. For ease of simplicity of managing the stored data, I decided to write all of the programme data to the same Core Data entity [think database table], and when the user repeats the download, with or without changing the channel configuration, I delete the existing data – a fairly blunt but effective instrument.

When it comes to retrieving the data, which I do on a per-channel basis, I need to search using an NSPredicate:

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(channel = %@) AND (startDate >= %@) AND (endDate <= %@)", channelName, startDateForSearch, endDateForSearch];
NSFetchRequest *allChannelDataReq = [[NSFetchRequest alloc] init];
[allChannelDataReq setEntity:[NSEntityDescription entityForName:@"Programme" inManagedObjectContext:managedObjectContext]];
[allChannelDataReq setPredicate:predicate];
[allChannelDataReq setIncludesPropertyValues:NO];
NSError *error = nil;
tempArrayOfFilteredProgData = [managedObjectContext executeFetchRequest:allChannelDataReq error:&error];

So this block of code is searching for hits on a given channel, startDate and endDate [attributes of the Entity], and loading them into the array. I’ve already noted these are unsorted, so…

NSSortDescriptor *sortByRunningOrderInt = [[NSSortDescriptor alloc] initWithKey:@"progOrderNum" ascending:YES];
NSArray *descriptors = [NSArray arrayWithObject:sortByRunningOrderInt];
NSArray *sortedProgDataForChannel = [channelData sortedArrayUsingDescriptors:descriptors];

…where the progOrderNum is the corresponding attribute in the Entity.

The sortedProgDataForChannel can now be looped through, for each programme:

- (NSMutableDictionary *)drawRect:(float)width startXPosn:(float) topLeftX rowNum:(int)rowNum forColour:(UIColor *)cellColour
{
NSMutableDictionary *viewAndCoords = [[NSMutableDictionary alloc] init];
float topLeftY = rowNum * rowHeight;
float topLeftXForNextView = topLeftX + width;
CGRect rectangle = CGRectMake(topLeftX, topLeftY, width, rowHeight);
UIView *thisProgView = [[UIView alloc] initWithFrame:rectangle];
thisProgView.backgroundColor = cellColour;
thisProgView.layer.borderColor = [UIColor blackColor].CGColor;
thisProgView.layer.borderWidth = 1.0;
[viewAndCoords setObject:thisProgView forKey:@"view"];
[viewAndCoords setObject:[NSNumber numberWithFloat: topLeftXForNextView] forKey:@"newTopLeftCoord"];
return viewAndCoords;
}

The idea here is to calculate the width, as described above, and create the UIView [with a couple of frills like a border], and then return the X co-ordinate for the starting point for the next view. The Y coordinate is a simple calculation: I assign the channels a number, and then multiply that by the height of each cell.

That, in essence is the primary logic for building the ‘wireframe’ for the programmes. I extract the text for a given programme’s title, start and end times, etc, and add those as labels programmatically. Again, these are calculated as offsets from the starting X coordinate and the calculated Y based on channel number.

There are a couple of gotchas, and I had to take a couple of bites at this – which is a polite way of saying I made a couple of idiotic mistakes – and the visual results of which were so bad that I decided to record them for posterity. The first is an example of not reading the documentation. I misremembered the parameters for a UIView as being X+Y for the top left corner, and then X+Y for the bottom right corner. This resulted in the following:

IMG_3084

It really is a thing of beauty :).

Next up was dealing with the simple matter of the start of the day. The NSPredicate I use extracts per-channel programme data for start times on a given date. I was then merrily plastering these onto the scrollView with the following results:

IMG_3098

Obviously a refinement is required, so first port of call was to make 5am the equivalent of the left edge of the scrollView. I decided that, rather than hardwiring this into the search parameter for the NSPredicate – in case I changed my mind later – I’d do it when looping through the channel data.

So first of all, I see if the start time of the given programme is >= 5am. If not I disregard it. If it is, I then calculate the offset for the X coordinate. This is based on calculating the difference between the start time of the programme and 5am in minutes. I then divide this by the number of minutes in the day and multiply it by the width of the scrollView. There’s an additional offset [which actually applies to all of the programme UIViews] for a channel title list down the left hand side of the scrollView.

-(int) calcPosOfFirstProg:(NSDate *)progStartTime
{
int xPos = 0;
// This start x posn allows for the channel name label
int offSetForChanTitle = 100;
// So: the left edge of the view equates to 5am.
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
// formatting from just HH:mm didn't work because the progStartTime has a date.
// need to extract today's date from progStartTime and bake it into the viewEdgeTime...

[dateFormatter setDateFormat:@"dd/MM/yyyy"];
NSString *dayDateForDisplayDay = [dateFormatter stringFromDate:progStartTime];
NSString *viewEdgeTimeString = [NSString stringWithFormat:@"%@ 05:00", dayDateForDisplayDay];
[dateFormatter setDateFormat:@"dd/MM/yyyy HH:mm"];
NSDate *viewEdgeTime = [dateFormatter dateFromString:viewEdgeTimeString];
// Time interval gives us the number of seconds:
NSTimeInterval timeDiff = [progStartTime timeIntervalSinceDate:viewEdgeTime];
// All other calculations on positioning have been in minutes so:
float minsBetweenTimes = timeDiff / 60;
NSLog(@"minsBetweenTimes is %f", minsBetweenTimes);
float minutesInDay = 1140;
xPos = ((minsBetweenTimes / minutesInDay) * scrollViewWidth) + offSetForChanTitle;
return xPos;
}

That channel listing is a bit ugly – it’s why I bake the channel name into the progamme summary. What I’d really like to do is have a “floating” listing that the scrollView passes under. I’ve used key value observers a lot in the past, so one approach would be to set a KVO on the delegate that returns the current scroll scale value, and to resize based on the changes in scale. One to come back to…

It would, of course, be necessary to do the same for the right hand of the scrollView, to make sure that for a given cell, there’s enough room to display it. I haven’t gotten round to this yet, so late finishing programmes dangle precariously into space.

Next up I’ll talk about creating the pop-up view for the programme details, including navigating an ARC related memory error, which took me a loooooooong time to figure out!

Building an Electronic Programme Guide [Part 1]

[Edit: 12/06/2016. The source I used for the programme data, called Radio Times XML TV, is just about to be shut down and replaced with another service which restricts the amount of data that can be hauled down to 24 hours. The whole premise of this app was to facilitate offline access. If you landed here via Google note that, while you might find some utility in the various postings on the UI, the app itself is useless without the data source. If I find another suitable data source I may update the app.]

This is the first in a series of posts about a new TV guide app that I’ve been working on in my spare time for about the last 6 weeks or so. The motivation for this comes from a number of quarters. I’d been scratching around for ideas for a reasonably useful Apple Watch app. I’d also been finding the guide that I got off the shelf from the app store is getting more reliant on in app advertising. While I’m sure there are premium, ad-free variants out there, I thought it was an interesting area for development, and one that I could extend to the watch with a ‘what’s on now / later’ mini view.

I now have a working app for my phone, but without all of the UI frills I’m aiming for. The main guide looks like this…
 epg
… and with programme previews that look like this:
preview
I’m continuing the development, and it’s already a pretty large codebase. It’s not one that’ll end up on GitHub [for reasons I’ll get on to shortly], but I think there are some pretty interesting challenges that I didn’t come across anything approaching kitform answers for, so I’m going to pluck out the interesting parts for a deeper dive.
The first and obvious question is where to get the Electronic Programme Guide data and, all importantly, data that is free. After some research, I settled on a service offered by the Radio Times, which uses a standard format called XMLTV. As I’m not into hacking PVRs, I’d never come across this standard before. The first thing to say is that the Radio Times variant isn’t actually in XML, it’s character delimited. The next is that it’s very clearly identified as being for personal use only – hence I’m not going to publish the app in full.
First, let’s deal with the data download from the per-channel URLs. Currently, I’ve hard-wired the app to download 12 channels’ worth of data, something that I ultimately plan on linking back to the UI so that the user can self select them. Each channel download contains two weeks of programme data, so over a mobile network, the overriding UX consideration is progress.
Having previously lost a couple of hours to this in the past, trying to hook up a fancy UI component showing progress graphically, it’s one to put front and centre when you’re designing the networking component. Quite commonly, servers will be generating responses dynamically, in which case, there’s a very good chance the Content-Length header won’t be set.
The progress indicator must have the total of bytes available to be able to calculate progress. The Radio Times site isn’t setting the required header, so no fancy progress meters – sad face. This is actually a more complex use case anyway, because we’d have needed to calculate the progress for a dozen downloads, and the progress of each is going to be interlaced.
What I’ve done is to initiate each download in a for loop using AFNetworking, and then in the setCompletionBlockWithSuccess block:

NSData *channelData = (NSData *)responseObject;
ChannelDataParser *channelProgs = [[ChannelDataParser alloc] init];
NSMutableArray *asProgObjects = [channelProgs programmeChunker:channelData andChannelName:thisPhoneChannel.channelName];
[self storeChannelProgData: asProgObjects forThisChannelName:thisPhoneChannel.channelName];

I’ll come back to the parser in a second, but one point to focus on is the method to store the parsed results. As the network responses are interleaved, and they’re all ultimately going to be written to the same Core Data entity, I’ve wrapped the method in an @synchronized(managedObjectContext).
In that same @synchronized method, I increment a completed download counter, which I use to set a label in the UI. Pretty crude, but given the progress limitations, it seems like a reasonable compromise. I’ve actually spent the vast majority of the time on the UI for the programme guide, so I might take another look at this later.
Back to the parser itself. First up, for the NSData that contains the AFNetworking response for the channel, the first step is to convert it into a string, and then an array of strings divided by line breaks:

NSString *stringFromData = [[NSString alloc] initWithData:channelData encoding:NSUTF8StringEncoding];
// The line break vs carriage return: done through experimentation:
NSArray *eachLineOfString = [stringFromData componentsSeparatedByString:@"\n"];

The parsing was something that required trial and error, as the file contained some copyright info at the start, and then some carriage returns at the end:
for (int progCount = 2; progCount <= [eachLineOfString count] -2 ; progCount++)
{
NSString *oneLine = eachLineOfString[progCount];
if (![oneLine isEqualToString:@"\r"])
{
[.....]
Within the loop, I then parse out the carriage returns, and finally split the data into an array, based on the separator character [a tilde]r:

NSString *oneLineNoBreak = [oneLine stringByReplacingOccurrencesOfString:@"\r" withString:@""];
//NSLog(@"oneLineNoBreak is %lu long", (unsigned long)oneLineNoBreak.length);
NSArray *oneProgrammeData = [oneLineNoBreak componentsSeparatedByString:@"~"];
I think I would have struggled with the whole \r versus \n thing if I hadn’t been bitten with it way back in the old days of the  mid 1990s. I can’t recall if it was FTPing files with the wrong transfer type or using Samba between Windows and Unix machines, but either way, you were prone to getting oddities with the control characters in text files. Anyway…
At this point we are ready to assign values, based on the content of the array conforming to the XMLTV standard. So the 0th element of the array is the programme title, then comes a subtitle, etc. I assign these to an instance of a custom class. I found it useful to add an additional three fields, all related to the UI. The first is the running order that the programmes appear in: once you save them to Core Data as individual records representing the programmes, they become unordered – standard database stuff. Next is a GUID: I’ll get into this in more detail in a subsequent posting on the UI, but the summary is that it’s to associate programme data with a button action. Finally, I also include the channel name for the programme: this is to do with a limitation in the UI design – again, I’ll come back to this later.
I cycle through parsing each channel, ultimately saving them to the core data in the @synchronised method I mentioned earlier.
One final point on the networking side. I do a simple check in a Core Data entity with a single value to see if it’s the first run. If it is, I haul down the data before presenting the UI. I also haven’t finished the part of the UI where the user can self select the channels. For now, it’s hardwired. This list of channels and the corresponding URLs also gets written to another simple entity during that first run.
I’ll follow up with a description of the UI design….