iOS
Comments

初来乍到

知道这个活动是一个在佛山的小伙伴邀请我一起参加,以前对黑客马拉松一直神往但总没机会尝试,于是抱着有些事现在不做就一辈子都不会做了的心态,我跟他还有另外两个95后小伙伴相约一起,朝深圳粗发。 从广州到深圳,我第一次体验顺风车,直接到会场而且价格比高铁便宜很多,很赞。

头脑风暴

题目是这样:https://shimo.im/doc/MyY7ltlPPo4F6YSo 拿到题目,我们4个人迅速头脑风暴了一下。 想到达芬奇,最耳熟能详的当然就是那副名画《蒙娜丽莎的微笑》,然后很迅速地,我想到一个idea。就是通过人脸识别,把自拍或者照片的笑脸替换成蒙娜丽莎的微笑,蜜汁喜感。但是考虑到实现的话,我们一时之间没有多大头绪,因为这需要动态的图形拼贴等处理,我们并不精于此道。好在得到这个灵感,我们迅速想到了另一个idea——做一个自拍表情PK的App。大致原理就是,通过表情识别,我们可以为一张照片评出他的情绪以及程度,这时候我们让两个人上传他们的自拍,然后对决PK,通过识别结果判断胜负。在我们一致认可这个idea之后,我给这次的项目取了个名字 FaceBattle。

开始coding

在丰盛的午餐以及下午茶的陪伴下,我们开始迅速讨论作战方案,由于我们4个人刚好是一个iOS,一个Android,一个UWP(😄情怀),还有一个服务端,简直一不小心就全平台了😂。当然,在开发之前,我们在纸上讨论了大致了原型以及需要的接口,然后负责WP的超哥也有模有样地迅速撸了几个界面。大家都像打了鸡血一样,争分夺秒这个词无比恰当地装饰着周遭的气氛中。

不眠之夜

俗话说得好,人生不如意之事十之八九。随着时间的推进,疲惫使我们渐渐力有未逮,而且在服务端与微软的表情识别接口的联调上也出现了一些问题,直到晚上,我们的进度开始捉襟见肘。吃完了棒呆的夜宵,困意如约而至,而我们唯有用意志对抗阻力,注定在这个不眠不夜继续开发与调试。 在我们的努力“马拉松”下,终于把APP完成了八九不离十的程度,小憩一会之后,太阳冉冉升起,晨曦笼罩了大地。

路演

到了中午就是“是骡子是马拉出来溜溜”的路演环节了,这次一共有12支队伍参赛,有一些经验十足的队伍,还有几个外国友人组成的队伍,说实话还是蛮有压力的。轮到我们解说作品的时候,我先扯了我们作品与文艺复兴的关系(因为这次马拉松的主题和参赛题目都跟文艺复兴有关),然后演示了在不同平台上进行battle的场景,当我看到在场的观众因为我们的新意而感到好奇和愉悦的时候,顿时觉得无论得奖与否,我们的作品已经发挥了应有的价值:) 最后出乎意料地,我们拿到了二等奖,并且与最佳人气奖只有一票之差,第一次参加能有这样的成绩说实话我已经非常知足了(果然抱对了大腿的感觉)。

尾声

我想这次经历无论对我还是另外几个小伙伴,都是宝贵的人生财富,而且我们本身也学到了很多。当然对我来说,更值得高兴的是又交到了几个志同道合的朋友。 最后,把我们的些许劳动成果开源一下,虽然时间有限可能质量并不是很好。

https://github.com/JuniperPhoton/FaceBattleUWP (WP版,里面还有设计稿)

https://github.com/KinoAndWorld/FaceBattle (iOS版)

还有 微软的https://www.azure.cn/cognitive-services是很棒的东西。

iOS
Comments

前几天在用UICollectionView实现组合式布局的时候,想到如果在CollectionView上要求sticky header(也就是header自动悬停置顶)的效果应当如何实现。 我们都知道如果是在UITableView上实现非常简单,只要设置style为group然后多个section,headerView就会自动吸附在顶部。而collectionView虽然功能强大得多,但是原生并没有支持直接设置这个效果,所以需要自己实现。

作为一个懒人,首先想到的自然是站在巨人的肩膀上,CSStickyHeaderFlowLayout是一个比较知名的CollectionView布局控件,它支持sticky header效果以及头部的视差滚动效果,效果很赞。但是感觉稍微重了些,需要做更多的设置以及修改基类等。

