博客園第三方客戶端-i博客園正式發佈App Store 1. 前言 算來從15年8月到現在自學iOS已經快7個月了,雖然中間也是斷斷續續的,不過竟然堅持下來了。年後要找實習啦,於是萌生了一個想法 —— 寫一個app練練手。這次我沒弄後臺了,直接使用了博客園的open api(嘿嘿)。之前也做過一個a
博客園第三方客戶端-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上。