ReactiveCocoa的实践 - 添加FunctionalReactivePixels

优质
小牛编辑
125浏览
2023-12-01

一个简单的画廊弄好了,但是我们是不是想看一下高清图呢?当用户点击画廊中的某一个单元格时,我们创建一个新的视图控制器并将其推入到导航堆栈中。

  1. - (void)collectionView:(UICollectionView *)collectionView
  2. didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
  3. FRPFullSizePhotoViewController * viewController = [[FRPFullSizePhotoViewController alloc] initWithPhotoModels:self.photosArray currentPhotoIndex:indexPath.item];
  4. viewController.delegate = self;
  5. [self.navigationController pushViewController:viewController animated:YES];
  6. }

这个方法没有任何特殊的,只是些一般的OC方法。当然别忘了在当前实现文件里加载视图控制器(FRPFullSizePhotoViewControler)的头文件.现在让我们来创建这个视图控制器(FRPFullSizePhotoViewControler).

创建一个UIViewController的子类FRPFullSizePhotoViewControler,这不会是一个特别的‘Reactive’的视图控制器,实际上大部分只是UIPageViewController子视图控制器的模版。

  1. @class FRPFullSizePhotoViewController;
  2. @protocol FRPFullSizePhotoViewControllerDelegate <NSOject>
  3. - (void)userDidScroll:(FRPFullSizePhotoViewController *)viewController toPhotoAtIndex:(NSInteger)index;
  4. @end
  5. @interface FRPFullSizePhotoViewController : UIViewController
  6. - (instancetype)initWithPhotoModels:(NSArray *)photoModelArray currentPhotoIndex:(NSInteger)photoIndex;
  7. @property (nonatomic , readonly) NSArray *photoModelArray;
  8. @property (nonatomic, weak) id<FRPFullSizePhotoViewControllerDelegate> delegate;
  9. @end

回到画廊视图控制器实现必要的代理方法:

  1. - (void)userDidScroll:(FRPFullSizePhotoViewController *)viewController toPhotoAtIndex:(NSInteger)index{
  2. [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]
  3. atScrollPosition:UICollectionViewScrollPositionCenteredVertically
  4. animated:NO];
  5. }

当我们滑到一个新的图像去查看其高清图片时,这个方法将更新collectionView滑动的位置。这样一来,当用户查看完高清图回到这个界面的时候,高清图所对应的缩略图将会显示在界面上,方便用户获知自己浏览的位置以及继续往下浏览。

#import这些必要的数据模型的头文件并追加一下两个私有属性:

  1. @interface FRPFullSizePhotoViewController () <UIPageViewControllerDataSource, UIPageViewControllerDelegate>
  2. //Private assignment
  3. @property (nonatomic, strong) NSArray *photoModeArray;
  4. //Private properties
  5. @property (nonatomic, strong) UIPageViewController *pageViewController;
  6. @end

photoModelArray是共有的只读属性,但是内部可读写。第二个属性是我们的子视图控制器。我们这样来初始化:

  1. - (instancetype)initWithPhotoModels:(NSArray *)photoModelArray currentPhotoIndex:(NSInteger)photoIndex{
  2. self = [self init];
  3. if (!self) return nil;
  4. //Initialized, read-only properties
  5. self.photoModelArray = photoModelArray;
  6. //Configure self
  7. self.title = [self.photoModelArray[photoIndex] photoName];
  8. //ViewControllers
  9. self.pageViewController = [UIPageViewController alloc]
  10. initWithTransitionStyle:UIPageViewControlerTransitionStyleScroll
  11. navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
  12. options:@{ UIPageViewControllerInterPageSpacingKey: @(30)};
  13. self.pageViewController.dataSource = self;
  14. self.pageViewController.delegate = self;
  15. [self addchildViewController:self.pageViewController];
  16. [self.pageViewController setViewController:@[[self photoViewControllerForIndex:photoIndex]]
  17. direction:UIPageViewControllerNavigationDirectionForward
  18. animated:NO completion:nil ];
  19. return self;
  20. }

赋值属性、设置标题、配置我们的pageViewController,一切都非常无聊,我们的viewDidLoad方法也同样简单。

  1. - (void)viewDidLoad{
  2. [super viewDidLoad];
  3. self.view,backGroundColor = [UIColor blackColor];
  4. self.pageViewController.view.frame = self.view.bounds;
  5. [self.view addSubView:self.pageViewController.view];
  6. }

