源码解读--知乎日报
阅读知乎日报源码--总结
第一部分:首页(home)
构成:
-
顶部的自定义pictureView轮播
- 设置定时器(self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(nextImage) userInfo:nil repeats:YES]😉
- 当scroll即将开始滚动时,停止定时器([self.timer invalidate]😉.
- 结束时又开启定时器,并且判断当前的x偏移值,设置scroll的contentsOffset
- 这个自定义pictureView只负责把点击的图片的integer值传递给它的代理(这里是HomeVC),然后具体的跳转事件由代理者完成
-
顶部的自定义的RefreshView刷新进度动画条
-
这个进度条其实是一个CAShapeLayer,,然后是被加载到这个RefreshView的本身的layer上面去的
CAShapeLayer *progressLayer = [CAShapeLayer layer]; [refreshView.layer addSublayer:progressLayer]; progressLayer.strokeColor = [UIColor whiteColor].CGColor; progressLayer.fillColor = [UIColor clearColor].CGColor; progressLayer.backgroundColor = [UIColor clearColor].CGColor; progressLayer.strokeEnd = 0.0; progressLayer.transform = CATransform3DMakeRotation(-M_PI_2, 0, 0, 1); progressLayer.lineWidth = 2.0;
2.然后监听这个RefreshView的调用者(TableView或者Scroll)的ContentOffSet值的改变,然后去加载动画(是否小于-80)
3.toDo:我认为这里作者应该还要把这个是否用户拖动小于-80(也就是用户完成刷新这个动作)用代理返回给调用者, 他是直接在HomeVC里面判断Scroll是否小于-80来进行刷新操作的 -
-
监听tableview的滚动,然后根据yOffset(偏移值)来确定headerView(这个不是自定义的,就是一个普通的)的透明程度
-
当点击侧滑按钮是是发的通知来弹出左侧抽屉的
-
数据源部分 分为两个部分:
- storyGroup还有一个array,是放当天的所有文章的数组
2.顶部的headerView是自定义的
其中会把顶部的移动的几个文章插入到storyGroup的第一个位置去
- storyGroup还有一个array,是放当天的所有文章的数组
-
HomeVC成为了detailVC的代理:目的是告诉该detailVC他的上一篇和下一篇文章是什么,然后就可以在里面直接进行加载了
知识点
1.注册tableview:
[self.tableView registerNib:[UINib nibWithNibName:@"SYTableViewCell" bundle:nil] forCellReuseIdentifier:@"useid"];
2.添加监听:
[self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
3.顶部headerView根据offset来设置的渐变效果
///渐变
CGFloat alpha = 0;
if (yoffset <= 75.) {
alpha = 0;
} else if (yoffset < 165.) {
alpha = (yoffset-75.) / (165.-75);
} else {
alpha = 1.;
}
self.headerView.backgroundColor = SYColor(23, 144, 211, alpha);
第二部分:详细文章(detailVC)
构成:
- 底部的导航板
-
第0个按钮: 直接pop
-
第1个:根据代理返回回来的文章,然后进行跳转
-
第2个: 增加点赞
-
第3个: 分享,首先根据这个文章是否被收藏然后来调用分享面板的instancetype方法,(因为收藏了的话,title应该是取消收藏)
-
第4个:评论
-
评论和点赞按钮上面的数字的实现都是在自定义底部的导航版(NavigationView)上实现的
-
图片浏览器:这个我不知道为什么会使用两个scrollview,其他的重要知识点可能就是保存图片至相册(见下)
-
说说点赞按钮:
- 点赞按钮功能的实现实在这个自定义底部的导航版(NavigationView)上实现的。首先根据点击的tag值来确定点击的是否是点赞按钮响应了,然后再navView上监听这个button 的selected值,如果其selected值改变了并且是yes,那么在该点赞按钮上添加一个label,并且动画效果从正上方15的位置出现值+1,并且消失(remove)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC))
说说分享面板:
- 它其实是加载到一个coverView(全屏的)上的,并且分享面板的y坐标是整个屏幕的高度,看到这里是纳闷的,然后接着往下看,才发现他是用动画的方式改变,简单说,就是动画开始时,他的y值变成了-320,也就是整个面板的高度,最后cover又是加载到主window上的
-
顶部和底部的箭头的转换和上一篇文章的获取也是根据contentoffSet来进行设置的
-
里面的文章的显示直接就是webview,当webView加载完成过后会获取网页上所有的图片(方法见下)
-
自己会成为图片浏览器的代理,以告诉该浏览器上一张和下一张图片
知识点
1.获取所有图片:
//js方法遍历图片添加点击事件 返回图片个数
static NSString * const jsGetImages = @"function setImages(){"\
"var images = document.getElementsByTagName(\"img\");"\
"for(var i=0;i<images.length;i++){"\
"images[i].onclick=function(){"\
"document.location=\"detailimage:\"+this.src;"\
"};};return images.length;};";
[webView stringByEvaluatingJavaScriptFromString:jsGetImages];
[webView stringByEvaluatingJavaScriptFromString:@"setImages()"];
// 获取网页上的所有图片
NSString *jsImage = @"var images= document.getElementsByTagName('img');"
"var imageUrls = \"\";"
"for(var i = 0; i < images.length; i++)"
"{var image = images[i];"
"imageUrls += image.src+\"...beyanger....\";"
"}"
"imageUrls.toString();";
NSString *imageUrls = [webView stringByEvaluatingJavaScriptFromString:jsImage];
self.allImages = [imageUrls componentsSeparatedByString:@"...beyanger...."];
2.判断是否需要加载上下文章:
if (yoffset < -80) {
story = [self.delegate prevStoryForDetailController:self story:self.story];
transform = CGAffineTransformMakeTranslation(0, kScreenHeight);
} else if ((kScreenHeight -60 - scrollView.contentSize.height + yoffset) > 80) {
story = [self.delegate nextStoryForDetailController:self story:self.story];
transform = CGAffineTransformMakeTranslation(0, -kScreenHeight);
}
if (!story) return;
3.上下文章的切换动画(有一个空白视图避免加载中给人不好的印象)
// 切换过程动画
UIView *v = [self.view snapshotViewAfterScreenUpdates:NO];
self.story = story;
UIView *backView = [[UIView alloc] initWithFrame:CGRectMake(0, -kScreenHeight, kScreenWidth, 3*kScreenHeight)];
backView.backgroundColor = kWhiteColor;
v.frame = CGRectMake(0, kScreenHeight, kScreenWidth, kScreenHeight);
[backView addSubview:v];
[[UIApplication sharedApplication].keyWindow addSubview:backView];
[UIView animateWithDuration:0.25 animations:^{
backView.transform = transform;
} completion:^(BOOL finished) {
[backView removeFromSuperview];
self.footer.transform = CGAffineTransformIdentity;
self.header.transform = CGAffineTransformIdentity;
}];
4.保存图片至相册
- (void)saveImage {
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) {
[MBProgressHUD showError:@"无法读取相册"];
}
UIImageWriteToSavedPhotosAlbum(self.imageView.image, self, @selector(image:didFinishSavingWithError:contextInfo:), NULL);
}
- (void)image: (UIImage *) image didFinishSavingWithError: (NSError *) error contextInfo: (void *) contextInfo{
[MBProgressHUD showSuccess:@"已保存至相册"];
}
第三部分:评论(commentVC)
构成:
- 底部的返回面板+长按和tap点击的手势出现的cell操作面板+自定义的cell
- 根据点击的位置判断是哪个cell,然后根据点击的CGPoint的x坐标,判断是否大于面板宽度的一半,然后决定面板的center应该在哪个位置(代码见下)
-
确定点击的cell和点击的位置
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressHandler:)];
///手势的点击事件
-(void)longPressHandler:(UILongPressGestureRecognizer *)longGesture { if (longGesture.state == UIGestureRecognizerStateEnded) { CGPoint location = [longGesture locationInView:self.tableView]; NSIndexPath * indexPath = [self.tableView indexPathForRowAtPoint:location]; self.cell = [self.tableView cellForRowAtIndexPath:indexPath]; if (!self.cell) return self.pannel;
///因为用户会有可能已经对其进行了点赞
SYCommentPannel *cv = [SYCommentPannel commentPannelWithLiked:self.cell.comment.isLike]; cv.delegate = self; CGFloat xoffset = cv.width*0.5+12; if (location.x < xoffset) { cv.center = CGPointMake(xoffset, location.y-20); } else if (location.x > (kScreenWidth-xoffset)) { cv.center = CGPointMake(kScreenWidth-xoffset, location.y-20); } else { cv.center = CGPointMake(location.x, location.y-20); } cv.alpha = 0; [self.tableView addSubview:cv]; [UIView animateWithDuration:0.5 animations:^{ cv.alpha = 1.0; }]; return cv;
2.调用系统方法进行复制
[UIPasteboard generalPasteboard].string = comment.content;
[MBProgressHUD showSuccess:@"复制成功"];
第四部分:侧滑栏(LeftDrawerVC)
构成:
- MainVC使用的是第三方:MMDrawerController
- MainVC里面设置了侧滑相关的属性
- MainVC里面设置了中心视图位Home,侧滑视图为LeftDrawerVC
- 在LeftDrawerVCVC里面有一个属性保存着当前的主视图(mainVC),方便跳转
-
侧滑栏上面的数据源由两部分构成:收藏的专题和未收藏的专题,收藏了的在数据源数组的前半部分,有一个固定的专题叫做首页,它是直接插入0的位置
-
根据点击的是哪一个专题进行跳转
[self.mainController setCenterViewController:navi withCloseAnimation:YES completion:nil];
-
cell的代理设置设置为self,目的是为了用户收藏后,获得该专题是第几个cell,把该收藏的专题移动到第二个位置
-
各个专题的VC的代理也要设置为self,目的是为了当用户在各VC里面进行收藏该专题了过后,LeftDrawerVC可以根据该主题的名字来查找到该专题在数据源数组中的位置,然后操作同上,并且还需要在LeftDrawerVC的代理方法中进行网络操作告诉服务器用户进行了该专题的收藏,而且需要重新设置themeCell,因为cell后面的+按钮需要变化成>按钮
// 重新设置theme,刷新cell的显示 SYLeftDrawerCell *cell = [self.tableView cellForRowAtIndexPath:sip]; cell.theme = theme; [self.tableView moveRowAtIndexPath:sip toIndexPath:dip]; [self tableView:self.tableView moveRowAtIndexPath:sip toIndexPath:dip];
第五部分:专题VC(ThemeVC)
就相当于HomeVC,不过肯定有不同
StoryListVC首先继承自baseVC(baseVC其实就是专门为theme设计的)
ThemeVC继承自StoryListVC
- 顶部的headerView和前面的实现类似
- headerView下面有tableview的tableHeader,紧贴着headerView,这个是属于编辑者的头像,最多只有5个头像,点击会进行跳转到editorVC,其中头像的圆角使用的是贝塞尔曲线,(见下)
- 如果用户点击headerView中的收藏按钮,则告诉代理者实现代理方法
-
圆角的贝塞尔曲线实现
///圆角的 贝塞尔实现 - (void)awakeFromNib { for (UIImageView *imageView in self.editorsImage) { CAShapeLayer *maskLayer = [CAShapeLayer layer]; maskLayer.frame = imageView.bounds; maskLayer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(imageView.bounds, 2, 2)].CGPath; imageView.layer.mask = maskLayer; }
}
第六部分:launchVC
launch界面,其实没什么讲的,还是说一下逻辑吧
appDelegate的window的rootVC就是设置的是这个launchVC
- 首先会去userDefaults里边查找是否存在缓存图片
- 没有则会去下载并缓存下来
- 初始化MainVC,然后调用APPdelegate来把mainVC保存到appdelegate里面
- 并且加载lanuchImage的消失动画
- 在动画完成过后,设置delegate的rootVC为mainVC
发现的问题:
这里发现一个问题,那就是如果没联网,程序应该会崩,因为他是在网络的completionBlock里面进行加载的MainVC...
经验证,果真崩了...
但是更改逻辑过后虽然不蹦了,但是里面什么内容都没有,这个是影响用户体验的.. 我猜真正的知乎日报应该会有很好的解决办法把,待会儿下载一个试一试
第七部分:登录VC和设置VC
登录VC和设置VC
-
登录界面就主要是它用了一个RAC进行绑定,监听登录按钮的变化
-
设置VC在viewillAppear中会根据登录用户的名字(存放在UserDefault中)来判断第一个section是应该放置个人资料cell还是放置登录的cell
-
其中setting界面的Model部分没看太懂... 不过我知道他是干什么的..
-
在SettingCell里面,会根据settingmodel来判断他的右侧的视图是一个什么view,所以才会有上面model的存在,同时也方便了保存每个cell的状态
- (UISwitch *)switchView { if (!_switchView) { _switchView = [[UISwitch alloc] init]; _switchView.onTintColor = kGroundColor; _switchView.on = [kUserDefaults boolForKey:self.item.title]; [_switchView addTarget:self action:@selector(clickedSwitch:) forControlEvents:UIControlEventValueChanged]; } return _switchView; } - (void)clickedSwitch:(UISwitch *)sender { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; [ud setBool:sender.isOn forKey:self.item.title]; }
-
SDwebimage的清除缓存的方法
+ (void)clearCache { [[SDImageCache sharedImageCache] clearDisk]; [self clearCacheTables]; }
其他知识点
-
通篇文章都喜欢使用这种便利化构造器
+ (instancetype)cellWithTableView:(UITableView *)tableView {
static NSString *reuse_id = @"setting_reuseid";
SYSettingCell *cell = [tableView dequeueReusableCellWithIdentifier:reuse_id];if (!cell) { cell = [[self alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuse_id]; CAShapeLayer *layer = [CAShapeLayer layer]; layer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(4, 4, 32, 32)].CGPath; cell.imageView.layer.mask = layer; } return cell; }
-
通篇文章都喜欢使用getter和setter方法
- (UILabel *)titleLabel {
if (!_titleLabel) {
UILabel *titleLabel = [[UILabel alloc] init];NSDictionary *attr = @{ NSFontAttributeName:[UIFont systemFontOfSize:18], NSForegroundColorAttributeName:[UIColor whiteColor]}; titleLabel.attributedText = [[NSAttributedString alloc] initWithString:@"今日要闻" attributes:attr]; [titleLabel sizeToFit]; titleLabel.center = CGPointMake(kScreenWidth*0.5, 35); _titleLabel = titleLabel; [self.view addSubview:titleLabel]; SYRefreshView *refresh = [SYRefreshView refreshViewWithScrollView:self.tableView]; refresh.center = CGPointMake(kScreenWidth*0.5 - 60, 35); [self.view addSubview:refresh]; _refreshView = refresh; } return _titleLabel; }
-
通篇文章都喜欢使用delegate来进行模块之间通信
-
// 本文件中API大部分来自于
// https://github.com/izzyleung/ZhihuDailyPurify/wiki/知乎日报-API-分析 -
数据库的实现是用的FMDB
///获得数据库大小 + (unsigned long long)dataSize { NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject; NSString *dbName = [NSString stringWithFormat:@"%@.cached.sqlite", @"zhihu"]; NSString *pathName = [path stringByAppendingPathComponent:dbName]; NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:pathName error:nil]; return attrs.fileSize; }
-
MJExtension的使用
#import "MJExtension.h" @implementation SYRecommender /** 归档的实现 */ MJCodingImplementation
或者是
+ (NSDictionary *)modelContainerPropertyGenericClass {
// value should be Class or Class name.
return @{@"stories" : @"SYStory"};
}或者是
+ (void)getThemeWithId:(int)themeId completed:(Completed)completed { NSString *themeUrl = [NSString stringWithFormat:@"http://news-at.zhihu.com/api/4/theme/%d", themeId]; [YSHttpTool GETWithURL:themeUrl params:nil success:^(id responseObject) { SYThemeItem *item = [SYThemeItem mj_objectWithKeyValues:responseObject]; !completed ? : completed(item); } failure:nil]; }
-
Masonry的使用
///轮播的适配 self.scrollerView = scrollerView; [scrollerView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.bottom.right.mas_equalTo(ws); }]; UIPageControl *pageControl = [[UIPageControl alloc] init]; [self addSubview:pageControl]; self.pageControl = pageControl; [pageControl mas_makeConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(CGSizeMake(60, 16)); make.centerX.mas_equalTo(ws); make.bottom.mas_equalTo(ws).offset(-14); }];
-
SDWebImage的使用
///获得图片大小
+ (NSUInteger)imageSize {
return [[SDImageCache sharedImageCache] getSize];
}///清除数据 + (void)clearCache { [[SDImageCache sharedImageCache] clearDisk]; [self clearCacheTables]; }
-
3元表达式
result ? (!success? :success()) : (!failure? :failure());
And
!isLike ? : [self addLikeAnimation]; self.multiImage.hidden = !story.multipic;
-
图片截图
-(UIImage *)snapshort { UIGraphicsBeginImageContext(self.bounds.size); [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
-
滚动
-(void)scrollViewDidScroll:(UIScrollView *)scrollView { CGFloat xoffset = scrollView.contentOffset.x; int currentPage = (int)(xoffset / kScreenWidth + 0.5); self.pageControl.currentPage = currentPage; }
-
给comment这个model里面的islike属性进行了KVO监听
[comment addObserver:self forKeyPath:@"isLike" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
//然后处理
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
BOOL isLike = [change[@"new"] boolValue];
!isLike ? : [self addLikeAnimation];self.likeLabel.text = [NSString stringWithFormat:@"%ld", self.comment.likes]; if (isLike) { self.likeImage.image = [UIImage imageNamed:@"Comment_Voted"]; self.likeLabel.textColor = kGroundColor; } else { self.likeImage.image = [UIImage imageNamed:@"Comment_Vote"]; self.likeLabel.textColor = SYColor(128, 128, 128, 1.0);
}
} -
Islicked的动画
if (self.isAnimatting) return; self.isAnimatting = YES; UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"+1"]]; CGRect frame = CGRectMake(-30., -24, 30, 24); UIWindow *window = [UIApplication sharedApplication].keyWindow; imageView.frame = [self.likeImage convertRect:frame toView:window]; [window addSubview:imageView]; [UIView animateWithDuration:0.48 animations:^{ CGRect endFrame = CGRectMake(0, 0, 5, 4); imageView.frame = [self.likeImage convertRect:endFrame toView:window]; } completion:^(BOOL finished) { [imageView removeFromSuperview]; self.isAnimatting = NO; }];
-
使用约束来控制是否存在图片时title的宽度
if (story.images.count > 0) { [self.image sd_setImageWithURL:[NSURL URLWithString:story.images.firstObject]]; self.image.hidden = NO; //控制约束 self.titleLeft.constant = 18; } else { self.image.hidden = YES; self.multiImage.hidden = YES; self.titleLeft.constant = 18-60; }
-
tableView的HeaderView也有重用机制
SYHomeHeaderView *header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:header_reuseid];