在一定条件下, 添加 All Exceptions 断点后, 每次运行都会在 main.m 中断, 根据具体情况不同, 还会有连续中断数次的情况, 严重影响开发效率, 但又没有任何提示告诉你哪出现了错误, 该怎么解决.

这个问题产生的原因之一就是在 info.plist 里面有项目中不存在的字体, 有多少个不存在的字体就会中断多少次, 相应的解决方法也明了了, 删除 info.plist 里面这些多余的字体即可. 此外根据 stackoverflow 的描述, xib/storyboard 里面如果有不存在的字体也会造成同样的情况, 所以你可能需要仔细检查一遍所有的 xib 文件来排除问题.

最后还有一种简单粗暴的方法: 右键点击 All Exceptions => Edit Brackpoint… => Exception 选项选择 Objective-C, 这种方法虽然会暂时回避掉中断的问题, 但实际上问题依然存在, 而且如果你的项目中有 C++ 代码, 也就自然没法自动在 C++ 代码中中断了(All Exception).

Tagged with:  

更新: 现在已不需要再按照下面方法安装, 按官方文档操作即可

—————————————————-

nuclide 是一款由 facebook 研发, 基于 atom 的, 针对 web/mobile 开发定制的 IDE, 目前(2015.10.06) 还处于初级阶段, 相应的支持很少. 但难得的是这是一款 IDE 而不是编辑器, 也是现阶段能对 react/react-native 提供主要支持的少有的几个工具之一.

本文主要针对 react-native 开发, 不涉及其他功能.

直接使用 atom 安装可能会发生 atom helper 长时间占用 100% cpu 进而导致 atom 无法使用的问题 github issue, 下面是解决方法.

正确的安装方法(之一):

环境: El Capitan 10.11.0, atom 1.0.19, Nuclide 截止至10月6日代码

安装方法:

  1. 清空 ~/.atom/packages 下面所有 nuclide 相关的包, 注意不仅仅是 nuclide 开头的, 还有个 hyperclick 也是 nuclide 的组件
  2. 删除 ~/.atom/config.cson 里面 disabledPackages 中禁用的 nuclide 相关 package
  3. 按照 nuclide readme 下面 Building from Source 的方法, 用 ./scripts/dev/setup 安装, 包实际上安装到了 nuclide 项目目录里, 以软连接的形式放到了 ~/.atom/packages 下面, 注意脚本运行过程中有没有错误信息, 如果有要解决.
  4. 安装后第一次启动 atom 会没响应, 问你要不要 wait 选 wait, 可以看到 atom helper 一直占用 100% cpu, 这是 because of the large number of Babel files that need to be transpiled, 等不到 10 分钟就正常了. 再次重开包括进 settings 也没有问题, 不需要禁用任何 package

PS1.可能要注意的:

  1. 在安装前保证 atom 没有任何打开的项目, 标签等, 包括 settings, timecop 这些东西

PS2. 安装后, 一定要注意的:

  1. 仔细阅读 nuclide-flow 的 readme, 会告诉你如何让这个插件正常运作:
    • 插件 settings 添加 flow 的路径,
    • .flowconfig (react-native 默认就会自动生成),
    • /* @flow */
  2. react-native 自动生成的 .flowconfig 可能会让 flow 无法运行, 原因是其中的 [version] 会限制 flow 的版本, 不匹配就不能运行, 删掉那部分即可
  3. flow 执行需要几秒钟, 过了这几秒就可以像 readme 展示的那样支持 cmd-click 等功能了
  4. 仔细观察 nuclide-flow 包的设置选项, 你会发现默认需要通过 save 来触发 flow 的错误检测, 并且提供了选项来让 flow 随时检测错误.

PS3.

  • 仔细读其他 nuclide 插件的 readme (如果有), 目前 nuclide 的相关东西特少, 有问题都搜不到, 所以仔细读说明才是正确的做法
Tagged with:  

首先说明, 这是一个会在 iOS 8.1 模拟器上稳定复现的关于 UINavigationController 的 bug, 在 iOS 8.2 已经修复, 在 8.1 的真机上会出现但不肯定会稳定复现, 因为在我查明这个 bug 的原因的时候, 我手头的设备刚刚升级到 8.2, 所以也没法测试了. 8.0 的系统完全没有测试过, 7.x 的系统可以肯定没有这个问题. 既然这个问题修复了本文也本没必要写, 但考虑到可能有些越狱用户会停留在 8.1 系统, 为了让其他遇到同样问题的开发者可以找到解决方案, 所以还是发一下.

