resignFirstResponder doesn't work on iPad

在 iPad 上,用 modalPresentationStyle = UIModalPresentationFormSheet 方式推出一个 viewController,这时这个 viewController 不会响应 resignFirstResponder,其他样式的 modalPresentationStyle 没有问题。苹果一个开发在开发者论坛说这是个 feature,不是 bug,devforums.apple.com

Was your view by any chance presented with the UIModalPresentationFormSheet style? To avoid frequent in-and-out animations, the keyboard will sometimes remain on-screen even when there is no first responder. This is not a bug.

就算不是 bug 也很恼人,有人给出了解决方法 devforums.apple.com。新建一个 UINavigationController category,禁掉 disablesAutomaticKeyboardDismissal

- (BOOL)disablesAutomaticKeyboardDismissal
{
    return NO;
}

然后把 viewController 挂在 UINavigationController 下即可:

MyViewController *myViewController = [[MyViewController alloc] initWithNibName:@"MyViewController" bundle:nil];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:myViewController];
theNavigationController.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentModalViewController:theNavigationController animated:YES];

SO 参考:

隐藏 UITableView 下不需要的分割线

UITableViewStylePlain 样式下的 UITableView 如果显示分割线,就会在 tableView 下显示额外的空白 cell 和分割线。在 SO 上发现一个小技巧来解决这个问题 Eliminate Extra separators below UITableView - in iphone sdk?

UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.tableView.width, 1)];
v.backgroundColor = [UIColor whiteColor];
[self.tableView setTableFooterView:v];

@autoreleasepool in loop or loop in @autoreleasepool

如果循环中会生成大量的 autorelease 对象,可以考虑用 autorelease pool 来进行封装。封装时候有两种方式:

1:

while ([rs next]) {
    @autoreleasepool {
        NSDictionary *dict = [self dictFromXX];
        //...
    }
}

2:

@autoreleasepool {
    while ([rs next]) {
        NSDictionary *dict = [self dictFromXX];
        //...
    }
}

第一种,也就是 @autoreleasepool in loop 方式下每次循环都会生成一个 pool,在单次循环结束后被 drain 掉,适用于每次循环都有大量的 autorelease 对象生成,在单次循环结束后可以及时的将资源释放。

第二种,loop in @autoreleasepool 下只有一个 pool,只会在整个循环结束后 drain 掉,也就是说第一次循环时生成的 autorelease 对象也要等到整个循环结束时候才会随着 pool 释放。适用于循环次数不太多,且每次循环只有少量的 autorelease 对象生成,毕竟这些对象都要等到循环结束后才会被释放。

ref @autoreleasepool in loop or loop in @autoreleasepool?

Difference between objectForKey and valueForKey in NSDictionary

从 NSDictionary 取值的时候有两个方法,objectForKey:valueForKey:,这两个方法具体有什么不同呢?

先从 NSDictionary 文档中来看这两个方法的定义:

objectForKey: returns the value associated with aKey, or nil if no value is associated with aKey. 返回指定 key 的 value,若没有这个 key 返回 nil.

valueForKey: returns the value associated with a given key. 同样是返回指定 key 的 value。

直观上看这两个方法好像没有什么区别,但文档里 valueForKey: 有额外一点:

If key does not start with “@”, invokes objectForKey:. If key does start with “@”, strips the “@” and invokes [super valueForKey:] with the rest of the key. via Discussion

一般来说 key 可以是任意字符串组合,如果 key 不是以 @ 符号开头,这时候 valueForKey: 等同于 objectForKey:,如果是以 @ 开头,去掉 key 里的 @ 然后用剩下部分作为 key 执行 [super valueForKey:]

比如:

NSDictionary *dict = [NSDictionary dictionaryWithObject:@"theValue"
                                                 forKey:@"theKey"];
NSString *value1 = [dict objectForKey:@"theKey"];
NSString *value2 = [dict valueForKey:@"theKey"];

这时候 value1value2 是一样的结果。如果是这样一个 dict:

NSDictionary *dict = [NSDictionary dictionaryWithObject:@"theValue"
                                                 forKey:@"@theKey"];// 注意这个 key 是以 @ 开头
NSString *value1 = [dict objectForKey:@"@theKey"];
NSString *value2 = [dict valueForKey:@"@theKey"];

value1 可以正确取值,但是 value2 取值会直接 crash 掉,报错信息:

Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[<__NSCFDictionary 0x892fd80> valueForUndefinedKey:]: this class is not key value coding-compliant for the key theKey.’

这是因为 valueForKey:KVC(NSKeyValueCoding) 的方法,在 KVC 里可以通过 property 同名字符串来获取对应的值。比如:

@interface Person : NSObject
@property (nonatomic, retain) NSString *name;
@end

...
Person *person = [[Person alloc] init];
person.name = @"fannheyward";

