App中愈来愈多的功能依赖用户实际的位置,例如基于用户位置提供推荐数据、基于定位判断某些功能是否可用,可是在开发调试中XCode却没有提供自定义的模拟定位的功能,因此本文主要的目的是现实一个能够在开发调试过程当中随时模拟定位的功能。git
PS: 当你的app在脱离了XCode的调试测试阶段,仍想修改定位的话。
github
咱们在iOS的app开发中一般采用的是CLLocationManager
来获取用户当前的位置,固然也能够采用MKMapView
的showUserLocation
来获取用户的位置,因此咱们分别针对这两种状况分析。bash
采用CLLocationManager
获取定位时,是根据CLLocationManagerDelegate
中- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
的回调来获取到定位的。咱们只须要在系统回调这个方法传递给业务代码的中间,插入一部分代码,来修改locations参数。本来的逻辑为系统回调
->业务代码
,如今变为系统回调
->模拟定位模块
->业务代码
,就实现了无侵入式的实现模拟定位功能。为了实现这个逻辑,能够有如下几个思路。app
由于业务代码
是根据- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
方法来接受回调的,因此能够采用Runtime swizzle
这个方法,来实现模拟定位的功能,可是咱们中间组件是不知道业务代码
中具体是哪一个类,因此没法具体指定runtime swizzle
哪一个类,因此只能遍历全部的类,判断当前类的方法列表中是否有locationManager:didUpdateLocations:
这个方法,若是存在则swizzle
。测试
这种思路是Swizzle了CLLocationManager
的setDelegate:
方法,当调用setDelegate
时,将真实的delegate object
保存下来,再将咱们定义的中间代理类swizzle delegate
对象设置为CLLocationManager
的delegate
,这样当系统回调CLLocationManagerDelegate
,会先回调到中间代理类swizzle delegate
中,再由swizzle delegate
将事件传递到真实的delegate object
。ui
swizzle
CLLocationManager
中的setDelegate:
方法便可。swizzle delegate
中须要实现所有的CLLocationManagerDelegate
方法,若是后续增长代理方法,仍须要修改这个类。Objective-C中有2个基类,经常使用的就是NSObject
,另外一个就是NSProxy
,NSProxy
主要用于消息转发处理,因此采用NSProxy
咱们能够更好的处理方法二中的缺点。atom
建立一个新的类MockLocationProxy
,集成自NSProxy
。spa
// MockLocationProxy.h
#import <CoreLocation/CoreLocation.h>
@interface MockLocationProxy : NSProxy
@property (nonatomic, weak, readonly, nullable) id <CLLocationManagerDelegate> target;
- (instancetype)initWithTarget:(id <CLLocationManagerDelegate>)target;
@end
复制代码
// MockLocationProxy.m
#import "MockLocationProxy.h"
@implementation MockLocationProxy
- (instancetype)initWithTarget:(id<CLLocationManagerDelegate>)target {
_target = target;
return self;
}
@end
复制代码
接着就来处理消息转发的逻辑,首先咱们要知道咱们想要的是什么效果,系统回调给MockLocationProxy
,MockLocationProxy
只处理locationManager:didUpdateLocations:
,其余的消息都仍然交给原target。代理
因此咱们在MockLocationProxy.m
中添加如下方法:调试
// MockLocationProxy.m
@implementation MockLocationProxy
- (instancetype)initWithTarget:(id<CLLocationManagerDelegate>)target {
_target = target;
return self;
}
- (BOOL)respondsToSelector:(SEL)aSelector {
if (aSelector == @selector(locationManager:didUpdateLocations:)) {
return YES;
}
return [self.target respondsToSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL sel = invocation.selector;
if ([self.target respondsToSelector:sel]) {
[invocation invokeWithTarget:self.target];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
if ([self.target respondsToSelector:_cmd]) {
// 模拟定位代码
CLLocation *mockLocation = [[CLLocation alloc] initWithLatitude:39.908722 longitude:116.397499];
locations = @[mockLocation];
[self.target locationManager:manager didUpdateLocations:locations];
}
}
@end
复制代码
当消息发送给MockLocationProxy
时,判断当前方法是不是locationManager:didUpdateLocations:
,若是是,则MockLocationProxy
响应事件,不然直接传递给本来的target。到此已经能够随时处理模拟定位。你只须要在模拟定位的代码作一些处理,就能够随时修改定位。
上述方法虽然能够模拟定位,可是每次修改模拟值都需从新build,那么有没有办法在运行时随时修改这个值呢?
LLDebugTool
固然能够,你只须要在你的项目中集成LLDebugTool
,调用其中的Location模块,LLDebugTool
提供了一个UI来随时修改这个模拟值,让你在调试时,随时模拟定位,LLDebugTool
仍提供了不少其余的功能,若是你只须要模拟定位的功能,则只须要集成LLDebugTool/Location
这个subspec就能够了。
前言说过,定位除了CLLocationManager以外,MKMapView
的showUserLocation
也能够获取定位信息,那么如何解决这个问题呢? 你能够在LLDebugTool/Location
中查看答案。