这个 bug 的具体复现方法如下:

  1. 准备三个 UIViewContoller(vc1 vc2 vc3), 三个 viewController 的 title 分别设置为 @”1″, @”2″, @”3″. vc1 和 vc2 均实现 - (BOOL)prefersStatusBarHidden {return YES;} 方法, vc3 保持原状.

  2. 将 vc1 作为 UINavigationControllerrootViewController, push vc2, push vc3.

  3. 点击 navigation bar 上面的返回按钮, 可以发现 UINavigationController 本应 pop vc3, 到达 vc2, 但实际上, 虽然 pop vc3, 显示了 vc 2, 但是 navigation bar 的 title 却显示为 @”1″, 如果你的 vc1 上设置了自定义的 barButtonItems, 那么这些 barButtonItems 也会同样显示出来. 换句话说, view 只 pop 了一层, 但是 navigation bar 却直接跳了两层. 更进一步, 如果在 vc1 前还有 vc0, vc-1… 那么你可以发现在逐一 pop 这些 viewController 的时候或有 viewController 被跳过.

Bug 的原因就是三个 viewController 的 prefersStatusBarHidden 设置不统一, 改成一致后就无此问题.

既然是系统的 Bug, 那就只能避免, 没法修复. 万幸的是这种 bug 的产生情况应该比较少见, 而且新的系统已经修复.

测试代码在 github

–以上–

Tagged with:  

英文标题: How to implement a user interactivable UIView implicit animation

这一篇教程将结合一个小 demo 讲解, 最终代码在 github 上可以下载到. demo 的运行效果如图所示, 通过手势拖动顶端红色矩形到虚线位置, 拖动距离超过 50% 松手, 矩形会自动移动到目标位置, 少于 50% 松手, 矩形自动归位:

How to implement a user interactive UIView implicit animation

实现一个可交互的动画大概有这么几种思路:

  1. 在响应 GestureRecognizer 的方法中实时更新 view 的状态, 在 GestureRecognizer 结束的时候创建动画完成后续的部分.
  2. 在 GestureRecognizer 开始的时候创建显式动画, 随着 Gesture 变化, 改变动画的 progress, 在 GestureRecognizer 结束之后完成动画.
  3. 在 GestureRecognizer 开始的时候创建隐式动画, 随着 Gesture 变化, 改变动画的 progress, 在 GestureRecognizer 结束之后完成动画.

注意 2 和 3 只有显式和隐式的区别.

对于简单的 view, 单一的动画 (比如 demo 中的这种, 线性移动一个简单的 view), 三种方法差别不大. 而对于复杂的 view (view 中嵌套多个 subView), 多个动画 (view 和 subview 都有各自的动画), 如果在不考虑可交互的前提下, 隐式的动画已经可以满足需求, 那么方法 3 可能会比方法 1 和 2 简洁得多, 本文将对方法 3 进行讲解.

要实现方法 3, 首先要了解 CALayer 的几个属性: speed, timeOffset, beginTime. 对于这几个属性, Controlling Animation Timing 这篇教程图文并茂地给出了直观的解释, 必读, 这里只做简单介绍:

  • speed: 控制动画的速度, 比如设置为 1 (默认), 动画正常运行, 设置为 0 动画暂停, 设置为 -1 动画反向运行.
  • timeOffset: 控制动画在整个动画时间轴上的位置, 比如一个 3 秒的动画, timeOffset 为 1.5, layer 就会处于整个动画过程中间的那个状态.
  • beginTime: 动画的开始时间.

有了这些基础, 我们就可以着手实现了, 首先我们需要建立一个 CALayer 的 Category, 添加一些动画辅助代码:

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
- (void)gs_pauseAnimation {
    self.speed = 0;
    self.timeOffset = 0;
}
 
- (void)gs_recoverToDefaultState {
    self.speed = 1;
    self.timeOffset = 0;
    self.beginTime = 0;
}
 
- (void)gs_setTimeProgress:(CFTimeInterval)timeProgress {
    self.timeOffset = timeProgress;
}
 
- (void)gs_continueAnimationWithTimeProgress:(CFTimeInterval)timeProgress {
    [self gs_recoverToDefaultState];
 
    self.beginTime = [self convertTime:CACurrentMediaTime() fromLayer:nil] - timeProgress;
}
 
