当我们花些时间来回溯我们在过去一年的经历的时候,有一点是清楚的:对专业的苹果开发者来说,2014 年是一个令人难以置信的一年。在这么短的时间跨度内发生了这么多的事情,都记不得在 Swift 之前我们跟 Objective-C 的关系,或者还有什么 API 比 iOS 8 或 WatchKit 更让我们着迷。
这有一个 NSHipster 传统问题要问你们,亲爱的读者,请把你在过去一年里最喜欢的技巧发送给我们,我们会在新年假期后公布结果。这一年随着大量新发展的出现,无论从苹果还是整个社区,都为读者分享了很多的有趣花絮。
谢谢 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, 和 Zak Remer 的贡献。
来自 Robert Widmann:
在 Swift 的类和结构里,使用静态时成员函数类总是有下列类型:
Object -> (Args) -> Thing
比如,你可以用两种方式来对一个数组调用 reverse()
:
[1, 2, 3, 4].reverse()
Array.reverse([1, 2, 3, 4])()
@( )
来封装 C-Strings来自 Samuel Defago:
鉴于文字大部分是用数字和集合关联的,我常常忘记它们可以在 UTF8 下工作良好,并且编码了
NULL
, 终结了 C-string,特别是当我使用运行时代码:
NSString *propertyAttributesString =
@(property_getAttributes(class_getProperty([NSObject class], "description")));
// T@"NSString",R,C
Nolan O’Brien 的 this Technical Q&A document 让我们对 AmIBeingDebugged
方法引起了关注:
#include <assert.h>
#include <stdbool.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/sysctl.h>
static Bool AmIBeingDebugged(void) {
int mib[4];
struct kinfo_proc info;
size_t size = sizeof(info);
info.kp_proc.p_flag = 0;
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PID;
mib[3] = getpid();
sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0);
return (info.kp_proc.p_flag & P_TRACED) != 0;
}
来自 Colin Rofls:
避免使用 optional。应尽量避免对 optional 进行隐式拆包(implicitly unwraped)。要声明一个变量,但不一定在初始化时赋初始值?就用惰性关键字,在你真的有值之前不调用 getter 方法。
lazy var someModelStructure = ExpensiveClass()
如果你对这个变量调用
set
之前没有调用过 getter,惰性表达式永远不会被执行。比如在视图里直到 viewDidLoad 之前你都不一定要初始化就很棒。
来自 Vadim Shpakovski:
有一种方便的方式来访问插入到 storyboard 容器视图的子控制器:
// 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:
如果你一遍又一遍的调试同样的问题,你可以不重新编译就运行你的应用程序: “Product > Perform Action > Run without Building” (
⌘⌃R
)。
来自 Jon Friskics:
Swift Playgrounds 跟所有的共享 Playground 数据都在
/Users/HOME/Documents/Shared Playground Data
下可以找到。
如果你喜欢使用很多的 Playgrounds,你会想要把各 Playground 使用到的数据放到该共享文件夹的子文件夹里面,但你得让 Playground 知道去哪里找。下面是我使用的辅助方法来让这事变得简单:
func pathToFileInSharedSubfolder(file: String) -> String {
return XCPSharedDataDirectoryPath + "/" + NSProcessInfo.processInfo().processName + "/" + file
}
在 NSProcessInfo 的 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"))
今年其余的读者意见的来自 Cédric Luthi,他(像去年或之前一样)贡献了很多的技巧和窍门值得占据一整篇文章。非常感谢,Cédric!
有一个快速方法来检查(闭源)应用程序使用的所有源:
$ class-dump -C Pods_ /Applications/Squire.app | grep -o "Pods_\w+"
CREATE_INFOPLIST_SECTION_IN_BINARY
查看 Xcode 中对命令行应用程序的设置
CREATE_INFOPLIST_SECTION_IN_BINARY
。它比-sectcreate__TEXT__info_plist
链接标志(linker flag)更容易使用,而且它把处理了的 Info.plist 文件嵌入了到二进制包中。
这也是在归档雷达的教训。在2006年,此功能被要求以
rdar://4722772
归档,在 7 年后才被认真对待。
来自 Sam Marshall 的这一招使黑客的生活更艰难:> >
把这一行加到你的 “Other Linker Flags” 里:
-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
有时候,你需要知道你的应用程序在什么语言环境下运行。通常,人们会用
NSLocale +preferredLanguages
。不幸的是这除了告诉你应用程序实际上显示的语言之外一无所知。它只是给你 iOS 中 “Settings → General → Language & Region → Preferred Language” 或是 OS X 里 “System Preferences → Language & Region → Preferred Languages” 同样的有序列表。
想象一下如果首选语言顺序是
{英语, 法语}
但是你的应用程序只支持德语。调用[NSLocale preferredLanguages] firstObject]
会返回英语而不是你想要的德语。
得到应用程序使用的准确语言环境的正确方式是使用
[[NSBundle mainBundle] preferredLocalizations]
。
文档是这么说的:
一个
NSString
对象的数组包含了 bundle 里的区域语言 ID。这些字符串是按用户系统设置和可用本地化来排序的。
NSBundle.h
里的注释说:
这个 bundle 本地化的子集,会对这个进程的当前执行环境的优先顺序上重新排序;主 bundle 的首选本地化显示了用户是最有可能在 UI 看到的语言(文本)
你大概还需要使用
NSLocale +canonicalLanguageIdentifierFromString:
来确保规范的语言标识。
如果你是从 dmg 里安装的 Xcode,参考一下这个来自 Joar Wingfors 的方法,通过保留所有权、权限和硬链接的方式避免不小心修改了 SDK 的头文件:
$ sudo ditto /Volumes/Xcode/Xcode.app /Applications/Xcode.app
void *
的实例变量因为逆向工程的原因,非常有用的常用方法是查看对象的实例变量。它通常很容易通过
valueForKey:
来达成,因为很少有类会重写+accessInstanceVariablesDirectly
来禁止变量通过 Key-Value Coding 访问。
但是有一种情况会让这个不起作用:当变量有一个
void *
类型的时候。
这有一个来自 iOS 6.1 里 MediaPlayer 库的摘录:
@interface MPMoviePlayerController : NSObject <MPMediaPlayback>
{
void *_internal; // 4 = 0x4
BOOL _readyForDisplay; // 8 = 0x8
}
由于
id internal = [moviePlayerController valueForKey:@"internal"]
不工作,有一个硬编码的方式访问内部变量:
id internal = *((const id*)(void*)((uintptr_t)moviePlayerController + sizeof(Class)));
不要发布这样的代码,这是非常不可靠的,因为变量布局可能会改变。只在逆向工程里使用!
NSDateFormatter +dateFormatFromTemplate:options:locale:
友情提示:如果你在使用
NSDateFormatter -setDateFormat:
而不同时使用NSDateFormatter +dateFormatFromTemplate:options:locale:
那么你很可能做错了。
文档是这样的:
+ (NSString *)dateFormatFromTemplate:(NSString *)template
options:(NSUInteger)opts
locale:(NSLocale *)locale
不同的语言对时间要素有不同的规范。你用这个方法来得到某个特定语言(通常使用当前的语言 - 参看 currentLocale)给定的时间要素的正确字符串格式。
下面的例子展示了时间在英国英语和美国英语下的不同格式:
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 上提问:
有没有人知道在 iOS 8 里面默认的动画时长和使 viewController 用模型方式展示的触发条件?
— Matthias Tretter (@myell0w) November 21, 2014
在 UIKit 的类堆栈里搜索 duration,找到了
UITransitionView +defaultDurationForTransition:
方法,然后在那个方法上加个断点:
(lldb) br set -n "+[UITransitionView defaultDurationForTransition:]"
展示一个模式视图控制器,就会进到这个断点,键入
finish
来执行这个方法:
(lldb) finish
在
defaultDurationForTransition:
执行的那个断点,你可以读到结果(在xmm0
里):
(lldb) register read xmm0 --format float64
xmm0 = {0.4 0}
答案:默认时长是 0.4 秒。
遗憾的是,关联对象
OBJC_ASSOCIATION_ASSIGN
的政策不支持零弱引用(zeroing weak references)。幸运的是,自己实现也很简单。你只需一个简单的类来封装一个弱引用的对象:
@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
属性来访问它以使得把零弱引用关联到需要的对象上:
id object = [objc_getAssociatedObject(self, &MyKey) object];
就是这样,我们迎来了全新的充满可能和机会的一年。大家 2015 年快乐!
祝愿你继续编译你的代码并且得到鼓舞。
除非另有声明,本文采用知识共享「署名-非商业性使用 3.0 中国大陆」许可协议授权。