然后我又找到了一篇文章,虽然已经是3年前的文章,但是写得非常简明清晰,而且阅读完有豁然开朗的感觉。

文章很短,全文如下:http://blog.radi.ws/post/32905838158/sticky-headers-for-uicollectionview-using

核心原理就是三句话:

  • The header should be positioned so it can never go further up than one header height above the first cell in the section.
  • The header should be positioned so it can never go further down than one header header height above the lower bounds of the last cell in the section.
  • The header should be positioned so it usually stays around the top edge, referencing the content offset of the collection view.

代码也很简单,只需要在UICollectionViewFlowLayout实现layoutAttributesForElementsInRectshouldInvalidateLayoutForBoundsChange两个方法。

接着我附上文中源代码以及自己写的注释,穿插了一些对文章初略的翻译。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    /**
     *  返回当前显示区域的所有布局信息
     */
    NSMutableArray *answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
    UICollectionView * const cv = self.collectionView;
    CGPoint const contentOffset = cv.contentOffset;

    NSMutableIndexSet *missingSections = [NSMutableIndexSet indexSet];

    /**
     *  找出所有UICollectionElementCategoryCell类型的cell
     */
    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
        if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
            [missingSections addIndex:layoutAttributes.indexPath.section];
        }
    }
    /**
     *  再从里面删除所有UICollectionElementKindSectionHeader类型的cell
     */
    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
        if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
            [missingSections removeIndex:layoutAttributes.indexPath.section];
        }
    }

    /**
     *  默认情况下,为missingSections手动插入attributes,应该是为rect外的section生成attributes
     */
    [missingSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {

        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];

        UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];

        [answer addObject:layoutAttributes];

    }];


    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {

        /**
         *  从answer中储存的布局信息中,针对UICollectionElementKindSectionHeader...
         */
        if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {

            NSInteger section = layoutAttributes.indexPath.section;
            NSInteger numberOfItemsInSection = [cv numberOfItemsInSection:section];

            /**
             *  为什么需要firstCellIndexPath和lastCellIndexPath呢?
             *  header应当保持着距离本section第一个cell的最大距离,简单来说就是header在置顶之前要贴着cell
             *  同理,header应当保持着与最后一个cell的最小距离,
             *  最后,在不违反上面两则约束的前提下,通过collection view的offset与header的高度来使header处于 origin.y = 0 的状态。
             */
            NSIndexPath *firstCellIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
            NSIndexPath *lastCellIndexPath = [NSIndexPath indexPathForItem:MAX(0, (numberOfItemsInSection - 1)) inSection:section];

            /**
             *  针对当前layoutAttributes的section, 找出第一个和最后一个普通cell的位置
             */
            UICollectionViewLayoutAttributes *firstCellAttrs = [self layoutAttributesForItemAtIndexPath:firstCellIndexPath];
            UICollectionViewLayoutAttributes *lastCellAttrs = [self layoutAttributesForItemAtIndexPath:lastCellIndexPath];

            /**
             *  获取当前处理header的高度和位置,然后通过firstCellAttrs和lastCellAttrs确定header是否置顶
             */
            CGFloat headerHeight = CGRectGetHeight(layoutAttributes.frame);
            CGPoint origin = layoutAttributes.frame.origin;
            origin.y = MIN(
                           MAX(
                               contentOffset.y,
                               (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)
                               ),
                           (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight)
                           );

            layoutAttributes.zIndex = 1024;
            layoutAttributes.frame = (CGRect){
                .origin = origin,
                .size = layoutAttributes.frame.size
            };
        }
    }
    return answer;
}

- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound {
    return YES;
}

感觉又涨了姿势,撒花。

iOS
Comments

相信我们开发的项目中,只要涉及到网络交互,都会遇到一个再普遍的不过的需求,那就是出于用户体验的需要,在请求开始的时候显示加载页,请求到空数据的时候显示空内容页,以及请求出错的时候显示的错误或重试页。这三类页面在一个项目中通常是一致的(至多会有图标和文案的变化),但却要求可能在每一个涉及网络请求的页面呈现。

