最终效果

先上图:

这个TableView的Section Header有什么特殊之处呢?
首先,它是半透明的,其次,当Cell滑动到Header下方的时候Cell的内容并不会因为Header是透明的而显示在Header的下方。
正常情况下应该是长这样的:

可以看见,的确有点丑,但是实现第一张图中的效果其实说难不难说简单也没那么简单,关键是要利用好一个东西:layer.mask

mask即遮罩,如果对一个UIViewlayer设置了mask,所达到的效果就是UIView只显示遮罩非透明部分所覆盖的那部分的内容,其他的地方都会被遮挡。利用mask可以做很多很多炫酷的动画,比如这个动画,不过一样,我们也可以使用mask来实现图1中的效果。

说说思路

假设Header的高度是20px,图中情况需要做特殊处理的Header一定位于UITableView的最顶部,我们所需要做的就是,根据当前UITableViewcontentOffSet以及Header的高度,计算出对应的Cell需要隐藏的部分,以此为依据为Cell创建一个大小与其一样大小的mask,其中需要隐藏的那部分设为透明,剩下的设为纯色,这个操作可以在scrollViewDidScroll的回调方法中进行

直接亮代码

1
2
3
4
5
6
7
8
9
10
11
12
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
UITableView* tableView = (UITableView*)scrollView;
//遍历TableView中所有显示的cell
for (UITableViewCell *cell in tableView.visibleCells) {
//计算需要隐藏的高度:当前scrollView的contentOffset的y坐标+Header的高度-cell的y坐标
CGFloat hiddenFrameHeight = scrollView.contentOffset.y + HEADERHEIGHT - cell.frame.origin.y;
if (hiddenFrameHeight >= 0 && hiddenFrameHeight <= cell.frame.size.height) {
//该方法用于对cell设置mask
[cell maskCellFromTop:hiddenFrameHeight];
}
}
}

下面就是cell的操作了,这里因为跟cell的具体内容无关,可以使用category直接写扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)maskCellFromTop:(CGFloat)margin {
if (margin > 0) {
self.layer.mask = [self visibilityMaskWithLocation:margin/self.frame.size.height];
self.layer.masksToBounds = YES;
} else {
self.layer.mask = nil;
}
}

- (CAGradientLayer *)visibilityMaskWithLocation:(CGFloat)location {
CAGradientLayer *mask = [CAGradientLayer layer];
mask.frame = self.bounds;
//上半部分是透明,下半部分非透明
mask.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:1 alpha:0] CGColor], (id)[[UIColor colorWithWhite:1 alpha:1] CGColor], nil];
mask.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:location], [NSNumber numberWithFloat:location], nil];
return mask;
}

贴效果图

另外还有一些细节
1.分割线不属于cell,所以要实现这个效果,分割线需要直接在cell里加而不是使用系统自带的
2.在cell初始化或者复用的时候记得重置mask的状态

最后我把项目推到了github上,大家可以直接去看下源码https://github.com/luckymore0520/LMTableViewHeaderMask

关于iOS项目的国际化,之前有写过一篇文章,不过不是很系统,也有不少纰漏,还特地写过一篇填坑文,但是因为太坑了,所以不了了事。这段时间又踩了不少坑,也啃了啃官方的文档,特此整理10条Tip,有一些问题不会经常遇到,有些细节也很容易被忽略,不过大部分应该还是很有用的!
如果你之前对iOS的国际化一无所知,推荐看一下这篇文章,很精炼也很完整——《Internationalization Tutorial for iOS [2014 Edition]》

阅读全文 »

先打广告

在@Cee菊苣的安利下报了思客的React Native训练营,从此入了React Native的坑。 亲测思客的课程、教学方式还有学习氛围都是很良心的~ 所以必须先安利一发。

个人背景

本人iOS算是入门,前端水平约等于小白,只是很早之前接触过一些html和css,所以这个入坑指南当然是从前端小白开始讲起的。算是边学习边记录啦~

一些声明

思客的教程是有偿的,所以这里肯定不会照搬教程原文,我只会放一些自己的总结,和一些踩过的坑,如果真心想好好学,建议去捧场 23333

OK 进入正题

补坑

假设你是前端小白
html和css的基础可以去w3school上看,当然上面也有一些javascript的教程,不过看javascript,感觉还是廖雪峰的官方网站上面的讲的更全一点,其实作为一门编程语言,只要有一点编程基础的学习起来都不会太困难,js里需要注意的主要是闭包、匿名函数、作用域这块地方的坑比较难踩。

不过学习React Native更重要的当然是学好javascript了,这个坑一定要踩稳

假设你是iOS小白
想要使用React Native开发iOS应用,一点iOS都不会肯定是不行的。我建议去iTunes U上面去看一下斯坦福的iOS视频教程,入门还是很不错的!!

假设你从来没有用过Git
这真是一件很悲伤的事情,快去Google一下Git的基本原理以及命令,如果不习惯用命令行,可以使用SourceTree,一个非常强大的Git图形工具。

后记

之后我会定期更新下学习进度,写这文的时候其实已经开课一天了,第一周暖身,主要是为没有前端基础的前端小白(比如说我)补补课,学习一些前端常用工具和组件的使用,以及一些基本原理,这对于我来说真是件好事,而第一天的教程也让我收获很大(毕竟小白),晚点就把新的放上来。

关于 Co!orMix

前几天, Cee 童鞋把 Co!orMix 开源了。当时刚上架的时候,团队里的另外两个童鞋都发了文,@Cee 讲产品,@Albus 讲设计(这两篇文章都在 V2EX 上发布过)。

既然代码开源了,那是时候说一下项目本身了。

Co!orMix 里面并没有什么特别有技术含量的东西,可以说,任何一个人都可以毫不费力地把它做出来。对于新手来说,也值得一试。不过毫不谦虚地说,如果要达到零错误率,还是有一定难度的,而上线至今,它的 Crash 率一直保持在 0 ,这是我所引以为豪的。(欢迎大家试玩!偷偷说一句,我可是跟组里承诺过,只要出现一个 crash ,就请吃饭的哦!)

阅读全文 »

写在最前面

其实这篇文章对于99%的开发者来说,真的真的不会有什么特别大的用处。比如我问一个问题,在AutoLayout中,leading和left的区别是什么?trailing和right的区别是什么?基本所有关于AutoLayout的教程都会一笔带过,或者说,绝大多数情况下两者没有什么区别,所以可以不用去管它了。
所以,两者到底有什么区别呢? leading和trailing会适配整个界面的布局方向,而left和right不会。何谓布局方向? 把你的设备语言切成阿拉伯语你就知道了。你会发现设置界面的Cell都倒过来了,如果你的应用支持阿拉伯语,你会发现你的所有界面都在X轴上被镜像了,简单的说,如果你的应用支持了阿拉伯语,那么在阿拉伯语言环境下,iOS除了会替换你的语言包外,还会把所有界面的布局变成从右往左。
如果你用Xib做布局,如果约束是leading,那么在从右往左的布局中,组件则会跑到右边,如果约束是left,那它就会始终留在左边。这就是leading和left的区别。

所以大家知道为什么这篇文章对于99%的开发者来说并没有什么用,国内有哪款应用会特地去针对阿拉伯语做国际化?(除了我司的应用。)

对于类似阿拉伯语这种从右到左布局的情况,遇到的坑数不胜数,数不胜数。
不过想到写了也没用,我就没动力写下去了。
先放在这,哪天突然想写了再补坑吧。

但是我需要强调一点的是,国际化这件事不单单是一个Localization.strings的问题!!!!

问题的触发点

前段时间看到了一个用swift写的下拉刷新的动画PullToBounce
https://github.com/entotsu/PullToBounce
觉得很酷炫,就想用oc来实现一遍,期间踩了不少坑,也算是发现了swift相对于Objective-C的一些方便之处,其中有一段swift代码是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
self.waveLayer.path = self.wavePath(amountX: 0, amountY: 0)
var bounce = CAKeyframeAnimation(keyPath: "path")
bounce.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
var values = [
self.wavePath(amountX: positionX, amountY: positionY),
self.wavePath(amountX: -(positionX * 0.7), amountY: -(positionY * 0.7)),
self.wavePath(amountX: positionX * 0.4, amountY: positionY * 0.4),
self.wavePath(amountX: -(positionX * 0.3), amountY: -(positionY * 0.3)),
self.wavePath(amountX: positionX * 0.15, amountY: positionY * 0.15),
self.wavePath(amountX: 0.0, amountY: 0.0)
]
bounce.values = values
bounce.duration = bounceDuration
bounce.removedOnCompletion = true
bounce.fillMode = kCAFillModeForwards
bounce.delegate = self
self.waveLayer.addAnimation(bounce, forKey: "return")

熟悉Core Animation的同学应该很容易看懂这段代码,waveLayer是一个CAShapeLayer,waveLayer.path是一个CGPathRef,wavePath方法返回的是一个CGPathRef,这段代码实现的是一个关键帧弹性动画,动画的KeyPath是”path”,在swift中,数组可以存放任意类型,当然也包括CF指针,然而在Objective-C中,NSArray只能存放NSObject,也就是说没有办法在数组中加入CGPathRef对象,那么这个动画该怎么实现呢?

后来,我在https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/8-%E6%98%BE%E5%BC%8F%E5%8A%A8%E7%94%BB/%E6%98%BE%E5%BC%8F%E5%8A%A8%E7%94%BB.md中找到了一段类似的代码,顿时恍然大悟。

1
- (IBAction)changeColor {
    //create a keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.duration = 2.0;
    animation.values = @[
                         (__bridge id)[UIColor blueColor].CGColor,
                         (__bridge id)[UIColor redColor].CGColor,
                         (__bridge id)[UIColor greenColor].CGColor,
                         (__bridge id)[UIColor blueColor].CGColor ];
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
}

刨根问底

Core Foundation

Core Foundation 是由C语言来实现的,而不是Objective-C,它提供了一些很基础的功能,例如CFString,CFNumber等。

以CFString为例,在OC中所对应的就是NSString
我们可以通过NSStringFromClass()方法来打印一下NSString的实质

1
NSLog(NSStringFromClass([@"This is An NSString" class]);

结果是__NSCFConstantString

NSString其实是一个类簇,所谓类簇,即:
将一些私有的、具体的子类组合在一个公共的、抽象的超类下面,以这种方法来组织类可以简化一个面向对象框架的公开架构,而又不减少功能的丰富性。
简单来说就是抽象的接口,String Objects不是NSString的实力,而是实现了NSString方法的私有类的实力,实质上就是CFString

相互转换

Objective-C和Core Foundation对象之间可以很轻松的转换,拿NSString和CFString为例

1
CFStringRef aCFString = (CFStringRef)aNSString;
NSString *aNSString = (NSString *)aCFString;

然而需要注意的一个问题是,对于我这个从来没有在非ARC环境下写过OC的程序员来说,往往不会太注重内存管理这件事,而一旦用到了CF,ARC就无能为力了,很简单,因为它是用C写的。编译器无法自动管理对CF的内存,所以我们一旦使用了CF对象,就必须手段管理。

我们可以使用CFRetain和CFRelease来进行CF对象的内存管理,然而,在Objective-C和Core Foundation相互转换的时候,内存管理又称了一个问题,到底是交给ARC来处理,还是手动处理,一不小心会不会释放两次?又或者造成内存泄露?

三种处理方案

  • __bridge 不改变对象所有权

    所谓不改变对象所有权,就是原来是CF就用CF的管理方式,原来是OC就用OC的管理方式
    比如对于一个NSString,通过(__bridge CFStringRef)将其转换成CFStringRef,它还是会通过ARC来管理内存,反之亦然。

  • __bridge_retained 或者 CFBridgingRetain() 所有权归CF

    这种情况下,ARC的管理权被剥夺,需要手动管理内存
    另外解释一下,CFBridgingRetain()是bridge_retain的宏方法
    即:(
    bridge_retaind CFStringRef) aNSString 相当于 (CFStringRef)CFBridgingRetain(aNSString)

  • __bridge_transfer 或者 CFBridgingRelease() 所有权归ARC

    和上面相反,无需多做解释。

关于Aspects

https://github.com/steipete/Aspects

aspects是针对面向切面编程:Aspect Oriented Programming(AOP)的一种实现方案。AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
以埋点为例,虽然现在已经有像友盟这样强大的第三方埋点方案,但是这并不能满足一些公司的业务需求,如果要完全自定义地进行埋点,监听并统计用户的行为,使用传统的方案,必然导致对整个项目的代码进行大范围的修改。而使用面向切面编程的思想,则可以将埋点和系统原有的逻辑解耦,悄悄地完成埋点。


下面来看看Aspects是如何帮助我们实现这点的。

还是举个例子来说明:
假设我们要统计某些页面的显示的次数,以往使用友盟来统计页面访问我们会这么做

1
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    //do something
    if(self.title){
        [MobClick beginLogPageView:self.title];
    }
}

聪明一点的做法当然是可以把它写在基类。这样至少不会影响太多的代码。但是如果是其他一些更加个性化的埋点,比如说点击某个按钮,通过不同的方法进入某个页面,注册所消耗的时间统计等等等等。。。。

有了Aspect 我们可以这么做

1
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
    UIViewController* viewController = aspectInfo.instance;
    if (viewController.title) {
        [MobClick beginLogPageView:viewController.title];
    }
} error:NULL];

当然,这块代码完全可以和原有的逻辑隔离。
先解释一下,Aspects针对NSObject实现了aspect_hookSelector的方法,顾名思义,使用一个钩hook住了UIViewController的viewWillAppear方法,withOptions可以有两个参数,一个是AspectPositionAfter,另一个是AspectPositionBefore,分别代表在原有方法执行前或者原有方法执行后执行该block,根本上相当于利用oc的runtime特性替换了原有的方法。
通过aspectInfo中的instance和arguments属性可以分别获得方法的方法体和参数,基本上也就是获得了整个方法执行的上下文。
特别需要注意的是,同一个继承树上的同一个方法只能被hook一次

阅读全文 »

Bug的发现

一次偶然的机会,发现了QQ的一个Bug。
复现过程如下:

  • 在QQ中通过任意途径打开一个pdf或者word文档
  • 单击全屏显示
  • 滑动返回到一半终止返回(再次回到浏览界面)
  • 再次滑动返回,就会观察到这样的场景————导航栏消失不见或者导航栏错位

当然,这是一个必现的Bug,而且我也相信类似的操作在很多别的应用中也会引发这个Bug,归根结底,这是iOS在引入了滑动返回后导航栏本身的一个Bug

阅读全文 »

事情的经过是这样的,项目的主页是一个由一个xib定义的scrollView,设置的contentSize是屏幕宽度的1.75倍,一共分三屏。

这样的布局设计在很多应用中应该是很常见的,并且从业务需求的角度往往需要在scrollView的delegate中根据scrollView的滑动状态和contentOffset做一些切换和调整。那么问题就来了!!!

源代码是这样的

1
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    if(_scrollView.contentOffset.x > 0){
    //do something
        if (_scrollView.contentOffset.x > kScreenWidth) {
            //代表scrollView进入了第三屏,需要做某些事情
        }
    }
}

但是这块业务在之后的测试中出现了一个很难复现的Bug。
那就是当滑动到中间屏幕(即contentOffset.x = kScreenWidth的时候),居然进入了>kScreenWidth的判断。
然后进入调试模式,居然发现此时的contentOffset = 321…..
321…..
坑爹么!这是!
于是就这么偶然地发现了这个惊天大坑!
在经过一系列的滑动之后,scrollView会出现contentOffset计算不准确的计算,这也就意味着在做一些针对contentOffset的判断的时候,用==的操作很容易出现问题。

那么解决方案就来了
把if里面的判断改成

1
fabs(_scrollView.contentOffset.x - 1.75 * kScreenWidth) <= 2.0

通过绝对值的模糊判断,解决了原先contentOffset计算不精确所导致的问题。

这也提醒了我,在程序中一些设计浮点数判断相等的时候,需要考虑到计算误差的情况。

当然,不管是此处的bug是因为通过autolayout来控制scrollView的contentOffset也好,还是本身scrollView的一个bug也好,反正这次踩过了坑,以后也就不会再踩了。

最近在给公司的项目做国际化,不得不说是一件很繁重的工作,因为公司项目使用的xib较多,所以既要考虑代码的国际化也要考虑xib的国际化,同时项目中还要求对权限请求提醒做国际化,种种种种。

网上相关国际化的博文有很多,不过并没有一个全面的针对代码、xib、storyboard以及项目配置整体的一个国际化的介绍,所以这里一口气就全部搬上来了。

代码的国际化

给项目添加国际化

给项目添加国际化

阅读全文 »