您的位置:首页 > 其它

源码解读--知乎日报

2016-06-03 21:37 295 查看

阅读知乎日报源码--总结

第一部分:首页(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的第一个位置去

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/%E7%9F%A5%E4%B9%8E%E6%97%A5%E6%8A%A5-API-%E5%88%86%E6%9E%90

数据库的实现是用的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 )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];
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: