因 NVIDIA WebDriver 证书过期造成的黑苹果无法正常启动的临时解决方案

最近(2022.06.0x)几天, 使用 NVIDIA WebDriver 的黑苹果用户应该都会遇到开机读条无法进入系统的问题(白苹果可能也会遇到), 开 verbose mode 会看到类似的错误:

AppleKeySotre: opteration failed ...
ACM: findCredentialSet: returning, err = -2.

进安全模式还会发现无法重装 WebDriver, 提示 cant' be opened. You should move it to the Trash.

一系列问题的根源是 Nvidia 驱动的证书过期.

幸好国外网友已经有了解决方案, MacRumors, Cannot install or use Nvidia Webdrivers anymore!, 在此总结一下:

  1. 下载 WebDriver, 后面会用到
  2. 断网, 包括有线和 WiFi, 有线可以网拔网线, WiFi 可以直接在偏好设置里关
  3. 添加两条 hosts, 将 ocsp.apple.com 和 ocsp2.apple.com 都指向 127.0.0.1, 阻断后续连接
    • 可以用命令行: sudo sh -c 'echo "127.0.0.1 ocsp.apple.com" >> /etc/hosts' && sudo sh -c 'echo "127.0.0.1 ocsp2.apple.com" >> /etc/hosts'
  4. 清 dns: sudo killall -HUP mDNSResponder
  5. 清证书缓存 crlrefresh rp
    • (可能不需要, 但是我做了)删除缓存文件: sudo rm -f /var/db/crls/*cache.db && sudo rm -f /var/db/crls/*cache2.db
  6. 更改日期到以前 sudo date -u 020200002020
  7. 安装 WebDriver, 如果前面操作正确, 这里已经可以正常运行 WebDriver 安装包
  8. 重启, 之后就应该可以正常进系统了

进系统之后:

  • 可以联网正常使用
  • (还未测试) 应该可以删除那两条 hosts, 换来此次开机周期系统功能的完整性, 代价是关机后可能需要重新执行一遍上面流程

目前只有这种”临时”解决方案, 后续只能看 NVIDIA 能否更新一版驱动了.

[react-native/react]如何安装及使用 nuclide

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 的相关东西特少, 有问题都搜不到, 所以仔细读说明才是正确的做法

[iOS]一个 iOS 8.1 上关于 UINavigationController 的 NavigationBar 混乱的 Bug

首先说明, 这是一个会在 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

–以上–

[iOS] 如何实现可交互的 UIView 隐式动画

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

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

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

  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, 添加一些动画辅助代码:

- (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 状态的代码:

- (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.

然后是手势控制:

- (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. 新的代码如下:

- (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.

–以上–

[Xcode]如何在command-line下编译带有sub-project的项目

针对含有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, 及本文参考见这里

Objective-C代码格式整理

团队合作中代码风格一致比较重要, 像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一个毛病.

–以上–

[iOS]UIScrollview自定义分页的实现方法

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

方法一

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

 

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;
}

–以上–

kernel_task占用大量CPU的解决方案

如果你在使用MacBook、MacBook Pro、MacBook Air或者其他可能的Mac设备(我不知道iMAC和Mac mini有没有这个问题)遇到kernel_task长时间占用大量CPU(通常是500%-600%,也有200%,视机型而定)的情况,如果你正在播放Flash、进行高强度运算,或者正值高温天气。那么本文很可能会挽救你于水深火热之中。

首先解释一下kernel_task为什么会占用如此多的CPU。相信你从前文的描述中就可以发现,这是机器过热造成的。事实上kernel_task并不是真的在占用大量CPU,这么做的目的只是为了从那些疯狂消耗CPU的程序中抢夺系统资源,从而达到降低系统开销的目的,进而降低了机器的温度,一旦温度低到一个值,kernel_task也就恢复正常了。这在环境温度比较低的情况下是非常有效的降温手段,而在炎热的夏季,就会有kernel_task迟迟不能恢复正常的情况发生。

根据上面的分析,我们可以发现解决问题的关键就是如何让机器温度快速降低下来。方法自然有很多了,搞个大风扇对着电脑狂吹,在保证机器安全的情况下往机器上浇点水什么的,或者找个调解风扇转速软件(显然我不知道什么软件好用,因为我用了前两种方法)。还可以参考一下这篇文章的内容及相关讨论。

希望本文能帮助到被这个问题困扰着的人。如果本文没有解决你的问题,那么也有可能是其他原因造成kernel_task疯狂占用CPU,你可以尝试一个一个关闭正在运行的程序,看看是哪个造成的问题,不行就去Genius Bar求助吧。

–以上–

[iOS]如何在UIWebView中响应双击事件

最近做的一个项目中涉及这样一个需求: 响应用户点击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文档就不能接受到任何点击事件了.

–以上–

如何在Cocoa中设置文件的权限

这篇属于技巧性文章, 没什么营养.

如果你常写NSTask相关的东西, 可能需要设置一个脚本的权限为可以执行. 有各种Unix方法, 也有只用Cocoa就能实现的:

NSNumber *permissions = [NSNumber numberWithUnsignedLong: 493];
NSDictionary *attributes = [NSDictionary dictionaryWithObject:permissions forKey:NSFilePosixPermissions];
[NSFileManager defaultManager] setAttributes:attributes ofItemAtPath:path error:&amp;err]

简单解释一下那个"493", 这个就是十进制的"755", 也就是"rwx-xr-x", 所以如果你想要"777"的权限, 就用"511".

用NSFileManager创建文件的时候也可以传递上面的attributes, 创建可执行的文件.

–以上–