(;′⌒`)

如果没有大局观,一开始接到需求就开始在某ViewController里面添加几个View用来展现。

举个例子,如果要添加一个loading view,

1
2
@property (strong, nonatomic) UIView *loadingView;
@property (strong, nonatomic) UIImageView *loadingImageView;

然后增加方法,视图的初始化和配置就省略了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 *  加载视图
 */
- (void)startLoading{
    [self.view addSubview:self.loadingView];
    [self.loadingImageView startAnimating];
}

/**
 *  停止加载并消失
 */
- (void)stopLoading{
    [self.loadingImageView stopAnimating];
    [self.loadingView removeFromSuperview];
}

OK,这样做实现上没问题,但是遇到下一个需要展示这些页面的ViewController,只能使用copy&paste大法,把property和方法实现都搬到另一个ViewController,倘若有10个以上的页面,再加上万一需要修改页面的视图结构,你就会深刻的体会到

总所周知有个大原则叫做Don’t repeat yourself。再运用上我们不为什么就很熟练的面向对象思维,自然而然可以想到,使用继承大法。

╮(╯_╰)╭

实现方法很简单

  • 首先创建一个BaseViewController,将几个property转移过来,并且展现方法也照搬,在.h文件暴露出来。

  • 然后把所有用到的Controller都继承自BaseViewController,调用的地方可以保持不变,会自动调用父类的方法。

但是这样做还是不够好,因为这需要我们把所有的Viewcontroller的头文件都改一遍,引入BaseViewController并集成,就是所谓这是带有侵入性的。更致命的是,如果你的ViewController本身集成了另外的BaseController,由于Objective-C不支持多继承,你只能去修改另一个BaseController……有点悲伤。

╭(′▽`)╯

通过标题的剧透,我们知道最后的实现跟runtime有关,那么主角也该出场了。 其实就是使用Category + runtime的对象关联。在上面的方案中,解决集成BaseViewController的侵入性的方案就是使用category为UIViewController添加方法,但是category是不能直接使用property保存私有变量的,于是引入runtime的AssociatedObject系列方法,可以动态为对象添加成员变量,这几乎是runtime最基础的应用。

非常简单的,只用到两个方法,其实就是一个Setter和Getter

id objc_getAssociatedObject(id object, const void *key)

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

如果想深入探究一下,有人已经写得挺全面了,可以点击这篇文章

在本例中,用法大概是这样

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
#pragma mark - Getter

- (UIView *)loadingView{
    UIView *loadingView = objc_getAssociatedObject(self, &PresnterLoadingViewKey);
    if (!loadingView) {
        loadingView = [[UIView alloc] initWithFrame:self.view.bounds];
        objc_setAssociatedObject(self, &PresnterLoadingViewKey, loadingView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

        loadingView.backgroundColor = [UIColor whiteColor];
        [loadingView addSubview:self.loadingImageView];
    }
    return loadingView;
}

- (UIImageView *)loadingImageView{
    UIImageView *imageView = objc_getAssociatedObject(self, &PresnterLoadingImageViewKey);

    if (!imageView) {
        imageView = [[UIImageView alloc] initWithFrame:
                         CGRectMake(self.view.bounds.size.width / 2 - 100, self.view.bounds.size.height/2 - 80, 200, 150)];

        NSMutableArray *tmpArr = [NSMutableArray array];
        for (int i = 0; i <= 80; i++) {
            UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"01-progress00%02d.jpg",i]];
            [tmpArr addObject:image];
        }
        [imageView setAnimationImages:[NSArray arrayWithArray:tmpArr]];
        imageView.animationDuration = 2.0;

        objc_setAssociatedObject(self, &PresnterLoadingImageViewKey, imageView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    return imageView;
}

把实例化放进Getter,这样实现方法可以同上保持不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 *  加载视图
 */
- (void)startLoading{
    [self.view addSubview:self.loadingView];
    [self.loadingImageView startAnimating];
}

/**
 *  停止加载并消失
 */
- (void)stopLoading{
    [self.loadingImageView stopAnimating];
    [self.loadingView removeFromSuperview];
}

最后只需要在你需要用到这些页面的ViewController引入UIViewController+Presenter.h 然后展示就好

1
[self startLoading]; //加载完成后调用 [self stopLoading];

具体的代码我写了个demo放在github上,地址在这里

另外实现了空白视图和失败重试视图的功能,跟loading页大同小异。 如此一来,这些公共页面的展示逻辑基本被封装进了category中,而且当我们需要修改展示的页面时,也只需修改文件里面的实现,然后暴露出方法,简单高效。

iOS
Comments

几乎每一个App都有一个设置菜单页,而且他们几乎都长这样:

反面教材

最简单最原始的做法是这样的:

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
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"
                                                          forIndexPath:indexPath];

  switch (indexPath.row) {
      case 0:
          // configure cell
          break;
      case 1:
          // configure cell
          break;
      case 2:
          // configure cell
          break;
  }
  return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];

    switch (indexPath.row) {
        case 0:
            // click cell 0
            break;
        case 1:
            // click cell 1
            break;
        case 2:
            // click cell 2
            break;
    }
}

