用Swift写WeexDemo 2

接上一篇,本篇主要记录我在把另一位iOS同事(OC)与web前端同事(写vue.js的)合作的半成品项目转化为Swift半成品的过程。

为什么说是半成品呢?因为iOS同事负责更重要的项目去了,这个weex试水项目就交给我跟进了,而我又是Swift死忠(不是不能写OC,是写过Swift不想再写OC),所以总想着用Swift来完成这个混合项目,但是又不能打包票能搞定……所以才会有这几篇文章,边折腾边记录。

又因为这个项目是web前端和Android一起调试在先,他们搞得差不多了,我这边才开始,并不了解他们是如何约定一些东西的,所以我先摸索看看。

好了,废话不多说,开干!

导入web前端的代码

weex支持用vue.js写页面,然后通过weex编译成bundlejs,我们web前端打包出来的一堆js文件,我把SwiftWeex项目下的bundlejs文件夹内容全部删掉,然后复制所有js进去,目录如下了:

可以看见index.js还是存在的,我们的ViewController加载代码也不用改,直接跑起来看看模拟器:

先不管中间的地图没加载出来,看看状态栏,似乎被vc的view盖过头了,我们得处理一下先,重写下ViewControllerviewDidLayoutSubviews

// 记录当前frame
fileprivate var currentFrame = CGRect.zero

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
var insets = UIEdgeInsets.zero
if #available(iOS 11.0, *) {
insets = view.safeAreaInsets
} else {
insets.top = UIApplication.shared.statusBarFrame.size.height
}
let width = view.bounds.width - insets.left - insets.right
let height = view.bounds.height - insets.top - insets.bottom
currentFrame = CGRect(x: insets.left, y: insets.top, width: width, height: height)
instance?.frame = currentFrame
}

