「iOS」通过CoreLocation Framework深入了解MVC架构
「iOS」通过CoreLocation Framework重新了解多界面传值以及MVC架构
文章目录
- 「iOS」通过CoreLocation Framework重新了解多界面传值以及MVC架构
- 前言
- CoreLocation了解
- 根据需求建模
- 设计属性
- 方法设计
- 协议传值
- Block传值
- KVO
- Notification通知方式
- 总结
- 参考文章
前言
在这个学期的前段时间进行了MVC的相关学习,并且使用MVC完成了知乎日报奥的项目,加上学习的一些博客,开始对MVC这个架构有着更加深刻的理解,也体会到了MVC架构之中的缺点,这篇文章就是利用CoreLocation 这个原生关于定位的架构,来总结MVC的一些使用技巧和理解。
CoreLocation了解
根据需求建模
我们可以先来认识CoreLocation
实现定位的相关内容,我们知道CoreLocation
是一个关于位置定位的库,我们分析一下相关的功能,要想实现定位首先就需要一个位置类来对位置进行描述,属于它的属性应该有经纬度,海拔等相关信息;一个地标类来描述所处位置的城市街道;一个管理器来控制位置的变更,以及位置的获取;一个解析器根据当前的位置信息转化为地标。那么逻辑如此,对应的图片关系如下
我们可以在CoreLocation Framework
之中找到了对应的这几个类的相关属性,这里我简单的给出这些类之中的部分头文件
CLLocation
:
@interface CLLocation : NSObject <NSCopying, NSSecureCoding>
@property(readonly, nonatomic) CLLocationCoordinate2D coordinate;//返回当前位置的坐标。
@property(readonly, nonatomic) CLLocationDistance altitude;// 返回位置的高度,正值表示海平面上,负值表示海平面下。
- (instancetype)initWithLatitude:(CLLocationDegrees)latitude
longitude:(CLLocationDegrees)longitude;//初始化指定经纬度的位置。
@end
CLPlacemark
:
@interface CLPlacemark : NSObject <NSCopying, NSSecureCoding>
// 通过已有地标初始化新地标并复制其数据
- (instancetype)initWithPlacemark:(CLPlacemark *) placemark;
// 获取与地标相关联的地理位置信息
@property (nonatomic, readonly, copy, nullable) CLLocation *location;
// 城市名称(如Cupertino)
@property (nonatomic, readonly, copy, nullable) NSString *locality;
......
// 一系列地标相关的具体属性,用于更详细地描述位置信息
CLLocationManager
:
@interface CLLocationManager : NSObject
// 获取调用应用的当前授权状态
@property (nonatomic, readonly) CLAuthorizationStatus authorizationStatus API_AVAILABLE(ios(14.0), macos(11.0), watchos(7.0), tvos(14.0));
// 获取最后接收到的位置信息,在接收到位置前为nil
@property(readonly, nonatomic, copy, nullable) CLLocation *location;
// 判断用户是否启用了位置服务
+ (BOOL)locationServicesEnabled API_AVAILABLE(ios(4.0), macos(10.7));
// 开始更新位置信息
- (void)startUpdatingLocation API_AVAILABLE(watchos(3.0)) API_UNAVAILABLE(tvos);
// 停止更新位置信息
- (void)stopUpdatingLocation;
@end
CLGeocoder
:
// 定义地理编码完成后的回调处理块,CLPlacemarks按可信度从高到低排列
@interface CLGeocoder : NSObject
typedef void (^CLGeocodeCompletionHandler)(NSArray< CLPlacemark *> * __nullable placemarks, NSError * __nullable error);
@interface CLGeocoder : NSObject
// 判断是否正在进行地理编码操作
@property (nonatomic, readonly, getter=isGeocoding) BOOL geocoding;
// 反向地理编码请求,根据给定位置获取对应的地标信息
- (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler;
// 用地址字符串进行地理编码(默认无区域和首选语言环境设置)
- (void)geocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler;
@end
CLLocationManagerDelegate
:
@protocol CLLocationManagerDelegate<NSObject>
- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray<CLLocation *> *)locations;
@end
设计属性
我们可以从这些类的设计学习一些MVC架构的设计思想
从刚刚的头文件我们可以看到,其实头文件之中的许多属性是被标注为只读,标志为只读其实对于解耦合实现规范代码有着很大的帮助,比如CLLocationManager
之中的location
属性,因为这个位置管理器只是用来管理这个当前的位置,对于外部的使用者来说,只需要在适当的时候进行访问而不是修改。对应数据的更新和维护其实就是这个类内部的负责。
我们就可以提炼出一个设计准则:外部使用者只要负责读取数据,具体的数据更新则是由提供者来完成
这种设计思想其实很清晰的将层次分开了,我们这样不仅成功保护了相关数据的安全,也能进一步的减少相关属性值的滥用。
具体的操作大佬的博客归结为:
- 业务类中的属性设计为只读。使用者只能通过属性来读取数据。而由业务类中的方法内部来更新这些属性的值。
- 数据模型类中的属性定义最好也设置为只读,因为数据模型的建立是在业务类方法内部完成并通过通知或者异步回调的方式交给使用者。而不应该交由使用者来创建和更新。
- 数据模型类一般提供一个带有所有属性的init初始化方法,而初始化后这些属性原则上是不能被再次改变,所以应该设置为只读属性。
这里的类全部指的是暴露在头文件之中的属性声明
至于对于类内容,我们需要在内部(即.m文件)修改相关的声明
@interface CLLocationManager ()
@property(nonatomic, copy, nullable) CLLocation *location;
@end
//也可以改用以下形式
@implementation CLLocationManager {
CLLocation *_location;
}
@end
方法设计
协议传值
当我们类设计结束之后,随之而来的就是类方法的设计类。无论如何,我们的业务模型最后总是会走向网络请求或者数据库调用这种需要,在获取操作之后再进行异步的操作。在这里CoreLocation
的框架,使用的是Delegate和Blockzhe这两种方式进行回调
先看CLLocationManager定义的属性
@property(assign, nonatomic, nullable) id<CLLocationManagerDelegate> delegate;
这个协议实现了
@protocol CLLocationManagerDelegate<NSObject>
@optional
- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray<CLLocation *> *)locations API_AVAILABLE(ios(6.0), macos(10.9));
@end
当位置管理器对象更新了当前的位置后就会调用delegate属性所指对象的didUpdateLocations方法来通知。
这就产生了几个问题:
-
谁来创建M层的位置管理对象?
C层,这个其实很简单,控制器是负责控制和协调M层,所以C层具有负责创建并持有M层对象的责任,C层也是一个使用观察者。 -
M层如何来实现实时的更新和停止更新?
对于位置管理器之中,有以下两个方法
- (void)startUpdatingLocation API_AVAILABLE(watchos(3.0)) __TVOS_PROHIBITED;
- (void)stopUpdatingLocation;
好像是在tableView也有相似的两个方法beginUpdates
和endUpdates
,通过方法通知tableView的数据源发生更新进而更新cell。位置管理器之中的这两个方法通过通知管理器,位置类的数据(即M层)在内部发生变化。至于这个两个方法是如何实现位置管理器之中持有的位置类的变化,就是内部实现的内容,相当于一个黑盒,我们作为M层的使用者不需要知道里面的实现原理。
-
谁来负责调用M层提供的那些方法?
答案是: 控制器C层。因为控制器既然负责M层对象的构建,那他当然也是负责M层方法的调用了。 -
谁来观察M层的数据变化通知并进行相应的处理?
答案是: 控制器C层。因为C层既然负责调用M层所提供的方法,那么他理所当然的也要负责对方法的返回以及更新进行处理。在这里我们的C层控制器需要实现CLLocationManagerDelegate接口,并赋值给位置管理器对象的delegate属性。
这里引用大佬博客之中的内容
我们知道MVC结构中,C层是负责协调和调度M和V层的一个非常关键的角色。而C和M以及V之间的交互协调方式用的最多的也是通过Delegate这种模式,Delegate这种模式并不局限在M和C之间,同样也可以应用在V和C之间。Delegate的本质其实是一种双方之间通信的接口,而通过接口来进行通信则可以最大限度的减少对象之间交互的耦合性。
Block传值
除了用Delegate外,我们还可以用Block回调这种方式来实现方法调用的异步通知处理。标准格式如下:
typedef void (^BlockHandler)(id obj, NSError * error);
在地标解析器CLGeocoder之中,采用的就是block回调这种方式来实现异步通知的。我们来看看类的部分定义:
typedef void (^CLGeocodeCompletionHandler)(NSArray< CLPlacemark *> * __nullable placemarks, NSError * __nullable error);
// 反向地理编码请求,根据给定位置获取对应的地标信息
- (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler;
// 用地址字符串进行地理编码(默认无区域和首选语言环境设置)
- (void)geocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler;
从反向地理编码请求可以看出来,我们根据一个CLLocation
方向解码出一个CLPlacemark
的对象,用一个block来完成内容的回调
//VC中的某个点击按钮事件:
-(void)ClickHandle:(UIButton*)sender
{
sender.userInteractionEnabled = NO;
__weak XXXVC *weakSelf = self;
//geocoder也可以是XXXVC里面的一个属性,从而可以避免重复建立
CLGeocoder *geocoder = [CLGeocoder new];
//假设知道了某个位置对象location
[geocoder reverseGeocodeLocation:location
completionHandler:^(NSArray< CLPlacemark *> * placemarks, NSError * error)){
if (weakSelf == nil)
return;
sender.userInteractionEnabled = YES;
if (error == nil)
{
//处理placemarks对象
}
else
{
//处理错误
}
}];
}
这里的block回调,其实在没有异步的情况下(即读取本地库)是可以不需要写的,但是在我们实际的编写过程当中,还是尽可能的遵循统一的相关的模式,有时候需求是会改变的,如果我们这个操作要改为异步操作的话,那么代码需要整段修改,还包括C层的代码,修改起来很麻烦。那么不如就在一开始就使用block进行回调,有统一的格式以及便于理解的优点。
KVO
上面简单展示了Delegate与Block机制用于实现M层数据的更新,前面介绍了这两个机制的优点,下面引用博客的内容,概括这两者的缺点,顺带引入对应的KVO机制监听的内容:
Delegate的方式必须要事先定义出一个接口协议来,并且调用者和实现者都需要按照这个接口规则来进行通知和数据处理交互,这样无形中就产生了一定的耦合性。也就是二者之间还是具有隐式的依赖形式。不利于扩展和进行完全自定义处理。
block方式的缺点则是使用不好则会产生循环引用的问题从而产生内存泄露,另外就是用block机制在出错后难以调试以及难以进行问题跟踪。 而且block机制其实也是需要在调用者和实现之间预先定义一个标准的BlockHandler接口来进行交互和处理。block机制还有一个缺陷是会在代码中产生多重嵌套,从而影响代码的美观和可读性。
Delegate和block方式虽然都是一种观察者实现,但却不是标准和经典的观察者模式。因为这两种模式是无法实现多观察者的。也就是说当数据更新而进行通知时,只能有一个观察者进行监听和处理,不能实现多个观察者的通知更新处理。
可惜的是在CoreLocation Framework并不支持KVO之中方式,下面是大佬假设其支持KVO写出的相关代码。
//再次申明的是CCLocationManager是不支持KVO来监听位置变化的,这里只是一个假设支持的话的使用方法。
@interface AppDelegate
@property(nonatomic, strong) CLLocationManager *locationManager;
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.locationManager = [CLLocationManager new];
[self.locationManager startUpdatingLocation]; //开始监听位置变化
return YES;
}
@end
//第一个页面
@implementation VC1
-(void)viewWillAppear:(BOOL)animated
{
[ [UIApplication sharedApplication].delegate.locationManager addObserver:self forKeyPath:@"location" options:NSKeyValueObservingOptionNew context:NULL];
}
-(void)viewWillDisappear:(BOOL)animated
{
[ [UIApplication sharedApplication].delegate.locationManager removeObserver:self forKeyPath:@"location" ];
}
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context
{
//这里处理位置变化时的逻辑。
}
@end
//第二个页面
@implementation VC2
-(void)viewWillAppear:(BOOL)animated
{
[ [UIApplication sharedApplication].delegate.locationManager addObserver:self forKeyPath:@"location" options:NSKeyValueObservingOptionNew context:NULL];
}
-(void)viewWillDisappear:(BOOL)animated
{
[ [UIApplication sharedApplication].delegate.locationManager removeObserver:self forKeyPath:@"location" ];
}
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context
{
//这里处理位置变化时的逻辑。
}
@end
//.. 其他页面
接下来分析一下在什么情况下使用KVO:
- 最显而易见的,当我这个数据更新可能会引起多个依赖该对象的对象更新时使用KVO
- 当某个对象,在对应流程之中会创建多个副本,而且在这个副本的状态会不断产生变化,当副本增多的情况下,我们就需要使用KVO机制来根据新的状态来处理。
下面是我们使用多副本且不使用KVO的相关流程
使用KVO+单例,KVO在这里的意义就是,通知这个单例属性状态已经被改变,进行对应的更新
Notification通知方式
KVO模式实现了一种对属性变化的通知观察机制。而且这种机制由系统来完成,缺点就是他只是对属性的变化进行观察,而不能对某些异步方法调用进行通知处理。而如果我们想要正真的实现观察者模式而不局限于属性呢?答案就是iOS的NSNotificationCenter。
但是这个模式对应的也存在一些缺点,就使用通知的代码较为松散,在一定程度上,不利于程序的解读。看前面的内容,通过Delegate或者block时来设计业务层方法的回调时,可以很清楚的知道业务调用方法和实现机制的上下文,因为这些东西在代码定义里面就已经固化了,我们必须额外的去学习和了解哪些业务层的方法需要添加观察者哪些不需要,而且代码中不管在什么时候需要都要在初始化时添加一段代码上去。通知处理逻辑的可读写性以及代码的可读性也比较差。
总结
其实这篇文章写着写着,和前面写的五大传值方法其实有些重合,主要就是当时在学习传值技巧的时候还没有学到MVC架构,之前的学习还是停留在较为表面的,通过这次学习加深了对传值和MVC架构结合的理解
参考文章
论MVVM伪框架结构和MVC中M的实现机制
控制层的设计方法
模型层设计方法