我要指出的是,简便起见,在我的应用里我禁用了横向展示,因为这不是一本关于autoresizingMask或者autoLayout的书。你可以通过Eria Sadun的书了解更多关于autoLayout方面的细节。

下面我们来了解一下UIPageViewController的数据源协议和代理协议。

  1. - (void)pageViewController:(UIPageViewController *)pageViewController
  2. didFinishAnimating: (BOOL)finished
  3. previousViewControllers:(NSArray *)previousViewControllers
  4. transitionCompleted:(BOOL)completed{
  5. self.title = [[self.pageViewController.viewControllers.firstObject photoModel] photoName];
  6. [self.delegate userDidScroll:self toPhotoAtIndex:[self.pageViewController.viewControllers.firstObject photoIndex]];
  7. }
  8. - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(FRPPhotoViewController *)viewController{
  9. return [self photoViewControllerForIndex:viewController.photoIndex - 1];
  10. }
  11. - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(FRPPhotoViewController *)viewController {
  12. return [self photoViewControllerForIndex:viewController.photoIndex + 1];
  13. }

虽然这些方法没有技术上的reactive,却体现出一定意义上的实用性。我很佩服这种在特殊类型的视图控制器上的抽像,干得漂亮,Apple!

我们的视图控制器创建方法,类似下面这样:

  1. - (FRPPhotoViewController *)photoViewControllerForIndex:(NSInteger)index{
  2. if (index >= 0 && index < self.photoModelArray.count){
  3. FRPPhotoModel *photoModel = self.photoModelArray[index];
  4. FRPPhotoViewController *photoViewController = [[FRPPhotoViewController alloc] initWithPhotoModel:photoModel index:index];
  5. return photoViewController;
  6. }
  7. //Index was out of bounds, return nil
  8. return nil;
  9. }

它基本上创建比配置了一个我们将要使用的UIViewController的子视图控制器FRPPhotoViewController。下面是他的头文件:

  1. @class FRPPhotoModel;
  2. @interface FRPPhotoViewController : UIViewController
  3. - (instancetype)initWithPhotoModel:(FRPPhotoModel *)photoModel index:(NSInteger)photoIndex;
  4. @property (nonatomic, readonly) NSInteger photoIndex;
  5. @property (nonatomic, readonly) FRPPhotoModel * photoModel;
  6. @end

这个视图控制器非常简单:显示一个photoModel下的高清图片,并提示photoImporter(单例对象)下载这个图片。它是如此简单,我现在就告诉你它的全部实现。

  1. //Model
  2. #import "FRPPhotoModel.h"
  3. //Utilities
  4. #import "FRPPhotoImporter.h"
  5. #import <SVProgressHUD.h>
  6. @interface FRPPhotoViewController ()
  7. //Private assignment
  8. @property (nonatomic, assign) NSInteger photoIndex;
  9. @property (nonatomic, strong) FRPPhotoModel *photoModel;
  10. //Private properties
  11. @property (nonatomic, weak) UIImageView * imageView;
  12. @end
  13. @implementation FRPPhotoViewController
  14. - (instancetype)initWithPhotoModel:(FRPPhotoModel *)photoModel index:(NSInteger)photoIndex{
  15. self = [self init];
  16. if (!self) return nil;
  17. self.photoModel = photoModel;
  18. self.photoIndex = photoIndex;
  19. return self;
  20. }
  21. - (void)viewDidLoad{
  22. [super viewDidLoad];
  23. //Configure self's view
  24. self.view.backGroundColor = [UIColor blackColor];
  25. //Configure subViews
  26. UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
  27. RAC(imageView, image) = [RACObserve(self.photoModel, fullsizeData) map:^id (id value){
  28. return [UIImage imageWithData:value];
  29. }];
  30. imageView.contentMode = UIViewContentModeScaleAspectFit;
  31. [self.view addSubView:imageView];
  32. self.imageView = imageView;
  33. }
  34. - (void)viewWillAppear:(BOOL)animated{
  35. [super viewWillAppear:animated];
  36. [SVProgressHUD show];
  37. //Fetch data
  38. [[FRPPhotoImporter fetchPhotoDetails:self.photoModel]
  39. subscribeError:^(NSError *error){
  40. [SVProgressHUD showErrorWithStatus:@"Error"];
  41. }
  42. completed:^{
  43. [SVProgressHUD dismiss];
  44. }];
  45. }
  46. @end