这样做不仅违反了DRY原则,在菜单项比较多的情况下烦不胜烦,而且一旦需要修改某个菜单项的内容,或者插入或者删除,修改起来都是非常容易出错的,换句话说,可维护性非常差。

抽象、依赖转移

通过观察我们可以发现,这些列表的结构非常相似,那么很自然地我们可以想到建立一个通用的模型来表示一个菜单.

KOSettingItem.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface KOSettingItem : NSObject

@property (copy, nonatomic) NSString *title;
@property (strong, nonatomic) UIImage *imageIcon;
@property (assign, nonatomic) UITableViewCellAccessoryType accessoryType;
@property (copy, nonatomic) void(^handleCallback)();

+ (instancetype) itemWithTitle:(NSString *)title
                          icon:(UIImage *)image;

+ (instancetype) itemWithTitle:(NSString *)title
                          icon:(UIImage *)image
                         block:(void(^)())handle;

- (id)initWithTitle:(NSString *)title
               icon:(UIImage *)image
              block:(void(^)())handle;

  
@end   
KOSettingItem.m
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
 @implementation KOSettingItem

  + (instancetype) itemWithTitle:(NSString *)title
                            icon:(UIImage *)image{
      return [[self alloc] itemWithTitle:title icon:image];
  }
  
  + (instancetype) itemWithTitle:(NSString *)title
                            icon:(UIImage *)image
                            block:(void(^)())handle{
      KOSettingItem *item = [[self alloc] initWithTitle:title icon:image block:handle];
      return item;
  }
  
  - (id)initWithTitle:(NSString *)title
                 icon:(UIImage *)image
                block:(void(^)())handle{
      if (self = [super init]) {
          self.title = title;
          self.imageIcon = image;
          self.handleCallback = handle;
          self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
      }
      return self;
  }
  
  @end
  

一个简单的模型就这样建好了,其中我们用block写了一个handleCallback用来处理表格的点击事件,接着我们来应用到TableView中,首先新增一个property @property (strong, nonatomic) NSArray *settingItems;

然后在ViewDidLoad中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
KOSettingItem *item1 = [KOSettingItem itemWithTitle:@"菜单1" icon:[UIImage imageNamed:@"Carrot"]];
[item1 setHandleCallback:^{
    NSLog(@"点击菜单1");
}];

KOSettingItem *item2 = [KOSettingItem itemWithTitle:@"菜单2" icon:[UIImage imageNamed:@"Owl"]];
[item1 setHandleCallback:^{
    NSLog(@"点击菜单2");
}];

KOSettingItem *item3 = [KOSettingItem itemWithTitle:@"菜单3" icon:[UIImage imageNamed:@"Rubber-Duck"]];
[item1 setHandleCallback:^{
    NSLog(@"点击菜单3");
}];

KOSettingItem *item4 = [KOSettingItem itemWithTitle:@"菜单4" icon:[UIImage imageNamed:@"Snowman"]];
[item1 setHandleCallback:^{
    NSLog(@"点击菜单4");
}];

self.settingItems = @[item1, item2, item3, item4];

最后修改一下TableView的Datasource和Delegate

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
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return self.settingItems.count;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return 44;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"
                                                            forIndexPath:indexPath];

    KOSettingItem *item = self.settingItems[indexPath.row];
    cell.textLabel.text = item.title;
    cell.imageView.image = item.imageIcon;
    cell.accessoryType = item.accessoryType;

    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];

    KOSettingItem *item = self.settingItems[indexPath.row];
    if (item.handleCallback) {
        item.handleCallback();
    }
}

我们发现再也不需要冗长的switch语句,如果需要增删菜单项,只需要修改settingItems里面的item顺序,并且由于点击事件绑定到KOSettingItem里面,也不需要担心事件与item不对应的情况,除非有特殊的样式需求,我们可以几乎不用再更改UITableViewDataSource与UITableViewDelegate的实现。

尾声

文章只是提供了一个思路,而且出于简明的目的KOSettingItem的模型非常简单,实际上你可以通过添加变量达到更多的可定制化效果,比如分割线的offset,自定义的accessoryView等等,完整的demo可以戳这里

运行一下,最后的结果如图,虽然还是平淡无奇 :)

最后吐槽一下自己,真的好久没写博客了(:з」∠)

iOS
Comments

前言

在我们当前的开发流程中,大致分为 开发-测试-修复Bugs-测试反馈 这样的循环中,特别是在功能需求频繁修改和UI细节不断调整的节奏中,我们通常需要不停地在Xcode上build-选择profile-archive,然后等待几分钟,导出ipa的包,然后再把包上传到一个版本仓库中,这个仓库或许是自己搭建的服务器,或许是TestFlight、Fir等更专业的内测版本平台,总之通常都需要几个步骤才能全部完成。倘若一日之内需要上传3到4个包,对工作效率的影响尚且不谈,重复同样的事情本身心情就不舒畅。

祭出利器

要如何做到把上述的N个步骤简化成一步呢,其实很简单。拿Fir为例,Fir提供了可供调用的app上传接口(其他平台我暂时不知道,如果是自己搭建的服务器肯定可以提供上传接口),而且还有好心人把接口写成了脚本(参考资源1),非常nice。但是这样的话还是要自己导出ipa,还是有些繁琐,于是我又找了些许资料,熟悉了xcodebuild的用法,还顺带找到了别人写好的另一个脚本(参考资源2),剩下的事情就非常简单了,我合并了二者的脚本,做了一些代码修改,详细的代码可以戳这里

这里边有些参数要修改成自己的项目,譬如

  • workspace name
  • provisioningProfile
  • scheme name
  • 还有Fir需要的AppID和UserToken

配置好之后,把这个脚本放在xcworkspace同级目录,然后终端运行 ./archive_to_upload_fir.sh 这时候你可以去喝杯咖啡奶茶美年达或者来一套广播体操,在一片宁静中完成了所有繁琐。

缺点与改进

当然我相信这并不是最终的解决方案,因为脚本还是存在着输出信息不够友好,如果出现编译错误还是要自己另外调试等不便,而且脚本本身还存在可以优化的地方,例如一些变量的参数化,让它变得更通用一些。


参考资源:

iOS
Comments

先说CGFloat

几乎众所周知CGFloat其实是float和double的大一统,32位与64位浮点的联姻

那么具体有什么不同?先来看看 CGFloat的一些常量,

CGFLOAT_TYPE
CGFLOAT_MAX
CGFLOAT_MIN
CGFLOAT_DEFINED
CGFLOAT_IS_DOUBLE

好奇心使然,看了下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Definition of `CGFLOAT_TYPE', `CGFLOAT_IS_DOUBLE', `CGFLOAT_MIN', and
   `CGFLOAT_MAX'. */
#if defined(__LP64__) && __LP64__
# define CGFLOAT_TYPE double
# define CGFLOAT_IS_DOUBLE 1
# define CGFLOAT_MIN DBL_MIN
# define CGFLOAT_MAX DBL_MAX
#else
# define CGFLOAT_TYPE float
# define CGFLOAT_IS_DOUBLE 0
# define CGFLOAT_MIN FLT_MIN
# define CGFLOAT_MAX FLT_MAX
#endif

/* Definition of the `CGFloat' type and `CGFLOAT_DEFINED'. */

typedef CGFLOAT_TYPE CGFloat;
#define CGFLOAT_DEFINED 1

然后很清晰地看到

CGFLOAT_TYPE         64位下是double 否则float
CGFLOAT_MAX          64位下是double的max 否则float的max
CGFLOAT_MIN          64位下是double的min 否则float的min
CGFLOAT_DEFINED      就是1 为定义而定义,暂时不知道用来判断什么
CGFLOAT_IS_DOUBLE    64位下是1 否则0

(:з」∠)我是不是很无聊

还有一个,当一个浮点型除以0的时候,有的语言会抛出异常,而oc会返回一个“非正常”的值+INF,我们可以用INFINITY常量判断。ex:(aFloat== INFINITY)

NSInteger

类似的, NSInteger也是囊括了int和long的大一统(还有NSUInteger,多个unsigned),定义跟上面的浮点型是一个模子的。 下面说说我踩过的一个坑。

在很久很久以前,我还是习惯用C语言的变量类型int,在5S出来以前大家相安无事,直到有一天一个bug袭击了我。

代码是这样的:

1
2
3
4
5
NSArray *array = [NSArray array];
int foundIndex = [array indexOfObject:@""];
if (NSNotFound == foundIndex) {
    NSLog(@"not found,handle it");
}

为什么iPhone4s,5之流都没问题,到5s上就不对了呢。

刚准备烧柱香拜一拜的时候,我点开了NSNotFound的定义,发现它==NSIntegerMax,而NSIntegerMax==LONG_MAX。

64位下,foundIndex因为超过表示范围会被截断成-1,因此它!=NSNotFound。

类似的还有很多,我们应该多多jump到源码,即使只是头文件,也能发现关于Objective-C的一些底层细节。

Comments

虽然对东野圭吾这个作家早就耳熟能详,但实际上,这本书是东野的作品中我看的第一本。 好久没有看到这么有意思的小说了,这样一本温柔平和却一针见血、节奏缓慢却扣人心弦的小说。

故事没有惊天动地的阴谋,没有精心策划的犯罪,有的只是一段段看似微不足道的小事,一个个有着各自烦恼的人。

每个人在生命的某个时刻一定有自己无法抉择的时刻,一定有迷茫于方向的时刻,而在一个机缘巧合之下,一个名为浪矢的杂货店开始接受写着烦恼的信件,并且用心地回复。逐渐地,更多的人开始投递自己的烦恼,而每一章的不同主人公,他们的身世和际遇,或温暖或坎坷地娓娓道来。

每个人命运的走向却和这些抉择有关,类似AVG(文字冒险游戏)一样,不同的抉择,有时候甚至细小到说了一句不同的话,露出了一个不同的表情,都有可能触发人生走向不同的境地,这就是所谓蝴蝶效应。而当每个人物面临自己人生的选择的时候,总能激起我思考,如果是我,我会怎么做。怎样才能幸福?

小说的剧情正以“来信->回信”的方式推进着,随着时间的推进,越来越多的人的烦恼被倾听,被以谓忠告。当然,如果仅仅是这样,这部小说仅仅是一部温情的纪录片,随着剧情的展开,一幅庞大的画卷展开,虽然每一章的人物不同,但他们却有着千丝万缕的关系,再辅以杂货屋的神秘时间操纵设定这一科幻元素,他们的命运锁链环绕着杂货屋交错缠绕,这在结局体现得淋漓尽致,俨然一个因果循环,看得我背脊一凉,大感佩服。

在命运面前,我们都要虔诚,然后斗争,努力,这样我们才能得到救赎。

PS:感觉这本书温和得像是儿童读物啊,没有任何三俗内容,建议编进小学教材。

文字拙劣,谨此为记。 2014/10/25

iOS
Comments

先说点题外话

貌似很久很久没更新博客,有半年了吧,这半年忙着毕业的各种事情,忙着小或者比较大的项目,感受着初入职场的艰辛与喜悦,以及过着平淡无奇的小日子————好吧虽然这些都可以成为懒惰的理由,但是回顾一些自己这半年真的过于放松,许多学习的时间被游戏动漫日剧所占据,如果平时不用上班恐怕要变成废宅了吧╮(╯_╰)╭,so,确定一下今后的目标:多写代码、多锻炼、多阅读、多写博客,写博客主要为了思考,因为要在脑中整理语言,沉淀思想,一方面可以巩固记忆,另一方面也算是[热爱生活]的一种方式吧。

好吧主题其实是

其实有点为了写博客而写博客的意思,因为要分享的东西挺简单的,更像是吐槽……好吧其实我就是吐槽。

  • 吐槽1:微信支付为啥没有iOS的官方demo……实在想不通,服务端有,Android甚至WindowsPhone都有,微信这是对iOS平台歧视的节奏- – 幸好我大天朝人才辈出,早有人根据渣成翔的官方文档写了微信支付的非官方demo