- (void)gs_continueReverseAnimationWithTimeProgress:(CFTimeInterval)timeProgress animationDuration:(CFTimeInterval)duration {
    [self gs_recoverToDefaultState];
 
    self.beginTime = [self convertTime:CACurrentMediaTime() fromLayer:nil] - (duration - timeProgress);
    self.timeOffset = duration;
    self.speed = -1;
}
  • gs_pauseAnimation: 通过将 speed 设置为 0, 来暂停动画.
  • gs_recoverToDefaultState: 将 layer 的各个属性恢复到默认状态
  • gs_setTimeProgress: 在 gs_pauseAnimation 的基础上, 设置动画的时间进度.
  • gs_continueAnimationWithTimeProgress: 从 timeProgress 时间进度开始, 继续播放之前暂停了的动画
    • 这里首先调用了 gs_recoverToDefaultState, 是为了让 convertTime:fromLayer: 得出正确的时间. 
    • [self convertTime:CACurrentMediaTime() fromLayer:nil] 将当前系统时间转换到 layer 的时间上, 减去 timeProgress, 假设这是一个 3 秒的动画, timeProgress 为 1 秒, 则这条代码就表示 layer 的动画在一秒之前就已经开始, 所以动画将从运行到第 1 秒的状态开始继续播放.
    • 如果不能理解, 可以结合一下文档中这篇: How do I pause all animations in a layer tree?
  • gs_continueReverseAnimationWithTimeProgress: 从 timeProgress 时间进度开始, 反向播放之前暂停了的动画.
    • 这里 beginTime 设置为 当前时间 - (整个动画时间 - timeProgress), 再假设这是一个 3 秒的动画, timeProgress 为 1 秒, 因为要让动画反向播放, 所以要把动画反过来看, 由终点到起点运动, timeProgress 这 1 秒, 是动画最后的部分, 前 3 – 1 = 2 秒是已经播放完的, 所以动画要从 第 2 秒开始.
    • 通过将 speed 设置为 -1 来反向播放动画

完成了动画控制的实现, 接下来是控制 view 状态的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)switchToState1 {
    [self.moveableView setCenter:CGPointMake(self.view.bounds.size.width / 2, 50)];
    [self.moveableView setTag:1];
}
 
- (void)switchToState2 {
    [self.moveableView setCenter:CGPointMake(self.view.bounds.size.width / 2, 500)];
    [self.moveableView setTag:2];
}
 
- (void)switchState {
    if (self.moveableView.tag == 1) {
        [self switchToState2];
    }
    else {
        [self switchToState1];
    }
}

很简单, 不用过多解释, view 的起始状态是 state1.

然后是手势控制:

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
39
40
41
42
43
44
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
    if (panGesture.state == UIGestureRecognizerStateBegan) {
        [self.moveableView.layer removeAllAnimations];
 
        [UIView animateWithDuration:kAnimationDuration animations:^{
            if (self.moveableView.tag == 1) {
                [self switchToState2];
            }
            else {
                [self switchToState1];
            }
        } completion:^(BOOL finished) {
            [self.moveableView.layer gs_recoverToDefaultState];
 
            if (self.animationProgress <= 0.5) {
                if (self.moveableView.tag == 1) {
                    [self switchToState2];
                }
                else {
                    [self switchToState1];
                }
            }
        }];
 
        [self.moveableView.layer gs_pauseAnimation];
    }
    else if (panGesture.state == UIGestureRecognizerStateChanged) {
        CGPoint translation = [panGesture translationInView:self.view];
        CGFloat length = ABS(translation.y);
 
        self.animationProgress = MAX(0, MIN(length / 500, 1));
 
        [self.moveableView.layer gs_setTimeProgress:self.animationProgress *  kAnimationDuration];
    }
    else if (panGesture.state == UIGestureRecognizerStateEnded || panGesture.state == UIGestureRecognizerStateFailed || panGesture.state == UIGestureRecognizerStateCancelled) {
 
        if (self.animationProgress > 0.5) {
            [self.moveableView.layer gs_continueAnimationWithTimeProgress:self.animationProgress * kAnimationDuration];
        }
        else {
            [self.moveableView.layer gs_continueReverseAnimationWithTimeProgress:self.animationProgress * kAnimationDuration animationDuration:kAnimationDuration];
        }
    }
}
  1. 在手势开始时, 创建 UIView 隐式动画并暂停;
  2. 随着手势移动, 调整动画的进度
  3. 在手势结束后, 根据当前手势位移来判断是将 view 归位, 还是移动到目标位置.
  4. 在动画结束后, 也就是在手势开始时隐式动画的 completion 中, 恢复 layer 状态, 并在 view 归位的情况下, 设置 view 到初始的状态.

运行一下 (这部分代码在 branch v1), 看起来得到了想要的效果, 但是如果细心观察, 会发现在手势拖动距离不够长, view 移动到原位的时候, view 会闪烁一下. 因为 demo 中 view 太简单了, 所以可能要多试几次才能观察到, 不过也可以在 completion block 里面第一行设置个断点, 可以看到, 执行到断点处时, view 消失了.

这里的原因我没有深入研究, 猜测是因为 speed 设置为 -1, 导致动画运行完成的时候 view 处于一个不存在的状态, 也就无法将这个状态的 view 绘制到屏幕上. 虽然通过将 speed 设置为 -1 来反向运行动画看起来很机智, 但很显然, 这不是一个完美的解决方法.

