Download images in a Table in watch kit async

752 views Asked by At

I am developing a WatchKit app, I need to get some images(sometimes 50 thumbnails) to fulfill a Table. I am downloading the images in the iOS app and passing them to the WatchKit Extension, but I am having problems.

First, I have three buttons, if I press one of them I see a Table with some elements, all of them with image and label. The main problem is when I am downloading those images and I press one item to see its detail, the main thread is blocked and the app doesn't make push to the DetailsController until all the images are downloaded.

Has anyone dealt with a Table with many elements and images?? How do you solved this?

Thank you

2

There are 2 answers

2
Stephen Johnson On

I ran into a very similar problem and here is how I solved it. I would be interested if anyone has a better solution, but this worked pretty well. Basically, you need to estimate how long the delay will be to transfer each image and make sure you only send them over that often. If you try to send them all at once you will block the main thread.

I tried to boil my code down to just the essential parts for this solution. You won't need to name your images like I do. The key parts are how the queue is used. When you need to stop sending images call cancelCurrentImageProcessQueue.

@interface GPWatchDataController()

@property NSMutableArray* stack;
@property NSMutableArray* stackLocations;
@property NSArray* listData;
@property dispatch_queue_t imageProcessQueue;
@property NSMutableDictionary* cancelImageProcessCreation;
@property NSInteger total;
@property GPWatchMapController* mapController;

@end


@implementation GPWatchDataController

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];

}

- (void)willActivate {
    // This method is called when watch view controller is about to be visible to user
    [super willActivate];
    [self loadTableData];
}

- (void)didDeactivate {
    // This method is called when watch view controller is no longer visible
    [super didDeactivate];

    [self cancelCurrentImageProcessQueue];
}

-(void)cancelCurrentImageProcessQueue
{
    if (self.imageProcessQueue != nil)
    {
        if (self.cancelImageProcessCreation == nil)
        {
            self.cancelImageProcessCreation = [[NSMutableDictionary alloc] init];
        }
        NSString* key = [self keyForQueue:self.imageProcessQueue];
        [self.cancelImageProcessCreation setObject:@(YES) forKey:key];
        self.imageProcessQueue = nil;
        self.total = 0;
    }
}

-(NSString*)keyForQueue:(dispatch_queue_t)queue
{
    NSString* key = [NSString stringWithFormat:@"%p", queue];
    return key;
}

-(BOOL)shouldCancelQueue:(dispatch_queue_t)queue
{
    if (queue != nil && self.cancelImageProcessCreation != nil)
    {
        NSString* key = [self keyForQueue:queue];
        return [self.cancelImageProcessCreation objectForKey:key] != nil;
    }

    return NO;
}

-(void)loadTableData
{
    NSArray* listData = [self FETCH_DATA];
    [self.table setNumberOfRows:listData.count withRowType:@"GPWatchSimpleTableRow"];
    for (int i = 0; i < self.listData.count; i++)
    {
        NSDictionary* data = [listData objectAtIndex:i];
        BOOL sendImageImmediatly = [data objectForKey:@"KEY"];
        NSString* imgName = [data objectForKey:@"KEY"];
        UIImage* img = [data objectForKey:@"KEY"];
        [self addRowAtIndex:i withTitle:title andImageName:imgName image:img sendImageImmediatly:sendImageImmediatly];
    }

}

-(void)addRowAtIndex:(NSInteger)index withTitle:(NSString*)title andImageName:(NSString*)imgName image:(UIImage*)theImage sendImageImmediatly:(BOOL)sendImageImmediatly
{
    GPWatchSimpleTableRow* row = [self.table rowControllerAtIndex:index];
    [row.label setText:title];

    __block NSString* iconType = type;
    __block UIImage* image = theImage;

    if (type != nil && imgName != nil)
    {
        NSString* key = imgName;
        if (![[WKInterfaceDevice currentDevice] cacheHasManagedImage:key])
        {
            if (self.imageProcessQueue == nil)
            {
                //Uses a serial queue
                self.imageProcessQueue = dispatch_queue_create("pk.glacier.watchImageProcess", NULL);
            }
            __block dispatch_queue_t queue = self.imageProcessQueue;

            self.total++;
            //NSLog(@"Add: %ld", (long)self.total);
            CGFloat dispatchDelay = 1.2 * self.total;

            void (^processImage)() = ^() {
                if (![self shouldCancelQueue:queue])
                {
                    NSData* rowImgData = UIImageJPEGRepresentation(theImage, .7);;

                    if (sendImageImmediatly)
                    {
                        self.total--;

                        [[WKInterfaceDevice currentDevice] addCachedImageWithData:rowImgData name:key];
                        [row.image setImageNamed:key];
                    }
                    else
                    {
                        //Transfering the image can take a long time.  When you have a long list of routes
                        //going though and generating the images goes very fast and then the extension spends
                        //a lot of time sending the images.  It makes the call to send them all very fast.
                        //The watch will wait until it has gotten all images before it updates the UI again.
                        //So I put a delay on each send which lets me cancel them and move on and do something
                        //else.
                        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, dispatchDelay * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
                            if (![self shouldCancelQueue:queue])
                            {
                                [[WKInterfaceDevice currentDevice] addCachedImageWithData:rowImgData name:key];
                                [row.image setImageNamed:key];
                            }
                            //NSLog(@"Done: %ld", (long)self.total);
                        });
                    }
                }
            };

            if (sendImageImmediatly)
            {
                processImage();
            }
            else
            {
                dispatch_async(self.imageProcessQueue, processImage);
            }
        }
        else
        {
            [row.image setImageNamed:key];
        }
    }

}
2
Mike Swanson On

It is safe to cache images on a background thread, so from your WatchKit extension, dispatch to a background queue, and use WKInterfaceDevice and one of its addImage-style methods. Then, be sure to dispatch back to the main queue before you actually update the interface.

I cover some of these techniques in more detail in my WatchKit Image Tips post.