iOS 圖示&啟動圖生成器(二)

QiShare發表於2019-04-18

級別: ★★☆☆☆
標籤:「iPhone app 圖示」「圖示生成」「啟動圖生成」「QiAppIconGenerator」
作者: Xs·H
審校: QiShare團隊


一個完整的app都需要多種尺寸的圖示和啟動圖。一般情況,設計師根據開發者提供的一套規則,設計出圖示和啟動圖供開發人員使用。但最近我利用業餘時間做了個app,不希望耽誤設計師較多時間,就只要了最大尺寸的圖示和啟動圖各一個。本想著找一下現成的工具,批量生成需要的的圖片,但最後沒有找到,只好使用Photoshop切出了不同尺寸的圖片。這期間,設計師還換過一次圖示和啟動圖,我就重複了切圖工作,這花費了我大量的時間。於是事後,作者開發了一個mac app——圖示&啟動圖生成器(簡稱生成器)以提高工作效率。作者用兩篇文章分別介紹生成器的使用和實現細節。

上篇文章,本篇文章介紹生成器的實現細節。

生成器的工程非常簡單,可以概括為一個介面一個資原始檔和一個ViewController。結構如下圖。

iOS 圖示&啟動圖生成器(二)

一、 介面

生成器app只有一個介面,因為介面複雜度較小,作者選用了Storyboard+Constraints的方式進行開發。下圖顯示了介面中的控制元件和約束情況。

iOS 圖示&啟動圖生成器(二)

其中各控制元件對應的類如下所示。

控制元件
圖片框 NSImageView
平臺選擇器 NSComboBox
路徑按鈕 NSButton
路徑文字框 NSTextField
匯出按鈕 NSButton

二、 資原始檔

app所支援的平臺規則資料從資原始檔QiConfiguration.plist中獲取。QiConfiguration.plist相當於一個字典,每個平臺對應著字典的一對keyvalue; value是一個陣列,儲存著該平臺所需要的一組尺寸規格資料(item); item是尺寸規格資料的最小單元,內部標記了該尺寸規格的圖片的用途、名稱和尺寸。

QiConfiguration.plist的具體結構如下圖所示。

iOS 圖示&啟動圖生成器(二)

三、 ViewController

工程使用預設的ViewController管理介面、資源資料和邏輯。 首先,介面控制元件元素在ViewController中對應下圖中的5個例項。

iOS 圖示&啟動圖生成器(二)

其中,imageViewplatformBoxpathField不需要響應方法。並且,platfromBox_pathField的預設/記憶資料由NSUserDefaults管理。

static NSString * const selectedPlatformKey = @"selectedPlatform";
static NSString * const exportedPathKey = @"exportedPath";
複製程式碼
- (void)viewDidLoad {
    
    [super viewDidLoad];
    
    NSString *selectedPlatform = [[NSUserDefaults standardUserDefaults] objectForKey:selectedPlatformKey];
    [_platformBox selectItemWithObjectValue:selectedPlatform];
    
    NSString *lastExportedPath = [[NSUserDefaults standardUserDefaults] objectForKey:exportedPathKey];
    _pathField.stringValue = lastExportedPath ?: NSHomeDirectory();
}
複製程式碼

這裡忽略這三個控制元件,重點介紹pathButtonexportButton的響應方法中的程式碼邏輯。

1. - pathButtonClicked:

pathButton的響應方法負責開啟檔案目錄,並回傳選擇的路徑給pathField,以顯示出來。 程式碼如下:

- (IBAction)pathButtonClicked:(NSButton *)sender {
    
    NSOpenPanel *openPanel = [NSOpenPanel openPanel];
    openPanel.canChooseDirectories = YES;
    openPanel.canChooseFiles = NO;
    openPanel.title = @"選擇匯出目錄";
    [openPanel beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse result) {
        if (result == NSModalResponseOK) {
            self.pathField.stringValue = openPanel.URL.path;
        }
    }];
}
複製程式碼
2. - exportButtonClicked:

exportButton的響應方法負責根據imageView中的源圖片、platform中選擇的平臺規則和pathField中顯示的匯出路徑生成圖片並開啟圖片所在的資料夾。 程式碼如下:

- (IBAction)exportButtonClicked:(NSButton *)sender {
    
    NSImage *image = _imageView.image;
    NSString *platform = _platformBox.selectedCell.title;
    NSString *exportPath = _pathField.stringValue;
    
    if (!image || !platform || !exportPath) {
        NSAlert *alert = [[NSAlert alloc] init];
        alert.messageText = @"請先選擇源圖片、平臺和匯出路徑";
        alert.alertStyle = NSAlertStyleWarning;
        [alert addButtonWithTitle:@"確認"];
        [alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse returnCode) {}];
    }
    else {
        [[NSUserDefaults standardUserDefaults] setObject:platform forKey:selectedPlatformKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        [[NSUserDefaults standardUserDefaults] setObject:exportPath forKey:exportedPathKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        [self generateImagesForPlatform:platform fromOriginalImage:image];
    }
}
複製程式碼
- (void)generateImagesForPlatform:(NSString *)platform fromOriginalImage:(NSImage *)originalImage {
    
    NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"QiConfiguration" ofType:@"plist"];
    NSDictionary *configuration = [NSDictionary dictionaryWithContentsOfFile:plistPath];
    NSArray<NSDictionary *> *items = configuration[platform];
    
    NSString *directoryPath = [[_pathField.stringValue stringByAppendingPathComponent:platform] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:nil];
    
    if ([platform containsString:@"AppIcons"]) {
        [self generateAppIconsWithConfigurations:items fromOriginalImage:originalImage toDirectoryPath:directoryPath];
    }
    else if ([platform containsString:@"LaunchImages"]) {
        [self generateLaunchImagesWithConfigurations:items fromOriginalImage:originalImage toDirectoryPath:directoryPath];
    }
}