里面用了cocoapods, 需要下载的话好好看下说明。

  • 吐槽2:微信官方文档 虽然我知道,也理解 写代码的人一般比较讨厌写文档,但是毕竟你是一个支付组件,总有很多人需要仔细阅读的甚至copy文档里边的代码的,你可以写得很简单,但是不要犯一些低级错误好吗 (#‵′)。说明含糊不清我就不提了(其实上面的github主页有吐槽),实例代码都写错这就有点。。。 贴两段:
1
2
3
4
5
PayReq *request = [[[PayReq alloc] init] autorelease];
request.partnerId = _pactnerid;
request.prepayId= _prapayid;
Request.package = _package;
request.nonceStr= _noncestr;

第三行好好的request突然变成Request了……当时看到整个人都不太好

And

1
2
3
// 构造参数列表
NSMutableDictionary params = [NSMutableDictionary dictionary];
[params setObject:@"1234567" forKey:@"appid"];

NSMutableDictionary params。。。原来NSMutableDictionary不是引用类型啊T.T

有些文档看了真的会哭。

  • 吐槽3:微信支付SDK

处于安全便捷考虑,几乎所有的操作都在服务端完成,然后今天服务端给出API,也跟Android调通了。我本以为既然Android端都OK了,那iOS端应该也没多大问题……但是,我错了,我发现iOS端调用微信支付不成功,马上弹回原应用,拿到的是errCode = -1的错误。这些我就思密达了,然后一直在找是不是自己哪里调用不对。弄了一个多小时,未果。 然后很郁闷地吃了个饭,回去之后我跟服务端一步步联调……才发现问题出在PayReq的sign变量和package变量不对应。

再深入纠结原因,发现原来Android的文档或者SDK和iOS的处理是不一样的,而服务端是参考了Android的文档来做,我当场就呵呵了。 如果两端的处理方法不一样,至少也该说明一下吧。


Comments

二维码扫描库ZXingObjC功能完善

对于做过二维码扫描的大概都对ZXing这个库不陌生,然而这个库由于跨平台,代码文件过于臃肿,而且各种步骤非常麻烦。。。 还有不知道什么原因,ZXing的iOS库在最新发布版本不见了。 还是我找不到? 总之因为种种原因,ZXingObjC 作为良好的替代方案粗线了。

顺带一提,支持 cocoapods => ‘ZXingObjC’, ‘~> 2.2.6’

好吧,现在进入主题,这个库很好用,但是我们在做二维码的时候一般会有一个需求,让二维码进入一个区域方才扫描,即是扫描区域 让我比较郁闷的是,这么完善的库居然貌似没有实现这个很常见的功能。

然后上GitHub上看了一下这个Issue , 以为终于找到解决方案……却发现是个坑。具体怎么坑就不说了……也许是我不会用吧。

然后重新阅读了源码,发现了一个更简单的设置裁剪区域的方法。

Let’s go

首先,进入ZXCapture.h ,添加一个property

1
@property (nonatomic, assign) CGRect scanCrop;

然后进入ZXCapture.m 的这个方法:

1
2
3
4
5
  - (void)captureOutput:(ZXCaptureOutput *)captureOutput
ZXQT(didOutputVideoFrame:(CVImageBufferRef)videoFrame
     withSampleBuffer:(QTSampleBuffer *)sampleBuffer)
ZXAV(didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer)
       fromConnection:(ZXCaptureConnection *)connection;

将大概570行位置的代码修改为

1
2
3
4
5
6
7
8
9
10
CGImageRef videoFrameImage = NULL ;
  if(CGRectEqualToRect(self.scanCrop,CGRectZero)){
      videoFrameImage = [ZXCGImageLuminanceSource createImageFromBuffer:videoFrame];
  }else{
      videoFrameImage = [ZXCGImageLuminanceSource createImageFromBuffer:videoFrame
                                                                   left:_scanCrop.origin.x
                                                                    top:_scanCrop.origin.y
                                                                  width:_scanCrop.size.width
                                                                 height:_scanCrop.size.height];
  }

就这么简单。。。最后只要在客户端做类似这样的设置就好

1
2
3
4
self.capture.scanCrop = CGRectMake(_scanImageView.frame.origin.y,
                                   _scanImageView.frame.origin.x,
                                   _scanImageView.frame.size.width,
                                   _scanImageView.frame.size.height);

唯一需要注意的是,这个frame的 x 和 y 需要颠倒,具体原因应该跟capture的rotation有关。

暂时这么多吧- -虽然没人看~默默记录。