开发随笔

[toc]

开发随笔

使用AVPlayerViewController之前先导入AVKit头文件

支持的视频编码格式很有限:H.264、MPEG-4,扩展名(压缩格式):.mp4、.mov、.m4v、.m2v、.3gp、.3g2等,如果是RMVB就不行了,需要借助第三方的框架来实现更多格式的支持。

iOS视频播放的基本方法

https://www.jianshu.com/p/b304694af77a

浅谈iOS的AVPlayerViewController播放视频

https://www.jianshu.com/p/3f8e5045c087

AVPlayer 循环播放本地视频

https://blog.csdn.net/tailoffairylu/article/details/83142802

开发随笔

[toc]

开发随笔

iOS研发助手DoraemonKit

iOS研发助手DoraemonKit技术实现(一)

https://www.jianshu.com/p/00763123dbc4

iOS 常用调试方法:LLDB命令

iOS 线程保活

iOS RunLoop(一)

iOS中NSOperation详解

iOS 逆向开发16:HOOK原理上(HOOK 系统C函数)

另类iOS上的C函数hook

iOS被开发者遗忘在角落的NSException-其实它很强大

UITextView对齐

textView中的文字默认离顶部和底部是有间距的

1
2
3
4
UITextView *textView = [[UITextView alloc] init];
NSLog(@"...%@", NSStringFromUIEdgeInsets(textView.textContainerInset)); // 默认值{8, 0, 8, 0}

textView.textContainerInset = UIEdgeInsetsZero;

iOS 侧滑返回详解FDFullscreenPopGesture

https://www.jianshu.com/p/62015b4c9076

哆啦A梦 只能显示nslog,无法显示debugPrint

podfile中 use_frameworks! 和 #use_frameworks!区别

https://www.jianshu.com/p/ac629a1cb8f5

iOS 效果处理(内阴影、外阴影、外发光、内发光、投影)

https://blog.csdn.net/qq_34534179/article/details/109180717

IOS微信分享调起微信后立刻返回到app中无法分享的问题

https://blog.csdn.net/qq_35153373/article/details/112285261

iOS 14 popToRootViewControllerAnimated 底部tabbar消失

https://www.jianshu.com/p/c6b3ccff9e5b

美图秀秀的拼图功能

https://github.com/hxxyyangyong/MeituDemo

iOS 不规则(多边形)图形,贝塞尔曲线绘制自定义图形

https://www.jianshu.com/p/6786cc4d28a7?from=groupmessage

iOS UIButton 渐变色、边框渐变色、字体渐变色

https://blog.csdn.net/liwenjie0912/article/details/87548692

textfield

1、不要delloc

2、不要delegate = self. shouldchange不调用

3、二分法查找

iOS根据网络图片的size大小设置UIImageView的大小

https://www.cnblogs.com/sunfuyou/p/6284986.html

iOS 如何让button上的字体居左居右对齐

https://www.jianshu.com/p/737553cd8eb5

iOS 使用CGAffineTransform 使视图平移|旋转|缩放

https://www.jianshu.com/p/58d810cec41d

UILabel *titleLabel = [UILabel alloc]; 空指针

UILabel *titleLabel = [[UILabel alloc] init];

[container addSubview:titleLabel];

iOS开发 怎么删除UICollectionView的cell

https://jingyan.baidu.com/article/ceb9fb10bd32a08cac2ba053.html

UICollectionView执行performBatchUpdates 奔溃

https://blog.csdn.net/jamy08/article/details/50505859?utm_source=blogxgwz7

ios扩大按钮的点击区域

https://www.jianshu.com/p/9107be4cd84a

iOS 利用UICollectionView横向滚动、余弦函数曲线特性实现居中放大的卡片浏览工具 XLCardSwitch

https://blog.csdn.net/u013282507/article/details/54136812

iOS之事件穿透

https://www.jianshu.com/p/0bece5f27650

iOS 13-beta presentViewController 样式变化

https://www.jianshu.com/p/67901ae2323d

cocoapods 1.8.0版本之后,CDN: trunk 推荐解决方法

https://blog.csdn.net/ZHFDBK/article/details/106949342

Commond + alt + /

Pasted Graphic.png

让你不知道怎么死的RAC

[TOC]

前言:RAC学习起来的特点

  • 学习起来比较难
  • 团队开发的时候需要谨慎使用
  • 团队代码需要不断的评审,保证团队中所有人代码的风格一致!避免阅读代码的困难

一、RAC双向绑定UITextField的正确姿势

1、先说结果

1
2
3
4
5
6
7
8
9
// textField1: 键盘修改textField有问题的例子
RACChannelTo(self.viewModel, text1) = RACChannelTo(self.textField1, text);

// textField2: 代码修改textField有问题的例子
RACChannelTo(self.viewModel, text2) = self.textField2.rac_newTextChannel;

// textField3: 键盘和代码修改textField都没问题的例子
RACChannelTo(self.viewModel, text3) = RACChannelTo(self.textField3, text);
[self.textField3.rac_textSignal subscribe:RACChannelTo(self.textField3, text)];

比较结果如下列表所示:

未完整的双向绑定1 未完整的双向绑定2 完整的双向绑定
代码文本 RACBindTextField1 RACBindTextField2 RACBindTextField3
代码截图
textField1: 键盘修改textField有问题的例子```
```RACChannelTo(self.viewModel, text1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| textField               | textField1: 键盘修改textField有问题的例子                    | textField2: 代码修改textField有问题的例子                    | textField3: 键盘和代码修改textField都没问题的例子            |
| (通过代码)改变model时 | textField会改变 | textField会改变 | textField会改变 |
| 通过代码改变textField时 | model会改变 | <u>model不会改变</u> | model会改变 |
| 通过键盘改变textField时 | <u>model不会改变</u> | model会改变 | model会改变 |

#### 2、分析原因

要弄清为什么通过如上两种方式分别对textField1和textField2进行双向绑定会有问题,那么你需要先认识一下以下两个与textField有关的值的比较

| | | |
| ---- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| | ```RACChannelTo(self.textField, text)``` | ```self.textField.rac_newTextChannel``` |
| 原理 | 当通过code改变self.textField.text的值的时候,才会把RACChannelTo(self.textField, text)这个值发送出去 | 当通过键盘改变self.textField.text的值的时候,才会把self.textField.rac_newTextChannel这个值发送出去 |
| 后果 | 所以只使用这个时,键盘修改textField会有问题 | 所以只使用这个时,代码修改textField会有问题 |



如果你还存疑惑,那么我们拿`textField1: 键盘修改textField有问题的例子`来说明:

```objective-c
// textField1: 键盘修改textField有问题的例子
RACChannelTo(self.viewModel, text1) = RACChannelTo(self.textField1, text);

我们在RACKVOProxy中的observeValueForKeyPath处设置断点,会发现,当我们只设置如上代码时候,通过键盘改变textField的值的时,其并未走入所设断点中,即其此时并未能检测到文本框的文本已经改变了。所以,也就出现了只设置如上代码,会出现当通过键盘改变文本框(键盘未收起)的时候,viewModel中的值没法改变的情况。

RACKVOProxy observeValueForKeyPath

同理,另一个的验证也如此。

3、解决问题

下面我们对RACChannelTo(self.viewModel, text3) = RACChannelTo(self.textField3, text);绑定方式进行键盘修改的完善,完善方式如下:

1
2
3
4
5
6
RACChannelTo(self.viewModel, text3) = RACChannelTo(self.textField3, text);
@weakify(self);
[self.textField3.rac_textSignal subscribeNext:^(NSString * _Nullable x) {
@strongify(self);
self.viewModel.text3 = x;
}];

该部分代码,可简化为:

1
2
RACChannelTo(self.viewModel, text3) = RACChannelTo(self.textField3, text);
[self.textField3.rac_textSignal subscribe:RACChannelTo(self.textField3, text)];

即我们上诉开头时候的正确代码。

4、实际应用中还存在的bindViewModel的问题

4.1、在RAC绑定常见view中的textField的例子RACBindNorTextFieldViewController
1
2
3
4
5
6
7
8
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = NSLocalizedString(@"RAC Bind Normal TextField", nil);
[self setupViews];

[self bindViewModel];
}
4.2、在RAC绑定tableView中的textField的例子RACBindTvTextFieldViewController
1
2
3
4
5
6
7
8
9
10
11
12
13
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RACTextFieldBindTableViewCell *cell = (RACTextFieldBindTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"RACTextFieldBindTableViewCell" forIndexPath:indexPath];
cell.backgroundColor = [UIColor greenColor];

if (indexPath.row == 0) {
self.textField1 = cell.textField;
} else if (indexPath.row == 1) {
self.textField2 = cell.textField;
} else if (indexPath.row == 2) {
self.textField3 = cell.textField;
}
return cell;
}

那么bindViewModel的时机比较容易出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = NSLocalizedString(@"RAC Bind TableView TextField", nil);

[self setupViews];
//[self bindViewModel]; //textField未获取,无法进行绑定
}

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
//[self bindViewModel]; //textField未获取,无法进行绑定
}

- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self bindViewModel]; //tableView中的textField绑定的正确时机
}

5、遗留问题

遗留问题1:为什么只有键盘没回收时候的修改文本框值才有问题,而键盘回收时候不会也存在问题?

二、RAC监听的属性如何正确改变值(KVO的坑)

假设有如下需求,根据viewModel中的@property (nonatomic, assign) BOOL textFieldValid;值修改UIViewController中textField的自定义属性leftButtonSelected的值,怎么做???

答:RAC(self.textField, leftButtonSelected) = RACObserve(viewModel, textFieldValid);

问题是:是不是只要实现了这行代码就没什么问题了?或者说在实现这行代码前,你有什么需要注意的?如果你不知道,你不搞清楚原因,那么有一天RAC让你怎么死的,你都不知道。

1、下面将常见的你修改属性时候使用的代码及其效果,列举如下:

修改属性时候使用的代码 代码位置 效果
self.inTextFieldValid1 = inTextFieldValid1; viewModel中 正确
_inTextFieldValid2 = inTextFieldValid2; viewModel中 错误
[self setValue:@(inTextFieldValid3) forKey:@"inTextFieldValid3"]; viewModel中 正确
[self setValue:@(inTextFieldValid4) forKey:@”_inTextFieldValid4”]; viewModel中 错误
self.viewModel.outTextFieldValid1 = outTextFieldValid1; UIViewController中 正确
[self.viewModel setValue:@(outTextFieldValid2) forKey:@"outTextFieldValid2"]; UIViewController中 正确
[self.viewModel setValue:@(outTextFieldValid3) forKey:@”_outTextFieldValid3”]; UIViewController中 错误

上面正确与否的判断标准是什么?

其实如果熟悉KVO机制的你,应该知道KVO的本质是通过isa-swizzling新建了一个子类,并且重写了属性的setter方法,在setter方法的头和尾分别执行了willChangeValueForKey:didChangevlueForKey:两个方法来实现监听的。

所以,如果你修改属性时候使用的代码不会走setter方法,那么也就无法触发监听了。因而也就出现了你明明监听了属性,却无法正确运行的情况。

如上表格中的错误方法皆是不会走setter的。

下面是一张别人的图:

img

2、有无规避方法

问:通过如上解析,我们知道问题的根源是没调用setter,那我们可否通过编译器提示不能使用_xxx来规避???

在这里我们补充讲下@synthesize

写法1 写法2
源代码 @synthesize student; @synthesize student = _student;
等价代码 @synthesize student = student; —-同上—-

synthesize的作用就是让student = ?中的后者这个变量来“代替”属性,从而可以通过操作变量来进行属性的操作。但是有一点最关键的是,使用变量进行操作,属性本身的引用计数是不会增加的,因为没有经过调用setter方法或者是getter方法。但是如果使用self.student这种操作方式的话,实质上是通过setter或者是getter方法进行操作,引用计数会随着不同的操作而改变,了解了这点后就能够更好的避免内存泄露问题。

三、RAC监听数组的变化(KVO的坑)

iOS默认不支持KVO的形式来监听数组的变化,数组改变的时候,只是数组里面的值变化,但数组的地址没有变化,KVO监听的对象地址的变化。

由于不支持KVO来监听数组变化,就无法使用RAC来监听数组。

1、传统方式(我们不需要监听时候常使用的代码):
1
2
3
4
// 只是修改数组,无法触发监听
if (self.flawArray.count) {
[self.flawArray removeLastObject];
}
2、需要使用监听时候的数组修改
1
2
3
4
5
   // 修改数组时候同时能确保触发KVO的操作
if (self.okArray.count) {
NSMutableArray *kvo_okArray = [self mutableArrayValueForKey:@"okArray"];
[kvo_okArray removeLastObject];
}

四、RAC中监听通知的坑

请查看:RAC中监听通知的坑!

五、结束语

暂时到此!感谢查阅!

生活常识

高楼晃动正常吗?买房可能遇到哪些质量问题

文章来源:https://zhidao.baidu.com/special/view?id=7ca55a24626975510400

专业人士称,超高层建筑来回摇动是正常的,有利于楼体安全,高层建筑大部分都是钢结构,对于侧向压力会起到缓冲作用。高层建筑“打太极”,摇摆而“定”,也是对风压的一种缓冲。高层建筑不会摇摆才是危险的,一般来说越高的建筑,摇摆幅度越大。

在建造高层建筑时,风是首先要考虑的要素之一,高空风力增大的原因是高楼林立的“狭管效应”。由于高楼阻挡,如果风的通行面减小一半的话,其穿行速度则会增加约一倍。遇见高楼狭小的通道,风就更猛烈了。因此,超高层建筑顶部一般都会有位移表现,只是幅度不同。楼层越高,幅度越大。因此住在顶楼的居民感觉晃动是正常的。

编程思想-面向协议

[toc]

一、产生背景

1.1 传统面向对象编程(OOP)的弊端/继承的痛点:

继承的痛点:滥用继承的话,后面如果要抽离功能就会牵一发而动全身。即抽离一个功能会导致也要把继承链上涉及的功能都抽出来。显然太麻烦了。

痛点的解决:使用面向协议,将每个单独功能一个协议,复杂功能将协议组合起来即可。如此后续抽离时,抽单独协议和实现类即可(swift中可以将实现写在扩展中,可能不涉及实现类)。

我们将网络库CJNetwork使用面向协议编程后

1、定义协议CQNetworkRequestCompletionClientProtocal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NS_ASSUME_NONNULL_BEGIN

@protocol CQNetworkRequestCompletionClientProtocal <NSObject>

@required
/*
* 请求
*
* @param model 请求相关的信息(包含请求方法、请求地址、请求参数等)real\simulate\local
* @param completeBlock 请求结束的回调(为方便接口的重复利用回调中的responseModel使用id类型)
*
* @return 执行请求的任务
*/
- (NSURLSessionDataTask *)requestModel:(__kindof NSObject<CJRequestModelProtocol> *)model
completeBlock:(void (^)(CJResponeFailureType failureType, CJResponseModel *responseModel))completeBlock;
@end

NS_ASSUME_NONNULL_END

2、使用CQNetworkRequestCompletionClientProtocal

1
2
3
4
5
6
7
NS_ASSUME_NONNULL_BEGIN

@interface CJNetworkClient (Completion) <CQNetworkRequestCompletionClientProtocal>

@end

NS_ASSUME_NONNULL_END

1.2 什么是面向协议编程(POP)?

这个问题,我感觉比较明确的定义就是2015年Apple WWDC中说的一句话很直了的解释了:

1
2
Don't start with a class.
Start with a protocol.

即在程序设计中,不要以一个类开始设计,应该从一个协议开始,应抛弃之前OOP的对象设计理念,设计协议,这样不同的继承链之间也可以使用同一个协议。可以将协议看做一个组件,哪里需要哪里继承协议即可,而且协议是可以多继承的,iOS中的类只能单继承,这也是面向协议相对面向对象的一大优势。

1.3 Objective-C 和Swift的面向协议编程区别

OC和swift面向协议编程一个最大区别是OC的 Protocol 没有默认的实现,需要依赖具体的实现类实现协议定义的方法,而Swift2.0开始提供了Protocol + Extension,协议可以再 Extension中提供默认的实现,这样上层调用可以直接调用协议的默认实现。

严谨来说,OC不是一门面向协议编程的语言,因为 Protocol 只提供定义,而不提供实现,所以叫他 面向接口编程 更合适一些。

面向协议和依赖注入

依赖注入要求我们传入的参数遵循协议,这样做的好处是:

  • 我们可以在外部注入一个通用类,只要它遵循协议。
  • 也可以注入一个可测试的模拟类,便于测试。

参考文章:iOS-面向协议编程(POP)

二、替代继承的方式

参考文章:

1、协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> // 在屏幕中间弹出
> @protocol ShowInCenterProtocol <NSObject>
> - (void)show;
> @end
>
> // 在屏幕底部弹出
> @protocol ShowInBottomProtocol <NSObject>
> - (void)show;
> @end
>
> @class NormalVoicePlayer : NSObject <ShowInCenterProtocol>
>
> @end
>
>

2、用组合替代继承

eg1:想要为一个视图做一个show操作。(设想这个show方法后面可能随时会变,比如show的位置,或者动画)

AlertUtil:使用由AlertView/PopupView+PopupAction来组合,而不使用继承AlertView+为其添加show方法。

eg2:常见的举例还有,文本框TextField的根据历史记录填充、根据@邮箱填充。

3、类别category

1
2
3
@interface NSArray (OBJExtras)
- (void)removingFirstObject;
@end

三、使用面向协议为视图添加抖动功能

举例:为UIImageView和UIButton等添加视图的抖动

1、方法1:添加Category方式

如果你写过 Objective-C, 你很可能会把 shake() 写到一个 UIView 的分类(Category) 中(也就是 Swift 中的拓展 (extension)):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  UIViewExtension.swift

import UIKit

extension UIView {

func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}

2、方法2:面向协议方式

但在swift里,我们除了对UIView添加extension外,其实还有另一种更好的方式:创建面向协议的视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FoodImageView: UIImageView, Shakeable {
// 其他自定义写在这儿
}

class ActionButton: UIButton, Shakeable {
// 其他自定义写在这儿
}

class ViewController: UIViewController {

@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!

@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}

附上述面向协议的视图里所对的Shakeable 协议如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//  Shakeable.swift

import UIKit

protocol Shakeable { }

// 你可以只为 UIView 添加 shake 方法!
extension Shakeable where Self: UIView {

// shake 方法的默认实现
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}

参考文章:

四、使用面向协议处理reuseIdentifier

tableView的cell注册的基本代码如下

1
2
let foodCellNib = UINib(NibName: "FoodTableViewCell", bundle: nil)
tableView.registerNib(foodCellNib, forCellReuseIdentifier: "FoodTableViewCell")

改进1:FoodTableViewCell字符串改成由类获得

1
2
let foodCellNib = UINib(NibName: String(FoodTableViewCell), bundle: nil)
tableView.registerNib(foodCellNib, forCellReuseIdentifier: String(FoodTableViewCell))

改进2:

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol ReusableView: class {}
// 你可以只为 UIView 添加 ReusableView 方法!
extension ReusableView where Self: UIView {
static var reuseIdentifier: String {
return String(self)
}
}


extension UITableViewCell: ReusableView { }

let foodCellNib = UINib(NibName: “FoodTableViewCell”, bundle: nil)
tableView.registerNib(foodCellNib, forCellReuseIdentifier:FoodTableViewCell.reuseIdentifier)

END

架构模式-①概览

[Toc]

前言:架构模式、框架、设计模式的概念

iOS—架构模式、框架、设计模式的理解

架构模式:架构模式的出现是为了管理复杂的应用程序,这样可以在一个时间内专门关注一个方面。例如,您可以在不依赖业务逻辑的情况下专注于视图设计。同时也让应用程序的测试更加容易。同时也简化了分组开发。不同的开发人员可同时开发视图、控制器逻辑和业务逻辑。我们经常说的MVC架构MVVM架构属于此类。

框架:这个最好理解了,通常是代码重用。框架与设计模式的概念容易弄混,两者有相似之处,但却有着根本的不同。设计模式是对在某种环境中反复出现的问题以及解决该问题的方案的描述规范,它比框架更抽象;框架为已经解决问题的具体实现方法,能直接执行或复用;设计模式是比框架更小的元素,一个框架中往往含有一种或多种设计模式。Xcode自带的FoundationUIKit,以及我们经常使用的AFNetworkingMJExtensionSVProgressHUD就属于这一类。

设计模式:设计模式可以通俗的理解为实现/解决某些问题,而形成的解决方案规范。增加代码的可重用性,让代码能更容易理解和可靠。我们通常说所的代理模式迭代器模式策略模式就属于这一类。对各种设计模式的了解可以帮助我们更快的解决编程过程中遇到的问题。

三者关系:架构(动词)>框架>设计模式。

软件通过架构,可以设计出很多不同的框架。在一个框架中,也可以使用很多的设计模式。设计模式不是哪儿哪儿都可以用的,只有当出现了某一特定的问题时,才利用设计模式去解决。设计模式不是用的越多越好,在维护的时候,过多的设计模式会极大的增添维护成本。

一、架构模式

1、架构模式的演变

1.1、MVC

1、架构的设计其实是为了更好的维护和迭代,而不是只考虑眼前当下的开发。

2、最基本的架构是MVC,试想下如果我一个应用的所有功能都是像”关于“功能的,那我还需要其他架构干嘛?显然MVC就已经很足够,而且还显得不冗余(这里指的是设计或者粒度不冗余)。

3、其他的架构都是从MVC演变而来。再讲其他架构的演变之前,我们先明确最原始的MVC代码是如何的。

①、首先M主要负责数据、V主要负责视图、C主要负责数据的视图显示。而这里的M是原始的瘦Model

所以,我们拿常见的”我的”页面来举例,部分原始的MVC代码一般为如下样子:

1
2
3
4
5
6
7
8
9
UserModel


View


ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = user.sex == 1? "女" :"男";

4、在MVC+瘦Model的演变下,还有MVC+胖Model

胖Model就是瘦Model+部分弱业务逻辑(这些弱业务重复一般都是经常出现,或者说是要求可复用性的,如根据枚举获取性别字符串)。它使得Controller可以从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上.

1
2
3
4
5
6
7
8
9
UserModel
+ (NSString *)sexStringFrom:(int)sex;

View


ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = [UserModel sexStringFrom:user.sex];

1.2、MVP?

5、有时候在MVC+瘦Model的演变下,不是转成MVC+胖Model,而是MVC+瘦Model+Helper

其实这个思路已经很接近MVP了、但是还差提点。MVP还需要为View提供数据

1
2
3
4
5
6
7
8
9
10
UserModel

View

UserHelper
+ (NSString *)sexStringFrom:(int)sex;

ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = [UserHelper sexStringFrom:user.sex];

1.3、MVVM

6、关于MVC+瘦Model+Helper,还可以改为MVC+瘦Model+ViewModel

刚才的胖Model只从Controller移植走了一些简单的弱业务。

而ViewModel则干脆把数据的处理全部从Controller移植了出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
UserModel

View

UserViewModel
name
sexString
- (id)initWithName:(NSString *)name sex:(int)sex;


ViewController
self.userInfoView.name = userViewModel.name;
self.userInfoView.sexString = userViewModel.sexString;

1.4、MVCS -> MVVM+S

拿有数据操作的”购物车“页面来说,

假设其除了有网络数据请求外,还有本地数据库处理。(附:购物车是否需要本地数据库,个人认为是基于服务端的压力来考虑,不然一般是放在服务端合适点。压力设想如下,千万级用户,购物车加减商品频繁,假设放在后台,则操作和计算也就频繁,压力也就大了。如果放在前端,则后台只需要负责是否可加减商品的检验,而不用再建购物车数据表和计算价格等,压力就小了。)

每个页面一般都会有网络请求,这些网络请求最原始的时候,是放在ViewController,也就是MVC的C中。

为了避免MVC中的ViewController后期变得十分臃肿。我们肯定要在MVC的基础上额外增加一层来处理这个。

综合考虑后,我们选用MVCS来处理。

其中S代表着Store,且我们这里的S代表着是整个模块的S,而不是每个页面都有一个S。其下层又包含着Storage和Service,即 数据库DB部分网络Network部分

Store类名,个人习惯是使用Manager来命令。如UserManager。

MVCS1

所以,最终我们选用的是 MVCS+MVVM 或者说 MVVM+S。

S层(service 层)提供一种和外界(比如远程服务 API 或文件系统)交互的独立机制。

MVVM+S 纵向的数据流就不用多说,横向的数据流,我们采用自定义的类系统通知。区别在于它有着系统Notification的一对多方便,又有着delegate的接口对接方便。

image-20200917024611311

抛开数据共享问题(如每个请求都要带userToken),每个页面

二、架构分层(Architectural Layering)

架构分层是一种将系统分解为多个逻辑层次的方法,每一层都有特定的职责和功能。

  • 架构分层有助于实现关注点分离(Separation of Concerns),使得系统的不同部分可以独立开发和维护。
  • 典型的分层包括表示层(或用户界面层)、业务逻辑层、数据访问层等。

架构分层图片来源于 《架构分层.graffle》中的【一、架构分层】

其他图片见我的项目 CJStandardProject 中的 Screenshots

其他参考文章

iOS:

三、常见的项目目录结构

常见的项目目录结构:

框架①项目目录结构

项目目录结构规范说明 请点击本链接跳转下文附录进行查看

完整的 项目目录结构树 请点击链接,跳转到本问附文中查看。

四、页面与服务类设计

1、核心理念

页面执行自己的动作,调用Service服务类。无需关心数据请求、数据请求结束后是否需要缓存等等。

Service服务中心,内部包含请求和可能的数据存储等处理,不对外暴露。

2、设计举例

页面调用服务类举例

2.1、设计

arc_layer_2

图片来源于 《架构分层.graffle》中的【二、页面与服务类设计】

2.2、服务类中的代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// UserRequest
class UserService {
// 登录-普通
static login_normal(String mobile, String smsCode) {
return UserRequest.login_normal(
mobile: mobile,
smsCode: smsCode,
completeBlock: (UserModel userModel) {
UserInfoManager().loginSuccessWithUserModel(userModel);
},
);
}
}

class UserRequest {
// 登录-普通
static login_normal({
String mobile,
String smsCode,
void Function(UserModel userModel) completeBlock,
}) {
return AppNetworkKit.post(
UrlPath.doLogin,
params: {
"mobile": mobile,
"smsCode": smsCode,
"grantType": "sms_code",
},
).then((ResponseModel responseModel) {
if (responseModel.isSuccess) {
User_manager_bean baseUserModel;
if (responseModel.result != null) {
baseUserModel = UserModel.fromJson(responseModel.result);
} else {
baseUserModel = null;
}
completeBlock(baseUserModel);
} else {
completeBlock(null);
}
});
}
}

附一:项目目录结构规范说明

一个完整的项目目录结构一定至少包含有以下几个方面。

附:完整的 项目目录结构树 请点击链接,跳转到本问附文中查看。

1、程序入口

1
│  ├─ main.dart

2、依赖库package

1
2
3
4
5
├─ package
│ ├─ network
│ └─ route
│ ├─ errors
│ └─ manager

项目依赖库,统一存放在package下。

3、资源文件assets

1
2
3
4
5
6
7
8
9
├─ assets
│ ├─ fonts
│ ├─ images
│ │ ├─ base
│ │ ├─ order
│ │ └─ user
│ └─ json
│ ├─ config
│ └─ mock

资源文件:统一存放在assets下。区分字体fonts、图片images、json(eg城市配置信息)

4、公共组件common

1
2
3
4
5
6
7
8
9
10
├─ lib
│ ├─ common
│ │ ├─ extensions
│ │ │ └─ num_extension.dart
│ │ ├─ styles
│ │ │ └─ colors.dart
│ │ │ └─ typography.dart
│ │ └─ widgets
│ │ ├─ common_button.dart
│ │ └─ common_text.dart

common/:包含通用的小部件、样式和颜色等。

——-widgets:包含通用的小部件,例如按钮、文本、图标等。

——-styles: 包含通用的颜色和字体等。例如,colors.dart 文件中可以定义一些通用的颜色常量,typography.dart 文件中可以定义一些通用的字体样式(RegularTextStyle、BoldTextStyle、MediumTextStyle、DDINPROBoldTextStyle)。

——-extensions:系统类的扩展方法(eg: 20.w_pt_bj)

5、通用工具util

1
2
│  └─ util
│ └─ textsize_util.dart

utils/:包含一些通用的工具函数或类,例如日期格式化、字符串处理、数学计算等。

6、数据模型models

1
2
3
4
5
6
7
8
9
10
11
12
│  ├─ model
│ │ ├─ global_config
│ │ │ ├─ app_config_model.dart
│ │ │ ├─ game_config_model.dart
│ │ │ ├─ global_config_model.dart
│ │ │ └─ web_config_model.dart
│ │ ├─ order
│ │ │ ├─ order_base_model.dart
│ │ │ └─ order_detail_model.dart
│ │ └─ user
│ │ ├─ user_base_model.dart
│ │ └─ user_detail_model.dart

models/:包含应用程序的数据模型,例如用户、订单等。

7、服务类service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
│  ├─ service
│ │ ├─ global_config
│ │ │ ├─ manager
│ │ │ │ └─ global_config_service.dart
│ │ │ ├─ request
│ │ │ │ └─ global_config_request.dart
│ │ │ └─ cache
│ │ │ └─ global_config_cache.dart
│ │ └─ user
│ │ ├─ manager
│ │ │ └─ user_service.dart
│ │ ├─ request
│ │ │ └─ user_info_request.dart
│ │ └─ cache
│ │ └─ user_info_cache.dart

services/:包含应用程序的服务类或者管理中心,例如用户信息中心、配置信息中心等。

——-manager: 管理中心

——-request: 管理中心会需要的请求功能

——-cache: 管理中心可能需要的存储功能

8、模块功能module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
│  ├─ module
│ │ ├─ auth
│ │ │ ├─ pages
│ │ │ │ ├─ login_page
│ │ │ │ │ └─ login_page.dart
│ │ │ │ └─ register_page
│ │ │ │ └─ register_page.dart
│ │ │ ├─ routes
│ │ │ │ ├─ auth_route_handel.dart
│ │ │ │ ├─ auth_route_interceptor.dart
│ │ │ │ └─ auth_route_name.dart
│ │ │ └─ widgets
│ │ │ ├─ code_input_widget.dart
│ │ │ ├─ name_input_widget.dart
│ │ │ └─ tel_input_widget.dart

modules/:包含应用程序的不同模块,例如身份验证、主页和个人资料等。

——-page: 页面

——-routes: 路由

——-widgets: 视图部件

附1、项目目录结构树

项目目录结构(以Flutter为例).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
std_demo
├─ .gitignore
├─ .metadata
├─ README.md
├─ analysis_options.yaml
├─ android
│ ├─ .gitignore
│ ├─ .gradle
│ │ ├─ 7.5
│ │ │ ├─ checksums
│ │ │ │ └─ checksums.lock
│ │ │ ├─ dependencies-accessors
│ │ │ │ ├─ dependencies-accessors.lock
│ │ │ │ └─ gc.properties
│ │ │ ├─ executionHistory
│ │ │ │ └─ executionHistory.lock
│ │ │ ├─ fileChanges
│ │ │ │ └─ last-build.bin
│ │ │ ├─ fileHashes
│ │ │ │ └─ fileHashes.lock
│ │ │ ├─ gc.properties
│ │ │ └─ vcsMetadata
│ │ ├─ buildOutputCleanup
│ │ │ ├─ buildOutputCleanup.lock
│ │ │ └─ cache.properties
│ │ └─ vcs-1
│ │ └─ gc.properties
│ ├─ app
│ │ ├─ build.gradle
│ │ └─ src
│ │ ├─ debug
│ │ │ └─ AndroidManifest.xml
│ │ ├─ main
│ │ │ ├─ AndroidManifest.xml
│ │ │ ├─ java
│ │ │ │ └─ io
│ │ │ │ └─ flutter
│ │ │ │ └─ plugins
│ │ │ │ └─ GeneratedPluginRegistrant.java
│ │ │ ├─ kotlin
│ │ │ │ └─ com
│ │ │ │ └─ example
│ │ │ │ └─ std_demo1
│ │ │ │ └─ MainActivity.kt
│ │ │ └─ res
│ │ │ ├─ drawable
│ │ │ │ └─ launch_background.xml
│ │ │ ├─ drawable-v21
│ │ │ │ └─ launch_background.xml
│ │ │ ├─ mipmap-hdpi
│ │ │ │ └─ ic_launcher.png
│ │ │ ├─ mipmap-mdpi
│ │ │ │ └─ ic_launcher.png
│ │ │ ├─ mipmap-xhdpi
│ │ │ │ └─ ic_launcher.png
│ │ │ ├─ mipmap-xxhdpi
│ │ │ │ └─ ic_launcher.png
│ │ │ ├─ mipmap-xxxhdpi
│ │ │ │ └─ ic_launcher.png
│ │ │ ├─ values
│ │ │ │ └─ styles.xml
│ │ │ └─ values-night
│ │ │ └─ styles.xml
│ │ └─ profile
│ │ └─ AndroidManifest.xml
│ ├─ build.gradle
│ ├─ gradle
│ │ └─ wrapper
│ │ ├─ gradle-wrapper.jar
│ │ └─ gradle-wrapper.properties
│ ├─ gradle.properties
│ ├─ gradlew
│ ├─ gradlew.bat
│ ├─ local.properties
│ └─ settings.gradle
├─ assets
│ ├─ fonts
│ ├─ images
│ │ ├─ base
│ │ ├─ order
│ │ └─ user
│ └─ json
│ ├─ config
│ └─ mock
├─ ios
│ ├─ .gitignore
│ ├─ Flutter
│ │ ├─ AppFrameworkInfo.plist
│ │ ├─ Debug.xcconfig
│ │ ├─ Generated.xcconfig
│ │ ├─ Release.xcconfig
│ │ └─ flutter_export_environment.sh
│ ├─ Runner
│ │ ├─ AppDelegate.swift
│ │ ├─ Assets.xcassets
│ │ │ ├─ AppIcon.appiconset
│ │ │ │ ├─ Contents.json
│ │ │ │ ├─ Icon-App-1024x1024@1x.png
│ │ │ │ ├─ Icon-App-20x20@1x.png
│ │ │ │ ├─ Icon-App-20x20@2x.png
│ │ │ │ ├─ Icon-App-20x20@3x.png
│ │ │ │ ├─ Icon-App-29x29@1x.png
│ │ │ │ ├─ Icon-App-29x29@2x.png
│ │ │ │ ├─ Icon-App-29x29@3x.png
│ │ │ │ ├─ Icon-App-40x40@1x.png
│ │ │ │ ├─ Icon-App-40x40@2x.png
│ │ │ │ ├─ Icon-App-40x40@3x.png
│ │ │ │ ├─ Icon-App-60x60@2x.png
│ │ │ │ ├─ Icon-App-60x60@3x.png
│ │ │ │ ├─ Icon-App-76x76@1x.png
│ │ │ │ ├─ Icon-App-76x76@2x.png
│ │ │ │ └─ Icon-App-83.5x83.5@2x.png
│ │ │ └─ LaunchImage.imageset
│ │ │ ├─ Contents.json
│ │ │ ├─ LaunchImage.png
│ │ │ ├─ LaunchImage@2x.png
│ │ │ ├─ LaunchImage@3x.png
│ │ │ └─ README.md
│ │ ├─ Base.lproj
│ │ │ ├─ LaunchScreen.storyboard
│ │ │ └─ Main.storyboard
│ │ ├─ GeneratedPluginRegistrant.h
│ │ ├─ GeneratedPluginRegistrant.m
│ │ ├─ Info.plist
│ │ └─ Runner-Bridging-Header.h
│ ├─ Runner.xcodeproj
│ │ ├─ project.pbxproj
│ │ ├─ project.xcworkspace
│ │ │ ├─ contents.xcworkspacedata
│ │ │ └─ xcshareddata
│ │ │ ├─ IDEWorkspaceChecks.plist
│ │ │ └─ WorkspaceSettings.xcsettings
│ │ └─ xcshareddata
│ │ └─ xcschemes
│ │ └─ Runner.xcscheme
│ └─ Runner.xcworkspace
│ ├─ contents.xcworkspacedata
│ └─ xcshareddata
│ ├─ IDEWorkspaceChecks.plist
│ └─ WorkspaceSettings.xcsettings
├─ lib
│ ├─ common
│ │ ├─ extensions
│ │ │ └─ num_extension.dart
│ │ ├─ styles
│ │ │ ├─ colors.dart
│ │ │ └─ typography.dart
│ │ └─ widgets
│ │ ├─ common_button.dart
│ │ └─ common_text.dart
│ ├─ main.dart
│ ├─ model
│ │ ├─ evaluate
│ │ │ └─ evaluate_model.dart
│ │ ├─ global_config
│ │ │ ├─ app_config_model.dart
│ │ │ ├─ game_config_model.dart
│ │ │ ├─ global_config_model.dart
│ │ │ └─ web_config_model.dart
│ │ ├─ goods
│ │ │ ├─ goods_base_model.dart
│ │ │ └─ goods_detail_model.dart
│ │ ├─ order
│ │ │ ├─ order_base_model.dart
│ │ │ └─ order_detail_model.dart
│ │ ├─ user
│ │ │ ├─ user_base_model.dart
│ │ │ └─ user_detail_model.dart
│ │ └─ wish
│ │ ├─ wish_base_model.dart
│ │ └─ wish_detail_model.dart
│ ├─ module
│ │ ├─ auth
│ │ │ ├─ pages
│ │ │ │ ├─ login_page
│ │ │ │ │ └─ login_page.dart
│ │ │ │ └─ register_page
│ │ │ │ └─ register_page.dart
│ │ │ ├─ routes
│ │ │ │ ├─ auth_route_handel.dart
│ │ │ │ ├─ auth_route_interceptor.dart
│ │ │ │ └─ auth_route_name.dart
│ │ │ └─ widgets
│ │ │ ├─ code_input_widget.dart
│ │ │ ├─ name_input_widget.dart
│ │ │ └─ tel_input_widget.dart
│ │ ├─ game
│ │ │ ├─ routes
│ │ │ │ ├─ game_route_handle.dart
│ │ │ │ ├─ game_route_interceptor.dart
│ │ │ │ └─ game_route_name.dart
│ │ │ └─ widgets
│ │ ├─ mall
│ │ │ ├─ pages
│ │ │ │ └─ mall_home_page
│ │ │ │ └─ mall_home_page.dart
│ │ │ ├─ routes
│ │ │ │ └─ mall_route_handle.dart
│ │ │ └─ widgets
│ │ ├─ order
│ │ │ ├─ pages
│ │ │ ├─ routes
│ │ │ └─ widgets
│ │ ├─ profile
│ │ │ ├─ pages
│ │ │ │ └─ profile_page
│ │ │ │ └─ profile_page.dart
│ │ │ └─ widgets
│ │ │ └─ profile_avatar.dart
│ │ ├─ web
│ │ │ └─ routes
│ │ │ ├─ web_route_handel.dart
│ │ │ ├─ web_route_interceptor.dart
│ │ │ └─ web_route_name.dart
│ │ └─ wish
│ │ ├─ pages
│ │ │ ├─ wish_create_add_sku_page
│ │ │ │ └─ wish_create_add_sku_page.dart
│ │ │ ├─ wish_create_page
│ │ │ │ └─ wish_ceate_page.dart
│ │ │ ├─ wish_detail_dark_page
│ │ │ │ └─ wish_detail_dark_page.dart
│ │ │ ├─ wish_detail_light_page
│ │ │ │ └─ wish_detail_light_page.dart
│ │ │ ├─ wish_goods_control_page
│ │ │ │ └─ wish_goods_control_page.dart
│ │ │ └─ wish_home_page
│ │ │ └─ wish_home_page.dart
│ │ └─ widgets
│ │ ├─ wish_create_add_sku_widget1.dart
│ │ └─ wish_create_add_sku_widget2.dart
│ ├─ service
│ │ ├─ global_config
│ │ │ ├─ cache
│ │ │ │ └─ global_config_cache.dart
│ │ │ ├─ manger
│ │ │ │ └─ global_config_service.dart
│ │ │ └─ request
│ │ │ └─ global_config_request.dart
│ │ ├─ order
│ │ │ ├─ cache
│ │ │ │ └─ order_cache.dart
│ │ │ ├─ manager
│ │ │ │ └─ order_service.dart
│ │ │ └─ request
│ │ │ └─ order_request.dart
│ │ ├─ publish
│ │ │ ├─ cache
│ │ │ │ └─ publish_cache.dart
│ │ │ ├─ manager
│ │ │ │ └─ publish_service.dart
│ │ │ └─ request
│ │ │ └─ publish_request.dart
│ │ └─ user
│ │ ├─ cache
│ │ │ └─ user_info_cache.dart
│ │ ├─ manager
│ │ │ └─ user_service.dart
│ │ └─ request
│ │ └─ user_info_request.dart
│ └─ util
│ └─ textsize_util.dart
├─ linux
│ ├─ .gitignore
│ ├─ CMakeLists.txt
│ ├─ flutter
│ │ ├─ CMakeLists.txt
│ │ ├─ generated_plugin_registrant.cc
│ │ ├─ generated_plugin_registrant.h
│ │ └─ generated_plugins.cmake
│ ├─ interceptors
│ │ ├─ game_interceptor.dart
│ │ └─ login_interceptor.dart
│ ├─ main.cc
│ ├─ my_application.cc
│ └─ my_application.h
├─ macos
│ ├─ .gitignore
│ ├─ Flutter
│ │ ├─ Flutter-Debug.xcconfig
│ │ ├─ Flutter-Release.xcconfig
│ │ ├─ GeneratedPluginRegistrant.swift
│ │ └─ ephemeral
│ │ ├─ Flutter-Generated.xcconfig
│ │ └─ flutter_export_environment.sh
│ ├─ Runner
│ │ ├─ AppDelegate.swift
│ │ ├─ Assets.xcassets
│ │ │ └─ AppIcon.appiconset
│ │ │ ├─ Contents.json
│ │ │ ├─ app_icon_1024.png
│ │ │ ├─ app_icon_128.png
│ │ │ ├─ app_icon_16.png
│ │ │ ├─ app_icon_256.png
│ │ │ ├─ app_icon_32.png
│ │ │ ├─ app_icon_512.png
│ │ │ └─ app_icon_64.png
│ │ ├─ Base.lproj
│ │ │ └─ MainMenu.xib
│ │ ├─ Configs
│ │ │ ├─ AppInfo.xcconfig
│ │ │ ├─ Debug.xcconfig
│ │ │ ├─ Release.xcconfig
│ │ │ └─ Warnings.xcconfig
│ │ ├─ DebugProfile.entitlements
│ │ ├─ Info.plist
│ │ ├─ MainFlutterWindow.swift
│ │ └─ Release.entitlements
│ ├─ Runner.xcodeproj
│ │ ├─ project.pbxproj
│ │ ├─ project.xcworkspace
│ │ │ └─ xcshareddata
│ │ │ └─ IDEWorkspaceChecks.plist
│ │ └─ xcshareddata
│ │ └─ xcschemes
│ │ └─ Runner.xcscheme
│ └─ Runner.xcworkspace
│ ├─ contents.xcworkspacedata
│ └─ xcshareddata
│ └─ IDEWorkspaceChecks.plist
├─ package
│ ├─ network
│ └─ route
│ ├─ errors
│ └─ manager
├─ pubspec.lock
├─ pubspec.yaml
├─ test
│ └─ widget_test.dart
├─ web
│ ├─ favicon.png
│ ├─ icons
│ │ ├─ Icon-192.png
│ │ ├─ Icon-512.png
│ │ ├─ Icon-maskable-192.png
│ │ └─ Icon-maskable-512.png
│ ├─ index.html
│ └─ manifest.json
└─ windows
├─ .gitignore
├─ CMakeLists.txt
├─ flutter
│ ├─ CMakeLists.txt
│ ├─ generated_plugin_registrant.cc
│ ├─ generated_plugin_registrant.h
│ └─ generated_plugins.cmake
└─ runner
├─ CMakeLists.txt
├─ Runner.rc
├─ flutter_window.cpp
├─ flutter_window.h
├─ main.cpp
├─ resource.h
├─ resources
│ └─ app_icon.ico
├─ runner.exe.manifest
├─ utils.cpp
├─ utils.h
├─ win32_window.cpp
└─ win32_window.h

END

好文分享:

架构模式-④MVC与MVVM

[toc]

一、框架

二、MVC,MVP 和 MVVM 的图示

MVC,MVP 和 MVVM 的图示

三、MVC与MVVM

1、MVC的开发模式

MVC是模型、视图、控制器开发模式,对于iOS SDK,所有的View都是视图层的,它应该独立于模型层,由视图器来控制。所有的用户数据都是模型层,它应该独立于视图。所有的ViewController都是视图器,由它负责控制视图,访问模型数据。

2、关于MVC 和 MVVM 的区别

MVVM 介绍

1.传统的MVC设计模式

mvc_1

MVP设计模式

mvp_1

用户在view上的操作,会通过vm对model进行数据的修改;

数据的修改引起的属性变化会通知到vm上;

vm根据变化进行view UI的更新;

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

img

唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。

MVVM的代码实现

ViewModel设计的了解,请进入项目:《DvlproadDesignPatternCollect》中的 CJViewModelDemo
该项目采用blockdelegateKVORAC四种方式介绍ViewModel的设计。 详情请查看:CJViewModelDemoViewModelDemos 文件夹下的代码

Flutter 中可查看 001-UIKit-CQDemo-Flutter 项目中的 tsdemodemo_flutter 工程里的 architecture 文件目录

该MVVM设计在其他项目中的使用,可见:CJStandardProject 的 LoginViewModel.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//  LoginViewModel.h
#import <Foundation/Foundation.h>
@protocol LoginViewModelDelegate <NSObject>

/// userName 的有效性发生变化
- (void)logic_checkUserNameWithValid:(BOOL)valid;
/// password 的有效性发生变化
- (void)logic_checkPasswordWithValid:(BOOL)valid;
/// 登录按钮 的有效性发生变化
- (void)logic_checkLoginWithValid:(BOOL)valid;

@end



@interface LoginViewModel : NSObject {

}
@property (nonatomic, weak) id<LoginViewModelDelegate> delegate;
@property (nonatomic, copy, readonly) NSString *userName;
@property (nonatomic, copy, readonly) NSString *password;

- (instancetype)initWithUserName:(NSString *)userName password:(NSString *)password;

#pragma mark - Update
- (void)updateUserName:(NSString *)userName;
- (void)updatePassword:(NSString *)password;

#pragma mark - Do
/// 检查是否可登录
- (NSString *)checkLoginCondition;

/// 执行登录
- (void)loginWitLoginSuccess:(void (^)(NSString *successMessage, DemoUser *user))loginSuccess
loginFailure:(void (^)(NSString *errorMessage))loginFailure;

@end

其他MVVM说明示例

设有如下原始Model

1
2
3
4
5
6
7
8
9
10
@interface Person : NSObject

- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;

@property (nonatomic, readonly) NSString *salutation;
@property (nonatomic, readonly) NSString *firstName;
@property (nonatomic, readonly) NSString *lastName;
@property (nonatomic, readonly) NSDate *birthdate;

@end

则如果仅有此Model的话,我们在PersonViewController中

1
2
3
4
5
6
7
8
9
10
11
12
- (void)viewDidLoad {
[super viewDidLoad];

NSString *nameText = nil;
if (self.personModel.salutation.length > 0) {
nameText = [NSString stringWithFormat:@"%@ %@ %@", self.personModel.salutation, self.personModel.firstName, self.personModel.lastName];
} else {
nameText = [NSString stringWithFormat:@"%@ %@", self.personModel.firstName, self.personModel.lastName];
}

self.nameLabel.text = nameText;
}

而如果我们进而增加一个ViewModel,则如果我们PersonViewController中可以简化成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@implementation PersonViewModel

- (instancetype)initWithPerson:(Person *)person {
self = [super init];
if (!self) return nil;

_person = person;
if (person.salutation.length > 0) {
_nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
} else {
_nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName];
}

return self;
}

@end

// 此时我们PersonViewController中可以简化成
- (void)viewDidLoad {
[super viewDidLoad];

self.nameLabel.text = self.personViewModel.nameText;
}

END

瘦身-⑥AppDelegate瘦身

必备知识架构-瘦身-⑥AppDelegate瘦身

[toc]

前言

CJModuleManager:CJUIKitDemo-CJBaseUtil-CJModuleManager

1、AppDelegate瘦身

举个更实际的例子,就是你在为AppDelegate瘦身的时候,将AppDelegate上的方法,分散到各自的Manager(UserManager、PushManager、LocationManager等)的时候,就肯定会需要处理这些dele

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.

[[CJModuleManager sharedInstance] application:application didFinishLaunchingWithOptions:launchOptions]; //即一启动,就会去执行所有添加到ModuleManager中所有的 application:application didFinishLaunchingWithOptions:launchOptions 方法

// 设置主窗口,并设置根控制器
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

UIViewController *viewController = [[CTMediator sharedInstance] BBXPLogin_loginViewControllerWithLoginSuccessBlock:loginSuccessBlock getUserInfoSuccessBlock:getUserInfoSuccessBlock];

self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:viewController];

[self.window makeKeyAndVisible];

return YES;
}

// 附:上述AppDelegete.m执行的CJModuleManager.m中方法为
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
for (id<CJModule> module in self.modules) {
if ([module respondsToSelector:_cmd]) {
[module application:application didFinishLaunchingWithOptions:launchOptions];
}
}
return YES;
}

END