既然不能让动画自动反向运行, 那就稍微绕个远, 手动反向运行. CADisplayLink 是一个不错的选择, 它与 NSTimer 类似, 但是是在屏幕每一帧刷新之前调用 selector. 新的代码如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
    if (panGesture.state == UIGestureRecognizerStateBegan) {
        [self.moveableView.layer removeAllAnimations];
 
        [self.displayLink invalidate];
 
        [UIView animateWithDuration:kAnimationDuration animations:^{
            [self switchState];
        } completion:^(BOOL finished) {
 
        }];
 
        [self.moveableView.layer gs_pauseAnimation];
    }
    else if (panGesture.state == UIGestureRecognizerStateChanged) {
        CGPoint translation = [panGesture translationInView:self.view];
        CGFloat length = ABS(translation.y);
 
        self.animationProgress = MAX(0, MIN(length / 500, 1));
 
        [self.moveableView.layer gs_setTimeProgress:self.animationProgress *  kAnimationDuration];
    }
    else if (panGesture.state == UIGestureRecognizerStateEnded || panGesture.state == UIGestureRecognizerStateFailed || panGesture.state == UIGestureRecognizerStateCancelled) {
 
        if (self.animationProgress > 0.5) {
            [self.moveableView.layer gs_continueAnimationWithTimeProgress:self.animationProgress * kAnimationDuration];
        }
        else {
            [self.displayLink invalidate];
 
            self.displayLinkStarTime = CACurrentMediaTime();
            self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateAnimation:)];
            [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        }
    }
}
 
- (void)updateAnimation:(id)sender {
    CFTimeInterval timeInterval = CACurrentMediaTime() - self.displayLinkStarTime;
    CFTimeInterval animationTimeProgress = self.animationProgress * kAnimationDuration - timeInterval;
 
    if (animationTimeProgress >= 0) {
        [self.moveableView.layer gs_setTimeProgress:animationTimeProgress];
    }
    else {
        [self.displayLink invalidate];
 
        [self.moveableView.layer gs_continueAnimationWithTimeProgress:kAnimationDuration];
        [self switchState];
    }
}

可以看到, 在手势介素后, 我们创建了一个 CADisplayLink 来手动反向运行动画, 并在动画结束后, 重新设置 view 的状态.

运行, 闪烁消失, Cheers.

–以上–

Tagged with:  

针对含有sub-project的project, 在依赖关系正确设置(关于如何正确设置, 参考本文最后提供的链接)的情况下, 如果直接用下面命令, 基本上编译会出错, 提示找不到sub-project里面的头文件:

xcodebuild -target TestApp -configuration Release clean build

这个命令会在当前目录下创建一个build文件夹, 然后将编译的中间产物和结果放进去, 而Xcode GUI在编译时是将这些放到DerivedData下面, 这一区别导致了在编译时找不到sub-project包含的一些东西.

解决方法很简单, 加上-scheme, 命令如下:

xcodebuild -scheme TestApp -target TestApp -configuration Release clean build

指定了-scheme之后, 命令行编译的行为就跟在Xcode GUI下编译一样了.

如果使用添加header search path的方法, 也能让编译通过, 但其实不是最正确的方法, 因为在GUI下编译不需要这一步.

如何正确设置sub-projct, 及本文参考见这里

Tagged with:  

Objective-C代码格式整理

On 04/07/2014, in iDev, iOS Dev, Mac Dev, by ultragtx

团队合作中代码风格一致比较重要, 像Google NewYorkTimes这些公司都公开了各自的Objective-C代码风格, 有需要的搜索一下就可以找到, 这里主要介绍借助Uncrustify自动整理代码格式的一些技巧.

Uncrustify: Source Code Beautifier for C, C++, C#, ObjectiveC, D, Java, Pawn and VALA, 篇幅及时间有限, 没必要过多介绍, 总之是一款定制性很高的代码格式整理工具, 但高定制性意味着配置起来很烦, 所以这里推荐一个开源图形界面配置文件编辑器UncrustifyX, 这个工具对各个配置都有详细的说明, 并且有直观的预览功能, 可以较为方便地编辑出你需要的配置, 当然更方便的方式是直接搜索别人写好的配置文件, UncrustifyX本身就带一个.

其他的关于如何安装, 如何更方便地在Xcode中使用, 可以参考这篇. 本文不再详细说明.

最后要说一下Uncrustify的一个缺点(Feature):Xcode会自动对代码中的空行添加空格缩进, 这样会对编辑代码带来方便, 而Uncrustify会删除所有行尾多余的空格, 这就意味着如果你用Uncrustify对一个已有的代码进行处理, 所有的空行缩进都会被删除, 产生大量无意义的diff, 同时影响下次编辑. 如果你期待可以通过配置文件来改变这个行为, 那恐怕就要失望了, 而且Uncrustify的作者基本上也明确表示了不会添加这个功能(现在有个issue, 里面有几个人在求这个功能).

这里提供一个简单技巧, 轻松解决这一问题, 在Xcode中, 我们可以通过全选代码, Move Line up(option+command+[) + Move Line Down(option+command+]), 来自动添加回那些消失的空格, 上面说的那个问题也就迎刃而解了. 另外说一下, AppCode自带的代码格式整理, 估计也是用Uncrustify实现的, 所以跟Uncrustify一个毛病.

–以上–

Tagged with:  

UIScrollView及其子类UITableView, UICollectionView为iOS开发带来了极大的方便, 其分页(pagingEnabled)功能也很常用, 但是功能却有些局限, 页只能按UIScrollviewbounds尺寸划分, 如果要实现自定义分页宽度或高度就需要一些技巧.

方法一

类似iOS 6中MobileSafari, 如图所示的分页方法:

HGPageScrollView_Screen_shot_2

 

StackOverflow上提供的实现思路如下:

 

  1. UIScrollviewbounds限制再屏幕中间那一页的位置
  2. 禁用UIScrollviewclipsToBouds, 从而能显示超出bounds的内容

此时已经可以在借助UIScrollview原有分页功能的基础上实现了"视觉上"与MobileSafari类似的效果, 但因为UIScrollview的尺寸限制, 超出范围的触摸事件不会被UIScrollview收到, 影响体验, 要把触摸事件传递给UIScrollview, 方法如下:

  1. 用一个全屏的(或者其他你想要的尺寸)UIView子类, 这里我们命名为CustomUIView来做UIScrollviewsuperView
  2. 重写CustomUIViewhitTest:withEvent方法:

     - (UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event {
       if ([self pointInside:point withEvent:event]) {
         return scrollView;
       }
       return nil;
     }
    

    通过重写hitTest, 就可以将CustomUIView接受到的触摸事件传递给UIScrollView

方法二

前一种方法保留了UIScrollView原有的分页功能, 而下面这种更加灵活的自定义分页方法则完全自制了一个分页功能:

DMPagingScrollView就是一个很好的实现, 其自定义分页的思路大致如下(需要参考DMPagingScrollView.m代码):

  1. 用实例变量pageWidth记录自定义分页的宽度
  2. 通过实现UIScrollView的delegate方法, 来记录每次拖动UIScrollView动作的相关参数(位移, 加速度…)
  3. scrollViewDidEndDecelerating:等一些标志拖动结束的delegate方法中调用snapToPage方法, 在snapToPage方法中, 借助之前收集的动作参数计算出contentOffset, 并通过setContentOffset:offset animated:YES来移动.

这种方式的关键就在contentOffset的计算中(DMPagingScrollView.m pageOffsetForComponent:方法), 如果计算的不正确, 就会严重影响拖动UIScrollView的手感.

DMPagingScrollView通过subclassing UIScrollview将自定义分页相关的过程操作封装在DMPagingScrollView里面, 同时支持横向纵向的自定义分页, 十分方便. 类似地, 我们也可以对UITableView, UICollectionView进行同样的处理.

此外, 通过参考pageOffsetForComponent:中的计算方法, 我们甚至可以让每一页的宽度都不同, 从而大大提升了分页的灵活性, 比如我实现的这种:

- (CGFloat)pageOffset {
    CGFloat totalWidth = self.collectionView.contentSize.width;
    CGFloat visibleWidth = self.collectionView.bounds.size.width;

    CGFloat currentOffset = self.collectionView.contentOffset.x;
    CGFloat dragVelocity = _dragVelocity.x;
    CGFloat dragDisplacement = _dragDisplacement.x;

    NSInteger nearestIndex = 0;
    CGFloat minDis = CGFLOAT_MAX;
    CGFloat nearestOffset = 0;
    CGFloat prevOffset = 0;
    CGFloat nextOffset = 0;
    CGFloat totalOffset = 0;

    // self.pagesWidths is an NSArray that contains each page's width
    for (NSInteger i = 0; i < self.pagesWidths.count; i++) {
        CGFloat dis = ABS(totalOffset - currentOffset);

        if (dis < minDis) {
            minDis = dis;

            prevOffset = nearestOffset;
            
            nearestOffset = totalOffset;

            if (i == self.pagesWidths.count - 1) {
                nextOffset = totalOffset;
            }
            else {
                nextOffset = totalOffset + [(NSNumber *)self.pagesWidths[i] floatValue];
            }
        }

        totalOffset += [(NSNumber *)self.pagesWidths[i] floatValue];
    }

    NSInteger lowerIndex;
    NSInteger upperIndex;
    CGFloat lowerOffset;
    CGFloat upperOffset;
    if (currentOffset - nearestOffset < 0) {
        lowerIndex = nearestIndex - 1;
        upperIndex = nearestIndex;

        lowerOffset = prevOffset;
        upperOffset = nearestOffset;
    }
    else {
        lowerIndex = nearestIndex;
        upperIndex = nearestIndex + 1;

        lowerOffset = nearestOffset;
        upperOffset = nextOffset;
    }

    CGFloat newOffset;
    if (ABS(dragDisplacement) < DRAG_DISPLACEMENT_THRESHOLD || dragDisplacement * dragVelocity < 0) {
        if (currentOffset - lowerOffset > upperOffset - currentOffset) {
            newOffset = upperOffset;
        } else {
            newOffset = lowerOffset;
        }
    } else {
        if (dragVelocity > 0) {
            newOffset = upperOffset;
        } else {
            newOffset = lowerOffset;
        }
    }

    if (newOffset > totalWidth - visibleWidth)
        newOffset = totalWidth - visibleWidth;

    if (newOffset < 0)
        newOffset = 0;

    return newOffset;
}

–以上–

Tagged with:  

最近做的一个项目中涉及这样一个需求: 响应用户点击html元素的事件.这其中包含响应单击(single tap/click)和双击(double tap/dblclick).

单击事件

通常单击的解决方案是用javascript实现, 为html元素绑定onclick事件, javascript接收事件后跳转url, 在CocoaTouch中用 webView:shouldStartLoadWithRequest:navigationType: 拦截url跳转, 从而实现单击事件的响应, 大致的代码如下:

Javascript:

function myClick() {
    var e = window.event
    var url = "click:" + e.target.id;
    document.location = url;
}

var ele = document.getElementById(ELEMENT_ID);
ele.onclick = myClick;

CocoaTouch:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

    NSString *requestString = [[request URL] absoluteString];
    NSArray *components = [requestString componentsSeparatedByString:@":"];
    if (components.count == 2 && [(NSString *)[components objectAtIndex:0] isEqualToString:@"click"]) {
        NSString *eleIdStr = (NSString *)[components objectAtIndex:1];
        NSInteger eleId = [eleIdStr integerValue];

        // Do something

        return NO;   // Do not load request, just receive the event
    }
    return YES;
}

