diff --git a/Limelight/UIAppView.h b/Limelight/UIAppView.h index a85e01a..ca6c565 100644 --- a/Limelight/UIAppView.h +++ b/Limelight/UIAppView.h @@ -17,7 +17,7 @@ @interface UIAppView : UIView -- (id) initWithApp:(App*)app andCallback:(id)callback; +- (id) initWithApp:(App*)app cache:(NSCache*)cache andCallback:(id)callback; - (void) updateAppImage; @end diff --git a/Limelight/UIAppView.m b/Limelight/UIAppView.m index 6dcc47a..89bcc7a 100644 --- a/Limelight/UIAppView.m +++ b/Limelight/UIAppView.m @@ -13,36 +13,35 @@ UIButton* _appButton; UILabel* _appLabel; UIImageView* _appOverlay; + NSCache* _artCache; id _callback; } -- (id) initWithApp:(App*)app andCallback:(id)callback { +static UIImage* noImage; + +- (id) initWithApp:(App*)app cache:(NSCache*)cache andCallback:(id)callback { self = [super init]; _app = app; _callback = callback; + _artCache = cache; + + // Cache the NoAppImage ourselves to avoid + // having to load it each time + if (noImage == nil) { + noImage = [UIImage imageNamed:@"NoAppImage"]; + } _appButton = [UIButton buttonWithType:UIButtonTypeCustom]; - UIImage* noImage = [UIImage imageNamed:@"NoAppImage"]; [_appButton setBackgroundImage:noImage forState:UIControlStateNormal]; [_appButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)]; [_appButton sizeToFit]; [_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside]; - _appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]]; - _appOverlay.layer.shadowColor = [UIColor blackColor].CGColor; - _appOverlay.layer.shadowOffset = CGSizeMake(0, 0); - _appOverlay.layer.shadowOpacity = 1; - _appOverlay.layer.shadowRadius = 2.0; - [_appOverlay setHidden: YES]; - [self addSubview:_appButton]; - [self addSubview:_appOverlay]; [self sizeToFit]; _appButton.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height); - _appOverlay.frame = CGRectMake(0, 0, noImage.size.width / 2.f, noImage.size.height / 4.f); self.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height); - [_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)]; // Rasterizing the cell layer increases rendering performance by quite a bit self.layer.shouldRasterize = YES; @@ -56,13 +55,37 @@ } - (void) updateAppImage { - [_appOverlay setHidden:!_app.isRunning]; + if (_appOverlay != nil) { + [_appOverlay removeFromSuperview]; + _appOverlay = nil; + } + + if (_app.isRunning) { + // Only create the app overlay if needed + _appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]]; + _appOverlay.layer.shadowColor = [UIColor blackColor].CGColor; + _appOverlay.layer.shadowOffset = CGSizeMake(0, 0); + _appOverlay.layer.shadowOpacity = 1; + _appOverlay.layer.shadowRadius = 2.0; + + [self addSubview:_appOverlay]; + + _appOverlay.frame = CGRectMake(0, 0, noImage.size.width / 2.f, noImage.size.height / 4.f); + [_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)]; + } // TODO: Improve no-app image detection BOOL noAppImage = false; if (_app.image != nil) { - UIImage* appImage = [UIImage imageWithData:_app.image]; + // Load the decoded image from the cache + UIImage* appImage = [_artCache objectForKey:_app]; + if (appImage == nil) { + // Not cached; we have to decode this now + appImage = [UIImage imageWithData:_app.image]; + [_artCache setObject:appImage forKey:_app]; + } + // This size of image might be blank image received from GameStream. if (!(appImage.size.width == 130.f && appImage.size.height == 180.f)) { _appButton.frame = CGRectMake(0, 0, appImage.size.width / 2, appImage.size.height / 2); diff --git a/Limelight/ViewControllers/MainFrameViewController.m b/Limelight/ViewControllers/MainFrameViewController.m index d786661..a432ff2 100644 --- a/Limelight/ViewControllers/MainFrameViewController.m +++ b/Limelight/ViewControllers/MainFrameViewController.m @@ -5,6 +5,8 @@ // Copyright (c) 2014 Moonlight Stream. All rights reserved. // +@import ImageIO; + #import "MainFrameViewController.h" #import "CryptoManager.h" #import "HttpManager.h" @@ -38,6 +40,7 @@ UIScrollView* hostScrollView; int currentPosition; NSArray* _sortedAppList; + NSCache* _boxArtCache; } static NSMutableSet* hostList; @@ -195,6 +198,10 @@ static NSMutableSet* hostList; } - (void) receivedAssetForApp:(App*)app { + // Update the box art cache now so we don't have to do it + // on the main thread + [self updateBoxArtCacheForApp:app]; + dispatch_async(dispatch_get_main_queue(), ^{ [self.collectionView reloadData]; }); @@ -500,6 +507,8 @@ static NSMutableSet* hostList; hostList = [[NSMutableSet alloc] init]; } + _boxArtCache = [[NSCache alloc] init]; + [self setAutomaticallyAdjustsScrollViewInsets:NO]; hostScrollView = [[ComputerScrollView alloc] init]; @@ -524,10 +533,28 @@ static NSMutableSet* hostList; [self retrieveSavedHosts]; _discMan = [[DiscoveryManager alloc] initWithHosts:[hostList allObjects] andCallback:self]; + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(handleReturnToForeground) + name: UIApplicationWillEnterForegroundNotification + object: nil]; + [self updateHosts]; [self.view addSubview:hostScrollView]; } +-(void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +-(void)handleReturnToForeground +{ + // This will refresh the applist + if (_selectedHost != nil) { + [self hostClicked:_selectedHost view:nil]; + } +} + - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; @@ -540,10 +567,7 @@ static NSMutableSet* hostList; [_discMan startDiscovery]; - // This will refresh the applist - if (_selectedHost != nil) { - [self hostClicked:_selectedHost view:nil]; - } + [self handleReturnToForeground]; } - (void)viewDidDisappear:(BOOL)animated @@ -552,6 +576,9 @@ static NSMutableSet* hostList; // when discovery stops, we must create a new instance because you cannot restart an NSOperation when it is finished [_discMan stopDiscovery]; + // Purge the box art cache + [_boxArtCache removeAllObjects]; + // In case the host objects were updated in the background [[[DataManager alloc] init] saveData]; } @@ -605,6 +632,7 @@ static NSMutableSet* hostList; [hostScrollView addSubview:compView]; } } + prevEdge = [self getCompViewX:addComp addComp:addComp prevEdge:prevEdge]; addComp.center = CGPointMake(prevEdge, hostScrollView.frame.size.height / 2); @@ -620,6 +648,30 @@ static NSMutableSet* hostList; } } +// This function forces immediate decoding of the UIImage, rather +// than the default lazy decoding that results in janky scrolling. ++ (UIImage*) loadBoxArtForCaching:(App*)app { + + CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)app.image, NULL); + CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)@{(id)kCGImageSourceShouldCacheImmediately: (id)kCFBooleanTrue}); + + UIImage *boxArt = [UIImage imageWithCGImage:cgImage]; + + CGImageRelease(cgImage); + CFRelease(source); + + return boxArt; +} + +- (void) updateBoxArtCacheForApp:(App*)app { + if (app.image == nil) { + [_boxArtCache removeObjectForKey:app]; + } + else if ([_boxArtCache objectForKey:app] == nil) { + [_boxArtCache setObject:[MainFrameViewController loadBoxArtForCaching:app] forKey:app]; + } +} + - (void) updateAppsForHost:(Host*)host { if (host != _selectedHost) { Log(LOG_W, @"Mismatched host during app update"); @@ -629,6 +681,15 @@ static NSMutableSet* hostList; _sortedAppList = [host.appList allObjects]; _sortedAppList = [_sortedAppList sortedArrayUsingSelector:@selector(compareName:)]; + // Start populating the box art cache asynchronously + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + Log(LOG_I, @"Starting per-computer box art caching job"); + for (App* app in host.appList) { + [self updateBoxArtCacheForApp:app]; + } + Log(LOG_I, @"Per-computer box art caching job completed"); + }); + [hostScrollView removeFromSuperview]; [self.collectionView reloadData]; } @@ -636,9 +697,8 @@ static NSMutableSet* hostList; - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"AppCell" forIndexPath:indexPath]; - App* app = _sortedAppList[indexPath.row]; - UIAppView* appView = [[UIAppView alloc] initWithApp:app andCallback:self]; + UIAppView* appView = [[UIAppView alloc] initWithApp:app cache:_boxArtCache andCallback:self]; [appView updateAppImage]; if (appView.bounds.size.width > 10.0) {