func render() {
guard jsURL != nil else {
Log("jsURL is nil")
return
}
instance?.destroy()
instance = WXSDKInstance()
instance?.viewController = self
instance?.frame = currentFrame // 重新render的时候用最新的frame
instance?.onCreate = { [unowned self] view in
...
...
(略)
}

尽管顶部确实避开了状态栏,但是web前端写的css控制了子view的高度,超出了我们父view的范围了:

你问我怎么知道的……当然是对比一下我们上一篇使用的官方demo里的index.js咯,官方的写了一句:

.wrapper { align-items: center; margin-top: 120px; }

所以总是离顶部有60pt(6s)的距离:

好了,这些都暂时不是我关心的地方,到时再和web前端商讨下。

添加插件

再看回app主界面,发现地图没有显示,而地图是native负责显示的,提供部分接口给weex调用,weex用了一个插件机制,把地图以及相应的native接口暴露给js。

描述能力不行,具体看看我之前写的一篇weex插件开发(iOS),虽然也是流水账,但是能看清如何写一个简单的插件。

根据官方文档说的,如果你想使用插件市场的插件,你可以使用:

weex plugin add plugin_name

我们这步应该把我们的SwiftWeex工程置于整个weex项目的platforms/ios下,然后用上述命令安装地图插件。但是刚好今天周六,周五忘了问前端拿整个weex项目的代码,所以,下面先用上一篇文章提到的darkweex这个demo工程折腾一下试试。

这里我已知我们的weex项目是用高德地图插件,我们去插件市场看看高德地图怎么玩。

添加地图插件,有两种方式:

  • 从插件市场下载安装: weexpack plugin add weex-amap
  • 地图插件代码clone到本地后安装,weexpack plugin add /users/abcd/Code/weex-plugins/weex-amap (这后面是地图插件本地代码的目录)

下面我们试试安装插件,看看安装后会增减些什么东西:

cd ~/Documents/darkweex
weexpack plugin add weex-amap

记得惯例加上梯子,不然你会怀疑人生……但是还是出错了:

dark-xps:darkweex darkhandz$ weexpack plugin add weex-amap
Fetching plugin "weex-amap" via npm
Install Dependences ...
added 4 packages in 2.777s

Installing "weex-amap@0.0.4" for ios
Fetching plugin "weex-adapter-image" via npm
Installing "weex-adapter-image@1.0.2" for ios
Failed to install 'weex-adapter-image':Error
at new Api (/usr/local/lib/node_modules/weexpack/lib/src/platforms/ios_pack/Api.js:85:15)
at Object.getPlatformApi (/usr/local/lib/node_modules/weexpack/lib/src/platforms/platforms.js:76:23)
at handleInstall (/usr/local/lib/node_modules/weexpack/lib/src/plugman/install.js:614:27)
at /usr/local/lib/node_modules/weexpack/lib/src/plugman/install.js:410:28
at _fulfilled (/usr/local/lib/node_modules/weexpack/node_modules/q/q.js:787:54)
at self.promiseDispatch.done (/usr/local/lib/node_modules/weexpack/node_modules/q/q.js:816:30)
at Promise.promise.promiseDispatch (/usr/local/lib/node_modules/weexpack/node_modules/q/q.js:749:13)
at /usr/local/lib/node_modules/weexpack/node_modules/q/q.js:509:49
at flush (/usr/local/lib/node_modules/weexpack/node_modules/q/q.js:108:17)
at _combinedTickCallback (internal/process/next_tick.js:131:7)
Failed to install 'weex-amap':Error
at new Api (/usr/local/lib/node_modules/weexpack/lib/src/platforms/ios_pack/Api.js:85:15)
at Object.getPlatformApi (/usr/local/lib/node_modules/weexpack/lib/src/platforms/platforms.js:76:23)
at handleInstall (/usr/local/lib/node_modules/weexpack/lib/src/plugman/install.js:614:27)
at /usr/local/lib/node_modules/weexpack/lib/src/plugman/install.js:410:28
at _fulfilled (/usr/local/lib/node_modules/weexpack/node_modules/q/q.js:787:54)
at self.promiseDispatch.done (/usr/local/lib/node_modules/weexpack/node_modules/q/q.js:816:30)
at Promise.promise.promiseDispatch (/usr/local/lib/node_modules/weexpack/node_modules/q/q.js:749:13)
at /usr/local/lib/node_modules/weexpack/node_modules/q/q.js:509:49
at flush (/usr/local/lib/node_modules/weexpack/node_modules/q/q.js:108:17)
at _combinedTickCallback (internal/process/next_tick.js:131:7)
Error: The provided path "/Users/darkhandz/Documents/darkweex/platforms/ios/Weexplugin" is not a weexpack iOS project.

从最后一行可以看出,和Weexplugin文件夹有关,我看了一下,我们的项目根本没有这个目录,于是再看看高德的插件说明,在最后有:

如何将地图插件集成到自己的项目呢,请参考weexpack文档说明

我们打开链接看看,原来需要一个组件容器,获取方式也有两种,我采取第二种:通过repo clone下来:

cd platforms/ios/
git clone https://github.com/weexteam/weexpluginContainer-iOS.git

可以看到weexpluginContainer-iOS的目录结构是这样的:

我们要的就是template目录而已,把它剪切出去ios目录下,然后改名为Weexplugin,然后删除weexpluginContainer-iOS,然后cd ../..切换回darkweex目录,再执行安装插件的命令试试:

相当顺利,然后看看当前的目录结构:

这时候我们把整个Weexplugin复制到我们Source目录下,然后把Weexplugin\Weexplugin拖入Xcode。

这时候留意下外层Weexplugin目录里面的Podfile里面添加了什么依赖:

就是高德地图咯,把这行复制到我们SwiftWeexPodfile里,来一次Pod install,安装完之后,CMD+R走一波,一切顺利的话,你依然看不见地图的😆。

这时候我们看看控制台的输出:

说明Weex里的JS代码有调用weex-amap,但是找不到这个component……就是我们没有注册咯,刚才导入的Weexplugin就是用来注册各种插件的,我们只是简单地导入了它的代码,却没有用起来。

首先在桥接头文件里引入:#import "WeexPluginManager.h",然后在AppDelegateconfigWeex()最后面加入WeexPluginManager.registerWeexPlugin(),再CMD+R就看见神奇的地图啦:

至于我是如何知道怎样使用WeexPluginManager的,如果你创建过插件demo工程的话,你就会知道了:

这个方法的源码你看看就知道是怎么加载插件的了,下面简单说说。

插件的自动注册

WeexPluginManager.m里有这么一段:

[pluginNames enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSDictionary *pluginInfo = (NSDictionary *)obj;
if ([pluginInfo[@"category"] isEqualToString:@"handler"] && pluginInfo[@"protocol"]) {
[WXSDKEngine registerHandler:[NSClassFromString(pluginInfo[@"ios-package"]) new]
withProtocol:NSProtocolFromString(pluginInfo[@"protocol"])];
}else if ([pluginInfo[@"category"] isEqualToString:@"component"] && pluginInfo[@"ios-package"]) {
[WXSDKEngine registerComponent:pluginInfo[@"api"] withClass:NSClassFromString(pluginInfo[@"ios-package"])];
}else if ([pluginInfo[@"category"] isEqualToString:@"module"] && pluginInfo[@"ios-package"]) {
[WXSDKEngine registerModule:pluginInfo[@"api"] withClass:NSClassFromString(pluginInfo[@"ios-package"])];
}
}];