- (void)generateAppIconsWithConfigurations:(NSArray<NSDictionary *> *)configurations fromOriginalImage:(NSImage *)originalImage toDirectoryPath:(NSString *)directoryPath {
    
    for (NSDictionary *configuration in configurations) {
        NSImage *appIcon = [self generateAppIconWithImage:originalImage forSize:NSSizeFromString(configuration[@"size"])];
        NSString *filePath = [NSString stringWithFormat:@"%@/%@.png", directoryPath, configuration[@"name"]];
        [self exportImage:appIcon toPath:filePath];
    }
    [[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:directoryPath isDirectory:YES]];
}

- (void)generateLaunchImagesWithConfigurations:(NSArray<NSDictionary *> *)configurations fromOriginalImage:(NSImage *)originalImage toDirectoryPath:(NSString *)directoryPath {
    
    for (NSDictionary *configuration in configurations) {
        NSImage *launchImage = [self generateLaunchImageWithImage:originalImage forSize: NSSizeFromString(configuration[@"size"])];
        
        NSString *filePath = [NSString stringWithFormat:@"%@/%@.png", directoryPath, configuration[@"name"]];
        [self exportImage:launchImage toPath:filePath];
    }
    [[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:directoryPath isDirectory:YES]];
}

- (NSImage *)generateAppIconWithImage:(NSImage *)fromImage forSize:(CGSize)toSize  {
    
    NSRect toFrame = NSMakeRect(.0, .0, toSize.width, toSize.height);
    toFrame = [[NSScreen mainScreen] convertRectFromBacking:toFrame];
    
    NSImageRep *imageRep = [fromImage bestRepresentationForRect:toFrame context:nil hints:nil];
    NSImage *toImage = [[NSImage alloc] initWithSize:toFrame.size];
    
    [toImage lockFocus];
    [imageRep drawInRect:toFrame];
    [toImage unlockFocus];
    
    return toImage;
}

- (NSImage *)generateLaunchImageWithImage:(NSImage *)fromImage forSize:(CGSize)toSize {
    
    // 計算目標小圖去貼合源大圖所需要放大的比例
    CGFloat wFactor = fromImage.size.width / toSize.width;
    CGFloat hFactor = fromImage.size.height / toSize.height;
    CGFloat toFactor = fminf(wFactor, hFactor);
    
    // 根據所需放大的比例,計算與目標小圖同比例的源大圖的剪下Rect
    CGFloat scaledWidth = toSize.width * toFactor;
    CGFloat scaledHeight = toSize.height * toFactor;
    CGFloat scaledOriginX = (fromImage.size.width - scaledWidth) / 2;
    CGFloat scaledOriginY = (fromImage.size.height - scaledHeight) / 2;
    NSRect fromRect = NSMakeRect(scaledOriginX, scaledOriginY, scaledWidth, scaledHeight);
    
    // 生成即將繪製的目標圖和目標Rect
    NSRect toRect = NSMakeRect(.0, .0, toSize.width, toSize.height);
    toRect = [[NSScreen mainScreen] convertRectFromBacking:toRect];
    NSImage *toImage = [[NSImage alloc] initWithSize:toRect.size];
    
    // 繪製
    [toImage lockFocus];
    [fromImage drawInRect:toRect fromRect:fromRect operation:NSCompositeCopy fraction:1.0];
    [toImage unlockFocus];
    
    return toImage;
}

- (void)exportImage:(NSImage *)image toPath:(NSString *)path {
    
    NSData *imageData = image.TIFFRepresentation;
    NSData *exportData = [[NSBitmapImageRep imageRepWithData:imageData] representationUsingType:NSPNGFileType properties:@{}];
    
    [exportData writeToFile:path atomically:YES];
}
複製程式碼

上述是工程的所有程式碼,程式碼較多。建議有需要的同學移步至工程原始碼閱讀。


小編微信:可加並拉入《QiShare技術交流群》。

iOS 圖示&啟動圖生成器(二)

關注我們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號)

推薦文章:
演算法小專欄:“D&C思想”與“快速排序”
iOS 避免常見崩潰(二)
演算法小專欄:選擇排序
iOS Runloop(一)
iOS 常用除錯方法:LLDB命令
iOS 常用除錯方法:斷點
iOS 常用除錯方法:靜態分析
奇舞週刊

相關文章