NSLog(@"name:%@", [person name]);
//name:fannheyward
NSLog(@"name:%@", [person valueForKey:@"name"]);
//name:fannheyward
[person release];

valueForKey: 取值是找和指定 key 同名的 property accessor,没有的时候执行 valueForUndefinedKey:,而 valueForUndefinedKey: 的默认实现是抛出 NSUndefinedKeyException 异常。参考Getting Attribute Values Using Key-Value Coding

回过头来看刚才 crash 的例子, [dict valueForKey:@"@theKey"]; 会把 key 里的 @ 去掉,也就变成了 ` [dict valueForKey:@”theKey”];,而 dict 不存在 theKey 这样的 property,转而执行 [dict valueForUndefinedKey:@”theKey”];,抛出 NSUndefinedKeyException` 异常后 crash 掉。

objectForKey:valueForKey: 在多数情况下都是一样的结果返回,但是如果 key 是以 @ 开头,valueForKey: 就成了一个大坑,建议在 NSDictionary 下只用 objectForKey: 来取值。

多 Target 下不同的 Bundle Display Name

真不好用一个标题来概括这个东西。Xcode 4.2+ 在项目多语言包 xx.lproj 里引入了一个叫 InfoPlist.strings 的文件,可以对同一个 App 在不同系统语言下显示不同的 Display Name。比如:

InfoPlist.strings (English) - "CFBundleDisplayName" = "English Name";
InfoPlist.strings (Chinese) - "CFBundleDisplayName" = "中文";

在单 Target 下很容易做,多 Target 的时候就需要做一点额外的处理。在项目目录下新建与 Target 同名的文件夹(同名是为了方便区分),然后将 xx.lproj 文件夹 复制 到各个 Target 下面,目录结构会是这个样子:

./Target1/
          en.lproj/InfoPlist.strings
          zh-Hans.lproj/InfoPlist.strings
./Target2/
          en.lproj/InfoPlist.strings
          zh-Hans.lproj/InfoPlist.strings

复制后保持项目目录下还有 xx.lproj 文件夹,里面保留 Localizable.strings,因为多语言化一般是通用的,没必要针对每一个 Target 做多语言。复制后的 Target1/xx.lproj 下只有 InfoPlist.strings。然后添加到 Xcode 项目里,打开 Xcode - Views - Utilities (Command+Option+0),在 Target Membership 下针对不同的 Target 把对应文件夹下的 InfoPlist.strings 对应连接起来,Done。

最大化 Xcode Debug Console 窗口

参考 How to get back the Console window in XCode4 做了一点点改动,Run 的时候自动切换到 Console Tab 并且是最大化展示,效果还不错。

  1. 打开 Tab 支持,View - Show Tab Bar.
  2. 双击或点 + 添加一个 Tab.
  3. 双击新加的 Tab 改名,比如 CONSOLE.
  4. 激活 Console 显示,View - Debug Aera - Activate Console,或者直接 Command+Shift+C.
  5. 拖动 Console 区至顶端,整个 Tab 只显示这个 Console.
  6. Command+, 进入 Preferences - Behaviors, 在 Run Start 里勾选 Show Tab,填 CONSOLE,就是刚才的 Tab 名。
  7. Done。

再运行项目时会自动的切换到新 Tab 页查看输出结果,然后通过 Command+Shift+[/] 来切换 Tab.

Enable SPDY in Firefox 11

SPDY is a new network protocol developed by Google for faster web. Google Chrome has a build-in support of SPDY long long ago. Now Firefox brings SPDY support in Firefox 11, but is disabled by default. You can enable it as follow:

Open about:config in Firefox, search network.http.spdy.enabled and set to true. via here

Git-flow 使用笔记

git-flow 原理:A successful Git branching model,两篇不错的中文翻译: Git开发管理之道一个成功的Git分支模型

简单来说,git-flow 就是在 git branch git tag基础上封装出来的代码分支管理模型,把实际开发模拟成 master develop feature release hotfix support 几种场景,其中 master 对应发布上线,develop 对应开发,其他几个在不同的情况下出现。通过封装,git-flow 屏蔽了 git branch 等相对来说比较复杂生硬的命令(git branch 还是比较复杂的,尤其是在多分支情况下),简单而且规范的解决了代码分支管理问题。

安装 git-flow:

brew install git-flow

在一个全新目录下构建 git-flow 模型:

➜ git flow init
Initialized empty Git repository in /Users/fannheyward/flowTest/.git/
No branches exist yet. Base branches must be created now.
Branch name for production releases: [master]
Branch name for "next release" development: [develop]

How to name your supporting branch prefixes?
Feature branches? [feature/]
Release branches? [release/]
Hotfix branches? [hotfix/]
Support branches? [support/]
Version tag prefix? []

或者在现有的版本库构建:

➜ git flow init
Which branch should be used for bringing forth production releases?
   - master