而读取的文件是WeexpluginConfig.xml,部分内容是这样的:

<feature name="WXImgLoaderImpl">
<param name="category" value="handler" />
<param name="ios-package" value="WXImgLoaderImpl" />
<param name="protocol" value="WXImgLoaderProtocol" />
</feature>
<feature name="WXMapViewComponent">
<param name="category" value="component" />
<param name="ios-package" value="WXMapViewComponent" />
<param name="api" value="weex-amap" />
</feature>
<feature name="WXMapViewMarkerComponent">
<param name="category" value="component" />
<param name="ios-package" value="WXMapViewMarkerComponent" />
<param name="api" value="weex-amap-marker" />
</feature>
<feature name="WXMapPolylineComponent">
<param name="category" value="component" />
<param name="ios-package" value="WXMapPolylineComponent" />
<param name="api" value="weex-amap-polyline" />
</feature>
<feature name="WXMapPolygonComponent">
<param name="category" value="component" />
<param name="ios-package" value="WXMapPolygonComponent" />
<param name="api" value="weex-amap-polygon" />
</feature>
<feature name="WXMapCircleComponent">
<param name="category" value="component" />
<param name="ios-package" value="WXMapCircleComponent" />
<param name="api" value="weex-amap-circle" />
</feature>
<feature name="WXMapInfoWindowComponent">
<param name="category" value="component" />
<param name="ios-package" value="WXMapInfoWindowComponent" />
<param name="api" value="weex-amap-info-window" />
</feature>
<feature name="WXMapViewModule">
<param name="category" value="module" />
<param name="ios-package" value="WXMapViewModule" />
<param name="api" value="amap" />
</feature>

所以嘛,我们如果想手动注册一些modulehandlercomponent什么的,完全可以自己[WXSDKEngine registerXXXX: withClass:]来代替插件管理器的自动注册,当然了,插件的源码你总是要自己下载回来导入项目的。

用Swift给Amap module添加方法扩展

地图是显示出来了,但是左下角那个点击回到用户当前定位位置的按钮根本没有用,我看了一下index.js,发现点击的时候执行的代码是:

//回到当前位置
gotCurPos: function gotCurPos() {
var _this3 = this;

// map.setCenter(this.curPosition)
this.setUserLocation().then(function (position) {
_this3.mapCenter = position;
});
},

再看看setUserLocation方法的定义:

// weex-amap 定位
setUserLocation: function setUserLocation() {
var _this = this;
return new Promise(function (resolve, reject) {
Amap.getUserLocation(null, function (data) {
_this.curPosition = data.data.position;
_this.curCity = data.data.position[2].replace('市', '');
resolve(data.data.position);
});
});
},

哟西,还有Promise,高大上的感觉……简单看来就是调用Amap.getUserLocation(param, callback)方法咯,看看Amap的定义:

// 地图
var Amap = weex.requireModule('amap');

就是我们的高德地图module咯,意思就是调用module里面的getUserLocation方法,我们去SwiftWeex里全局搜索一下这个方法名:

很明显的一个module写法,先来看看getUserLocation这个方法的作用,就是调用componentgetUserLocation方法,把得到的结果传给callback。

我们再看看performBlockWithRef干了什么:

- (void)performBlockWithRef:(NSString *)elemRef block:(void (^)(WXComponent *))block {
if (!elemRef) {
return;
}

__weak typeof(self) weakSelf = self;

WXPerformBlockOnComponentThread(^{
WXComponent *component = (WXComponent *)[weakSelf.weexInstance componentForRef:elemRef];
if (!component) {
return;
}

[weakSelf performSelectorOnMainThread:@selector(doBlock:) withObject:^() {
block(component);
} waitUntilDone:NO];
});
}

大概意思就是根据elemRef找到对应的component,然后执行block……可是web前端传了nullelemRef!不知道他是如何和Android那边配合的,还是说Android版的高德地图插件方法和我们iOS的有所不同?不管了,今天是周六,先自己折腾看看……

前端想拿到的是position,一个坐标:curPosition: [116.48635, 40.00079],还有就是城市名,不过为什么城市名作为一个字符串却在position数组第三个元素出现?不懂前端,迷之尴尬……看来还是得改改他的index.js代码。

现在我想用Swift为高德地图module提供一个方法让web前端调用,所以我决定在WXMapViewModule.m里增加一个方法签名暴露给weex调用:

WX_EXPORT_METHOD(@selector(getUserLocation:callback:))
WX_EXPORT_METHOD(@selector(getLineDistance:marker:callback:))
WX_EXPORT_METHOD_SYNC(@selector(polygonContainsMarker:ref:callback:))
WX_EXPORT_METHOD(@selector(getUserLocation:)) // 增加了这行

然后新建一个WXMapViewModule+Extension.swift文件:

import Foundation
import WeexSDK

public extension WXMapViewModule {
@objc func getUserLocation(_ callback: WXModuleCallback) {
callback(["result": "success", "data": ["position": [113.999, 22.500], "title": "中山市"] ])
}
}

别忘了在桥接头文件添加#import "WXMapViewModule.h"。然后把index.js里面的getUserLocation调用改成:

Amap.getUserLocation(function (data) {
_this.curPosition = data.data.position;
_this.curCity = data.data.title.replace('市', '');
resolve(data.data.position);
});

再跑起来,哎哟,怎么去了深圳湾,我不是故意的,左边我随意返回的……但是左上角的中山就没错了:

真正的定位嘛,目前我暂时不知道前面那个elemRef是什么鬼,周一问问web前端才知道了,我先自己折腾一个定位工具类出来,把坐标交给getUserLocation返回,应该也说得通。

先在Info.plist添加相应的定位权限:

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>获取你的位置,显示周围的服务</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>获取你的位置,显示周围的服务</string>
<key>NSLocationUsageDescription</key>
<string>获取你的位置,显示周围的服务</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>获取你的位置,显示周围的服务</string>

然后写一个单例出来,我随意创建的文件LocationManager+Extension.swift,请别挑剔:

import UIKit
import CoreLocation

class LocationUtil: NSObject {
static let shared = LocationUtil()
/// 当前最新定位坐标
var currentLocation = CLLocation()
fileprivate var locationMgr = CLLocationManager()
private override init (){
super.init()
locationMgr.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationMgr.delegate = self
}

func requestLocationAuth() {
guard CLLocationManager.locationServicesEnabled() else { return }
let status = CLLocationManager.authorizationStatus()
switch status {
case .notDetermined:
locationMgr.requestWhenInUseAuthorization()
case .authorizedWhenInUse, .authorizedAlways:
locationMgr.startUpdatingLocation()
case .denied, .restricted:
alert()
}
}

func alert() {
let vc = UIAlertController(title: "定位授权", message: "请打开本App的定位授权才可以正常使用", preferredStyle: .alert)
vc.addAction(UIAlertAction(title: "前往设置", style: .default, handler: { _ in
UIApplication.shared.openURL(URL(string: UIApplicationOpenSettingsURLString)!)
}))
vc.addAction(UIAlertAction(title: "取消", style: .destructive, handler: nil))
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
UIApplication.shared.keyWindow?.rootViewController?.present(vc, animated: true, completion: nil)
}
}
}

extension LocationUtil: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let last = locations.last {
currentLocation = last
}
}
}

接下来当然是给getUserLocation返回最新的定位坐标咯:

public extension WXMapViewModule {
@objc func getUserLocation(_ callback: WXModuleCallback) {
let loc = LocationUtil.shared.currentLocation.coordinate
Log(loc)
callback(["result": "success", "data": ["position": [loc.longitude, loc.latitude], "title": "中山市"] ])
}
}

然后在ViewControllerviewDidLoadrender()下面加入LocationUtil.shared.requestLocationAuth(),跑起来试试。

坐标就是模拟器设置的位置了咯。

小结

折腾了这么久,才勉强搞好一个地图和定位,尴尬……其他的,留给下一篇吧。

本次折腾的代码涉及公司的业务,虽然不是什么神秘高端技术,但为免落人口实,我去掉bundlejs文件夹再分享吧。

github链接在这里

参考: