自动化小工具

人类通过不断制造先进的工具来改造世界。学习Mac软件开发,制造我们自己的生产力工具吧。

图片资源适配自动化工具

无论是iOS还是OSX App开发完成,发布前都需要设置各种尺寸大小的AppIcon图标。
通常的做法是设计师做一张1024*1024的大图,按App的要求缩成不同大小的图,然后工程师在逐个加到Xcode项目中。

我们希望能开发一个图片适配自动化工具自动实现这个过程。

实现思路

不同平台的AppIcon安装图标

这是iOS/iPad应用图标
AppIcon-iOS

OSX应用图标
AppIcon-OSX

点击AppIcon右键菜单上的Show In Finder

ImagesxcassetsMenu

看到AppIcon.appiconset是一个文件夹,里面是不同大小的图标。另外有一个Contents.json文件来描述AppIcon中包括的图片的大小和路径.

AppIconXcassetsFolder

这是iOS项目中的Contents.json内容。
Contents.json中的idiom分别为iPhone、iPad、Mac表示不同的系统平台。

Contens.jsonIOS

图片自动化工具的实现思路

通过上面的分析,我们想法是先对1024*1024的大图按不同平台的要求进行缩放;
然后在生成对应的Contents.json内容;最后把各个图标和Contents.json copy到AppIcon指定的目录下。

这样设计师做一张大图,通过我们的工具自动化完成缩放裁剪,自动添加到项目路径中,替代之前手工一个个添加到项目的繁琐的过程。

工程实现

使用Xcode新建一个OSX应用命名为ImageAassetAutomator。MainMenu.xib界面设计如下:

ImageAassetAutomatorXib

界面上红色区域为ImageView,可以从Finder中拖动图片文件到这个区域,ImageView支持拖放事件,能自动识别文件的路径。

下面ComboBox下拉选择列表,可以选择iPhone,iPad,iPhone+iPad,OSX 四种模式。

Export按钮,点击后让用户选择一个路径,根据从ComboBox选择的平台,生成相应尺寸的图片和图片资源Contents.json,将这些图片资源文件复制到用户指定的路径中.

ImageView拖放处理

新建一个DragImageZone类,继承NSImageView,实现NSDraggingDestination拖放协议。

定义DragImageZoneDelegate协议,图片拖放完成后通过delegate将图片路径返回。

DragImageZone类的接口定义

 #import <Cocoa/Cocoa.h>
@protocol DragImageZoneDelegate <NSObject>
@optional
-(void)didFinishDragWithFile:(NSString*)filePath;
@end;

@interface DragImageZone : NSImageView<NSDraggingDestination>
@property(weak) id<DragImageZoneDelegate> delegate;
@end

DragImageZone类的实现

 #import "DragImageZone.h"
@implementation DragImageZone
- (id)initWithCoder:(NSCoder *)decoder {
    self = [super initWithCoder:decoder];
    if (self) {
        [self registerForDraggedTypes:[NSArray arrayWithObjects:
                                     NSFilenamesPboardType, nil]];
        
        [self dropAreaFadeOut];
    }
    
    return self;
    
}
- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self registerForDraggedTypes:[NSArray arrayWithObjects:
                                        NSFilenamesPboardType, nil]];
        
        [self dropAreaFadeOut];
    }
    
    return self;
}

- (void)dropAreaFadeIn {
    [self setAlphaValue:1.0];
}

- (void)dropAreaFadeOut {
    [self setAlphaValue:0.2];
}

- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
    NSPasteboard *pboard;
    NSDragOperation sourceDragMask;
    
    NSLog(@"drag operation entered");
    sourceDragMask = [sender draggingSourceOperationMask];
    pboard = [sender draggingPasteboard];
    
    if ( [[pboard types] containsObject:NSFilenamesPboardType] ) {
        
        [self dropAreaFadeIn];
        
        if (sourceDragMask & NSDragOperationLink) {
            return NSDragOperationLink;
        } else if (sourceDragMask & NSDragOperationCopy) {
            return NSDragOperationCopy;
        }
    }
    return NSDragOperationNone;
}

- (void) draggingExited: (id <NSDraggingInfo>) info {
    NSLog(@"drag operation finished");
    
    [self dropAreaFadeOut];
}

- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
    NSPasteboard *pboard = [sender draggingPasteboard];
    
    NSLog(@"drop now");
    [self dropAreaFadeOut];
    
    if ( [[pboard types] containsObject:NSFilenamesPboardType] ) {
        
        NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
        NSInteger numberOfFiles = [files count];
        if(numberOfFiles>0)
        {
            NSString *filePath = [files objectAtIndex:0];
            
            if(self.delegate){
                [self.delegate didFinishDragWithFile:filePath];
            }
            return YES;
       
        }
        else{
            NSLog(@"drag file num =0 return!");
        }
    }
    else{
        NSLog(@"pboard types(%@) not register!",[pboard types]);
    }
    return YES;
    
}
@end

NSDraggingDestination协议中实现3个的方法说明:

  1. (NSDragOperation)draggingEntered:(id )sender
    返回支持的拖放操作的类型,对于图片操作这里返回的是NSDragOperationLink

  2. (void) draggingExited: (id ) info
    拖放结束的处理

  3. (BOOL)performDragOperation:(id )sender
    最关键的方法,返回拖放的文件路径

另外在在类的init方法中,注册了拖放类型为NSFilenamesPboardType。

[self registerForDraggedTypes:[NSArray arrayWithObjects:
                                        NSFilenamesPboardType, nil]];

更多Drag and Drop编程#Drag/Drop操作#章节。

图片尺寸定义

通过plist文件对iPhone、iPad、OSX 定义如下

以每个平台的图片大小为key,value对应为1x,2x,3x的数组。

ImageSizePlist

图片裁剪和保存

通过NSImage的Category类扩展来实现指定大小裁剪和保存到指定路径

NSImage+Catgory.h 头文件定义

@interface NSImage (Catgory)
-(NSImage*)reSize:(NSSize)resize;
-(void)saveAtPath:(NSString*)path;
@end

NSImage+Catgory.m 实现

#import "NSImage+Catgory.h"
@implementation NSImage (Catgory)
- (void)saveAtPath:(NSString*)path{
    NSData *imageData = [self TIFFRepresentation];
    NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData];
    NSDictionary *imageProps = [NSDictionary dictionaryWithObject:[NSNumber numberWithFloat:1.0] forKey:NSImageCompressionFactor];
    imageData = [imageRep representationUsingType:NSPNGFileType properties:imageProps];
    [imageData writeToFile:path atomically:NO];
    
}

- (NSImage*)reSize:(NSSize)resize{
    float resizeWidth  = resize.width/2; 
    float resizeHeight = resize.height/2;

    NSImage *resizedImage = [[NSImage alloc] initWithSize: NSMakeSize(resizeWidth, resizeHeight)];
    NSSize originalSize = [self size];
    
    [resizedImage lockFocus];
    [self drawInRect: NSMakeRect(0, 0, resizeWidth, resizeHeight) fromRect: NSMakeRect(0, 0, originalSize.width, originalSize.height) operation: NSCompositeSourceOver fraction: 1.0];
    [resizedImage unlockFocus];
    return resizedImage;
}
@end

Contents.json文件生成

根据用户选择的平台是iPhone、iPad、iPhone+iPad、OSX,从Plist 图片尺寸规格中获取到配置参数,依次生成类似下面的字典对象。最后转换为string写入到文件。

{
    "size" : "512x512",
    "idiom" : "mac",
    "filename" : "icon_mac_512.png",
    "scale" : "1x"
}

默认情况下用户点击Export按钮,裁剪图片和json文件会生成到用户当前document目录.

对于Export按钮,我们定义成为DragExportButton类,可以支持导出文件路径的拖放选择。当用户从一个需要增加AppIcon的Xcode工程中直接拖放Images.xcassets目录到Export按钮,可以记录这个Images.xcassets的文件目录路径。点击Export按钮后能把裁剪图片和json文件直接生成到Images.xcassets目录,实现真正的一键导入。

国际化

App全球发行推向国际市场一个重要的事情就是语言文字信息国际化。

App语言国际化过程

我们来看看在Xcode中App国际化的处理过程。

先需要创建Localizable.strings文本文件。File菜单创建文件,选择Strings File模版创建,输入Localizable.strings名字完成字符国际化文件创建。

TextFileCreate

查看Localizable.strings文件内容,里面是空的。后边面板的Localization部分包括Base和English 2项。
Base表示默认的国际化定义。English表示当前项目支持的语言。每个语言名字前面的勾选框表示是否已经生成了国际化语言文件。

LocalizableInit

我们对english勾选一下,让系统生成english的语言包。这时候会发现工程目录数中Localizable.strings节点多了2个子节点Base和English,表示生成了语言包。

LocalizableEn

接下来我们增加其他的国际化语言。点击Project中info面板区,看到Localizations部分,再点击
下面的+按钮,出现多语言选择列表,从这里可以选择几十种国际化语言。

ProjectLocalizable

我们点击选择增加一个French(fr)法语国际化。我们只对Localizable.strings文件做国际化,因此取消掉列表中的MainMenu.xib的选择。

LocalizableFr

这时候回到工程目录数中点击Localizable.strings,再看右边的信息面板的localization中多了French项。再勾选这个French项,会在工程中生成French的国际化语言文件。

LocalizableListFr

如果想增加其他语言,继续重复之前的过程。如果要增加10多种国家主流市场的话,还是非常繁琐的过程。

我们希望能自动化语言增加的过程,一次性就能增加多种语言的支持。这就是我们对国际化自动化的目标要求。

一次性选择多个语言界面的原型

LocalizableTools

最终项目中的生成好的多语言。
LocalizableMultiList

另外更高一层,还可以对语言包的翻译,借助于google翻译实现从标准的base语言包,自动完成多语言的翻译。

自动化思路

我们打开Xcode国际化完成的项目文件的工程目录

LocalizableFolder

可以看到每个国际化语言都单独有一个目录,名称为语言的简写加上lproj的后缀名,目录里面都是Localizable.strings 文件。

灵光一现我们知道怎么做了嘛?

好了我们就照猫画虎吧,让用户一次性选择多个语言,按Xcode希望的命名规则生成每个语言的文件夹,文件夹里面在放Localizable.strings文件。

我们可以新建一个项目,增加Localizable.strings文件文件,做好Base和English 2个基本的国际化。然后打开这个工程文件目录,找到国际化Base.lproj, copy一份重命名为fr.lproj,然后在到Xcode中看这个新项目工程目录数的Localizable.strings里面是不是增加了French 法语呢?

当然事情远远不是这么简单,令我们失望的是,工程里面根本没有新加的这个French语言!

继续分析,找到工程文件右键菜单点击显示包内容,里面有个project.pbxproj文件.

ProjectFileContens

使用TextEditor打开后搜索.strings字符串,看到Localizable.strings节点,里面children部分包括了全部的国际化语言。

        42D7AC661BA53A1900A06108 /* Localizable.strings */ = {
            isa = PBXVariantGroup;
            children = (
                42D7AC651BA53A1900A06108 /* Base */,
                42D7AC671BA53A1B00A06108 /* en */,
                C7B3F6368313A4CF1D681C97 /* ca */,
                06C6FD41AEC6E183E80A430A /* cs */,
                ECF571A5B5FF57D1F05AD025 /* da */,
                19DBC8F1B74EDC8940D57812 /* de */,
                A67D1AD135AE6795C6E42B69 /* el */,
                4C5084CA63F410186567BF80 /* es */,
                8EDD34B539B91FBED8B33535 /* fi */,
                5EC046661FCF814B3B75652E /* fr */,
                3D157C274FEAD6F70AC57E00 /* he */,
                46C8CC8A951CFE2BA14B102B /* hr */,
                DFAEF7A8E90402F5136B0F9D /* hu */,
                6A1760F945632553A133B15B /* it */,
                63E8C42A03774C21F4F87CAA /* ja */,
                B31ECD799EADEA078C6D3087 /* ko */,
                11F63A44627932DAFEFAF3C8 /* ms */,
                589D3278FCAFD388CE4E0ABF /* nb */,
                EC3D7200D0AF7022196B8B75 /* nl */,
                7503FE3C3B4B40D91A1674A4 /* pl */,
                859A1CC5010CB89EB2445A8C /* pt */,
                77B19E78D9BCD3C92B7130B5 /* pt-PT */,
                E4AAC19B903035BC6CCC1EAA /* ro */,
                78A311EBF5E2CD5502DFD655 /* ru */,
                B78E65D1CCD064C9066C502F /* sk */,
                EC3C11AF4CDA79457668676A /* sv */,
                FA3BEFDD82AEEDC6BAE989D5 /* th */,
                DA891D4E9EC4C322DA0A9171 /* tr */,
                5C72DBFF453FA88CAC965C9D /* uk */,
                CAFB93E4D6E60490C7D93AD0 /* vi */,
            );
            name = Localizable.strings;
            sourceTree = "<group>";
        };

这样看来我们需要将国际化多语言信息添加到工程文件信息里面。

这样我们的思路明确了,需要做2件事情 1)创建多语言文件夹和string文件;2)将多语言信息增加到工程文件信息里面。

第一步已经很容易实现了,下面看看第2步怎么做吧?

Xcode工程文件编辑修改

对一个工程文件iOSAppIcon.xcodeproj其实是一个文件夹,里面project.pbxproj是工程核心文件,可以转换为NSMutableDictionary进行操作。

projectpbxproj

对于工程文件的操作,开源的第三方库xcodeEditor提供了非常方便的API,我们直接使用就能对工程文件进行各种操作。

XCProject表示工程类对象,XCSourceFile代表文件类对象,XCGroup代表工程组类对象

1.初始化工程类操作类XCProject,参数为工程路径

XCProject *project = [XCProject projectWithFilePath:filePath];

2.获取工程中Localizable.strings文件所在的group和已经添加的国际化语言

获取到工程中所有的Localizable.strings文件,为XCSourceFile对象;在根据XCSourceFile的key获取到文件归属的XCGroup。

NSString *kKeyStringsFile = @"Localizable.strings";
NSArray *files = [self.project.files filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(XCSourceFile *evaluatedObject, NSDictionary *bindings) {
         return [[NSSet setWithObjects:kKeyStringsFile, nil] containsObject:[evaluatedObject.name lastPathComponent] ];
    }]];

for(XCSourceFile *file in files)
    {
        
        NSString *fullpath =[file pathRelativeToProjectRoot];
        //组信息
        XCGroup *group = [self.project groupForGroupMemberWithKey:file.key];
        
        XCGroup *parentGroup = [[self.project groupForGroupMemberWithKey:file.key] parentGroup];
       //语言名称信息
        NSString *language = [[[file.name stringByDeletingLastPathComponent] lastPathComponent] stringByDeletingPathExtension];
        
}

3.增加新语言到group

只要增加一个新语言的引用给语言所在的XCGroup,保存project就完成了语言的增加。

NSString *relretivePath = [NSString stringWithFormat:@"%@.lproj/%@",lang.name,fileName];
[self.group addPlistFileReferenceWithPath:relretivePath name:lang.name ];
[self.project save];