双击事件

Failed 1

使用与上面同样的方法为html元素添加ondblclick来响应双击事件就无法实现, 现象为响应函数从不被调用. 用setTimeout等方法尝试用单击事件来模拟双击事件也无法实现, 无法接收到double tap的第二次点击.

Failed 2

根据之前的现象来看, 显然双击事件在html之外就被拦截了, 要从CocoaTouch层下手才行.

系统的UIWebView中包含了一个UIScrollView, 绑定了UIScrollViewDelayedTouchesBeganGestureRecognizerUIScrollViewPanGestureRecognizer这两个事件, 后者响应拖动事件, 前者相应点击事件.

尝试添给UIScrollView加UITapGestureRecognizer:

UITapGestureRecognizer *dblTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fakeTapGestureHandler:)];

经测试, fakeTapGestureHandler:从未被调用过.

Success

尝试曲线救国的方式, 用UIGestureRecognizer的delegate方法获取事件:

Javascript:

function eleIdFromPoint(x, y) {
    var ele = document.elementFromPoint(x, y)
    return "" + ele.id;
}

CocoaTouch:

// Add gesture
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fakeTapGestureHandler:)];

[tapGestureRecognizer setDelegate:self];
[_webView.scrollView addGestureRecognizer:tapGestureRecognizer];

// Delegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    CGPoint tapPoint = [touch locationInView:_webView];
    NSString *script = [NSString stringWithFormat:@"eleIdFromPoint(%f, %f)", tapPoint.x, tapPoint.y];
    NSString *eleIdStr = [self stringByEvaluatingJavaScriptFromString:script];
    if (touch.tapCount == 2) {
        if (eleIdStr.length > 0) {
            NSInteger eleId = [eleIdStr integerValue];

            // Do somthing

        }
    }
    return YES; // Return NO to prevent html document from receiving the touch event.
}

上面代码通过UIGestureRecognizer的delegate方法成功获取到点击事件, 需要说明的几点:

  • locationInView:的参数为UIWebView, 后面的document.elementFromPoint传递的也是UIWebView中对应的坐标, 不需要算上UIScrollView的Offset.
  • delegate方法中单击事件也可以被拦截到, 这样单击双击就可以在这块儿一起实现, 让代码看起来比较统一.
  • delegate方法返回NO会拦截事件, html文档就不能接受到任何点击事件了.

–以上–

Tagged with:  

开篇扯淡是惯例(其实这个博客是一个AI维护的你信么?), 又是一个多月没写东西,折腾各种东西,写不出来什么有营养的,也就放着了。本文虽然谋划已久,但一直都没动手写,在iOS这块,虽然坚持2年了,但还是担心会误导别人,不敢写这种类型的东西。最近要集中点别的技能点,iOS会放缓,先把这篇放出来吧,供大家参考,为了方便搜索,给本文起个中文标题:学习iOS,从菜鸟到小强。

啰嗦版

先说下我开始折腾iOS这块之前的知识背景,“碰过“C/C++,“看过”Java,“碰过”Android,当然这些都是比较保守的说法,不过也基本可以认为就是大菜的水平,认为什么都不会也可以,但是有过几个较完整的项目经验(比如我写过俩android小破项目)。

