iOS实现模拟定位功能

前言

App中愈来愈多的功能依赖用户实际的位置,例如基于用户位置提供推荐数据、基于定位判断某些功能是否可用,可是在开发调试中XCode却没有提供自定义的模拟定位的功能,因此本文主要的目的是现实一个能够在开发调试过程当中随时模拟定位的功能。git

思路

咱们在iOS的app开发中一般采用的是CLLocationManager来获取用户当前的位置,固然也能够采用MKMapViewshowUserLocation来获取用户的位置,因此咱们分别针对这两种状况分析。github

CLLocationManager

采用CLLocationManager获取定位时,是根据CLLocationManagerDelegate中`- (void)locationManager:(CLLocationManager *)managerapp

didUpdateLocations:(NSArray<CLLocation *> *)locations`的回调来获取到定位的。咱们只须要在系统回调这个方法传递给业务代码的中间,插入一部分代码,来修改locations参数。本来的逻辑为`系统回调`->`业务代码`,如今变为`系统回调`->`模拟定位模块`->`业务代码`,就实现了无侵入式的实现模拟定位功能。为了实现这个逻辑,能够有如下几个思路。

一、 Runtime swizzle

由于业务代码是根据`- (void)locationManager:(CLLocationManager *)managerui

didUpdateLocations:(NSArray<CLLocation *> *)locations`方法来接受回调的,因此能够采用`Runtime swizzle`这个方法,来实现模拟定位的功能,可是咱们中间组件是不知道`业务代码`中具体是哪一个类,因此没法具体指定`runtime swizzle`哪一个类,因此只能遍历全部的类,判断当前类的方法列表中是否有`locationManager:didUpdateLocations:`这个方法,若是存在则`swizzle`。
  • 优势:便于理解。
  • 缺点:须要遍历全部的类和类的方法列表。

二、中间代理对象

这种思路是Swizzle了CLLocationManagersetDelegate:方法,当调用setDelegate时,将真实的delegate object保存下来,再将咱们定义的中间代理类swizzle delegate对象设置为CLLocationManagerdelegate,这样当系统回调CLLocationManagerDelegate,会先回调到中间代理类swizzle delegate中,再由swizzle delegate将事件传递到真实的delegate objectatom

  • 优势:相对于第一种方法,不须要遍历类和类的方法列表,只需swizzle CLLocationManager中的setDelegate:方法便可。
  • 缺点:在中间代理类swizzle delegate中须要实现所有的CLLocationManagerDelegate方法,若是后续增长代理方法,仍须要修改这个类。

三、采用NSProxy实现中间代理对象

Objective-C中有2个基类,经常使用的就是NSObject,另外一个就是NSProxyNSProxy主要用于消息转发处理,因此采用NSProxy咱们能够更好的处理方法二中的缺点。代理

3.1

建立一个新的类MockLocationProxy,集成自NSProxy调试

// 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

接着就来处理消息转发的逻辑,首先咱们要知道咱们想要的是什么效果,系统回调给MockLocationProxyMockLocationProxy只处理locationManager:didUpdateLocations:,其余的消息都仍然交给原target。code

因此咱们在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。到此已经能够随时处理模拟定位。你只须要在模拟定位的代码作一些处理,就能够随时修改定位。事件

One more.

上述方法虽然能够模拟定位,可是每次修改模拟值都需从新build,那么有没有办法在运行时随时修改这个值呢?

固然能够,你只须要在你的项目中集成LLDebugTool,调用其中的Location模块,LLDebugTool提供了一个UI来随时修改这个模拟值,让你在调试时,随时模拟定位,LLDebugTool仍提供了不少其余的功能,若是你只须要模拟定位的功能,则只须要集成LLDebugTool/Location这个subspec就能够了。

后记

前言说过,定位除了CLLocationManager以外,MKMapViewshowUserLocation也能够获取定位信息,那么如何解决这个问题呢? 你能够在LLDebugTool/Location中查看答案。

不要忘记点Star! ✨✨✨

相关文章
相关标签/搜索