(没时间维护,已下架)博客园第三方客户端-i博客园正式发布App Store

(没时间维护,已下架)博客园第三方客户端-i博客园正式发布App Store

1. 前言


算来从15年8月到现在自学iOS已经快7个月了,虽然中间也是断断续续的,不过竟然坚持下来了。年后要找实习啦,于是萌生了一个想法 —— 写一个app练练手。这次我没弄后台了,直接使用了博客园的open api(嘿嘿)。之前也做过一个app,叫做魔界-魔术,前后端都是我弄的,不过后端使用的是Bmob后端云(一个Baas服务),但是作为第一个app,代码上感觉很混乱,而且基本上都是用的第三方控件。这次的i博客园是我完全独立开发的(包括UI设计),整体使用的是MVC模式,并且尽量不去使用别人第三方控件(虽然还是用了。后面会提到具体使用)。

先放出几张app的gif预览图片:

1 2 3

Appstore地址:

大家可以在AppStore搜索i博客园。或者扫描下面二维码:

2016-02-18-1009004908

 

2. 使用的资料和工具


  • 博客园官方open web api网址:
  1. http://wcf.open.cnblogs.com/news/help (新闻)
  2. http://wcf.open.cnblogs.com/blog/help (博客)
  • 使用到的第三方控件
    • AFNetworking
    • SDWebImage
    • HMSegmentedControl(Segmented Control)
    • RESideMenu (侧滑控制器视图)
    • MJRefresh
    • Masonry (AutoLayout)
    • UITableView+FDTemplateLayoutCell (动态计算UITableViewCell的高度)
    • XMLDictionary (解析XML文件,因为博客园web api传回来的是xml数据)
  • UI资源和工具

3. 解决的问题


问题一:实现引导页(不是启动页)上的RippleButton(有水波涟漪动画的按钮,第一张gif图片上的那个粉红色按钮)

解决思路:

1. 使用UIBesierPath构建一个圆形的path

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:pathFrame cornerRadius:self.layer.cornerRadius];

2. 将上面的path赋值给circleShape(CAShapeLayer对象)的path属性,同时添加该circleShape到RippleButton(UIView类型)上

CAShapeLayer *circleShape = [CAShapeLayer layer];
circleShape.path = path.CGPath;
[self.layer addSublayer:circleShape];

3. 这时,就可以使用Core Animation来操作RippleButton的layer了,细节我就不详细说了,无非是通过动画控制圆圈的scale和alpha

CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
scaleAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2.5, 2.5, 1)];

CABasicAnimation *alphaAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
alphaAnimation.fromValue = @1;
alphaAnimation.toValue = @0;

CAAnimationGroup *animation = [CAAnimationGroup animation];
animation.animations = @[scaleAnimation, alphaAnimation];
animation.duration = 1.0f;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[circleShape addAnimation:animation forKey:nil];

4. 但是如果仅仅添加一个circleShape,那么不会有多个水波散开的效果。于是我又将上述123步代码封装成createRippleEffect函数,并添加到定时器中

- (void)setupRippleTimer
{
    __weak __typeof__(self) weakSelf = self;
    NSTimeInterval repeatInterval = self.repeatInterval;
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, repeatInterval * NSEC_PER_SEC, 0);
    
    __block NSInteger count = 0;
    dispatch_source_set_event_handler(self.timer, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            count ++;
            // 水波纹重复次数,默认-1,表示永久
            if (self.repeatCount != -1 && count > weakSelf.repeatCount) {
                [weakSelf stopRippleEffect];
                return;
            }
            [weakSelf createRippleEffect];
        });
    });
}

问题二:48小时阅读和十日推荐中使用了UICollectionView,自定义了UICollectionViewLayout,实现轮盘旋转的效果(部分代码参考了AWCollectionViewDialLayout

解决思路:

1. 首先得知道自定义UICollectionViewLayout的具体流程

实现自定义的UICollectionViewLayout的具体流程请参考这篇文章,很详细!

2. 整个自定义UICollectionViewLayout实现过程中,最核心的要数layoutAttributesForElementsInRect这个函数了

2.1 首先根据rect的y值来计算出哪几个cell在当前rect中:

// 在rect这个区域内有几个cell,返回每个cell的属性
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray *layoutAttributes = [NSMutableArray array];
    
    CGFloat minY = CGRectGetMinY(rect);
    CGFloat maxY = CGRectGetMaxY(rect);
    // 获取到rect这个区域的cells的firstIndex和lastIndex,这两个没啥用,主要是为了获取activeIndex
    NSInteger firstIndex = floorf(minY / self.itemHeight);
    NSInteger lastIndex = floorf(maxY / self.itemHeight);
    NSInteger activeIndex = (firstIndex + lastIndex) / 2; // 中间那个cell设为active
    // maxVisiableOnScreeen表示当前屏幕最多有多少cell
    // angularSpacing表示每隔多少度算一个cell,因为这里是轮盘,每个cell其实看做一个扇形
    NSInteger maxVisiableOnScreeen = 180 / self.angularSpacing + 2;
    
    // firstItem和lastItem就表示哪几个cell处于当前rect
    NSInteger firstItem = fmax(0, activeIndex - (NSInteger)maxVisiableOnScreeen/2);
    NSInteger lastItem = fmin(self.cellCount, activeIndex + (NSInteger)maxVisiableOnScreeen/2);
    if (lastItem == self.cellCount) {
        firstItem = fmax(0, self.cellCount - (NSInteger)maxVisiableOnScreeen);
    }
    // 计算rect中每个cell的UICollectionViewLayoutAttributes
    for (NSInteger i = firstItem; i < lastItem; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes *attributes= [self layoutAttributesForItemAtIndexPath:indexPath];
        [layoutAttributes addObject:attributes];
    }
    
    return layoutAttributes;
}

2.2 计算每个cell的UICollectionViewLayoutAttributes

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    // 默认offset为0
    CGFloat newIndex = (indexPath.item + self.offset);
    UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    
    attributes.size = self.cellSize;
    
    CGFloat scaleFactor, deltaX;
    CGAffineTransform translationT;
    CGAffineTransform rotationT;
    
    switch (self.wheetAlignmentType) {
        case WheetAlignmentTypeLeft:
            scaleFactor = fmax(0.6, 1 - fabs(newIndex * 0.25));
            deltaX = self.cellSize.width / 2;
            attributes.center = CGPointMake(-self.radius + self.xOffset, self.collectionView.height/2+self.collectionView.contentOffset.y);
            rotationT = CGAffineTransformMakeRotation(self.angularSpacing * newIndex * M_PI / 180);
            translationT = CGAffineTransformMakeTranslation(self.radius + deltaX * scaleFactor, 0);
            break;
        case WheetAlignmentTypeRight:
            scaleFactor = fmax(0.6, 1 - fabs(newIndex * 0.25));
            deltaX = self.cellSize.width / 2;
            attributes.center = CGPointMake(self.radius - self.xOffset  + ICDeviceWidth, self.collectionView.height/2+self.collectionView.contentOffset.y);
            rotationT = CGAffineTransformMakeRotation(-self.angularSpacing * newIndex * M_PI / 180);
            translationT = CGAffineTransformMakeTranslation(- self.radius - deltaX * scaleFactor, 0);
            break;
        case WheetAlignmentTypeCenter:
            // 待实现
            break;
        default:
            break;
    }
    
    CGAffineTransform scaleT = CGAffineTransformMakeScale(scaleFactor, scaleFactor);
    attributes.alpha = scaleFactor; // alpha和scaleFactor一致
    // 先scale缩小,在translation到对应位置(因为是扇形,每个cell的x值和对应位置有关),最后rotation(形成弧形)
    attributes.transform = CGAffineTransformConcat(scaleT, CGAffineTransformConcat(translationT, rotationT));
    attributes.zIndex = indexPath.item;
    
    return attributes;
}

问题三:实现带动画的TabBarItem

解决思路:

不详细说了,我将代码提交到了Github - animated-tab-bar-Objective-CPJXAnimatedTabBarController is a Objective-C version of RAMAnimatedTabBarController(https://github.com/Ramotion/animated-tab-bar))。

主要就是自定义UITabBarItem,以及自定义UITabBarItem的AutoLayout构建。代码封装的很好,尤其动画实现部分,结构很清晰,符合OOP思想。

问题四:博客园使用的xml形式的open web api。解析困难。

解决思路:

这里我还是使用MVC思路,使用AFNetworking获取到XML数据,再使用XMLDictionary来解析XML数据(本质是NSXMLParserDelegate),并将其转化为Model(需要自己实现)。

关于NSXMLParserDelegate可以参考这篇文章 - iOS开发之解析XML文件

问题五:设计部分,不是很擅长,每个页面的布局都需要想很久,尽量做得简洁,有科技风。

解决思路:

基本上就是多看别人app设计,模仿,或者自己想啊想。也是第一次用Sketch,话说还挺好用的。

4. 存在问题和TODO


  • 分享到微信微博等等,准备使用友盟。
  • 涉及到UIWebView界面的排版,很丑。不是很懂CSS、JS、HTML5。之前为了一个图片适配搞了半天,其实只要在<head>中加上"img{max-width:100%%;height:auto;}"就行。恳请大家指点一下我。
  • 使用自定义TabBarItem后,隐藏TabBar很麻烦。
  • 离线阅读
  • ……

5. 后记


自己亲手去写代码确实感觉上是不一样,很多细节问题,虽然不难,但是很有挑战性。代码目前很挫,后面修改规范点,准备放到Github上。

posted @ 2016-02-18 10:14  桑果  阅读(7570)  评论(30编辑  收藏  举报