博客园第三方客户端-i博客园正式发布App Store
1. 前言
算来从15年8月到现在自学iOS已经快7个月了,虽然中间也是断断续续的,不过竟然坚持下来了。年后要找实习啦,于是萌生了一个想法 —— 写一个app练练手。这次我没弄后台了,直接使用了博客园的open api(嘿嘿)。之前也做过一个app,叫做魔界-魔术,前后端都是我弄的,不过后端使用的是Bmob后端云(一个Baas服务),但是作为第一个app,代码上感觉很混乱,而且基本上都是用的第三方控件。这次的i博客园是我完全独立开发的(包括UI设计),整体使用的是MVC模式,并且尽量不去使用别人第三方控件(虽然还是用了。后面会提到具体使用)。
先放出几张app的gif预览图片:
Appstore地址:
大家可以在AppStore搜索i博客园。或者扫描下面二维码:
2. 使用的资料和工具
- 博客园官方open web api网址:
- 使用到的第三方控件
- AFNetworking
- SDWebImage
- HMSegmentedControl(Segmented Control)
- RESideMenu (侧滑控制器视图)
- MJRefresh
- Masonry (AutoLayout)
- UITableView+FDTemplateLayoutCell (动态计算UITableViewCell的高度)
- XMLDictionary (解析XML文件,因为博客园web api传回来的是xml数据)
- UI资源和工具
- http://www.easyicon.net/ (图标素材)
- Sketch
- PhotoShop
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-C(PJXAnimatedTabBarController 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上。