开始的时候,我的路线是在基本什么都不会的情况下,以一个不大不小的项目驱动,快速产出,这里最重要的一点就是学会如何在什么也不懂的情况下搜索到你想要实现的那些东西的代码,或者是相似的代码,然后就是如何照猫画虎把别人写的几块东西改改,用胶水代码沾一起,形成一些“能用的”东西,快速的产出有助增强信心,有效避免半途而废,同时一旦开始写代码了,所学到的知识就自然而然记牢了,不会发生看一遍忘一遍这种情况。

当然如果我这么建议别人,一些大神就会喷了(比如这个:【豆瓣校招-面试官发声】近期面试感受),说这种搜索式学习方式会打下不好的基础,因为你接触到的都是二手,三手以上的信息(这里定义一下,一手信息为文档,质量最高,二手信息可以认为是StackOverflow这类,也很不错了,三手就是这个链接里所表达的那个“二手”),对这些信息就要比较谨慎,有些东西可能太过技巧,不是一种好的解决方法,另外就是难免有错误,而且这个错误可能会被你继承很久,因为是新手,你会倾向于相信所有“好使“的代码。

这些弊端确实存在,不过恭喜你学习的是iOS,只要将你寻找问题解决方法的搜索范围限定在官方文档,WWDC视频,StackOverFlow上就可以了,这些都是一手二手的信息,而且一手信息占了绝大多数,基本可以保证信息的优质性,同时也完全足够解决问题了。当然多看看其他人写的东西也是有好处的,学习不要死板,这里的限制只是在避免上一段提到的问题,搜索式学习虽有弊端,但还是要比啃一手资料快很多,对于优先实现功能是很有必要的,关键就是要和一手资料结合着用,如何掌握之间的度就靠自己来衡量吧。

这里不得不提一下文档,苹果的文档是我见过的最好的文档(MSDN的也很牛,但是我没做过windows那方面的开发,所以没什么了解),这里不仅仅有对类、方法的详细说明,还有非常多的XXX Programming Guide,这种导引性的文章,从一定程度上来说,有了文档我们甚至已经不再需要StackOverflow,其实在StackOverflow搜索问题的时候,很多回答就是一个指向官方文档的链接。同时,文档中包含了大量Sample Project,这些Sample都很有针对性,清晰地展示了各种实现方法,同时Sample的代码都是高手写的,多看看Sample Project对编程能力的提升也有很高价值。额外说一句,如果你认为英文的文档太难读懂,那就只能查着字典啃,如果还是不行,你去学英文吧,要么就别搞这一行,文档的英文要求已经很低了。

WWDC视频,也是快速解决问题的必备,在接触比较陌生的一块比如音频处理的时候,先看一看WWDC相关视频,就能大概了解一下相应的实现方法,对这一块有一个比较清晰全面的认识,这时候再借助文档的XXX Programming Guide,基本就可以非常深入地学习到你需要的知识。

前面一直都没提到,书–这一重要学习工具,在我看来买书基本上完全没有必要,有文档在看什么书啊,那种讲语言,讲SDK的书没有必要买,根本比不上文档,时效性也差。当然还是有些好书值得买来看看,比如Apress的iPhone Cool Projects,和More iPhone Cool Projects,虽然比较旧,但是里面那几个项目还是很有参考价值的。

面对iOS这一快速更新的系统,新手面临的一个常见问题就是该从哪个SDK版本入手。SDK版本对于新手老手都是个问题,要想要新功能,就要高版本的SDK,要想兼容旧设备,就要放弃新功能,或者自己实现新功能,选择低版本的SDK。这里根据我的经验,如果你是一个纯新手,不着急开发产品,就从最新的SDK版本入手,因为等你出师了,当时入门的SDK版本已经是旧版了,兼容旧设备的问题就不存在了。如果要快速出产品,那就只能从低版本SDK入手了,当然有些新API就用不了了,作为新手一个能用新API很方便实现的功能却要为了兼容用很复杂的方法实现还是有些不爽的。选择SDK要注意的一点就是,在iOS 4.0以后,引入了Auto Reference Counting(ARC)这一技术,个人感觉使用ARC虽然方便了内存管理,但是对于新手来说缺少了手动管理内存的历练,这里欠下的债,将来一定要还的。所以还是推荐先看文档学习一下内存管理方法,并在此后一段时间内关掉ARC实践手动管理内存,记得用Instruments测一下内存泄露,等到基本掌握了手动内存管理,就可以开ARC了。当然,我在折腾内存管理的时候还没有ARC,所以这部分建议可能存在一点主观因素,虽然不一定是最好的路线,但总不是错误的路线。

工欲善其事,必先利其器,XCode的熟练使用,对于提高效率至关重要,关于怎么使用XCode请搜索我以前的文章,关键词“XCode”,懒,不发链接了。

