源码解读--知乎日报
阅读知乎日报源码--总结
第一部分:首页(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, 3kScreenHeight)];
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.width0.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(kScreenWidth0.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];

浙公网安备 33010602011771号