就像我们的collectionViewCell中那样,我们将UIImageView的image属性和数据模型的某个属性映射后的值绑定,所不同的是ViewController不需要考虑复用,所以我们不必计较怎么取消imageView的订阅—-当imageView对象解除分配的时候,订阅将会被取消。

这个实现里面另一个有趣的部分在viewWillAppear:里:

  1. [SVProgressHUD show];
  2. //Fetch data
  3. [[FRPPhotoImporter fetchPhotoDetails:self.photoModel]
  4. subscribeError:^(NSError * error){
  5. [SVProgressHUD showErrorWithStatus:@"Error"];
  6. }
  7. completed:^{
  8. [SVProgressHUD dismiss];
  9. }];

没有收到错误或者完成信息之前,我们必须给用户展示网络请求的状态。你看,500px的受欢迎的照片的API接口只返回了一个照片的大概信息,但我们需要这个照片更详细的信息,所以我们必须调用第二个API接口来获取每一个照片的详细信息(包括全尺寸照片的URL)。

  1. + (NSURLRequest *)photoURLRequest:(FRPPhotoModel *)photoModel{
  2. return [AppDelegate.apiHelper urlRequestForPhotoID:photoModel.identifier.integerValue];
  3. }

我们还没有实现fetchPhotoDetails:方法,所以现在我们回到FRPPhotoImporter中,在头文件中定义这个方法,在实现文件中实现它。

  1. + (RACReplaySubject *)fetchPhotoDetails:(FRPPhotoModel *)photoModel {
  2. RACReplaySubject * subject = [RACReplaySubject subject];
  3. NSURLRequest *request = [self photoURLRequest:photoModel];
  4. [NSURLConnection sendAsynchronousRequest:request
  5. queue:[NSOperationQueue mainQueue]
  6. completionHandler:^ (NSURLResponse *response, NSData * data, NSError *connectionError){
  7. if(data){
  8. id results = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil][ @"photo" ];
  9. [self configurePhotoModel:photoModel withDictionary:results];
  10. [self downloadFullsizedImageForPhotoModel:photoModel];
  11. [subject sendNext:photoModel];
  12. [subject sendCompleted];
  13. }
  14. else{
  15. [subject sendError:connectionError];
  16. }
  17. }];
  18. return subject;
  19. }

这种方法跟前面我们看到的importPhotos方法模式一样,我们的downloadFullsizedImageForPhotoModel:方法跟downloadThumbnailForPhotoModel:方法也是一样的。除了这两者之外,还有什么重要的抽象方法呢?让我们来完成我们的缩略图方法。

  1. + (void)downloadThumbnailForPhotoModel:(FRPPhotoModel *)photoModel {
  2. [self download:photoModel.thumbnailURL withCompletion:^(NSData *data){
  3. photoModel.thumbnailData = data;
  4. }];
  5. }
  6. + (void)downloadFullsizedImageForPhotoModel:(FRPPhotoModel *)photoModel {
  7. [self download:photoModel.fullsizedURL withCompletion:^(NSData * data){
  8. photoModel.fullsizedData = data;
  9. }];
  10. }
  11. + (void)downloadFullsizedImageForPhotoModel:(FRPPhotoModel *)photoModel {
  12. [self download:photoModel.fullsizedURL withCompletion:^(NSData *data){
  13. photoModel.fullsizedData = data;
  14. }];
  15. }
  16. + (void)download:(NSString *)urlString withCompletion:(void(^)(NSData * data))completion{
  17. NSAssert(urlString, @"URL must not be nil" );
  18. NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
  19. [NSURLConnnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError){
  20. if (completion){
  21. completion(data);
  22. }
  23. }];
  24. }

我曾经与这样一位客户工作过,他认为如果你某行一样的代码重复写两次,这代码就应该得到某种程度的抽象。虽然我认为这有点偏激,但我喜欢这种态度。

好了。我们现在可以运行这个应用,点击一个图片去查看它的高清图片。我们也可以向前或者向后滑动来查看前一个或后一个高清图片。非常棒!

fullsize_gallerypictures