From 760a8d07b8901840c7263aa3e31da50102337a14 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Thu, 12 Jun 2014 14:39:16 +0200 Subject: [PATCH] Improve fetching the optimal icon of an app for the update view --- Classes/BITHockeyBaseManager.m | 20 +--- Classes/BITHockeyHelper.h | 4 + Classes/BITHockeyHelper.m | 108 ++++++++++++++++++ Classes/BITUpdateViewController.m | 58 ++-------- Support/HockeySDK.xcodeproj/project.pbxproj | 8 ++ Support/HockeySDKTests/BITHockeyHelperTests.m | 58 ++++++++++ Support/HockeySDKTests/Fixtures/AppIcon.png | Bin 0 -> 2790 bytes .../HockeySDKTests/Fixtures/AppIcon@2x.png | Bin 0 -> 3628 bytes 8 files changed, 189 insertions(+), 67 deletions(-) create mode 100644 Support/HockeySDKTests/Fixtures/AppIcon.png create mode 100644 Support/HockeySDKTests/Fixtures/AppIcon@2x.png diff --git a/Classes/BITHockeyBaseManager.m b/Classes/BITHockeyBaseManager.m index 1f21ff6997..334daa7e77 100644 --- a/Classes/BITHockeyBaseManager.m +++ b/Classes/BITHockeyBaseManager.m @@ -99,25 +99,7 @@ } - (BOOL)isPreiOS7Environment { - static BOOL isPreiOS7Environment = YES; - static dispatch_once_t checkOS; - - dispatch_once(&checkOS, ^{ - // we only perform this runtime check if this is build against at least iOS7 base SDK -#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 - // runtime check according to - // https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/TransitionGuide/SupportingEarlieriOS.html - if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) { - isPreiOS7Environment = YES; - } else { - isPreiOS7Environment = NO; - } -#else - isPreiOS7Environment = YES; -#endif - }); - - return isPreiOS7Environment; + return bit_isPreiOS7Environment(); } - (NSString *)getDevicePlatform { diff --git a/Classes/BITHockeyHelper.h b/Classes/BITHockeyHelper.h index a508dc6422..9a7e1ecf5f 100644 --- a/Classes/BITHockeyHelper.h +++ b/Classes/BITHockeyHelper.h @@ -46,6 +46,10 @@ NSString *bit_appName(NSString *placeHolderString); NSString *bit_UUIDPreiOS6(void); NSString *bit_UUID(void); NSString *bit_appAnonID(void); +BOOL bit_isPreiOS7Environment(void); + +NSString *bit_validAppIconStringFromIcons(NSArray *icons); +NSString *bit_validAppIconFilename(NSBundle *bundle); /* UIImage helpers */ UIImage *bit_roundedCornerImage(UIImage *inputImage, NSInteger cornerSize, NSInteger borderSize); diff --git a/Classes/BITHockeyHelper.m b/Classes/BITHockeyHelper.m index 0ab661e5f0..da8c13bdc0 100644 --- a/Classes/BITHockeyHelper.m +++ b/Classes/BITHockeyHelper.m @@ -216,6 +216,114 @@ NSString *bit_appAnonID(void) { return appAnonID; } +BOOL bit_isPreiOS7Environment(void) { + static BOOL isPreiOS7Environment = YES; + static dispatch_once_t checkOS; + + dispatch_once(&checkOS, ^{ + // we only perform this runtime check if this is build against at least iOS7 base SDK +#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_6_1 + // runtime check according to + // https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/TransitionGuide/SupportingEarlieriOS.html + if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) { + isPreiOS7Environment = YES; + } else { + isPreiOS7Environment = NO; + } +#else + isPreiOS7Environment = YES; +#endif + }); + + return isPreiOS7Environment; +} + +/** + Find a valid app icon filename that points to a proper app icon image + + @param icons NSArray with app icon filenames + + @return NSString with the valid app icon or nil if none found + */ +NSString *bit_validAppIconStringFromIcons(NSArray *icons) { + if (!icons) return nil; + if (![icons isKindOfClass:[NSArray class]]) return nil; + + BOOL useHighResIcon = NO; + BOOL useiPadIcon = NO; + if ([UIScreen mainScreen].scale == 2.0f) useHighResIcon = YES; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) useiPadIcon = YES; + + NSString *currentBestMatch = nil; + float currentBestMatchHeight = 0; + float bestMatchHeight = 0; + + if (bit_isPreiOS7Environment()) { + bestMatchHeight = useiPadIcon ? (useHighResIcon ? 144 : 72) : (useHighResIcon ? 114 : 57); + } else { + bestMatchHeight = useiPadIcon ? (useHighResIcon ? 152 : 76) : 120; + } + + for(NSString *icon in icons) { + // Don't use imageNamed, otherwise unit tests won't find the fixture icon + // and using imageWithContentsOfFile doesn't load @2x files with absolut paths (required in tests) + NSData *imgData = [[NSData alloc] initWithContentsOfFile:icon]; + UIImage *iconImage = [[UIImage alloc] initWithData:imgData]; + + if (iconImage) { + if (iconImage.size.height == bestMatchHeight) { + return icon; + } else if (iconImage.size.height < bestMatchHeight && + iconImage.size.height > currentBestMatchHeight) { + currentBestMatchHeight = iconImage.size.height; + currentBestMatch = icon; + } + } + } + + return currentBestMatch; +} + +NSString *bit_validAppIconFilename(NSBundle *bundle) { + NSString *iconFilename = nil; + NSArray *icons = nil; + + icons = [bundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]; + iconFilename = bit_validAppIconStringFromIcons(icons); + + if (!iconFilename) { + icons = [bundle objectForInfoDictionaryKey:@"CFBundleIcons"]; + if (icons && [icons isKindOfClass:[NSDictionary class]]) { + icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; + } + iconFilename = bit_validAppIconStringFromIcons(icons); + } + + // we test iPad structure anyway and use it if we find a result and don't have another one yet + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + icons = [bundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]; + if (icons && [icons isKindOfClass:[NSDictionary class]]) { + icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; + } + NSString *iPadIconFilename = bit_validAppIconStringFromIcons(icons); + if (iPadIconFilename && !iconFilename) { + iconFilename = iPadIconFilename; + } + } + + if (!iconFilename) { + NSString *tempFilename = [bundle objectForInfoDictionaryKey:@"CFBundleIconFile"]; + if (tempFilename) { + iconFilename = bit_validAppIconStringFromIcons(@[tempFilename]); + } + } + + if (!iconFilename) { + iconFilename = bit_validAppIconStringFromIcons(@[@"Icon.png"]); + } + + return iconFilename; +} #pragma mark UIImage private helpers diff --git a/Classes/BITUpdateViewController.m b/Classes/BITUpdateViewController.m index 28c99f6450..e75e22b19c 100644 --- a/Classes/BITUpdateViewController.m +++ b/Classes/BITUpdateViewController.m @@ -294,57 +294,19 @@ } [self updateAppStoreHeader]; - NSString *iconString = nil; - NSArray *icons = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIconFiles"]; - if (!icons) { - icons = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIcons"]; - if ((icons) && ([icons isKindOfClass:[NSDictionary class]])) { - icons = [icons valueForKeyPath:@"CFBundlePrimaryIcon.CFBundleIconFiles"]; + NSString *iconFilename = bit_validAppIconFilename([NSBundle mainBundle]); + if (iconFilename) { + BOOL addGloss = YES; + NSNumber *prerendered = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIPrerenderedIcon"]; + if (prerendered) { + addGloss = ![prerendered boolValue]; } - if (!icons) { - iconString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIconFile"]; - if (!iconString) { - iconString = @"Icon.png"; - } + if (addGloss && [self.updateManager isPreiOS7Environment]) { + _appStoreHeader.iconImage = [self addGlossToImage:[UIImage imageNamed:iconFilename]]; + } else { + _appStoreHeader.iconImage = [UIImage imageNamed:iconFilename]; } - } - - if (icons) { - BOOL useHighResIcon = NO; - BOOL useiPadIcon = NO; - if ([UIScreen mainScreen].scale == 2.0f) useHighResIcon = YES; - if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) useiPadIcon = YES; - - for(NSString *icon in icons) { - iconString = icon; - UIImage *iconImage = [UIImage imageNamed:icon]; - - if ( - (iconImage.size.height == 57 && !useHighResIcon && !useiPadIcon) || - (iconImage.size.height == 114 && useHighResIcon && !useiPadIcon) || - (iconImage.size.height == 120 && useHighResIcon && !useiPadIcon) || - (iconImage.size.height == 72 && !useHighResIcon && useiPadIcon) || - (iconImage.size.height == 76 && !useHighResIcon && useiPadIcon) || - (iconImage.size.height == 144 && !useHighResIcon && useiPadIcon) || - (iconImage.size.height == 152 && useHighResIcon && useiPadIcon) - ) { - // found! - break; - } - } - } - - BOOL addGloss = YES; - NSNumber *prerendered = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIPrerenderedIcon"]; - if (prerendered) { - addGloss = ![prerendered boolValue]; - } - - if (addGloss && [self.updateManager isPreiOS7Environment]) { - _appStoreHeader.iconImage = [self addGlossToImage:[UIImage imageNamed:iconString]]; - } else { - _appStoreHeader.iconImage = [UIImage imageNamed:iconString]; } self.tableView.tableHeaderView = _appStoreHeader; diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 1115044ead..7e4c4843aa 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ 1E1127C916580C87007067A2 /* buttonRoundedRegular@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E1127C116580C87007067A2 /* buttonRoundedRegular@2x.png */; }; 1E1127CA16580C87007067A2 /* buttonRoundedRegularHighlighted.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E1127C216580C87007067A2 /* buttonRoundedRegularHighlighted.png */; }; 1E1127CB16580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E1127C316580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png */; }; + 1E494AEC19491943001EFF74 /* AppIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E494AEA19491943001EFF74 /* AppIcon.png */; }; + 1E494AED19491943001EFF74 /* AppIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 1E494AEB19491943001EFF74 /* AppIcon@2x.png */; }; 1E49A43C1612223B00463151 /* BITFeedbackComposeViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E49A42D1612223B00463151 /* BITFeedbackComposeViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1E49A43F1612223B00463151 /* BITFeedbackComposeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E49A42E1612223B00463151 /* BITFeedbackComposeViewController.m */; }; 1E49A4421612223B00463151 /* BITFeedbackListViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E49A42F1612223B00463151 /* BITFeedbackListViewCell.h */; }; @@ -226,6 +228,8 @@ 1E1127C316580C87007067A2 /* buttonRoundedRegularHighlighted@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "buttonRoundedRegularHighlighted@2x.png"; sourceTree = ""; }; 1E20A57F181E9D4600D5B770 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/HockeySDK.strings; sourceTree = ""; }; 1E36D8B816667611000B134C /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/HockeySDK.strings; sourceTree = ""; }; + 1E494AEA19491943001EFF74 /* AppIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = AppIcon.png; sourceTree = ""; }; + 1E494AEB19491943001EFF74 /* AppIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "AppIcon@2x.png"; sourceTree = ""; }; 1E49A42D1612223B00463151 /* BITFeedbackComposeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackComposeViewController.h; sourceTree = ""; }; 1E49A42E1612223B00463151 /* BITFeedbackComposeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITFeedbackComposeViewController.m; sourceTree = ""; }; 1E49A42F1612223B00463151 /* BITFeedbackListViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITFeedbackListViewCell.h; sourceTree = ""; }; @@ -601,6 +605,8 @@ 1EA1170216F53B49001C015C /* Fixtures */ = { isa = PBXGroup; children = ( + 1E494AEA19491943001EFF74 /* AppIcon.png */, + 1E494AEB19491943001EFF74 /* AppIcon@2x.png */, 1EA1170316F53B49001C015C /* StoreBundleIdentifierUnknown.json */, 1EA1170816F53E3A001C015C /* StoreBundleIdentifierKnown.json */, 1E70A22F17F2F982001BB32D /* live_report_empty.plcrash */, @@ -949,12 +955,14 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1E494AEC19491943001EFF74 /* AppIcon.png in Resources */, 1EA1170C16F54A64001C015C /* HockeySDKResources.bundle in Resources */, 1E5A459B16F0DFC200B55C04 /* InfoPlist.strings in Resources */, 1E70A23417F2F982001BB32D /* live_report_signal.plcrash in Resources */, 1EA1170416F53B49001C015C /* StoreBundleIdentifierUnknown.json in Resources */, 1E70A23317F2F982001BB32D /* live_report_exception.plcrash in Resources */, 1E70A23217F2F982001BB32D /* live_report_empty.plcrash in Resources */, + 1E494AED19491943001EFF74 /* AppIcon@2x.png in Resources */, 1EA1170916F53E3A001C015C /* StoreBundleIdentifierKnown.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Support/HockeySDKTests/BITHockeyHelperTests.m b/Support/HockeySDKTests/BITHockeyHelperTests.m index 315a7c4238..3cd800ade3 100644 --- a/Support/HockeySDKTests/BITHockeyHelperTests.m +++ b/Support/HockeySDKTests/BITHockeyHelperTests.m @@ -90,5 +90,63 @@ assertThatInteger([resultString length], equalToInteger(36)); } +- (void)testValidAppIconFilename { + NSString *resultString = nil; + NSBundle *mockBundle = mock([NSBundle class]); + NSString *validIconPath = [[NSBundle bundleForClass:self.class] pathForResource:@"AppIcon" ofType:@"png"]; + NSString *validIconPath2x = [[NSBundle bundleForClass:self.class] pathForResource:@"AppIcon@2x" ofType:@"png"]; + + // No valid icons defined at all + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:@"invalidFilename.png"]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, nilValue()); + + // CFBundleIconFiles contains valid filenames + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[validIconPath, validIconPath2x]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); + + // CFBundleIcons contains valid dictionary filenames + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:@{@"CFBundlePrimaryIcon":@{@"CFBundleIconFiles":@[validIconPath, validIconPath2x]}}]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + // CFBundleIcons contains valid ipad dictionary and valid default dictionary filenames + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:@{@"CFBundlePrimaryIcon":@{@"CFBundleIconFiles":@[validIconPath, validIconPath2x]}}]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:@{@"CFBundlePrimaryIcon":@{@"CFBundleIconFiles":@[validIconPath, validIconPath2x]}}]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); + + // CFBundleIcons contains valid filenames + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:@[validIconPath, validIconPath2x]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:nil]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); + + // CFBundleIcon contains valid filename + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFiles"]) willReturn:@[@"invalidFilename.png"]]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIcons~ipad"]) willReturn:nil]; + [given([mockBundle objectForInfoDictionaryKey:@"CFBundleIconFile"]) willReturn:validIconPath]; + + resultString = bit_validAppIconFilename(mockBundle); + assertThat(resultString, notNilValue()); +} + @end diff --git a/Support/HockeySDKTests/Fixtures/AppIcon.png b/Support/HockeySDKTests/Fixtures/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..5473e5e8de977643534715d284906b506c1c58ab GIT binary patch literal 2790 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 literal 0 HcmV?d00001