美文网首页牛叉的demoIos@IONICiOS高级开发
iOS 基于MVVM + RAC + ViewModel-Bas

iOS 基于MVVM + RAC + ViewModel-Bas

作者: CoderMikeHe | 来源:发表于2017-11-01 12:22 被阅读6264次
    前言
    • 由于最近两个多月,笔者正和小伙伴们忙于对公司新项目的开发,笔者主要负责项目整体架构的搭建以及功能模块的分工。首先,该项目采用MVVM + RAC + ViewModel-Based Navigation的设计模式,其次,尝试利用ViewModel-Based来实现导航(push/poppresent/dismiss)操作。最后,该项目在经过两个月的埋头苦干,也于近期成功上架AppStore【轻空-母婴二手用品寄售平台】。考虑到公司项目文件的保密性,这里笔者绝不会共享源码,而是采用笔者公司项目的同一套架构,来一步一步实现微信整体架构功能的开发。其目的就是让大家更加深沉次的领会 MVVM设计模式,以及利用ViewModel-Based来实现导航(push/poppresent/dismiss)操作的优越性。
    • MVVM With ReactiveCocoa的架构设计以及ViewModel-Based Navigation导航方式,主要参照的是雷纯锋大神开源的MVVMReactiveCocoa的框架,在其架构的基础上进行一系列改进和一些新特性的增加,不断丰富该架构以此来满足不同的开发场景,从而一步一步实现微信的基本架构,同时也侧面验证了雷纯锋大神的MVVM + RAC + ViewModel-Based Navigation的理论正确性和有效性,同时也希望能够打消你对 MVVM + RAC + ViewModel-Based Navigation 模式的顾虑。
    • 本文将着重分析利用MVVM + RAC + ViewModel-Based Navigation的方式来设计和实践微信(WeChat)大体功能的开发,希望大家能有所收获,并将其运用到自己的实际项目中去,这才是此文的最大意义。笔者也将知无不言言无不尽的将其里面的核心分享给大家,同时在运用到实际开发中遇到问题以及解决办法贡献出来,希望大家在使用这套模式来开发的时候知其然知其所以然,为大家提供一点思路,少走一些弯路,填补一些细坑。文章仅供大家参考,若有不妥之处,还望不吝赐教,欢迎批评指正。
    • MVVM + ReactiveCocoa 的使用不了解的,请猛戳我iOS 关于MVVM With ReactiveCocoa设计模式的那些事
    • ViewModel-Based Navigation 的使用不了解的,请猛戳我 MVVM With ReactiveCocoa
    • 文章略长,先马后看。
    代码结构
    1. 结构

      CodeStructure.png
    2. 说明

      • Model :存放数据-模型(data-model),例如:MHUser.
      • View:存放功能模块自定义的View。例如:MHMainFrameTableViewCell.
      • ViewController:存放功能模块的是视图控制器。例如:MHMainFrameViewController.
      • ViewModel:存放功能模块的是视图对应的视图模型。例如:MHMainFrameViewModel.
      • Utils:存放工具类和管理类。例如:分类Category,网络服务层MHHTTPService,管理类MHFileManager...
      • Vendor:存放第三方框架。例如:MJRefresh...
      • Macros:存放常量。例如:宏(#define)定义常量,const常量,枚举(NS_ENUM)常量,inline函数,URL路径常量。
      • Resource:存放资源文件,例如:图片,DataSQL文件。
    3. 细节

      • 代码结构完全按照MVVM来设计命名,实际上MVVMV应该包括视图控制器(ViewController)视图(View),这里只是将其单独分开,以便于更好的阅读和开发。
      • 必须强调文件夹的命名,这里笔者是按照主功能模块来命名,相信大家可以很清楚的看到 ViewViewControllerViewModel三个文件夹里面的子模块文件夹都是一样的。而后期若在设计子文件夹的时候,参照这种方式来创建文件夹,那么大家会发现,你的代码目录会非常非常的整齐漂亮,同时方便后期维护和其他开发人员阅读代码,何乐而不为呢。
      • 同时强调一下自定义的视图控制器和视图模型的命名,理论上,一个视图控制器配备一个视图模型,所以笔者这里只是将视图控制的名字的ViewController替换成ViewModel即为配备的视图模型的名字:例如:视图控制器的名字为MHMainFrameViewController,则视图模型的名字为MHMainFrameViewModel。这样整个项目开发下来,你会发现ViewControllerViewModel文件下的文件都是对称的。
      • 目录层级不能超过三层。因为层级越深,越不易查找,且不易阅读。这里就以我的(Profile)为例,我的(Profile)界面有一个用户信息(UserInfo)子模块,用户信息(UserInfo)里面有一个更多(MoreInfo)子模块,更多(MoreInfo)模块当然也有子模块等等。如果这样划分,必然会导致目录结构很深,所以为了避免其发生,就尽量限制在三层即可,正所谓事不过三嘛,所谓三层目录可想而知,就是ViewController - Profile - UserInfo这三层便是,那么我们就可将更多(MoreInfo)模块与用户信息(UserInfo)并列即可,当然你也可以将更多(MoreInfo)模块的写在用户信息(UserInfo)里面,但是只创建文件,而不创建文件夹。只要保证不超过三层目录即可。即如下图所示:
    ProfileCodeStructure.png
    第三方框架

    第三方框架想必对与小伙伴在熟悉不过了,其作用简而言之就是:辅助。让我们更专注于产品的业务逻辑开发,而不是某个功能点开发。这里简单介绍一下此次搭建微信(WeChat)基本架构中主要用到的第三方框架。目的希望能够让大家学习更多更好用的轮子,以及结合自身项目的实际情况集成进去,减少不必要的开发。更多详见Demo的Podfile文件。

    • AFNetworking :用于网络数据请求。
    • SDWebImage:图片异步加载和缓存。
    • ReactiveCocoa:函数响应式编程工具,主要用于MVVM设计模式的数据绑定。本项目使用的是 pod 'ReactiveCocoa' ,'2.5'的版本。
    • Masonry:是一个轻量级的布局框架,拥有自己的描述语法,采用更优雅的链式语法封装自动布局,简洁明了并具有高可读性。
    • IQKeyboardManager:键盘管理工具,优雅的解决弹起键盘遮盖输入框的问题。
    • YYKit:一套比较齐全的iOS开发组件。以下是项目中常用到的几个组件。
      • YYCategories:为Foundation and UIKit提供许多有用的分类。
      • YYText:强大的iOS富文本组件。
      • YYModel:高性能的字典转模型的框架。
      • YYImage:功能强大的图像框架。
      • YYWebImage:异步图片加载框架。[注:本项目主要使用:YYWebImage来加载图片,而SDWebImage主要兼容其他第三方框架]
      • YYCache:高性能 iOS 缓存框架,提供内存缓存磁盘缓存
    • UITableView+FDTemplateLayoutCell:自动计算cell高度并缓存cell高度。
    • FDFullscreenPopGesture:全屏左滑pop手势。
    • FMDB:SQLite数据库。
    • MJExtension:字典转模型框架。[注:该项目使用YYModel来做字典转模型,而MJExtension作为辅助.]。
    • MJRefresh:下拉刷新和上拉加载控件。
    • pop:动画引擎,用于动画过渡。若不会使用,请参照popping
    • DZNEmptyDataSet:UITableView/UICollectionView数据内容为空时展示的空白页。
    • MBProgressHUD:加载loading以及显示提示蒙版的HUD。
    • JPFPSStatus:通过FPS(Frames Per Second)每秒传输帧数的高低来检查列表滚动的流畅度。
    BaseClass

    本项目中采用的是继承的方式来设计的,所以BaseClass的存在在所难免,但是它在项目中的作用是举足轻重的,简直神一样的存在。笔者这里主要详述ModelViewControllerViewModel中的BaseClass,而View中的BaseClass无非是实际项目中开发者自定义的功能View,方便后期要使用只需继承该功能View就可以了,减少了开发中的冗余代码。比如:笔者项目中的MHButton是继承于UIButton,而其作用只是去掉了按钮的高亮状态- (void)setHighlighted:(BOOL)highlighted {},以及MHImageView是继承于UIImageView,而其作用只是增加了允许用户的交互self.userInteractionEnabled = YES;。这里主要解析的各个是BaseClass的头文件的属性和方法,以及各自的使用场景和注意点。基类主要文件如下:

    MHObject:所有数据模型的基类。
    MHViewModel/MHViewController:所有自定义视图控制器的基类,以及配备的视图模型。
    MHTableViewModel/MHTableViewController:所有需要显示UITableView的自定义视图控制器的基类,以及配备的视图模型。
    MHWebViewModel/MHWebViewController:所有需要显示WKWebView的自定义视图控制器的基类,以及配备的视图模型。
    MHTabBarViewModel/MHTabBarController:需要展示UITabBarController的自定义视图控制器,以及配备的视图模型。
    
    • Model -- BaseClass
      MHObject是整个项目的数据-模型(Data-Model)的基类,即:JSON转成的模型的基类。MHObject遵守YYModel协议,MHObject.h文件的API也参照NSObject+YYModel.hAPI的实现,内部封装了YYModel对应的字典转模型的主要方法。所以使用前提你得会使用YYModel,这里笔者仅说明MHObjec.h的属性和方法,具体的实现请移步笔者提供的Demo来阅读和理解。MHObject.h内容如下:

    • ViewModel -- BaseClass
      MHViewModel是整个项目所有自定义的视图模型的基类,主要提供数据给MHViewController,主要职责就是从 model 层获取 view 所需的数据,并且将这些数据转换成view能够展示的形式。当然这里笔者为其配备了许多常用的属性:是否允许左滑pop到上一层的interactivePopDisabled是否需要隐藏导航栏的prefersNavigationBarHidden是否需要隐藏导航栏底部细线的prefersNavigationBarBottomLineHidden是否启用IQKeyboardManager来管理键盘的弹起和关闭的keyboardEnable等...大家可以根据项目中的实际情况来配置各个属性的值,当然你也可以为其配备更多更好用的功能,以次来快速实现产品需求和避免冗余代码的产生。MHViewModel的其他属性或方法这里就不一一叙述了,大家可以根据笔者的属性注释设置其值,运行起来看看具体的效果即可。MHViewModel.h的内容如下:

      /// MVVM View
      /// The base map of 'params'
      /// The `params` parameter in `-initWithParams:` method.
      /// Key-Values's key
      /// 传递唯一ID的key:例如:商品id 用户id...
      FOUNDATION_EXTERN NSString *const MHViewModelIDKey;
      /// 传递导航栏title的key:例如 导航栏的title...
      FOUNDATION_EXTERN NSString *const MHViewModelTitleKey;
      /// 传递数据模型的key:例如 商品模型的传递 用户模型的传递...
      FOUNDATION_EXTERN NSString *const MHViewModelUtilKey;
      /// 传递webView Request的key:例如 webView request...
      FOUNDATION_EXTERN NSString *const MHViewModelRequestKey;
      
      @protocol MHViewModelServices;
      
      @interface MHViewModel : NSObject
      /// Initialization method. This is the preferred way to create a new view model.
      /// services - The service bus of the `Model` layer.
      /// params   - The parameters to be passed to view model.
      ///
      /// Returns a new view model.
      - (instancetype)initWithServices:(id<MHViewModelServices>)services params:(NSDictionary *)params;
      
      /// The `services` parameter in `-initWithServices:params:` method.
      @property (nonatomic, readonly, strong) id<MHViewModelServices> services;
      
      /// The `params` parameter in `-initWithParams:` method.
      /// The `params` Key's `kBaseViewModelParamsKey`
      @property (nonatomic, readonly, copy) NSDictionary *params;
      
      /// navItem.title
      @property (nonatomic, readwrite, copy) NSString *title;
      /// 返回按钮的title,default is nil 。
      /// 如果设置了该值,那么当Push到一个新的控制器,则导航栏左侧返回按钮的title为backTitle
      @property (nonatomic, readwrite, copy) NSString *backTitle;
      
      /// The callback block. 当Push/Present时,通过block反向传值
      @property (nonatomic, readwrite, copy) VoidBlock_id callback;
      
      /// A RACSubject object, which representing all errors occurred in view model.
      @property (nonatomic, readonly, strong) RACSubject *errors;
      
      /** should fetch local data when viewModel init  . default is YES */
      @property (nonatomic, readwrite, assign) BOOL shouldFetchLocalDataOnViewModelInitialize;
      /** should request data when viewController videwDidLoad . default is YES*/
      /** 是否需要在控制器viewDidLoad */
      @property (nonatomic, readwrite, assign) BOOL shouldRequestRemoteDataOnViewDidLoad;
      /// will disappear signal
      @property (nonatomic, strong, readonly) RACSubject *willDisappearSignal;
      
      /// FDFullscreenPopGesture
      /// Whether the interactive pop gesture is disabled when contained in a navigation
      /// stack. (是否取消掉左滑pop到上一层的功能(栈底控制器无效),默认为NO,不取消)
      @property (nonatomic, readwrite, assign) BOOL interactivePopDisabled;
      /// Indicate this view controller prefers its navigation bar hidden or not,
      /// checked when view controller based navigation bar's appearance is enabled.
      /// Default to NO, bars are more likely to show.
      /// 是否隐藏该控制器的导航栏 默认是不隐藏 (NO)
      @property (nonatomic, readwrite, assign) BOOL prefersNavigationBarHidden;
      
      /// 是否隐藏该控制器的导航栏底部的分割线 默认不隐藏 (NO)
      @property (nonatomic, readwrite, assign) BOOL prefersNavigationBarBottomLineHidden;
      
      /// IQKeyboardManager
      /// 是否让IQKeyboardManager的管理键盘的事件 默认是YES(键盘管理)
      @property (nonatomic, readwrite, assign) BOOL keyboardEnable;
      /// 是否键盘弹起的时候,点击其他局域键盘弹起 默认是 YES
      @property (nonatomic, readwrite, assign) BOOL shouldResignOnTouchOutside;
      
      /// An additional method, in which you can initialize data, RACCommand etc.
      ///
      /// This method will be execute after the execution of `-initWithParams:` method. But
      /// the premise is that you need to inherit `BaseViewModel`.
      - (void)initialize;
      @end
      

      MHWebViewModel主要是为要加载网页(WKWebView)的视图MHWebViewController提供数据的数据模型基类,继承于MHViewModel。其头文件暴露的属性也比较简单,都是平常开发中会遇到的,只要大家稍加利用,就能完成一些常用的功能。MHWebViewModel.h内容如下:

      @interface MHWebViewModel : MHViewModel
      /// web url quest
      @property (nonatomic, readwrite, copy) NSURLRequest *request;
      /// 下拉刷新 defalut is NO
      @property (nonatomic, readwrite, assign) BOOL shouldPullDownToRefresh;
      /// 是否取消导航栏的title等于webView的title。默认是不取消,default is NO
      @property (nonatomic, readwrite, assign) BOOL shouldDisableWebViewTitle;
      /// 是否取消关闭按钮。默认是不取消,default is NO
      @property (nonatomic, readwrite, assign) BOOL shouldDisableWebViewClose;
      @end
      

      这里笔者讲讲shouldDisableWebViewTitleshouldDisableWebViewClose这两个属性的作用以及使用场景。
      shouldDisableWebViewTitle: 是否取消导航栏的title等于webViewtitle。默认做法是MHWebViewController及其子类的导航栏titleWebViewtitle,而不是MHViewModeltitle属性。即控制器通过KVO的形式监听WKWebViewtitle属性,从而设置导航栏的titleself.navigationItem.title = self.webView.title。但是可能有几个H5界面想要设置导航栏的titleMHViewModeltitle属性,正所谓需求拉动生成,所以就产生了该属性。
      shouldDisableWebViewClose:是否导航栏左侧取消关闭按钮,默认是不取消。这主要是为了解决点击网页里面的链接继续加载另一个网页,如果重复前面的步骤几次,则网页层次就会非常的深(A - B - C - D - E ...)。如果我们点击MHWebViewController导航栏的左侧的返回按钮,其默认做法是返回到上一个网页([self.webView goBack]),这样由于前面的步骤,导致网页层次过深,我们需要点击多次返回按钮,才能返回到最初的网页,继而才能返回上一个界面,这样用户操作过多,用户体验下降(PS:干着程序猿的活,抄着产品经理的心)。MHWebViewController的导航栏返回按钮的事件处理代码如下:

      - (void)_backItemDidClicked{ /// 返回按钮事件处理
          /// 可以返回到上一个网页,就返回到上一个网页
          if (self.webView.canGoBack) {
              [self.webView goBack];
          }else{/// 不能返回上一个网页,就返回到上一个界面
              /// 判断 是Push还是Present进来的,
              if (self.presentingViewController) {
                  [self.viewModel.services dismissViewModelAnimated:YES completion:NULL];
              } else {
                  [self.viewModel.services popViewModelAnimated:YES];
              }
          }
      }
      

      所以,这时候为了解决此类问题,于是就出现了,当发现WKWebView能返回到上一个网页(self.webView.canGoBack),那么就会让导航栏左侧(leftBarButtonItems)同时显示返回和关闭按钮,当我们点击关闭按钮,就直接返回到上一层页面而不是返回上一个网页。当然有些页面是不要显示关闭按钮的,比如一些网页点击跳转顶多两三层。所以该属性就是为了显示和隐藏关闭按钮而产生的。下面就是MHWebViewController中显示关闭按钮以及关闭按钮的事件处理的代码:

      /// 内容开始返回时调用
      - (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation {
          /// 不显示关闭按钮
          if(self.viewModel.shouldDisableWebViewClose) return;
      
          UIBarButtonItem *backItem = self.navigationItem.leftBarButtonItems.firstObject;
          if (backItem) {
              if ([self.webView canGoBack]) {
                  [self.navigationItem setLeftBarButtonItems:@[backItem, self.closeItem]];
              } else {
                  [self.navigationItem setLeftBarButtonItems:@[backItem]];
              }
          }
      }
      
      - (void)_closeItemDidClicked{
          /// 判断 是Push还是Present进来的
          if (self.presentingViewController) {
              [self.viewModel.services dismissViewModelAnimated:YES completion:NULL];
          } else {
              [self.viewModel.services popViewModelAnimated:YES];
          }
      }
      

      MHTableViewModel主要是提供数据给MHTableViewController的视图模型的基类,继承于MHViewModel,且MHTableViewModel在本项目中使用最为广泛。当然笔者也为其增添许多功能属性,以此来加快了开发的便捷度以及减少了子类代码的冗余度。具体的的使用请根据笔者提供的属性注释,根据自身项目来配置其属性的值。MHTableViewModel.h具体内容如下:

      @interface MHTableViewModel : MHViewModel
      /// The data source of table view. 这里不能用NSMutableArray,因为NSMutableArray不支持KVO,不能被RACObserve
      @property (nonatomic, readwrite, copy) NSArray *dataSource;
      
      /// tableView‘s style defalut is UITableViewStylePlain , 只适合 UITableView 有效
      @property (nonatomic, readwrite, assign) UITableViewStyle style;
      
      /// 需要支持下来刷新 defalut is NO
      @property (nonatomic, readwrite, assign) BOOL shouldPullDownToRefresh;
      /// 需要支持上拉加载 defalut is NO
      @property (nonatomic, readwrite, assign) BOOL shouldPullUpToLoadMore;
      /// 是否数据是多段 (It's effect tableView's dataSource 'numberOfSectionsInTableView:') defalut is NO
      @property (nonatomic, readwrite, assign) BOOL shouldMultiSections;
      /// 是否在上拉加载后的数据,dataSource.count < pageSize 提示没有更多的数据.default is NO 默认做法是数据不够时,隐藏mj_footer
      @property (nonatomic, readwrite, assign) BOOL shouldEndRefreshingWithNoMoreData;
      
      /// 当前页 defalut is 1
      @property (nonatomic, readwrite, assign) NSUInteger page;
      /// 每一页的数据 defalut is 20
      @property (nonatomic, readwrite, assign) NSUInteger perPage;
      
      /// 选中命令 eg:  didSelectRowAtIndexPath:
      @property (nonatomic, readwrite, strong) RACCommand *didSelectCommand;
      /// 请求服务器数据的命令
      @property (nonatomic, readonly, strong) RACCommand *requestRemoteDataCommand;
      
      /// 占位empty类型
      //@property (nonatomic, readwrite, assign) SBDefaultEmptyBackgroundType emptyType;
      /// 网络不可用 default is NO
      @property (nonatomic, readwrite, assign) BOOL disableNetwork;
      
      /** fetch the local data */
      - (id)fetchLocalData;
      
      /// 请求错误信息过滤
      - (BOOL (^)(NSError *error))requestRemoteDataErrorsFilter;
      
      /// 当前页之前的所有数据
      - (NSUInteger)offsetForPage:(NSUInteger)page;
      
      /** request remote data or local data, sub class can override it
       *  page - 请求第几页的数据
       */
      - (RACSignal *)requestRemoteDataSignalWithPage:(NSUInteger)page;
      @end
      
    • ViewController -- BaseClass
      MHNavigationController :是整个项目所使用的导航栏控制器,用于替代系统的导航栏控制器(UINavigationController),当开发需要Push/Present一个导航栏控制器,我们应该Push/Present的是MHNavigationController,而不是UINavigationController。当然MHNavigationController不是单纯只是简单的继承UINavigationController就完事了,笔者也是赋予了MHNavigationController一些使命的。MHNavigationController.h内容如下:

      @interface MHNavigationController : UINavigationController
      /// 显示导航栏的细线
      - (void)showNavigationBottomLine;
      /// 隐藏导航栏的细线
      - (void)hideNavigationBottomLine;
      @end
      

      默认情况下,系统导航栏控制器的navigationBar底部有一根深灰色的细线(UIImageView),现实开发中,大家肯定遭遇到产品经理这样的Diss

      " 该界面能否隐藏导航栏底部这根细线?"
      " 该界面为何要隐藏导航栏底部这根细线?"
      " 有没有觉得导航栏底部这根细线颜色太深?"
      " 有没有觉得导航栏底部这根细线过高?"
      ...
      

      理想很丰满,现实很骨感,哎,说多了都是泪。于是乎,为了满足产品的需求,便诞生了MHNavigationController.h中显示和隐藏导航栏底部细线的方法,一般这两个方法都是成对出现的,在ViewControllerviewWillAppear:viewWillDisappear:来控制导航栏底部细线的显示和隐藏。
      其实网络上有很多隐藏导航栏底部细线的方法,这里讲讲笔者的做法,其实很简单,就是:找到它,隐藏它,自定义细线。代码如下:

      // 查询最后一条数据
      - (UIImageView *)_findHairlineImageViewUnder:(UIView *)view{
         if ([view isKindOfClass:UIImageView.class] && view.bounds.size.height <= 1.0) {
             return (UIImageView *)view;
         }
         for (UIView *subview in view.subviews){
             UIImageView *imageView = [self _findHairlineImageViewUnder:subview];
             if (imageView){ return imageView; }
         }
         return nil;
      }
      
      #pragma mark - 设置导航栏的分割线
      - (void)_setupNavigationBarBottomLine{
         //!!!:这里之前设置系统的 navigationBarBottomLine.image = xxx;无效 Why? 隐藏了系统的 自己添加了一个分割线
         // 隐藏系统的导航栏分割线
         UIImageView *navigationBarBottomLine = [self _findHairlineImageViewUnder:self.navigationBar];
         navigationBarBottomLine.hidden = YES;
         // 添加自己的分割线
         CGFloat navSystemLineH = .5f;
         UIImageView *navSystemLine = [[UIImageView alloc] initWithFrame:CGRectMake(0, self.navigationBar.mh_height - navSystemLineH, MH_SCREEN_WIDTH, navSystemLineH)];
         navSystemLine.backgroundColor = MHColor(223.0f, 223.0f, 221.0f);
         [self.navigationBar addSubview:navSystemLine];
         self.navigationBottomLine = navSystemLine;
      }
      

      其实,MHNavigationController最大的使命是:拦截系统的Push进来的所有子控制器,以便于统一处理:隐藏和显示系统底部的UITabBar统一处理Push过来的子控制器的导航栏的左侧按钮(navigationItem.leftBarButtonItem)的返回样式以及事件处理。当然返回按钮(leftBarButtonItem)的样式虽是多种多样的,比如:直接显示返回二字的 ,也有显示一张<图片的,也有显示< xxx的。但事件是统一的,都是调用popViewControllerAnimated:来返回上一个界面。当然,你也可以在指定的ViewController里面,自定义设置导航栏左侧的navigationItem.leftBarButtonItem的样式,以及实现该leftBarButtonItem的事件即可。这里笔者以统一处理微信(WeChat)的返回按钮样式为例。说说笔者的思路,首先讲讲微信(WeChat)返回按钮的样式的需求伪代码:假设有两个控制器(A/B),且A.title = @"KKK"B.title = @"ZZZ",假设[A Push B],那么微信的默认做法,则B的导航栏返回按钮是 < KKK,也就是B的导航栏返回按钮的titleA.title 。当然如果考虑到A.title的文字很长,那么需要自定义B的导航栏返回按钮的title< XXX。(大家没绕晕吧...)。这种自定义的做法需要结合MHViewModelbackTitle属性。详见代码如下:

        /// 能拦截所有push进来的子控制器
       - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{
           // 如果现在push的不是栈底控制器(最先push进来的那个控制器)
           if (self.viewControllers.count > 0){
               /// 隐藏底部tabbar
               viewController.hidesBottomBarWhenPushed = YES;
               NSString *title = @"返回";
               /// eg: [A push B]
               /// 1.取出当前的控制器的title , 也就是取出 A.title
               title = [[self topViewController] title]?:@"返回";
           
               /// 2.判断要被Push的控制器(B)是否是 MHViewController ,
               if ([viewController isKindOfClass:[MHViewController class]]) {
               
               MHViewModel *viewModel = [(MHViewController *)viewController viewModel];
               
               /// 3. 查看backTitle 是否有值
               title = viewModel.backTitle?:title;
           }
           
           // 4.这里可以设置导航栏的左右按钮 统一管理方法
           viewController.navigationItem.leftBarButtonItem = [UIBarButtonItem mh_backItemWithTitle:title imageName:@"barbuttonicon_back_15x30" target:self action:@selector(_back)];
       }
           // push
           [super pushViewController:viewController animated:animated];
       }
       /// 事件处理
       - (void)_back{
           [self popViewControllerAnimated:YES];
       }
      

      MHNavigationController当然还有一些其他使命,比如统一设置UINavigationBarUIBarButtonItem的主题。这里就不一一阐述了,详见Demo里面的MHNavigationController.m文件。(PS:天青色等烟雨,而我在等你)。

      MHViewController 是整个项目中所有自定义的视图控制器的基类。其主要使命是绑定MHViewModel提供的一系列属性来完成一些初始化工作和基础性的配置。MHViewController.h内容如下:

      @interface MHViewController : UIViewController
      /// The `viewModel` parameter in `-initWithViewModel:` method.
      @property (nonatomic, readonly, strong) MHViewModel *viewModel;
      
      /// 截图(Push/Pop Present/Dismiss 过度过程中的缩略图)
      @property (nonatomic, readwrite, strong) UIView *snapshot;
      /**
       统一使用该方法初始化,子类中直接声明对于的'readonly' 的 'viewModel'属性,
       并在@implementation内部加上关键词 '@dynamic viewModel;'
       @dynamic A相当于告诉编译器:“参数A的getter和setter方法并不在此处,
       而在其他地方实现了或者生成了,当你程序运行的时候你就知道了,
       所以别警告我了”这样程序在运行的时候,
       对应参数的getter和setter方法就会在其他地方去寻找,比如父类。
       */
      /// Initialization method. This is the preferred way to create a new view.
      ///
      /// viewModel - corresponding view model
      ///
      /// Returns a new view.
      - (instancetype)initWithViewModel:(MHViewModel *)viewModel;
      
      /// Binds the corresponding view model to the view.(绑定数据模型)
      - (void)bindViewModel;
      @end
      

      通过API可见MHViewController的功能其实是比较单一的,只做了绑定视图模型(MHViewModel及其子类)的一些基础性配置。更多内容详见Demo的MHViewController.m文件,笔者这里讲讲根据MHViewModeltitle的属性设置导航栏title的细节,代码和细节处理如下所述:

      /// set navgation title
      // CoderMikeHe Fixed: 这里只是单纯设置导航栏的title。 不然以免self.title同时设置了navigatiItem.title, 同时又设置了tabBarItem.title
      RAC(self.navigationItem , title) = RACObserve(self, viewModel.title);
      

      MHWebViewController是整个项目中所有需要显示WebView(WKWebView)的自定义的视图控制器的基类。其内部添加了一个全屏的WKWebView作为视图控制器View的子控件,主要目的是为了加载一些网页链接以及本地H5,开发中只需要直接使用MHWebViewController即可,很少需要将其子类化。通过绑定MHWebViewModelrequest属性来加载指定的网页,只要你能熟练使用WkWebView即可,其他的细节问题比如下拉刷新网页、WKWebView自适应屏幕、点击网页链接跳转处理,以及多次跳转网页后的导航栏关闭按钮的事件处理等... 请参考MHWebViewController.mMHWebViewController.h的头文件内容如下:

      @interface MHWebViewController : MHViewController<WKNavigationDelegate,WKUIDelegate,WKScriptMessageHandler>
      /// webView
      @property (nonatomic, weak, readonly) WKWebView *webView;
      /// 内容缩进 (64,0,0,0)
      @property (nonatomic, readonly, assign) UIEdgeInsets contentInset;
      @end
      

      MHTabBarController在本项目继承于MHViewController,主要作用是将UITabBarController作为自己的子控制器,并将tabBarController作为一个只读(readonly)属性暴露在头文件中,以便子类能够获取并使用,即关键代码如下:

       self.tabBarController = [[UITabBarController alloc] init];
       /// 添加子控制器
       [self.view addSubview:self.tabBarController.view];
       [self addChildViewController:self.tabBarController];
       [self.tabBarController didMoveToParentViewController:self];
      

      大家可能普遍会认为,MHTabBarController为何是继承MHViewController,而不是直接继承UITabBarController(PS:若为MVC模式,笔者定会直接继承UITabBarController),这样岂不更加清晰明了。笔者认为这主要是为了保证整个项目继承的连续性,以便更好的使用到基类的属性和方法,保证代码的规范性。
      本项目主模块的视图控制器继承关系为:
      MHHomePageViewController → MHTabBarController → MHViewController
      本项目主模块的视图模型的继承关系为:
      MHHomePageViewModel → MHTabBarViewModel → MHViewModel
      如果直接单纯的继承UITabBarController,则继承关系为:
      MHHomePageViewController → MHTabBarController → UITabBarController
      然而,UITabBarController是继承于UIViewController的,这样就使得与MHViewController失去了联系,从而无法使用MHViewController中的属性和方法。同理,视图模型的继承连续性也可以以此类比。
      当然,MHTabBarController内部还利用了KVC将其系统的tabBar替换成MHTabBar(PS:继承UITabBar)。代码如下:

       // kvc替换系统的tabBar
        MHTabBar *tabbar = [[MHTabBar alloc] init];
        //kvc实质是修改了系统的_tabBar
        [self.tabBarController setValue:tabbar forKeyPath:@"tabBar"];
      

      其目的就是便于更好的定制适合产品需求的UITabBar,比如:UITabBar顶部的细线颜色问题,高度问题 ,中间添加加号按钮等...解决方案类似导航栏的navigationBar类似,即找到它,隐藏它,自定义细线。更多内容请参见Demo中的MHTabBarControllerMHTabBar即可。MHTabBarController.h内容如下

      @interface MHTabBarController : MHViewController<UITabBarControllerDelegate>
      /// The `tabBarController` instance
      @property (nonatomic, readonly, strong) UITabBarController *tabBarController;
      @end
      

      MHTableViewController是整个项目中所有需要显示列表(UITableView)的自定义的视图控制器的基类,也是项目中使用最多的基类。MHTableViewController内部添加了一个全屏的UITableView作为其子控件,通过配合绑定MHTableViewModel的属性来实现 tableView的展示样式tableView的数据展示tableView是否支持上拉加载和下拉刷新以及加载和刷新的逻辑tableView无数据或无网络的展示tableView选中cell的事件处理。开发中我们绝大多数都是通过子类化MHTableViewController,然后重写(Override)父类提供的方法来配置tableView的contentInsert提供tableView展示数据的cell绑定cell显示的数据模型等等。关键是要学会根据项目需求来配置MHTableViewModel的属性,依次来达到产品的需求。在此可见MVVMVM(视图模型)的重要性。MHTableViewController.h的内容如下:

      @interface MHTableViewController : MHViewController<UITableViewDelegate , UITableViewDataSource>
      
      /// The table view for tableView controller.
      /// tableView
      @property (nonatomic, readonly, weak) UITableView *tableView;
      
      /// `tableView` 的内容缩进,default is UIEdgeInsetsMake(64,0,0,0),you can override it
      @property (nonatomic, readonly, assign) UIEdgeInsets contentInset;
      
      /// reload tableView data , sub class can override
      - (void)reloadData;
      
      /// dequeueReusableCell
       - (UITableViewCell *)tableView:(UITableView *)tableView dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;
      
      /// configure cell data 
      - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath withObject:(id)object;
      @end
      

      这里笔者讲讲在设计MHTableViewController时遇到的坑和填坑的办法,以及部分关键代码的解析,希望可以帮助大家在开发中更好的理解和避免被坑。
      内置tableView的尺寸布局的坑。由于项目中纯代码部分笔者都是利用Masonry来实现布局的,所以在MHTableViewController中布局tableView时,利用Masonry来布局,关键代码如下:

      UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectZero style:self.viewModel.style];
      [self.view addSubview:tableView];
      [tableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(UIEdgeInsetsZero);
      }];
      

      其实,正常情况下完全没问题,但是MHTableViewController子类化后,在子类中设置了tableViewcontentInset属性,然而tableViewcontentOffset始终是(0,0),非常的神奇,到目前为止笔者也不知其原因(PS:若知道的大神, 请说一声哦),这样就导致了笔者一个需求上的Bug,就是笔者项目中首页是个商品列表,当你向下滑动到一定距离,屏幕右下角处会出现一个能够点击滚动到顶部的按钮,点击向上按钮就可以滚动到顶部即可。实现过程无非就是监听按钮的点击方法,实现[self.tableView setContentOffset:CGPointMake(0, 0) animated:YES];即可(理论上)。但是如果采用Masonry布局,就会出现点击向上按钮,你怎么也滚动不到顶部去,感觉tableView抽风了。当然,大家可以利用笔者提供的MHDevelopExample_Objective_CMVVM那块的内容进行复现或调试。
      笔者采取的解决办法是:笔者首先觉得可能tableView还未布局好而导致的,所以在利用Masonry布局tableView时,在MHTableViewController中强制布局了子控件,即调用[self.view layoutIfNeeded];,结果也很神奇,就可以实现点击向上按钮,能滚动到顶部了。
      但是...BUG还是出现了。如果MHTableViewModeldataSource的数据不是通过- (RACSignal *)requestRemoteDataSignalWithPage:(NSUInteger)page来获取的网络数据,而是在- (void)initialize中就初始化的死数据,例如发现模块页面中cell的数据源。当我们的Cellxib创建,且一般开发中会在MHTableViewController的子类中的-(void)viewDidLoad里面注册tableViewCell。切记:Bug复现条件必须是:TableViewModeldataSource是必须死(本地)数据,而非网络数据,并且是Cell是用tableView注册来获取的,缺一不可。这样会导致如下图所示的Bug。

      UITableView崩溃.png

      如果开启全局断点,那么会崩溃定位到[self.view layoutIfNeeded]的位置,由于强制布局(layoutIfNeeded)视图控制器的子控件,那么会导致tableView提前刷新(reloadData)其数据源的方法,而此时TableViewModeldataSource的数据又是本地数据,一开始是会有值,从而会调用tableView的数据源方法:- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath,而一般初始化cell的工作都是交个子类来重写MHTableViewController- (UITableViewCell *)tableView:(UITableView *)tableView dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath的方法。所以当我们在子类的-(void)viewDidLoad中注册TableViewCell,这样就会因为代码调用顺序的原因,使得子类通过在重写- (UITableViewCell *)tableView:(UITableView *)tableView dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath来返回一个cell,然而return [tableView dequeueReusableCellWithIdentifier:@"XXXXXX"];来获取出来注册(其实还未注册)的cellnil而导致崩溃。子类的伪代码调用顺序如下:

        /// 子类代码逻辑顺序
        - (void)viewDidLoad {
            /// ①:子类调用父类的viewDidLoad方法,而父类主要是创建tableView以及强行布局子控件,从而导致tableView刷新,这样就会去走tableView的数据源方法
            [super viewDidLoad];
      
            /// ③:注册cell
            [self.tableView mh_registerNibCell:MHMainFrameTableViewCell.class];
        }
      
        /// 返回自定义的cell
        - (UITableViewCell *)tableView:(UITableView *)tableView dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath{
            // ②:父类的tableView的数据源方法的获取cell是通过注册cell的identifier来获取cell,然而此时子类并未注册cell,所以取出来的cell = nil而引发Crash
            return [tableView dequeueReusableCellWithIdentifier:@"MHMainFrameTableViewCell"];
        }
      

      当然,笔者平常开发都是通过纯代码来创建Cell的,极少使用到通过注册Cell的方式(PS:个人编码习惯问题而已)。一般笔者的做法都会在新建的Cell里面暴露一个获取创建好的Cell的方法:+ (instancetype)cellWithTableView:(UITableView *)tableView。代码实现如下:

      + (instancetype)cellWithTableView:(UITableView *)tableView{
          static NSString *ID = @"LiveRoomCell";
          MHMainFrameTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
          if (!cell) {
              cell = [self mh_viewFromXib];
              cell.selectionStyle = UITableViewCellSelectionStyleNone;
          }
          return cell;
       }
      

      所以起初笔者在调试这个BUG的时候,我也是一脸懵逼,因为我这里完美运行,而同事那里就蹦擦拉卡。后面才发现就是上面的伪代码逻辑②处获取的cellnil导致的,而如果②采用笔者的获取cell的方法,是绝逼不会有问题的。但是考虑到同事是比较偏向于通过UITableView+FDTemplateLayoutCell来自动计算cell高度并缓存cell高度的方式开发,然而这框架的使用前提就是必须通过为Cell注册一个identifier的方式。
      所以笔者为了兼容同事的开发习惯,最终的做法是在MHTableViewController中不使用Masonry来布局tableView,也不强制刷新(layoutIfNeeded)视图控制器的子控件。而是直接指定tableViewframe,即:UITableView *tableView = [[UITableView alloc] initWithFrame:[UIScreen mainScreen].bounds style:self.viewModel.style];。如果子类想要修改tableView的尺寸,再使用Masonry来布局即可。所以,这就是最终的做法...
      当然还有MHTableViewController还有许多逻辑细节处理,这里就不在过多赘述,更多内容请参考Demo中的MHTableViewController设计。

    Q&A

    Q:项目中若同时集成 YYCategoriesReactiveCocoa,使用@weakify(self)@strongify(self);将会报Ambiguous expansion of macro weakifyAmbiguous expansion of macro strongify的警告。

    weakify&strongify警告.png

    A:由于 YYCategoriesReactiveCocoa都定义了weakifystrongify引起的。解决办法如下:

    weakify&strongify警告解决.png

    知识点:怎样去除Xcode中的警告️


    Q:Xcode 9.0上,ReactiveCocoa(2.5)Unknown warning group '-Wreceiver-is-weak', ignored的警告。

    Wreceiver-is-weak警告.png

    A:RACObserve定义如下:

    #define RACObserve(TARGET, KEYPATH) \
        ({ \
            _Pragma("clang diagnostic push") \
            _Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \
            __weak id target_ = (TARGET); \
            [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \
            _Pragma("clang diagnostic pop") \
        })
    

    在之前的Xcode中如果消息接受者是一个weak对象,clang编译器会报receiver-is-weak警告,所以加了这段push&pop,最新(iOS 11)的clang已经把这个警告给移除,所以没必要加push&pop了。
    解决办法:修改Podfile文件,将 pod 'ReactiveCocoa' ,'2.5' 改成如下

    pod 'ReactiveCocoa', :git => 'https://github.com/zhao0/ReactiveCocoa.git', :tag => '2.5.2'
    

    该方法原文参照:简书App适配iOS 11


    Q:在Xcode 9.0上报 error: Illegal Configuration: Safe Area Layout Guide before iOS 9.0错误。

    SafeAreaLayoutGuide.png

    A:SafeArea的概念是在iOS 9.0以后才支持,所以只需要设置项目支持的版本:设置Deployment TargetiOS Deployment Target9.0以上即可。

    SafeAreaLayoutGuide解决①.png SafeAreaLayoutGuide解决②.png
    总结

    本篇主要介绍了笔者在使用MVVM + RAC + ViewModel-Based Navigation来搭建微信基本架构过程中的一点见解,其更深次的实践还需要各位小伙伴去自行体会,建议结合笔者文末提供的Demo以及雷纯锋大神开源的MVVMReactiveCocoa来实践。
    当然实践过程如人饮水,冷暖自知,多多重复,百炼成钢。希望小伙伴通过阅读这篇文章,能对MVVM + RAC + ViewModel-Based Navigation的使用有一定基本的了解和使用,不一定要求完全去掌握它,这仅仅是我们众多开发模式的一个参考罢了,最主要的还是编程思想细节处理。显然你也可以将其运用到MVC设计模式中去,比如代码规范文件目录BaseClass等等。使得MVVM真正做到从群众(MVC)中来,到群众(MVC)中去。
    或许还有许多细小逻辑和细小Bug需要我们去优化和处理,当然这便是此篇文章的存在的意义:集众人之智,成众人之事

    未完...待续...(PS:点关注,不迷路,笔者带你上高速)

    考虑到文章篇幅过长影响阅读性,讲述其中技术的拓展性和全面性。笔者在接下来的时间内,会陆续将在开发WeChat中的好用的技术以及细节处理分享出来,希望提供大家一个参考,并且可以运用到自己的实际的项目中去。主要是关于以下几个问题的解释和分析,还请小伙伴移步续篇👉iOS 基于MVVM + RAC + ViewModel-Based Navigation的微信开发(二)

    • 项目中的整体服务(Service)层解析。
    • 项目中的网络(Network)层解析。
    • 项目中如何快速搭建类似发现我的设置、...等界面解析。
    • 如何利用该设计模式搭建游客模式(PS: 微信是登录模式的架构)的架构。
    • 搭建Debug调试工具。
    期待
    1. 文章若对您有些许帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议💗,切记学无止境。
    2. 针对文章所述内容,阅读期间任何疑问;请在文章底部评论指出,我会火速解决和修正问题。
    3. GitHub地址:https://github.com/CoderMikeHe
    4. 源码地址:WeChat
    参考链接

    相关文章

      网友评论

      • csii993:PhotoBrowser这个文件夹下面的两个文件是第三方的还是自己写的网上没找到
        CoderMikeHe:@csii993 YYKit
      • 千_城:你好,为什么是“pod 'ReactiveCocoa', :git => 'https://github.com/zhao0/ReactiveCocoa.git', :tag => '2.5.2'”,而不是直接“pod 'ReactiveCocoa”呢?你那样写是为了什么?
        千_城:@CoderMikeHe 好的,明白了,谢谢!
        CoderMikeHe:@千_城 之前是因为适配 iOS 11 加的,具体请参考 https://www.jianshu.com/p/26fc39135c34
      • 伤心的EasyMan:楼主,我发现你的Tableview controller,在上拉加载更多的时候,如果最后一页的数据和pagesize刚好一样,后面的页没数据了还一直会显示点击或上拉加载更多,但其实这个时候应该显示暂无更多数据才对
        CoderMikeHe:@伤心的EasyMan 如果是第三方控制器,考虑到有些第三方控制器是通过CocoaPods导入的, 所以,你可以直接按照平常的MVC方式去跳转(push , present)即可,不影响的。这样反而可以间接解耦了。
        伤心的EasyMan:@CoderMikeHe 注意到你的viewModel里面有这个方法,
        /// 当前页之前的所有数据
        - (NSUInteger)offsetForPage:(NSUInteger)page;
        所以我用这个在_requestDataCompleted方法里多判断了一下,解决了这个问题
        楼主代码写的很赞!还想问一下,如果我用了某个第三方资源库的viewcontroller,需要再为它写一个viewModel吗?我现在是在自己的viewcontroller直接跳转过去,没有经过viewModel 的command去跳转,是不是不大好?怎么做比较好呢?可以提个建议吗?谢谢了
        CoderMikeHe:@伤心的EasyMan 好,到时候改改逻辑,
        /// 是否在上拉加载后的数据,dataSource.count < pageSize 提示没有更多的数据.default is NO 默认做法是数据不够时,隐藏mj_footer
        @property (nonatomic, readwrite, assign) BOOL shouldEndRefreshingWithNoMoreData;
        到时候改改这个属性的逻辑
      • Masyn:非常感谢楼主提供了基于RAC实现MVVM的编码思路,收下我的膝盖~
        CoderMikeHe:@Masyn 哈哈哈 谢谢咯
      • 414a9adc2b68:楼主,请教个问题,你项目中的VC是通过VM的Router去实例化的。如果这个VC是在storyboard中,该如何实例化呢,目前直接使用你的方式创建的对象关联属性是nil的。
        CoderMikeHe:@OutOfBounds 这个不用担心,主流依然还是OC
        414a9adc2b68:@CoderMikeHe 哦,好的,谢谢楼主了。还有个问题想请教下,目前ReactiveCocoa已经不再支持ObjC版本的更新维护了。那么新的工程考虑到后期系统版本升级之类的问题,是否已经不适合再使用该框架了啊,或者说应该转型swift?
        CoderMikeHe:@OutOfBounds 这个可能不太实用SB,也可能需要额外处理了,到一般还是尽量少用sb咯,
      • 柯丕安德柯丕:有个问题。我新建VC【继承自MHViewController】,在其.m文件
        @interface OshaPersonalViewController ()
        /// viewModel
        @property (nonatomic, readwrite, strong) OshaPersonalViewModel *viewModel;
        @end
        出现以下警告
        Auto property synthesis will not synthesize property 'viewModel' because it is 'readwrite' but it will be synthesized 'readonly' via another property
        CoderMikeHe:@黑化的小猪 嗯嗯,我在基类里面说明了
        柯丕安德柯丕:知道为什么了,需要加上如下代码
        @dynamic viewModel;
      • 清蒸鱼跃龙门:说到基类,我知道用到过baseViewController baseModel ,基本上只会在控制器和模型数据方面会有高度重合的情况 这样省的每个地方去写代码。但总的来说量很少,这篇文章还得花时间研究研究
        CoderMikeHe:@清蒸鱼跃龙门 这里多抽出几个基类,方便单一职责化。
      • 阿不不不不:谢谢楼主分享,其实有没有考虑过用分类干掉基类继承的方式呢,毕竟深层次的继承对于后期的开发成本和新同事接手的时间成本还是有一定高的
        CoderMikeHe:@X堇色 保证单一职责罢了,如果写在一堆,业务场景过多,反而会迷惑。还不如抽离出来更好;比如下来刷新,上拉加载应该是列表(UITaleView 、UICollectionView)独有的,就可以放在MHTableViewModel中。如果全部写在MHViewModel中,个人感觉加载,刷新这个放在MHViewModel反而显得比较突兀,至于不好维护,其实不然,苹果一向推崇单一功能原则,所以就有了`封装`这个概念,其实其目的就是为了维护和单元测试,比如:MJRefresh 其也是采用基类的继承的形式,进行拓展。当然也可以用协议的方式,来做也可以的。
        F麦子:@CoderMikeHe 不感觉你这么多继承有多好,不好维护
        CoderMikeHe:@阿不不不不 其实用协议的的方式更好点,但是这里用继承,其目的就是为了想满足,苹果的那一套,单一职责。这样便于更好的理解。
      • 枫叶知秋:楼主,有个小的建议。我之前听过MVVM,具体的实现流程,不是特别清楚。而你简书上是将MVVM + RAC + ViewModel-Based Navigation的相关文章全部讲述了一遍,我个人猜想,这几个之间是有相关的联系的。对于没有了解过MVVM的小白(像我这样),你给个阅读顺序,这样就能很完整的阅读你的文章,也可以更好的理解。还没看,感谢楼主分享:clap: :+1:
        CoderMikeHe:@枫叶知秋 哈哈,进入我的博客主页,按照我文章的发布时间,阅读即可。
      • 一个程序员浩:我下载你的项目 pod install [!] Could not automatically select an Xcode project. Specify one in your Podfile like so:

        project 'path/to/Project.xcodeproj'

        出现这个,请问怎么回事
        CoderMikeHe:@一个程序员浩 好像需要这个工程的路径,你将路径设置成提示的那样试试,百度一下应该有答案。你先试试
      • 泽Je:你好,首先感谢你的文章写的很好,看了代码,有个疑问,就是Router那的设计, 在多人开发时,感觉容易出现冲突。不知开发时是否存在这样的问题
        CoderMikeHe:@黑化的小猪 多人开发把 Router 的viewModelViewMappings这个方法,内部用可变字典来处理,比如 [mutDict setDictionary:otherDict]。otherDict 就是其他同事的字典。只要约定好各自的字典,冲突将不再发生。
        柯丕安德柯丕:你好,对于Router那的设计,还是不理解,多人开发时如何避免冲突
        CoderMikeHe:@泽Je 嗯嗯,Router中这个方法 - (void)viewModelViewMappings ,在团队中容易产生冲突,但是只要在这个方法中字典不是NSDictionary,而是改成可变的字典去动态增加每一个开发人员数据即可,因为字典中 key 是唯一的 。
      • 镜花水月忆存逝兮:大神,你这才是真真吃透了RAC,已star
        CoderMikeHe:@镜花水月忆存逝兮 吃透不敢讲,还有很多高级功能的。以后用到的时候再去挖掘
      • 苏苏_db24:我在App Store下载你公司的应用 微信取消登录后 返回APP 那个菊花还在一直转
        CoderMikeHe:好的 我稍后会看看 谢谢你的反馈
      • overla5:你好,类似于设置那种公共界面,如果我想重写2个分区的footview,在继承了公共的footview之后,控制器在返回footView的代理方法里绑定model的时候 应该绑定控制器的viewModel,还是自定义footView的ViewModel。每个分区设置的文字不同,是不是每次都要在 footView 的bindViewModel 里 重置?这样每次返回cell 就会一直创建?怎么控制比较好。 谢谢
        overla5:@CoderMikeHe 你好,首先感谢回答。但是我创建footview 的 内容,是在footview 重写的 bindviewmodel里 创建的,在初始化里 创建 好像拿不到 viewmodel 的属性,无法根据分区 来 区别显示。。但是在bindviewmolde 里,每次刷新cell 会重复创建。 我是应该判断是否创建来控制不让它 二次创建,还是说有更好的办法呢?
        CoderMikeHe:@失格人间 如果你footer个数只是两个的话,且内容仅仅是文字不一样的话,你完全在控制器的ViewModel暴露两个字段属性即可,这样你就只需要绑定控制器的viewModel即可。如果footer个数很多且需要复用的,你就类似于cell 绑定一样。主要是根据界面的复杂度来确定。
      • d6c010abb012:写的挺好的,代码我也仔细看了,但是我这边有两个疑问:(1)你的父类抽离我感觉太过了,不知道你对这个是怎么界定的?;(2)现在这个项目我感觉被RAC给绑架了,如果突然让你别用RAC,你这个项目貌似就废了?有什么方案吗? 期待回应,谢谢
        CoderMikeHe:@iOS阿佑 (1)父类的抽离,其实是符合苹果一直推崇的`单一职责化`的,可能前期会觉得父类过多,不易理解和阅读,但其实就是为了满足各自的使用场景罢了。比如:某个模块需要用到表格视图(UITableView)那么你就直接继承MHTableViewController,如果你的模块不需要使用表格视图,那么你就直接继承MHViewController,当然如果你要用到WebView,你就直接继承MHWebViewController即可。所有的这些都是根据你项目的实际情况来做处理,体现了`单一职责的`道理,当然也避免了一些冗余代码的产生,例如表格视图的创建,以及实现表格视图的代理和数据源方法,这些完全可以避免。(2)架构这个东西其实前期是需要调研和预留风险的,选择第三方会考虑一些成熟且维护度比较活跃的框架来避免不更新而引起项目中断的风险。其次,RAC本质上是基于KVO来实现的。当然RAC是函数响应式编程,使用起来更加优雅罢了。
      • 举举手123:码字不易,先吃颗糖
        CoderMikeHe:@举举手123 3Q...
      • 举举手123:大神,我想问你一下,我看到你推荐了雷纯锋大神的 MVVMReactiveCocoa ,他的那个是 MVVM + RAC + ViewModel-Based Navigation 这种架构。我想问的是 加上 ViewModel-Based Navigation 这个的目的只是为了实现像安卓版微信的那样滑动切换tab吗?
        CoderMikeHe:@举举手123 完全不是的,MVVM + RAC + ViewModel-Based Navigation主要是为了在`ViewModel`里面实现 `Push/Pop` 以及 `Present/Dismiss`,使得在`ViewModel`中能够处理事件的连贯性。具体,参考其Demo
      • 顺其自然西红柿:楼主,我想问下,大公司的代码都是这样写的吗?这么多这么复杂的继承关系。这代码怎么看啊,后期迭代怎么改得动,而且又和rac绑得那么深,能谈下你的想法吗
        CoderMikeHe:@顺其自然西红柿 嗯嗯 没事。相互交流 ,共同进步。
        顺其自然西红柿:@CoderMikeHe 嗯嗯,谢谢啊~ 我也刚开始看你的源码,过几天我熟悉了有什么问题再问你哈,感谢感谢🙏
        CoderMikeHe:@顺其自然西红柿 哈哈,我敢说这个代码算简单的了。这继承关系其实也挺简单的了,其目的就是把一些基础上的设置,封装在基类里面,以便我们后期少写冗余代码,使我们更加关注业务逻辑的开发,就拿Demo里面的首页来说,我们只管配置数据即可,重写几个方法即可,而不用每次都要去创建tableView,实现代理方法,你不觉得每次都是差不多的代码吗。这样把他封装在基类,岂不更加简单。后期迭代怎么会对基类产生影响,反而一些小迭代只需要在基类改动一下,就能影响整个项目,何乐而不为,当然某些迭代是针对某一个控制器,但这样更不会影响基类,你只需要对这个控制器进行迭代。关于和RAC绑定的深,那是在项目开启前,已经决定用RAC,如果你是半路使用RAC,那么我们可能用的只是其中的某些功能罢了。总之,每个项目基本都会使用基类,其作用不可小觑。
      • 阿新_会飞的猴子:在 MHTableViewController.m 文件下的下面代码中下面这行代码中
        if (@available(iOS 11.0, *)) {
        报下面的错 无法识别平台
        Unrecognized platform name iOS

        我是xcode8.3.1 版本

        我是小白,也是老师您的铁粉,求指教
        CoderMikeHe:@阿新_会飞的猴子 嗯嗯。尽量更新到9.0,这样才能适配11.0
        阿新_会飞的猴子:@CoderMikeHe 老师, 我是不是更新到xcode9.0就可以了
        CoderMikeHe:@阿新_会飞的猴子 哈哈,Xcode9.0你值得拥有。我周一把这个改成宏来判断11.0
      • 会跳舞的狮子:自古铁粉都评论
        CoderMikeHe:@会跳舞的狮子 :yum::yum::yum:
      • 怀念裸奔的童年:不错不错
        CoderMikeHe:@怀念裸奔的童年 感谢感谢
      • 照亮黑夜的曙光:已star 加油
        CoderMikeHe:@_SX six six six ...
      • beb57d443acc:为什么我看到的是2楼
        CoderMikeHe:@Atease_ 自古一楼出人才。
      • 小李广17:二愣子写的不错:clap:
        小李广17:@CoderMikeHe 大傻子
        CoderMikeHe:@小李广17 请叫我...三板斧
      • CoderMikeHe:自古一楼归楼主

      本文标题:iOS 基于MVVM + RAC + ViewModel-Bas

      本文链接:https://www.haomeiwen.com/subject/hpfduxtx.html