iOS地铁图

使用了高德地图的JS API,文档戳这里

原理

通过 WKWebView 加载一个 HTML 字符串,通过注入 JS 在页面加载完毕后加载高德地图的JS库,然后创建地铁地图,同时通过JS库提供的 getCityList 接口获取所有的城市信息,然后把城市数据回传给 Objective-C,OC 在拿到数据后在右上角添加一个切换城市的按钮,当选择所要切换的城市后,通过 WKWebView 调用 JS 代码并把城市 id 传递给 JS,JS 拿到数据后通过 setAdcode 接口来切换地铁数据。

实际上这个 Demo 中可以不考虑 WKUIDelegateWKNavigationDelegate 这两个协议,但是为了熟悉 WKWebView,我还是遵守了这两个协议。

XPWKScriptMessageHandlerObject 这个类的作用只是为了解决 WKScriptMessageHandler 协议由于强引用代理对象从而导致内存泄漏的问题,起到一个中间代理人的作用。

GIF演示效果

GIF

代码

  • .h 头文件
NS_CLASS_AVAILABLE_IOS(9_0) @interface XPSubwayMapViewController : UIViewController

@end
  • .m 源文件
#import "XPSubwayMapViewController.h"
#import <WebKit/WebKit.h>

#pragma mark -

@interface XPWKScriptMessageHandlerObject : NSObject<WKScriptMessageHandler>

@property (nonatomic, weak) id<WKScriptMessageHandler> scriptMessageHandler;

@end

@implementation XPWKScriptMessageHandlerObject

- (void)dealloc {
#ifdef DEBUG
    NSLog(@"%s", __FUNCTION__);
#endif
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    [self.scriptMessageHandler userContentController:userContentController didReceiveScriptMessage:message];
}

@end

#pragma mark -

@interface XPSubwayMapViewController ()<WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler>
{
    UIActivityIndicatorView *indicatorView;
    WKWebView *webViwe;
    /// 城市列表数据(二维数组,内层数据中第一个元素为id号,第二个元素为城市名称)
    NSArray<NSArray<NSString*> *> *cities;
    XPWKScriptMessageHandlerObject *scriptMessageHandlerObject;
}
@end

@implementation XPSubwayMapViewController

#pragma mark Lifecycle

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"北京";
    scriptMessageHandlerObject = [[XPWKScriptMessageHandlerObject alloc] init];
    scriptMessageHandlerObject.scriptMessageHandler = self;
    
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    [config.userContentController addUserScript:[self subwayJavaScript]];
    [config.userContentController addScriptMessageHandler:scriptMessageHandlerObject name:@"showToggleCityButton"];
    
    webViwe = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];
    webViwe.translatesAutoresizingMaskIntoConstraints = NO;
    webViwe.UIDelegate = self;
    webViwe.navigationDelegate = self;
    [self.view addSubview:webViwe];
    [webViwe.topAnchor constraintEqualToAnchor:self.view.topAnchor].active = YES;
    [webViwe.rightAnchor constraintEqualToAnchor:self.view.rightAnchor].active = YES;
    [webViwe.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = YES;
    [webViwe.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor].active = YES;
    
    indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
    indicatorView.hidesWhenStopped = YES;
    indicatorView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:indicatorView];
    [indicatorView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES;
    [indicatorView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES;
    
    [webViwe loadHTMLString:[self html] baseURL:nil];
}

- (void)dealloc {
    [webViwe.configuration.userContentController removeScriptMessageHandlerForName:@"showToggleCityButton"];
#ifdef DEBUG
    NSLog(@"%s", __FUNCTION__);
#endif
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark <WKNavigationDelegate>

/// 在发送请求之前调用,可以决定是否加载网页
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    decisionHandler(WKNavigationActionPolicyAllow);
}

/// 开始加载网页
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
    [indicatorView startAnimating];
}

/// 是否接收服务器返回的数据
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
    decisionHandler(WKNavigationResponsePolicyAllow);
}

/// 服务器返回内容时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
    
}

/// 页面加载完毕
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [indicatorView stopAnimating];
}

/// 页面加载失败
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
}

/// HTTPS时触发该回调(可以在此验证HTTPS证书)
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
    completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}

#pragma mark <WKUIDelegate>

/// 处理JavaScript原生的alert函数
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *action = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }];
    [alertController addAction:action];
    [self presentViewController:alertController animated:YES completion:nil];
}

/// 处理JavaScript原生的confirm函数
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }];
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }];
    [alertController addAction:cancelAction];
    [alertController addAction:okAction];
    [self presentViewController:alertController animated:YES completion:nil];
}

/// 处理JavaScript原生的prompt函数
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        [textField setText:defaultText];
        [textField setClearButtonMode:UITextFieldViewModeWhileEditing];
        [textField setReturnKeyType:UIReturnKeyDone];
        //[textField setDelegate:self];
    }];
    [alertController addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(nil);
    }]];
    [alertController addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        NSString *text = [alertController.textFields firstObject].text;
        completionHandler(text);
    }]];
    [self presentViewController:alertController animated:YES completion:nil];
}

#pragma mark <WKScriptMessageHandler>

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"showToggleCityButton"]) {
        NSArray *cities = message.body;
        if ([cities isKindOfClass:[NSArray class]] && cities.count) {
            UIBarButtonItem *button = [[UIBarButtonItem alloc] initWithTitle:@"切换城市" style:UIBarButtonItemStylePlain target:self action:@selector(toggleCityButtonAction:)];
            self.navigationItem.rightBarButtonItem = button;
            self->cities = cities;
        }
    }
}

#pragma mark Actions

- (void)toggleCityButtonAction:(UIBarButtonItem *)sender {
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
    [alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
    __weak __typeof(self) weakSelf = self;
    for (NSArray<NSString*> *array in cities) {
        NSString *cityId = array.firstObject;
        NSString *cityName = array.lastObject;
        [alert addAction:[UIAlertAction actionWithTitle:cityName style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            __strong __typeof(weakSelf) strongSelf = weakSelf;
            NSString *js = [NSString stringWithFormat:@"toggleCity(%@)", cityId];
            [strongSelf->webViwe evaluateJavaScript:js completionHandler:^(id _Nullable resp, NSError * _Nullable error) {
#ifdef DEBUG
                NSLog(@"Toggle city fail: %@", error);
#endif
            }];
            strongSelf.title = cityName;
        }]];
    }
    [self presentViewController:alert animated:YES completion:nil];
}

#pragma mark Private

- (NSString *)html {
    return @"<html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0,shrink-to-fit=no\"/></head><body><div id=\"SubwayContainer\"></div></body></html>";
}

- (WKUserScript *)subwayJavaScript {
    NSString *source = @"var script = document.createElement('script');script.setAttribute('src', 'https://webapi.amap.com/subway?v=1.0&key=98cb8314168440c4de1169ffac3a53e7&callback=cbk');document.body.appendChild(script);var mysubway = null;window.cbk = function(){mysubway = subway('SubwayContainer', {easy: 1});mysubway.getCityList(function(resp) {var maps = [];for (var id in resp) {var city = resp[id]['name'];maps.push([id, city]);}window.webkit.messageHandlers.showToggleCityButton.postMessage(maps);});};function toggleCity(id) {mysubway.setAdcode(id);};";
    return [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
}

@end