姚翔的部落格

Core Data 中‘一对一’到‘一对多’关系的轻量级迁移

| Comments

这次分享一个关于 Core Data 数据迁移的小经验。工作中遇到需求,要将原有数据结构中的一个 To-One 关系改成 To-Many(比方说把一个 Book 对应一个 Author 的关系改成对应多个 Authors)。根据 Core Data 的文档:

Changing a relationship from a to-one to a to-many, or a non-ordered to-many to ordered (and visa-versa)

轻量级迁移(Lightweight Migration)是完全可以做到的。于是立刻添加了新的 Model Version,然后修改了对应的 relationship,同时也把对应 Managed Object 中的 property 手动改成 NSSet,以及添加了 add 和 remove 的方法(当然也可以用 Xcode 去自动生成新的类文件)。在调整了对应的逻辑代码后运行应用,发现迁移是成功的,数据没有丢失,但是原先那个一对一的关系数据没有迁移到新的一对多关系里(也就是所有 Book 和 Author 的对应关系消失了)。

看这个现象,数据是没有丢失的,所以应该是原来的对应关系没有顺利转化到新的这个多对多关系表中。回想整个迁移过程,我意识到一点,就是我把 Book 中原来的 author 属性改名成了 authors,这在逻辑上讲是理所当然的,但程序可没那么智能,所以应该是这里出了问题。

解决方法:在Xcode中选择 authors 这个关系,然后在右边的 Inspector 中找到 “Renaming ID” 这个选项,在里面填上 “author",这样程序才能知道这个属性是从原来的 author 属性改名而来,数据当然也能成功迁移了。果然再次把应用回退到老版本后安装新版本,对应关系顺利地展示了出来。

Content Hugging vs. Compression Resistance

| Comments

iOS Auto Layout中的Content Hugging(内容吸附)和Compression Resistance(压缩阻力)曾一度让我非常困惑,看过一些说明的文章后当时觉得豁然开朗,但一段时间后便又忘记了,所以我决定再一次全面地整理一下并用博客的形式分享出去,这样可以帮助我自己加强记忆。 (注:“内容吸附”和“压缩阻力”是特别借用了objccn里文章的翻译内容,非常感谢作者,原文链接)

首先,我们先简单说明一下约束(Constraint)中的Priority概念:Auto Layout中,每个约束都有一个1-1000的priority值,其中1000是其默认的值,也是 NSLayoutPriorityRequired 的,意思就是这个约束必须完全得到满足;所有小于1000的值则都是非强制的。系统在实现自动布局时,首先会满足所有priority是1000的约束,然后按照从大到小的值依次去尽量满足非强制性的约束,这种情况下,系统会尽量使结果的值接近于约束要求的值。

现在让我们来具体说明一下Hugging和Resistance到底是什么,用通俗易懂的话来描述的话是这样的:

  • Content Hugging:我的内容区域不想被扩展(变高或变宽)
  • Compression Resistance:我的内容区域不想被压缩 (变矮或变短)

注意到了吧,描述里面都有“内容区域”这个概念,这个指的就是Intrinsic Content Size,所以压缩阻力和内容吸附只对定义了Intrinsic Content Size的UI元素有效,否则的话就不存在所谓的“我的内容区域”的概念了。

下面就以一个具体的例子来说明一下这两个属性的作用。假设我们有一个Button放置在界面中,同时设置了两个priority是500的约束:左右各距离父元素 30 points,即如下图所示:

|-30-[ Button ]-30-|

此时,如果我们给它设置一个水平priority是750的 Content Hugging 属性,那我们将看到它的实际布局会变成这样:

|—-60—[Button]—60—-|

但是如果这个 Content Hugging 属性的priority是小于500的,那我们看到的效果和初始状态是一样的:

|-30-[ Button ]-30-|

然后,因为某些原因,它的父元素的宽度变窄了,在没有任何 Compression Resistance 属性时,它就变成了这样(按钮的文字显示不全了):

|-30-[But..]-30-|

此时,如果我们给它设置一个水平priority是750的 Compression Resistance 属性,结果就会是:

|-25-[Button]-25-|

同样的,如果这个属性的优先级小于500,那结果就没有变化。另外,如果我们把初始的两个左右边距的约束优先级调整到1000,那不管我们怎么设置压缩阻力或内容吸附的属性,都不会改变布局效果,这个按钮会始终保持左右边距30的布局。

iOS的UI并发处理方案

| Comments

让用户体验更流畅是大部分应用必须要考虑的方面,我们知道iOS应用中所有UI相关的更新操作必须在主线程中进行,所以一个提升应用流畅性的大原则便是:尽量把那些耗时多,但不与UI更新直接相关的工作移出主线程。以下我将用一个比较常见的TableView Cell更新的例子来展示一下处理UI并发的几个最佳实践。我们的目标是让应用能在处理大量运算的同时,又能及时响应用户的交互以及界面上的事件。

假设我们有一个列表界面,每一行需要显示一个店铺的信息,通常的方案便是Subclass一个TableViewCell,然后提供自定义的方法接收店铺信息数据,并更新到UI上,大致代码如下:

1
2
3
4
5
6
7
8
9
10
# StoreInfoCell : UITableViewCell

@implementation StoreInfoCell

- (void)displayStoreInfo:(StoreInfo *)storeInfo {
  self.titleLabel.text = storeInfo.title;
  self.addressLabel.text = storeInfo.address;
}

@end

需求变化是软件开发的常态,不久PM便会说,在原有的店铺信息中,现在又增加了这个店铺过去七天的客流数据,于是要求应用能在Cell中多显示一个柱状图来表示这个客流数据。我们先来实现这个功能:

1
2
3
4
5
- (void)displayStoreInfo:(StoreInfo *)storeInfo {
  self.titleLabel.text = storeInfo.title;
  self.addressLabel.text = storeInfo.address;
  self.histogramImageView.image = [HistogramTool generateImageForTrafficData:storeInfo.trafficData];
}

功能已经实现,但是因为HistogramTool这个工具去生成柱状图耗时相当久,我们在运行应用测试时会发现滚动列表界面时有严重的卡顿。接下来我们就来进行优化,显而易见是generateImageForTrafficData这个方法阻塞了主线程从而造成卡顿,我们可以把它移到其他线程中去处理,这里我们采用GCD来实现这个方案:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)displayStoreInfo:(StoreInfo *)storeInfo {
  self.titleLabel.text = storeInfo.title;
  self.addressLabel.text = storeInfo.address;
  self.histogramImageView.image = nil;

  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      UIImage *histogramImage = [HistogramTool generateImageForTrafficData:storeInfo.trafficData];
      dispatch_async(dispatch_get_main_queue(), ^{
          self.histogramImageView.image = histogramImage;
      });
  });
}

经过上面的改进后,我们将看到页面的滚动变得流畅了,但是会发现页面有闪烁的现象,Cell上的柱状图会出现自我切换的问题,同时数据似乎会不匹配,这是为什么呢?原因就在于Reuse Cell,为了提升性能,所有TableView中的Cell都是从它的Reuse Pool中获取来的,所以当快速滚动列表时,新出现的Cell实例其实是一些已经移出界面可视范围被回收了的老Cell实例,但它们被回收时并没有取消异步生成柱状图的过程,所以当它们生成完后会又更新到界面上,这就会造成第7行的Cell显示出了第1行店铺的柱状图,然后过了一会儿又更新成了第7行店铺自己的柱状图。为了解决信息不匹配的问题,我们可以在更新前做一个check:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)displayStoreInfo:(StoreInfo *)storeInfo {
  self.titleLabel.text = storeInfo.title;
  self.addressLabel.text = storeInfo.address;
  self.histogramImageView.image = nil;

  self.trafficData = storeInfo.trafficData;
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      TrafficData *data = storeInfo.trafficData;
      UIImage *histogramImage = [HistogramTool generateImageForTrafficData:data];
      dispatch_async(dispatch_get_main_queue(), ^{
          if (data.id == self.trafficData.id) {
              self.histogramImageView.image = histogramImage;
          }
      });
  });
}

这样虽然解决了信息不匹配的问题,但却还是消耗了很多不必要的计算资源,因为当cell被移出屏幕后,对应cell的柱状图计算过程仍然还在queue中,我们应该取消它们,于是我们引入NSOperationQueue来进一步优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (void)displayStoreInfo:(StoreInfo *)storeInfo {
  self.titleLabel.text = storeInfo.title;
  self.addressLabel.text = storeInfo.address;
  self.histogramImageView.image = nil;

  self.trafficData = storeInfo.trafficData;
  [self.queue cancelAllOperations];
  [self asyncDisplayHistogramImage];
}

- (void)asyncDisplayHistogramImage {
  NSBlockOperation *operation = [NSBlockOperation new];
  __weak NSBlockOperation *weakOperation = operation;
  [operation addExecutionBlock:^{
    if ([weakOperation isCancelled]) {
      return;
    }
    TrafficData *data = storeInfo.trafficData;
    UIImage *histogramImage = [HistogramTool generateImageForTrafficData:data];
    if (![weakOperation isCancelled] && data.id == self.trafficData.id) {
      dispatch_async(dispatch_get_main_queue(), ^{
        self.histogramImageView.image = histogramImage;
      });
    }
  }];
  [self.queue addOperation:operation];
}

好了,现在我们有了取消计算的机制,但是如果用Time Profiler工具去查看应用运行情况时会发现,当快速滚动列表时,还是有一些时间浪费在不必要的计算上,这是因为在调用generateImageForTrafficData这个方法前并没有等待时间,当一个cell被显示在页面上时,它便已经开始了这个计算过程,而因为快速滚动的缘故它又被移出了界面,所以这个计算过程其实是浪费了的。所以我们可以加上一些等待时间来继续优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- (void)displayStoreInfo:(StoreInfo *)storeInfo {
  self.titleLabel.text = storeInfo.title;
  self.addressLabel.text = storeInfo.address;
  self.histogramImageView.image = nil;

  self.trafficData = storeInfo.trafficData;
  [self cancelPreviousOperations];
  [self performSelector:@selector(asyncDisplayHistogramImage) withObject:nil afterDelay:0.3];
}

- (void)asyncDisplayHistogramImage {
  NSBlockOperation *operation = [NSBlockOperation new];
  __weak NSBlockOperation *weakOperation = operation;
  [operation addExecutionBlock:^{
    if ([weakOperation isCancelled]) {
      return;
    }
    TrafficData *data = storeInfo.trafficData;
    UIImage *histogramImage = [HistogramTool generateImageForTrafficData:data];
    if (![weakOperation isCancelled] && data.id == self.trafficData.id) {
      dispatch_async(dispatch_get_main_queue(), ^{
        self.histogramImageView.image = histogramImage;
      });
    }
  }];
  [self.queue addOperation:operation];
}

- (void)cancelPreviousOperations {
  [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(asyncDisplayHistogramImage) object:nil];
  [self.queue cancelAllOperations];
}

// 然后在对于的TableView Delegate中也加上取消操作

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath {
  [cell cancelPreviousOperations];
}

通过以上一些方法,你将体会到流畅性上很大的提升,当然你还可以考虑加入image cache来再进一步优化。总之这里就是展示了一些UI并发处理的思路,希望抛砖引玉让大家打造出性能卓越的应用。

自定义Navigation Bar样式

| Comments

在开发iOS应用时,自定义Navigation Bar样式是非常普遍的一个需求,所以这里特别整理了一些常见的情况,以供快速查阅。照旧先附上参考的原文链接

> 更改导航栏背景色

这个应该是最常见的需求了,同时也只需一行代码就可以实现:

1
[[UINavigationBar appearance] setBarTintColor:[UIColor yellowColor]];

> 更改导航栏标题的样式

有时我们需要修改标题成特殊的字体样式以达到设计上得效果,可以通过设置titleTextAttributes的属性来实现,以下是一些常用的key,同时附上实现代码:

  • UITextAttributeFont – Key to the font
  • UITextAttributeTextColor – Key to the text color
  • UITextAttributeTextShadowColor – Key to the text shadow color
  • UITextAttributeTextShadowOffset – Key to the offset used for the text shadow
1
2
3
4
5
6
7
8
NSShadow *shadow = [NSShadow new];
shadow.shadowColor = [UIColor grayColor];
shadow.shadowOffset = CGSizeMake(0, 1);
[[UINavigationBar appearance] setTitleTextAttributes: @{
  NSForegroundColorAttributeName : [UIColor whiteColor],
  NSShadowAttributeName : shadow,
  NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue" size:21.0]
}];

有些设计会要求使用logo或其他更复杂的内容代替文字显示在标题位置,这个时候我们可以通过设置navigationItem的titleView来实现这些需求,这里以简单地替换成图片为例子:

1
2
// 这里的viewController就是对应的要替换标题的那个view controller,而不是navigation controller本身
viewController.navigationItem.titleView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"title"]];

> 修改导航栏上按钮的颜色

我们可以通过设置tintColor来实现,要注意的是,这个颜色会同时影响到所有按钮上的文字和图片:

1
[[UINavigationBar appearance] setTintColor:[UIColor whiteColor]];

> 添加更多的导航栏按钮

这种情况下,我们可以通过设置leftBarButtonItems和rightBarButtonItems来实现,直接给出一段实例代码,大家一看就懂:

1
2
3
UIBarButtonItem *markItem = [UIBarButtonItem barItemWithImage:[UIImage imageNamed:@"mark"] selectedImage:nil size:CGSizeMake(40, 40) target:self action:@selector(markButtonClicked:)];
UIBarButtonItem *starItem = [UIBarButtonItem barItemWithImage:[UIImage imageNamed:@"star"] selectedImage:[UIImage imageNamed:@"star_selected"] size:CGSizeMake(30, 40) target:self action:@selector(starButtonClicked:)];
self.navigationItem.rightBarButtonItems = @[starItem, markItem];

Vim无法打开Swap文件的错误

| Comments

事件之始是本人的老Mac加装SSD硬盘后在新系统中的一次git commit,提交失败,原因是用vim写comment的时候保存不成功,在提示信息中有如下内容:

1
Unable to open swap file for "{filename}", recovery impossible

因为我是在装完新系统后,把老硬盘里的配置文件都拷到新硬盘了,所以在我的.vimrc文件中确实是单独设置了swap的目录:set directory=~/.vim/tmp,于是首先怀疑是对应的目录不存在,如果是这种情况,可以使用mkdir -p ~/.vim/tmp这个命令去创建目录,但我的情况是这个目录已经存在。经过ls命令观察该目录内容,突然发现它的所有者是root用户的,恍然大悟,只要把它换成当前使用的用户名就可以了,于是用以下命令便可解决问题:

1
sudo chown -Rv username .vim

Mac上安装Ruby on Rails

| Comments

Mac加了SSD硬盘,重装了系统,所以需要重新安装Rails,找到一篇文章一步步照着来做,顺带记录下步骤分享给需要的人。本人Mac系统版本10.10.3。(原文链接)

  • 安装Homebrew

Homebrew能帮我们更便捷地从源代码编译安装其他软件,在Terminal里运行以下命令:

1
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  • 安装Ruby

原先是用rvm来安装ruby的,这里用的是rbenv,据同事说这个比rvm更好用,总之照着下面的命令来吧:

1
2
3
4
5
6
7
8
9
10
brew install rbenv ruby-build

# Add rbenv to bash so that it loads every time you open a terminal
echo 'if which rbenv > /dev/null; then eval "$(rbenv init -)"; fi' >> ~/.bash_profile
source ~/.bash_profile

# Install Ruby
rbenv install 2.2.1
rbenv global 2.2.1
ruby -v
  • 安装Rails

Terminal里运行以下命令:

1
2
3
4
gem install rails -v 4.2.0

#在这个过程中本人遇到安装失败,提示运行以下命令
xcode-select --install

装完rails,为了能让rails成为可执行命令,还需要运行以下命令:

1
rbenv rehash

OK,大功告成,最后分享一条Tip: 平时国内网络下安装gem包总是会连接不上,淘宝提供了mirror,所以可以用下面的命令把地址转到淘宝的链接:

1
bundle config mirror.https://rubygems.org http://ruby.taobao.org

iOS的毛玻璃效果View

| Comments

在iOS升级到7时,巨大的UI风格改变给我们带来了全新的视觉感受,而那个独特的Navigation Bar毛玻璃(模糊背景)效果更是让很多人赞叹不已。现在iOS 8中Apple已经添加了UIKit层对这个Blur Effect的原生支持,我是以WWDC 2014中某一个视频为契机找到了相关的内容,特此整理和记录下。我只是简单地做个Note,这里有个较完整的说明,想更深入了解的朋友请自行Google,关键词:UIBlurEffect、UIVibrancyEffect、UIVisualEffectView。

在最新的XCode里,IB中的Object Libary已经默认自带Visual Effect View的控件,可以直接拖拽使用,非常方便。如果想用代码来添加的话,大致是这样的:

1
2
3
4
5
6
  UIVisualEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
  UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
  effectView.contentView.backgroundColor = [UIColor colorWithWhite:0.7 alpha:0.3];

  [self.blurView addSubview:effectView];
  effectView.frame = self.blurView.bounds;

可以发现,就算用代码也非常简便,和使用其他UI元素一样,创建并加入到view的tree中(当然也可以用autolayout来控制位置)。附赠一个小贴士,经过测试,如果加入了visual effect view后,再去修改其父view(上面代码中的self.blurView)的alpha值时,其模糊效果就会失效了,具体底层原因是什么还没深究过,这只是我个人的实践结果。