Branch name for production releases: [master]
Branch name for "next release" development: [develop]

How to name your supporting branch prefixes?
Feature branches? [feature/]
Release branches? [release/]
Hotfix branches? [hotfix/]
Support branches? [support/]
Version tag prefix? []

中间会询问生成的分支名,直接回车默认。这样一个 git-flow 分支模型就初始化完成。

使用场景一:新功能开发,代号 f1

➜ git flow feature start f1
Switched to a new branch 'feature/f1'

Summary of actions:
- A new branch 'feature/f1' was created, based on 'develop'
- You are now on branch 'feature/f1'

Now, start committing on your feature. When done, use:

     git flow feature finish f1

git-flow 从 develop 分支创建了一个新的分支 feature/f1,并自动切换到这个分支下面。然后就可以进行 f1 功能开发,中间可以多次的 commit 操作。当功能完成后:

➜ git flow feature finish f1
Switched to branch 'develop'
Already up-to-date.
Deleted branch feature/f1 (was 7bb5749).

Summary of actions:
- The feature branch 'feature/f1' was merged into 'develop'
- Feature branch 'feature/f1' has been removed
- You are now on branch 'develop'

feature/f1 分支的代码会被合并到 develop 里面,然后删除该分支,切换回 develop. 到此,新功能开发这个场景完毕。在 f1 功能开发中,如果 f1 未完成,同时功能 f2 要开始进行,也是可以的。


使用场景二:发布上线,代号 0.1

➜ git flow release start 0.1
Switched to a new branch 'release/0.1'

Summary of actions:
- A new branch 'release/0.1' was created, based on 'develop'
- You are now on branch 'release/0.1'

Follow-up actions:
- Bump the version number now!
- Start committing last-minute fixes in preparing your release
- When done, run:

     git flow release finish '0.1'

git-flow 从 develop 分支创建一个新的分支 release/0.1,并切换到该分支下,接下来要做的就是修改版本号等发布操作。完成后:

➜ git flow release finish 0.1
Switched to branch 'master'
Merge made by the 'recursive' strategy.
 f1      |    1 +
 version |    1 +
 2 files changed, 2 insertions(+)
 create mode 100644 f1
 create mode 100644 version
Switched to branch 'develop'
Merge made by the 'recursive' strategy.
 version |    1 +
 1 file changed, 1 insertion(+)
 create mode 100644 version
Deleted branch release/0.1 (was d77df80).

Summary of actions:
- Latest objects have been fetched from 'origin'
- Release branch has been merged into 'master'
- The release was tagged '0.1'
- Release branch has been back-merged into 'develop'
- Release branch 'release/0.1' has been deleted

git-flow 会依次切换到 master develop 下合并 release/0.1 里的修改,然后用 git tag 的给当次发布打上 tag 0.1,可以通过 git tag 查看所有 tag:

➜ git:(master) git tag
0.1
0.2

使用场景三:紧急 bug 修正,代号 bug1

➜ git flow hotfix start bug1
Switched to a new branch 'hotfix/bug1'

Summary of actions:
- A new branch 'hotfix/bug1' was created, based on 'master'
- You are now on branch 'hotfix/bug1'

Follow-up actions:
- Bump the version number now!
- Start committing your hot fixes
- When done, run:

     git flow hotfix finish 'bug1'

git-flow 从 master 分支创建一个新的分支 hotfix/bug1,并切换到该分支下。接下来要做的就是修复 bug,完成后:

➜ git flow hotfix finish bug1
Switched to branch 'master'
Merge made by the 'recursive' strategy.
 f1 |    2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Switched to branch 'develop'
Merge made by the 'recursive' strategy.
 f1 |    2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Deleted branch hotfix/bug1 (was aa3ca2e).

Summary of actions:
- Latest objects have been fetched from 'origin'
- Hotfix branch has been merged into 'master'
- The hotfix was tagged 'bug1'
- Hotfix branch has been back-merged into 'develop'
- Hotfix branch 'hotfix/bug1' has been deleted

git-flow 会依次切换到 master develop 分支下合并 hotfix/bug1,然后删掉 hotfix/bug1。到此,hotfix 完成。

git-flow 的 feature release 都是从 develop 分支创建,hotfix support 都是从 master 分支创建。

Key Value Coding and Key Value Observing Notes

KVO(Key Value Obersving) 的基础是 KVC(Key Value Coding),现在我对 KVC 的理解还非常粗浅,对 KVO 只是使用阶段,下面这些是我的一些笔记,可能会有一些误差,后续有更多理解后会持续更新。

KVC

KVC 的全称是 NSKeyValueCoding,文档:

The NSKeyValueCoding informal protocol defines a mechanism by which you can access the properties of an object indirectly by name (or key), rather than directly through invocation of an accessor method or as instance variables.

KVC 是一种通过 name 或 key 间接访问对象属性(property)的机制。用 setValue:forKey: 设置 key 所指定属性的值,valueForKey: 对应来取值。

Code:

@interface Person : NSObject

@property (nonatomic, retain) NSString *name;
@property (nonatomic) int age;

@end

@implementation Person

@synthesize name;
@synthesize age;

@end

int main(int argc, char *argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.name = @"fannheyward";
        // equal to
        //[person setName:@"fannheyward"];
        // equal to
        //[person setValue:@"fannheyward" forKey:@"name"];

        person.age = 24;
        // equal to
        //[person setAge:24];
        // equal to
        //[person setValue:[NSNumber numberWithInt:24] forKey:@"age"];

        NSLog(@"name:%@, age:%d", person.name, person.age);
        NSLog(@"name:%@, age:%d", [person valueForKey:@"name"], [[person valueForKey:@"age"] intValue]);

        [person release];
    }
}

KVO

KVO 的全称是 NSKeyValueObserving,文档:

The NSKeyValueObserving (KVO) informal protocol defines a mechanism that allows objects to be notified of changes to the specified properties of other objects.

简单的说 KVO 提供了一个观察者机制,当被观察的对象属性变化时自动通知相应的观察者对象。KVO 就是通过 KVC 的 setValue:forKeyvalueForKey: 来监察属性变化。KVO 的使用分为三个步骤,注册观察者,实现变化回调方法,取消观察者。

注册观察者:

- (void)addObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context

其中 keyPath 就是要观察的属性值,options 是属性变化的选择,context 可以用来传递额外的数据等。

实现变化回调方法:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

keyPath 注册里的相对应,用来区分不同的被观察属性,change 包含了变化前后的数据。

取消观察者:

- (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath

一般放在 - (void)dealloc 方法里面。

Demo

Demo:一个列表 listTableView,数据存储在 NSMutableArray *cellArray 里面,cellArray 的数据发生变化时候刷新列表展示,比如滑动到 table 下部时自动后台 load 更多数据然后更新列表。

添加一个 @property (nonatomic) NSInteger cellCount 作为被观察者,为啥不直接用 cellArray作为被观察者?因为 NSArray 不能被注册为观察者,参考 NSArray(NSKeyValueObserverRegistration) in NSKeyValueObserving.h

NSArrays are not observable, so these methods raise exceptions when invoked on NSArrays. Instead of observing an array, observe the ordered to-many relationship for which the array is the collection of related objects.

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self addObserver:self forKeyPath:@"cellCount" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)loadCellArrayInBackground
{
    // ...
    self.cellCount = [cellArray count]; //change cellCount value.
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"cellCount"]) {
        // check change if nessary.
        [listTableView reloadData];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)dealloc
{
    [self removeObserver:self forKeyPath:@"cellCount"];

    [super dealloc];
}

以上就是一个简单的 KVO 实现。


PS. NSArray 也有方法注册观察者,[[self mutableArrayValueForKey:@"cellArray"] addObject:@"test"]; 就可以被观察到 cellArray 变化。mutableArrayValueForKey 返回的不是 cellArray 本身而是一个 proxy,而 proxy 是支持 KVO 的。不过这种方法感觉有点浪费,没必要每添加、删除一个数据就刷新列表,尤其大部分时候我们都是批量更新数据源,用 cellCount 这种方式反而会更好一点。参考 Key Value Observing NSArray and NSDictionary


PPS. 其实 KVO 的实现是比较笨重的,比如注册时候没有办法指定一个响应 selector,都需要在回调实现里面根据 keyPath 来区分不同的被观察者。Key-Value Observing Done Right 分析了 KVO 的一些缺点并给出了解决方案: MAKVONotificationCenter,找个机会在项目中实际运用一下。

Track iOS Device Model with Google Analytics Custom Variables

Add Google Analytics SDK for iOS to app project first.

#import <sys/utsname.h>

- (NSString*)deviceModel {
    struct utsname systemInfo;
    uname(&systemInfo);

    return [NSString stringWithCString:systemInfo.machine
                              encoding:NSUTF8StringEncoding];
}

NSString *model = [self deviceModel];
// model = @"iPhone3,1", as iPhone 4.
[[GANTracker sharedTracker] setCustomVariableAtIndex:1 //range from 1-5, not be re-used.
                                                name:@"DeviceModel"
                                               value:model
                                               scope:kGANSessionScope
                                           withError:NULL];

NSString *sysVersion = [[UIDevice currentDevice] systemVersion];
// sysVersion = @"5.0.1"
[[GANTracker sharedTracker] setCustomVariableAtIndex:2
                                                name:@"SystemVersion"
                                               value:sysVersion
                                               scope:kGANSessionScope
                                           withError:NULL];

Then, you can get the report on Google Analytics Audience / Demographics / Custom Variables.