[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, 创建可执行的文件.

–以上–

[iOS]借助VoiceOver来破解优秀App的实现方式

为了开发出更好的iOS App,我们经常会去破解(我更喜欢叫Hack)优秀的App的实现方式,一般我们做的无非就是拆了ipa包浏览一下App所用到的图片资源,找个抓包工具看看都访问了那些网络资源,更虎点的就是去反编译,当然最基础的就是把玩一下各个功能,凭经验看出实现方法。

今天就介绍一个比较另类的Hack方式——VoiceOver,估计大部分人都没在自己的iOS设备上打开过这个功能,我就简单说一下效果吧,更细节的东西还是要各位亲身体验一下。

VoiceOver需要在:通用 >> 辅助功能 >> VoiceOver 页面里开启,打开后会进入一个相对比较难操作的环境,随着手指在屏幕上移动,会有一个黑框附着在手指附近的UI元素上面,同时随着黑框的移动,iOS设备会自动朗读UI元素的内容,不难发现VoiceOver这个黑框具有以下几个特性:

  1. 黑框所在位置就是当前UI元素的真实边框
  2. 对于一些元素比如UIButton,当黑框附着在起上面时,系统就会朗读“xxxxx按钮”,对于UITableView更是可以朗读出当前所在行号,以及一共有多少行。朗读的内容通常包含了这些UI元素的独有特性。
  3. 由于大部分的App都不会对“辅助功能”有所支持,有些没有文字的UI元素系统甚至会朗读其变量名,这跟反编译已经比较相似了。

借助第一个特性我们可以破解App的UI布局,借助第二个特性我们可以找出一些UI元素的实现方式,比如有些Gird View就是用UITableView实现的,如果系统朗读了行号等UITableView所具有的特征内容,我们就可以基本确定这个Gird View就是用TableView来实现的,TabBar之类的也是如此。借助第三条特性,我们可以通过其变量名判断其功能和实现方法。此外对于那些开源应用,变量名也可以方便我们更快地研究开源代码。

相对的,如果我们想避免App的关键实现细节被破解,就要在代码中加入防止VoiceOver响应的代码。

综上,虽然VoiceOver属于辅助功能范畴,但其一些特性为我们Hack App提供了方便。在一般的应用中加入Accessibility支持可能会使开发成本有所提高,但对于某些领域的应用,Accessibility支持无疑会成为应用的一大两点,在此也推荐大家看一下 WWDC 2011 Session 127 – Design Patterns to Simplify Mac Accessibility,真的挺有爱的。

–以上—

Xcode 4.3+ NSLog中文不输出Bug及解决方法

这个Bug折腾了我近一天,读一个文件,NSLog输出文件内容,结果死活读不完整,不光中文没有,英文也不全,考虑了编码,文件大小,文件位置等各种可能的因素,尝试用各种方式重写这个操作,最后发现是NSLog的问题,跟文件一毛钱关系都没有,感谢这两个链接:link1 link2 。

鉴于第二个链接已经解释的很清楚了,我这里就简要说一下,毕竟Wall还是有些麻烦

重现Bug很简单,Xcode 4.3+,用lldb在真机上运行(模拟器没有问题)下面代码:

NSLog(@"English1");
NSLog(@"中文");
NSLog(@"English2");

中文那行神马都不输出啊!如果NSLog一个NSString,String里面有中文,那么输出也会悲剧。

解决方法两种:

  1. 如果你执着于lldb,那么用Organizer >> Devices >> 你的设备 >> Console 这里会显示中文
  2. 按住Option点Run(或者 Product >> Edit Scheme…),Info >> Debugger 设置为GDB

一切回归正常,WTF!

另外有人说4.3.2解决了这个问题,事实是:没有解决!

祝愿被这个问题折腾死的人能早日看到这篇文章或者link2那篇文章。

Becareful with XCode!!

–以上–

XCode 4.3.1一个小众Bug及解决方法

哦… 首先希望闲来无事发现此文的人,可以回复报下你们一天中XCode Crash的次数,附上版本,大家比比谁受气受得多。(我:4.3.1 10多次crash)

目前测试Bug的环境:XCode 4.3.1,用svn作为版本控制
PS. 4.3可能也有这个Bug,4.2.1貌似没有,但不排除svn下的其他毛病

Bug产生过程:

  1. Project处于svn版本控制之下
  2. (可有可无)向Project中添加文件,并加入版本控制(A)
  3. 从项目中Delete某个文件,并选择Move to trash
  4. 编译的时候会有Wraning提示缺少了之前删除的那个文件(Missing File xxxx)

虽然这是一个误报,对编译没有任何影响,但是有Warning看着就心烦,而且如果删除了很多文件就会造成满屏Warning,既影响心情,又影响工作。

下面是几种解决方法

  1. 如果你对XCode 4.3+移植抱有敌视态度,那么你的/Developer或者废纸篓里面可能还有XCode 4.2.1,用4.2.1进行删除的步骤,那些Warning就不会出现。
  2. 完全用XCode 4.3.1 + Terminal解决
    1. 对那些你要删除的并且有"A"(版本控制)标记的文件,右键 >> Source Control >> Discard Changes…
    2. 右键 >> Source Control >> Ignore
    3. Delete并Move to trash

这样编译就不会有warning,如果你删除的是一个文件夹,那么还会提示Missing这个文件夹,再或者上一步删除文件之前忘记设置ignore了,warning就会出现,用终端解决:
1. 打开终端,cd到项目主目录下
2. svn status,可以看到那个Warning的文件(夹)
3. svn rm xxxxx 从版本控制里去掉那个文件(夹)

这时再编译就不会有那个warning了,世界终于清净了

总结:XCode 4+对svn支持非常废,经常有莫名其妙错误,要手动解决,所以一个更方便的方法就是不用svn,现在git这么火,不怕开源的项目就直接放到github上得了,保密的就放到Bitbucket上,或者找个服务器自己搭一个,总比折腾svn这些破事好,不过广大开发者最终还是应该鄙视苹果的,照这么下去苹果还是回到几年前的小众状态比较好,那时无论是SL,还是Xcode 3都是精品中的精品。现在苹果到了风口浪尖上,做东西都很浮躁,各种神奇,傻X的Bug层出不穷,老乔也不托梦骂他们几句。

懒得喷了,话说中午写的这篇,晚上发的,发现XCode 4.3.2出了,不知道老乔显灵没,如果还Crash记得上App Store评一星啊!link

更新:恭喜,XCode 4.3.2下这个Bug依旧

–以上–

XCode 4.2 地点模拟技巧

XCode 4.2终于支持地点模拟了,不用忍受真机调试的各种不便了,模拟方法也很简单(恕我盗用几个别人的图):

当Debug一个需要地理位置信息的App时,在Debug栏默认就会有地点模拟的图标,如下图所示:

Xcode simulator location services3

这个有一个前提:必须是iOS5的模拟器.

默认只提供了几个地点,但是可以通过GPX文件来添加.你可以选择到网上找现成的GPX,但是Apple还是很为广大开发者考虑的,提供了GPX的模版,创建方法 新建>>Resource>>GPX File 如下图:

Xcode simulator location services2

其实所谓的GPX就是一XML文件,默认的内容如下:

<?xml version="1.0"?>

<gpx version="1.1" creator="Xcode">

    <wpt lat="37.331705" lon="-122.030237">

         <name>Cupertino</name>

    </wpt>

</gpx>

只要改下经纬度,改下名字,就是你想要的地点了,经纬度可以用GoogleEarth获取.

添加了GPX文件后就可以在之前选择地点那里看到你的GPX文件了.

—以上—