以上都是以项目驱动为前提,至于这个项目,规模不要太大也不要太小,有一定升级空间,最好是没有时间限制的长期项目,这样就有充足的时间来深入研究某一部分知识点,第一个版本功能实现了,但是代码不够漂亮,效率不够高,推到重来是一个非常好的选择,反复迭代几次,大有益处。

我的入门项目就是HIT Mobile(在关于页面里面有链接,现在也基本停止维护了,实际上也没什么人用)。这个项目我和一个队友分别负责不同的功能,持续时间将近一年(当然有上课考试等因素干扰),为了代码整洁或者效率等问题,每个功能都反复写了2、3遍,这几遍推到重来对能力的提升是非常大的。

虽然做单一的项目可能接触的不够全面,但如果你是一个渴求知识的人,必然会在做主线任务的同时,走一些分支路线,接触到其他方面的知识,另外一点就是,iOS虽然是个手机上的OS,但不要忘了这是基于Mac OS做出来的系统,Mac OS这么多年历史岂是我等弱菜一两年就能接触全面的,专注一两个点深入学习下去也是一种很好的选择。

我们还没有说github,github的重要性其实根本不必多提,开源的framework,控件什么的基本都在上面。其他网站比如,CocoaControls,有很多开源的UI控件,建议订阅个RSS。此外cocoaneticsCocoa is my girlfriend等博客类型的网站也经常会发布一些非常优秀的文章。

总之想“精通”iOS,绝对不是短时间可以做到的,贵在坚持不断学习。

精简版:

  • 项目驱动
  • 寻找帮助
    • 文档
    • WWDC视频
    • StackOverflow
    • Google
  • 书,不是很必要
  • 学习XCode使用
  • 手动管理内存,熟练以后再开启ARC
  • 找代码,开源库:github, CocoaControls
  • 好文章:
    • cocoanetics
    • Cocoa is my girlfriend
    • 等等

补充:

一些非常好的资源:

  • RAYWENDERLICH   |  简单高质量的Tutorial,涵盖iOS Mac Cocos2D Unity等内容

 

–以上–

Tagged with:  

继之前翻译的两篇[iOS]如何避免图像解压缩的时间开销iPad 3 图片解压缩测试,Cocoanetics又在iPhone 5发布后测试了一下iPhone 5的图片解压缩能力:iPhone 5 Image Decompression Benchmarked。这次还是挑重点翻译一下,以后的文章如果没什么技术因素只是单纯跑分的话就不继续翻译了,大家自行到Cocoanetics上看数据就行了,这些数据对我们评估应用的性能还是很有帮助的。


以下为译文:

我们希望在水果每个新的CPU的重复我们的benchmark,这次测试中,我们对benchmark进行了一些改进,将之前打印log的NSLog替换成了CFAbsoluteTime,NSLog本身每次都要消耗多大50ms的时间,新的方法更加精确,不会包含打印log本身的时间。

测试结果:iPhone 5 是上代处理器性能的2倍

我们之前在较早的iOS设备上运行benchmark的结果已经表明GPU性能的提升并不影响我们的测试结果,同时,渲染速度的提升看起来完全是CPU的功劳,这一点也也被一个在WWDC的苹果工程师证实,这位工程师告诉我所有的UIKit图片解压缩都依靠CPU完成。

唯一一个让图片解压缩由GPU完成的方法是使用CoreImage,但问题时虽然解压缩更快了,但将解压缩侯的图片传递到CALayer上并进入渲染树(render tree,这个概念文档和WWDC里都讲过,CALayer部分,不清楚的可以看看,基本就是字面意思)仍是一个瓶颈。所以通过CoreImage解压图片仅当你能够将它们保持在GPU的时候才有效果,比如视频处理以及使用作为3D纹理使用。

虽然我们的benchmark只关注CPU,但是我们仍认为这是一个评估每代CPU性能的一个有效方法。

iPhone 5 测试结果:

128x96

 

256x192

 

512x384

 

1024x768

 

2048x1536

 

iPhone 5对PNG的解压缩能力提升明显,优化过(crushed)的处理时间仅仅比未优化的(uncrushed)快一点,差距很小。但对于较早的设备来说,二者差距很明显。

总结:

A6处理器支持VFPv4,一个特殊的并行浮点操作指令集,VFP是“Vector Floating Point”的缩写,就是这个指令集的出现让Anandtech得出结论:A6是水果第一个完全自家设计的SoC(The iPhone 5's A6 SoC: Not A15 or A9, a Custom Apple Core Instead),虽然基于ARM 7。

如果我理解正确,那么VFP性能提升来自其中的32个寄存器(比VFPv3多一倍),这意味着可以同时处理两倍的浮点数,从而揭示了A6两倍浮点性能的提升,这同时也对降低电量消耗有帮助。

水果为iPhone 5自主设计的硅片让它以2倍的性能轻松战胜了前代设备。

–以上–

Tagged with: