源码解读--知乎日报

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

第一部分:首页(home)

构成:

  • 顶部的自定义pictureView轮播

    1. 设置定时器(self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(nextImage) userInfo:nil repeats:YES]😉
    2. 当scroll即将开始滚动时,停止定时器([self.timer invalidate]😉.
    3. 结束时又开启定时器,并且判断当前的x偏移值,设置scroll的contentsOffset
    4. 这个自定义pictureView只负责把点击的图片的integer值传递给它的代理(这里是HomeVC),然后具体的跳转事件由代理者完成
  • 顶部的自定义的RefreshView刷新进度动画条

    1. 这个进度条其实是一个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(这个不是自定义的,就是一个普通的)的透明程度

  • 当点击侧滑按钮是是发的通知来弹出左侧抽屉的

  • 数据源部分 分为两个部分:

    1. 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)

构成:

  • 底部的导航板
    1. 第0个按钮: 直接pop

    2. 第1个:根据代理返回回来的文章,然后进行跳转

    3. 第2个: 增加点赞

    4. 第3个: 分享,首先根据这个文章是否被收藏然后来调用分享面板的instancetype方法,(因为收藏了的话,title应该是取消收藏)

    5. 第4个:评论

    6. 评论和点赞按钮上面的数字的实现都是在自定义底部的导航版(NavigationView)上实现的

    7. 图片浏览器:这个我不知道为什么会使用两个scrollview,其他的重要知识点可能就是保存图片至相册(见下)


说说点赞按钮:

  1. 点赞按钮功能的实现实在这个自定义底部的导航版(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))

说说分享面板:

  1. 它其实是加载到一个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应该在哪个位置(代码见下)

  1. 确定点击的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)

构成:

  1. MainVC使用的是第三方:MMDrawerController
  2. MainVC里面设置了侧滑相关的属性
  3. MainVC里面设置了中心视图位Home,侧滑视图为LeftDrawerVC
  4. 在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中的收藏按钮,则告诉代理者实现代理方法

  1. 圆角的贝塞尔曲线实现

     ///圆角的 贝塞尔实现
     - (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

  1. 首先会去userDefaults里边查找是否存在缓存图片
  2. 没有则会去下载并缓存下来
  3. 初始化MainVC,然后调用APPdelegate来把mainVC保存到appdelegate里面
  4. 并且加载lanuchImage的消失动画
  5. 在动画完成过后,设置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];
      }
    

  1. SDwebimage的清除缓存的方法

     + (void)clearCache {
     	[[SDImageCache sharedImageCache] clearDisk];
     	[self clearCacheTables];
     }
    

其他知识点

  1. 通篇文章都喜欢使用这种便利化构造器
    + (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;
     }
    
  2. 通篇文章都喜欢使用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;
     }
    
  3. 通篇文章都喜欢使用delegate来进行模块之间通信

  4. // 本文件中API大部分来自于
    // https://github.com/izzyleung/ZhihuDailyPurify/wiki/知乎日报-API-分析

  5. 数据库的实现是用的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;
     	}
    
  6. 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];
     }
    
  7. 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);
     }];
    
  8. SDWebImage的使用
    ///获得图片大小
    + (NSUInteger)imageSize {
    return [[SDImageCache sharedImageCache] getSize];
    }

     ///清除数据
     + (void)clearCache {
     	[[SDImageCache sharedImageCache] clearDisk];
     	[self clearCacheTables];
     }
    
  9. 3元表达式

     result ? (!success? :success()) : (!failure? :failure());
    

    And

     !isLike ? : [self addLikeAnimation];
      self.multiImage.hidden = !story.multipic;
    
  10. 图片截图

    	-(UIImage *)snapshort {
    		UIGraphicsBeginImageContext(self.bounds.size);
    		 [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
    		 UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    		UIGraphicsEndImageContext();
    		return image;
    
    	}
    
  11. 滚动

    -(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    	 CGFloat xoffset = scrollView.contentOffset.x;
    	int currentPage  = (int)(xoffset / kScreenWidth + 0.5);
    	self.pageControl.currentPage = currentPage;
    	}
    
  12. 给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);
    

    }
    }

  13. 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;
    	}];
    
  14. 使用约束来控制是否存在图片时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;
    }
    
  15. tableView的HeaderView也有重用机制

    	SYHomeHeaderView  *header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:header_reuseid];
    

posted @ 2016-06-03 21:37  Wayne_Liu  阅读(1072)  评论(0编辑  收藏  举报