新年之际,盘点一些APP开发技巧

技术分享

(原文:Reader Submissions - New Year‘s 2015 作者:Mattt Thompson 译者:培子 校对:蓝魂)

回顾过去一年发生在我们身边的事情时,有一点不得不提:对苹果开发者来讲,2014年是令人难以置信的一年。在这短短的一年中(有关APP的开发)发生了如此多的变化:在充满吸引力的Swift面前,我们几乎忘了之前是如何痴迷于Objective-C;以及充满想象力的iOS 8和WatchKit,难以想象还有什么API能与之相比。

NSHipster的惯例:请可爱的童鞋们,在新年的第一天,为大家展示你们(在开发中)常使用的技巧和方法。如今,随着来自库比蒂诺(Cupertino,苹果总部,位于旧金山)和众多开源社区的一系列API的涌现,妈妈再也不用担心我们找不到有趣的东西来分享啦!

在此,感谢以下童鞋们所做的贡献:

Colin Rofls, Cédric Luthi, Florent Pillet, Heath Borders, Joe Zobkiw, Jon Friskics, Justin Miller, Marcin Matczuk, Mikael Konradsson, Nolan O‘Brien, Robert Widmann, Sachin Palewar,Samuel Defago, Sebastian Wittenkamp, Vadim Shpakovski, and Zak

