前言
本篇文章,主要以图片裁剪组件为契机,使用IOP编程进行需求实现。所以不会讲解图片裁剪组件的代码逻辑(这块逻辑也不复杂,要说难点主要是坐标系计算)。如果感兴趣的话,这是demo
一、简单介绍
IOP编程,有些装逼的叫法。实际上经常会从那些懂得架构的人嘴里说出来。翻译成中文就是面向接口编程或者面向协议编程,与AOP(面向切面编程)、OOP(面向对象编程)等,都是一种编程思想。编程思想按我自己的理解就是考虑到耦合性,封装性之后代码优雅的一种实现方式,其都是为架构(MVC、MVP、MVVM、VIPER等)服务的。一个好的架构离不开适合它的编程思想。最典型的就是MVVM架构下,常选用响应式编程思想(代表框架为:ReactiveCocoa)进行开发。
二、文章背景
第一次接触IOP编程,实际上是从一个三方库中看到的。这个库我相信大部分道友都用过,那就是环信SDK。环信SDK核心内容是不开源的,但是当你大致浏览其声明文件的时候,你会惊讶地发现我们主要用到的IEMChatManager、IEMCallManager等都不是类声明文件,而是协议文件。第一次看到这种情况后,我懵逼了,晕菜了。然后从心底觉得环信程序员真的很“屌丝”。
屌丝是我对程序员的最高尊称,意为非常大佬的意思~
- 我曾试过模仿着用这种写法进行编写代码,但也只是照葫芦画瓢。不能够理解其有什么好处,为什么要这样写。
- 每个阶段都会回顾之前不能够理解的东西,去尝试用现有的水平再次理解它,如果还是不能理解,继续放着。直到今天,这篇文章应运而生。
- 有时候理解一个东西都需要一个契机。就像是去了解一个人,你可能只是需要一次旅行或者一次愉快地就餐
这是工程代码!!!有时候一些想法说出来都是抽象的,不是那么容易理解。所以强烈建议结合着代码看文章
三、工程需求
下面我们以这一个需求为契机,去理解IOP。
需求:实现一个图片裁剪组件。其有两种模式
1.固定图片,拖动裁剪框进行裁剪。裁剪框支持缩放
2.固定裁剪框,拖动图片进行裁剪。图片支持缩放
两种模式下,都可以初始定义图片资源、裁剪框宽高比、裁剪框样式。
认真阅读上面的需求说明,然后我们就可以代码设计了。传统的设计方式,我们可以针对两种模式去对应创建两个类,然后相应地实现各个模式的功能。
这种实现没有任何问题,但是不够优雅。如果以后设计出第三种模式,你还需要创建一个类,并完全从头开始设计代码功能
而面向接口编程恰好就解决了这个尴尬的问题,我们可以根据需求设计出相应的接口,以后如果出现第三种模式,只需要按照原有设计的接口去实现功能就可以了。
四、IOP编程统一接口,为以后的扩展规范代码结构
iOS的所谓接口,其实就是我们在环信sdk中看到的协议。 让我们再次阅读上面的需求,然后去试着设计出相应的接口如下:
NS_ASSUME_NONNULL_BEGIN
@protocol XLLAdjustContainerProtocol
@required
//裁剪图片资源
@property (nonatomic, strong) UIImage *originImage;
//组装对应模式下子视图内容
- (void)setupAdjustContents;
//布局对应模式下子视图
- (void)layoutAdjustContents;
//获取对应模式下的裁剪后图片
- (UIImage *)getClipedImage;
@optional
//裁剪框样式
@property (nonatomic, assign) XLLAdjustWindowType windowType;
//裁剪框宽高比
@property (nonatomic, assign) CGFloat aspectRatio;
@end
NS_ASSUME_NONNULL_END
可以看到这个接口(协议)有一个必要属性和三个必要方法,以及两个可选属性。
以后有新的模式的话,只需要实现相应的接口方法就可以了。
五、接口(协议)中的属性
你可能见过分类中利用runtime去动态生成属性,但是很少会看到协议中会有属性出现。其实只要理解所谓属性就是一个变量的setter和getter就理解了。当协议中属性被@required修饰了,也就意味着其setter和getter方法必须要实现。
所以为了方便,当继承了这种协议后,我们可以加上属性同步语句@synthesize propertyName。
当然如果在setter或getter中需要一些其他操作,可以手动重写对应方法进行操作。本Demo就是需要在setter方法中重新对子控件布局所以进行了重写。
六、方便IB调试
涉及到UI方面的东西,我们为了方便调试希望能在IB上及时看到效果。具体可见我的另一篇文章关于使用故事版与xib的一些干货。所以我创建了一个裁剪视图的容器视图XLLAdjustContainerView,以进行IB调试。其也算是根据装饰者模式的思想进行编写的,声明的装饰属性如下:
NS_ASSUME_NONNULL_BEGIN
IB_DESIGNABLE
@interface XLLAdjustContainerView : UIView
#if TARGET_INTERFACE_BUILDER
@property (nonatomic, assign) IBInspectable NSInteger adjustStyle;
@property (nonatomic, assign) IBInspectable NSInteger windowType;
#else
//裁剪模式类型
@property (nonatomic, assign) XLLAdjustStyle adjustStyle;
//裁剪框样式
@property (nonatomic, assign) XLLAdjustWindowType windowType;
#endif
//裁剪框宽高比
@property (nonatomic, assign) IBInspectable CGFloat aspectRatio;
//裁剪资源图片
@property (nonatomic, strong) IBInspectable UIImage *originImage;
//接口代理
@property (nonatomic, weak, readonly) UIView *adjustView;
@end
- 用IB_DESIGNABLE、IBInspectable修饰,使得IB可视化。
- 并且其声明文件中拥有一个只读且类型为UIView的adjustView属性。我们根据adjustView就可以调用对应的接口方法。
- 我们根据用户指定的adjustStyle(裁剪模式类型)内部指定对应的接口代理。
- (void)setAdjustStyle:(XLLAdjustStyle)adjustStyle
{
//移除原有接口视图
[self.adjustView removeFromSuperview];
_adjustStyle = adjustStyle;
//根据adjustStyle重新指定接口代理
self.adjustView = (adjustStyle == XLLAdjustStyleFrame)?self.adjustFrameView:self.adjustImageView;
//对接口代理进行配置
self.adjustView.aspectRatio = self.aspectRatio;
self.adjustView.windowType = self.windowType;
self.adjustView.originImage = self.originImage;
//重新组装子控件并进行布局
[self.adjustView setupAdjustContents];
[self addSubview:(UIView *)self.adjustView];
}
- 在裁剪容器中,对接口代理进行控件组装与布局
#pragma mark - life circle
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
[self setupBase];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder])
{
[self setupBase];
}
return self;
}
#pragma mark - setupBase
- (void)setupBase
{
//默认配置
self.backgroundColor = [UIColor clearColor];
self.windowType = XLLAdjustWindowTypeRound;
self.aspectRatio = 1.0;
self.originImage = [UIImage imageNamed:@"1"];
self.adjustStyle = XLLAdjustStyleFrame;
//控件组装
[self.adjustView setupAdjustContents];
[self addSubview:self.adjustView];
}
#pragma mark - layout
- (void)layoutSubviews
{
[super layoutSubviews];
self.adjustView.frame = self.bounds;
//控件布局
[self.adjustView layoutAdjustContents];
}
七、裁剪功能实现
第一步:继承接口
@interface XLLAdjustFrameView : UIView
@end
第二步:实现必要的接口方法
//接口属性不管三七二十一,同步再说
@implementation XLLAdjustImageView
@synthesize windowType = _windowType;
@synthesize aspectRatio = _aspectRatio;
@synthesize originImage = _originImage;
#pragma mark - XLLAdjustContainerProtocol
- (void)setupAdjustContents
{
//组装子控件
}
- (void)layoutAdjustContents
{
//布局子控件
}
//重写接口的属性setter方法
- (void)setAspectRatio:(CGFloat)aspectRatio
{
_aspectRatio = aspectRatio;
//重新对子控件进行布局
[self layoutAdjustContents];
}
- (void)setOriginImage:(UIImage *)originImage
{
_originImage = originImage;
self.originImgView.image = originImage;
//重新对子控件进行布局
[self layoutAdjustContents];
}
- (void)setWindowType:(XLLAdjustWindowType)windowType
{
_windowType = windowType;
//重新对子控件进行布局
[self layoutAdjustContents];
}
完成上方的工作后,一个新的裁剪模式就诞生了。
IB效果图如下:
八、总结
以上就是我对面向接口编程的一次典型运用。对着demo认真看的话,你应该会感受到它的好处:
- 根据需要定义出相应接口,所有的模式都遵循这个接口规范完成任务。而且必要完成什么任务,可选完成什么任务都是分明的
- 裁剪视图容器与裁剪视图之间的耦合性比使用传统代码实现耦合性要小。我随时都可以改变接口代理,对容器没有任何影响
要说缺点的话,我也觉得有一个:
相比于传统的实现方式,我觉得IOP更适合组件化,方便拓展,代码耦合性低。而在这个场景下,不太适合sdk开发。因为接口里有一些必要的方法,完全是没必要对用户暴露的,像我这种强迫症有些接受不了。但是如果像环信那种纯逻辑sdk的话,面向接口编程还是很合适的。