成员函数的使用技巧
(来自 Robert Widmann

在用静态方式调用Swift类和结构中的成员函数时,通常使用以下格式:

Object->(参数)->Things

比如,你可以用以下两种方式调用reverse():

[1,2,3,4].reverse( )
Array.reverse([1,2,3,4])

用@()来封装C字符串
(来自 Samuel Defago

事实上文字大部分时候是数字和字母的集合,使用C字符串,尤其当我在使用运行时编码的时候,我常常会忘记用UTF8编码、以NULL结束:Objective-C字符串封装:

NSString *propertyAttributesString =
    @(property_getAttributes(class_getProperty([NSObject class], "description")));
// T@"NSString",R,C

AmIBeingDebugged

Nolan O‘Brien这篇Q&A技术文档中让我们注意到了AmIBeingDebugged函数方法:

技术分享

使用延迟存储属性
(来自 Colin Rofls

在开发过程中,应该避免使用Optionals类型,更不应该使用隐式解包optionals类型。你想声明一个var变量却不想给一个初始值?使用“lazy”吧,唯一要注意的就是:在你的属性被赋值之前不要调用getter方法即可(童叟无欺!)

lazy var someModelStructure = ExpensiveClass()

假如你仅仅对这var变量调用set方法,而没有调用getter方法的话,这个被lazy修饰的var变量不会被赋值。例如,用lazy修饰那些直到viewDidLoad时才需要初始化的views变量就会非常合适。

获取Storyboard视图容器里的子视图控制器
(来自 Vadim Shpakovski

有一个比较方便的方法来获取故事板视图容器里的子视图控制器:

// 1. A property has the same name as a segue identifier in XIB
@property (nonatomic) ChildViewController1 *childController1;
@property (nonatomic) ChildViewController2 *childController2;
 
// #pragma mark - UIViewController
 
- (void)prepareForSegue:(UIStoryboardSegue *)segue
                 sender:(id)sender
{
    [super prepareForSegue:segue sender:sender];
 
    // 2. All known destination controllers assigned to properties
    if ([self respondsToSelector:NSSelectorFromString(segue.identifier)]) {
        [self setValue:segue.destinationViewController forKey:segue.identifier];
    }
}
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    // 3. Controllers already available bc viewDidLoad is called after prepareForSegue
    self.childController1.view.backgroundColor = [UIColor redColor];
    self.childController2.view.backgroundColor = [UIColor blueColor];
}

重复运行项目,不重复构建项目
(来自 Heath Borders

假如你一直在不停地调试同一个问题,你可以在不重复构建的情况下运行你的APP,这样:“Product>Perform Action>Run without Building” (快捷键??R: Command + R)

快速获取Playground资源
(来自 Jon Friskics

Swift里的所有Playground共享相同的数据目录:/Users/HOME/Documents/Shared Playground Data

如果你喜欢使用很多Playgrounds,你将需要在上述共享目录下为每个Playground新建对应的子目录,来存储每个Playground用到的数据;但是那之后你需要告诉每个Playground在哪儿可以获取其对应的数据。下面是我常用的一个辅助解决方法:

func pathToFileInSharedSubfolder(file: String) -> String {
    return XCPSharedDataDirectoryPath + "/" + NSProcessInfo.processInfo().processName + "/" + file
}

processName属性是Playground文件的名字,因此只要你已经在Playground数据共享文件目录下以相同的名字新建了一个子目录,那么你可以很容易访问这些数据,和读取本地JSON数据一样:

var jsonReadError:NSError?
let jsonData = NSFileManager.defaultManager().contentsAtPath(pathToFileInSharedSubfolder("data.json"))!
let jsonArray = NSJSONSerialization.JSONObjectWithData(jsonData, options: nil, error: &jsonReadError) as [AnyObject]

....或者 访问本地图片

let imageView = UIImageView()
imageView.image = UIImage(contentsOfFile: pathToFileInSharedSubfolder("image.png"))

Please attention!本篇文章剩余的部分来自Cédric Luthi大神的贡献,他分享了一些比较有用的开发技巧和技术,这些内容足够自成一篇,值得细细品读。这里再次感谢Cédric!

CocoaPods大揭秘

这儿有一个快速的方法来检查APP里用到的所有pods:

$ class-dump -C Pods_ /Applications/Squire.app | grep -o "Pods_\w+"

CREATE_INFOPLIST_SECTION_IN_BINARY

注意Xcode中为命令模式APP(command-line apps)设置的CREATE_INFOLIST_SECTION_IN_BINARY属性。这比使用-sectcreate__TEXT__info_plist链接标志位更加容易,前者还把已经编译好的Info.plist文件嵌入在二进制编码中。

关于如何向苹果提需求,它也给我们上了一课,这个特性需求早在2006年的 rdar://4722772 被提出,但直到7年后才被满足。

(译者注:言外之意是它是反面教材,应该更有技巧的提需求)

禁用 dylib钩子
(来自 Sam Marshall

Sam Marshall这个技巧可谓是走自己的路,让黑客无路可走。

在你的“Other Linker Flags”里加上下面这行:

-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null

NSBundle -preferredLocalizations

某些时候,你需要知道APP当前使用的是什么语言。通常,大家会使用NSLocal+preferredLanguages. 可惜的是这个方法不会告诉你APP实际呈现的文字语种。你仅仅会得到iOS系统里“Settings->General->Language&Region->Preferred Language”列表中的选项,或者OSX系统里“System Preferences->Language & Region->Preferred Languages”列表中的选项。想象一下:优先语言列表中只有{英语,法语},但你的APP仅使用德语;调用[[NSLocal preferredLanguages] firstObject]返回给你的是英语,而不是德语。

正确的方法是用[[NSBundle mainBundle] preferredLocalizations]方法。

苹果的开发文档是这样说的:

一个包含了在bundle中本地化的语言ID的NSString对象的数组,里面的字符串排序是根据用户的语言偏好设置和可使用的地理位置而来的。

NSBundle.h里的备注:

一个bundle中本地化的子集,重新排序到当前执行坏境的优先序列里,main bundle的语言顺序中最前面的是用户希望在UI界面上看到的语种。

当然你也许需要调用这个方法:

NSLocal+canonicalLanguageIdentifierFromString:

来确保你使用的文字语种是规范的语种。

保护SDK头文件

如果你用dmg安装Xcode,那么看看这篇Joar Wingfors的文章它讲述了如何通过保留所有权来避免SDK头文件被意外修改:

$ sudo ditto /Volumes/Xcode/Xcode.app /Applications/Xcode.app

任意类型的实例变量检测

为了达到逆向处理的目的,查询对象的实例变量是一个常见可靠的途径。通常调用对象valueForKey:方法就能达到这一目的,除了少数重写了类方法+accessInstanceVariablesDirectly的类屏蔽了该操作。

下面是一个例子:当实例变量有一个为任意类型的属性时,上述提到的操作无效

这是iOS6.1 SDK中MediaPlayer 框架的一段引用:

@interface MPMoviePlayerController : NSObject {
    void *_internal;    // 4 = 0x4
    BOOL _readyForDisplay;  // 8 = 0x8
}

因为 id internal=[moviePlayerController valueForKey:@”internal”] 无效,下面有一个笨办法来取得这个变量:

id internal = *((const id*)(void*)((uintptr_t)moviePlayerController + sizeof(Class)));

注意!不要随意调用这段代码,因为ivar的布局可能改变(指针偏移量计算可能出错)。仅在逆向工程中使用!

NSDateFormatter +dateFormatFromTemplate:options:locale:

友情提示:假如你调用[NSDateFormatter setDateFormat],而没有调用[NSDateFormatter dateFormatFromTemplate:options:local:],n那么很可能出错。

苹果文档

+ (NSString *)dateFormatFromTemplate:(NSString *)template
                             options:(NSUInteger)opts
                              locale:(NSLocale *)locale

不同地区有不同的日期格式。使用这个方法的目的:得到指定地区指定日期字段的一个合适的格式(通常你可以通过currentLocal查看当前所属地区)

下面这个例子给我们表现了英式英语和美式英语不同的日期格式:

NSLocale *usLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
NSLocale *gbLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];
 
NSString *dateFormat;
NSString *dateComponents = @"yMMMMd";
 
dateFormat = [NSDateFormatter dateFormatFromTemplate:dateComponents options:0 locale:usLocale];
NSLog(@"Date format for %@: %@",
    [usLocale displayNameForKey:NSLocaleIdentifier value:[usLocale localeIdentifier]], dateFormat);
 
dateFormat = [NSDateFormatter dateFormatFromTemplate:dateComponents options:0 locale:gbLocale];
NSLog(@"Date format for %@: %@",
    [gbLocale displayNameForKey:NSLocaleIdentifier value:[gbLocale localeIdentifier]], dateFormat);
 
// Output:
// Date format for English (United States): MMMM d, y
// Date format for English (United Kingdom): d MMMM y

通过调试获取内部常量

近期, Matthias Tretter在Twitter上问到:

有人知道在iOS8里modal viewController presentation的默认动画时间和跳转方式吗?

我们在UIKit的类库中发现了这样一个函数:[UITransitionView defaultDurationForTransition:],并在这个方法的位置加一个断点:

(lldb) br set -n "+[UITransitionView defaultDurationForTransition:]"

模态显示一个viewController,就会停在这个断点,输入finish执行该方法:

(lldb)finish

在defaultDurationForTransition:被执行时,你就能读到结果(在xmm0寄存器里)

(lldb) register read xmm0 --format float64
    xmm0 = {0.4 0}

回复:默认动画时间0.4s

DIY 弱关联对象

不幸的是,关联对象OBJC_ASSOCIATION_ASSIGN策略不支持引用计数为0的弱引用。幸运的是,你可以很容易实现它,你仅仅需要一个简单的类,并在这个类里弱引用一个对象:

@interface WeakObjectContainter : NSObject
@property (nonatomic, readonly, weak) id object;
@end
 
@implementation WeakObjectContainter
- (instancetype)initWithObject:(id)object {
    self = [super init];
    if (!self) {
        return nil;
    }
 
    self.object = object;
 
    return self;
}
@end

然后,通过OBJC_ASSOCIATION_RETAIN(_NONATOMIC)关联WeakObjectContainter:

objc_setAssociatedObject(self, &MyKey, [[WeakObjectContainter alloc] initWithObject:object], OBJC_ASSOCIATION_RETAIN_NONATOMIC);

用object属性指向这个所需的引用计数为0的弱引用对象。

id object = [objc_getAssociatedObject(self, &MyKey) object];

在这么多日新月异的新技术推动下,我们将迎来一个充满无限可能和机会的一年。在此,祝大家2015年新年快乐!

愿编程路上,与君共勉。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。