用Swift写WeexDemo 3

前面写了两篇流水账,今天接着搞第三篇,这次重点是实现一些module给web前端通过weex调用。

web前端暂时不用我们native实现什么功能,主要就是套壳,然后提供一些系统功能给他调用就好了,例如打电话,选择图片,支付。

拨号

这个凭感觉就很简单,前端已经和Android实现了,并且确定了weex调用方式为:

const phone = weex.requireModule('phone');
phone.call('13800138000')

专门为拨号写一个单独的module,感觉有点小题大做,还不如叫SystemUtil,然后提供多种功能呢……得了,我就暗自吐槽一下,全力配合同事才是我的常态。

官方iOS扩展教程说得很详细了,首先创建一个OC版本的module,因为只有OC才有Weex的那些暴露方法的宏(WX_EXPORT_METHODWX_EXPORT_METHOD_SYNC)。

这里我就新建叫WXSystemUtilModule的OC类了,然后遵循WXModuleProtocol协议,在.m里添加@synthesize weexInstance;,再添加一个我们要暴露给weex的方法WX_EXPORT_METHOD(@selector(call:))

// .h
#import <Foundation/Foundation.h>
#import <WeexSDK/WeexSDK.h>

@interface WXSystemUtilModule : NSObject <WXModuleProtocol>

@end

// .m
#import "WXSystemUtilModule.h"

@implementation WXSystemUtilModule
@synthesize weexInstance;
WX_EXPORT_METHOD(@selector(call:))

@end

然后新建一个WXSystemUtilModule.swift,扩展刚才的OC类:

import Foundation

public extension WXSystemUtilModule {

@objc func call(_ phoneNumber: String) {
guard let url = URL(string: "tel:" + phoneNumber) else {
Log("号码\(phoneNumber)不正确!")
return
}
if #available(iOS 10.0, *) {
UIApplication.shared.open(url)
} else {
UIApplication.shared.openURL(url)
}
}

}

对了,如果你不知道WX_EXPORT_METHOD该暴露的selector怎么写的话,你可以看看Swift写的方法混编之后的OC方法签名是怎样的:

别忘了在头文件引入WXSystemUtilModule类:#import "WXSystemUtilModule.h",然后还要在AppDelegate注册这个module

// 注册各种module,component,handler
WXSDKEngine.registerHandler(WXImageLoader(), with: WXImgLoaderProtocol.self)
WXSDKEngine.registerModule("phone", with: WXSystemUtilModule.self)

然后在call方法里打个断点,点击界面上的拨号按钮,发现果然停下来了,说明一切正常:

完全没有什么技术含量嘛……下面继续搞图片选择。

拍照和图片选择

说到这个,当然是要在Info.plist里写上相册访问、写入权限咯,不细说。然后再看看web前端调用的代码:

const imagePicker = weex.requireModule('imagePicker')
imagePicker.openCamera(this.user_info.user_id, function (data) {
_this.test = data;
});
imagePicker.openAlbum(this.user_info.user_id, function (data) {
_this.test = data;
});

简单来说就是又让我写一个module,提供两个方法,方法接收两个参数,一个是用户ID,一个是callback(data),昨天听web前端和Android同事讨论的时候说是让native返回图片数据的base64编码,也就是说data参数是字符串。最后,由于这个方法是异步回调的,所以会比上面的拨号要复杂一点点。

老规矩,新建一个OC类叫WXImagePickerModule,然后用Swift扩展它,导入头文件到桥接头文件里。这里还有些问题,在extension里我们并不能声明储存变量,所以我临时写了些全局变量,稍后看看改回写在OC类里。

import UIKit
import WeexSDK


/// 图片最大边长
fileprivate let ImageMaxWidthHeight: CGFloat = 800

/// 图片选择器
fileprivate let picker : UIImagePickerController = {
$0.allowsEditing = true //正方形裁切框
$0.sourceType = .photoLibrary
return $0
}(UIImagePickerController())

/// weex回调
fileprivate var mcallback: WXModuleCallback?



public extension WXImagePickerModule {

/// 打开相机拍照图片
///
/// - Parameters:
/// - userID: 要上传照片的用户ID
/// - callback: 回调图片base64给weex
@objc func openCamera(_ userID: String, callback: WXModuleCallback? ) {
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
Log("相机暂时不可用")
return
}
mcallback = callback
picker.delegate = self
picker.sourceType = .camera
UIApplication.shared.keyWindow?.rootViewController?.present(picker, animated: true)
}


/// 打开相册选择图片
///
/// - Parameters:
/// - userID: 要上传照片的用户ID
/// - callback: 回调图片base64给weex
@objc func openAlbum(_ userID: String, callback: WXModuleCallback? ) {
mcallback = callback
picker.delegate = self
picker.sourceType = .photoLibrary
UIApplication.shared.keyWindow?.rootViewController?.present(picker, animated: true)
}

}



// MARK: - UIImagePickerControllerDelegate
extension WXImagePickerModule: UIImagePickerControllerDelegate{
public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if picker.sourceType == .camera {
UIImageWriteToSavedPhotosAlbum(info[UIImagePickerControllerOriginalImage] as! UIImage, nil, nil, nil)
}
// 缩放成最大限制
var selectedImage = UIImage()
if let img = info[UIImagePickerControllerEditedImage] as? UIImage{
selectedImage = img.scale(toMaxSize: ImageMaxWidthHeight)
} else if let img = info[UIImagePickerControllerOriginalImage] as? UIImage{
selectedImage = img.scale(toMaxSize: ImageMaxWidthHeight)
}
// 有损压缩图片质量
var data = Data()
if let compressed = UIImageJPEGRepresentation(selectedImage, 0.80) {
data = compressed
} else if let compressed = UIImagePNGRepresentation(selectedImage) {
data = compressed
}
// 转base64
let base64 = data.base64EncodedString()
Log("base64:\(base64)")
// 回调给weex
mcallback?(base64)
picker.dismiss(animated: true)
}

}

extension WXImagePickerModule: UINavigationControllerDelegate {

}

scale方法是UIImageextension,用来把过大的图片缩小到指定尺寸,这里不列出来了。
功能代码写好了,可别忘记在AppDelegate里注册module哦:

WXSDKEngine.registerModule("imagePicker", with: WXImagePickerModule.self)

最后就是用Weex提供的宏把两个方法暴露出去,这里加了几个预编译指令,用于消除未定义selector的警告:

#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
WX_EXPORT_METHOD(@selector(openCamera:callback:))
WX_EXPORT_METHOD(@selector(openAlbum:callback:))
#pragma clang diagnostic pop

测试一下似乎没问题,base64也打印出来了,至于web前端如何处理,那我可管不着咯。

对了,差点忘了说那个userID参数,web前端是希望我拿到照片之后把它上传给服务器,成功之后才把图片base64回调给他,后来说不用我上传了……忽略吧。

高德导航

今天周一了,可以问问web前端一些事了😝

看了一下Weex Market,没有这玩意呀,那就自己扩展module吧,话说如果不是很专业的定制导航,直接用高德导航SDK提供的方法就非常简单了,几行代码的事😆

高德官网看了一下,发现pod了导航SDK的话,就不用pod地图SDK了:

pod install之后跑一下似乎没什么问题,地图不缩放的问题也自动解决了,接下来就给amap写个扩展方法吧,前端给的方法调用是Amap.openAmapNavi(endlng,endlat),感觉超简单的。

首先要在桥接头文件加入#import <AMapNaviKit/AMapNaviKit.h>,然后写功能方法到WXMapViewModule里咯:

/// 全局变量,导航组件
fileprivate var compositeManager : AMapNaviCompositeManager!

public extension WXMapViewModule {
...
...(略)

/// 开启高德导航,起点是用户当前位置,终点是参数
@objc func openAmapNavi(_ desLongitude: String, _ desLatitude: String) {
compositeManager = AMapNaviCompositeManager()
// 如果需要使用AMapNaviCompositeManagerDelegate的相关回调(如自定义语音、获取实时位置等),需要设置delegate
// compositeManager.delegate = self
guard let lati = Float(desLatitude), let long = Float(desLongitude) else {
Log("传入经纬度不正确")
return
}
let destination = AMapNaviPoint.location(withLatitude: CGFloat(lati), longitude: CGFloat(long))
let destName = "终点"
let config = AMapNaviCompositeUserConfig()
config.setRoutePlanPOIType(AMapNaviRoutePlanPOIType.end, location: destination!, name: destName, poiId: nil)
compositeManager.presentRoutePlanViewController(withOptions: config)
}
}

然后在OC类暴露方法,还记得在哪里吧?weex-amap的源码里:

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:))
WX_EXPORT_METHOD(@selector(openAmapNavi::)) // 加入这行

跑起来试试,结果控制台报错了:

<Weex>[error]WXMonitor.m:223, <WXMapViewModule: 0x60c00000d130>, the parameters in calling method [openAmapNavi] 
and registered method [openAmapNavi::] are not consistent!

调用的方法和注册的方法不一致?我看看前端写的代码是如何调用的:

Amap.openAmapNavi(this.curPark.info.longitude, this.curPark.info.latitude, function () {});

好嘛,坑我咯……嘴上说两个参数,实际传3个…………改改:

@objc func openAmapNavi(_ desLongitude: String, _ desLatitude: String, callback: WXModuleCallback?) {

暴露宏也改改:

WX_EXPORT_METHOD(@selector(openAmapNavi::callback:))

然后再跑起来,发现可以调起高德导航了,可是一点击开始导航就崩了,原来是忘记添加后台定位模式:

我们LocationUtil也得换上后台定位了:

switch status {
case .notDetermined:
locationMgr.requestAlwaysAuthorization()
...
...(略)

再次跑起来就一切正常了:

支付宝

Weex Market里有两个插件:

  • weex-nat-pay-alipay
  • weex-nat-alipay

两个插件的描述页面都是无字天书,以我的悟性果然是看不懂的,唉,还是自己写吧……

记得支付宝的接入是相当简单的,只需要传个签名后的字符串给它SDK接口,再处理下结果回调,其他就不用管了,走起!

Alipay官方文档在此,详细的集成就不说了,简单描述下module的代码。

这次我们不要傻傻地听web前端说方法签名了,我们直接去看vue生成的js代码:

if (_this.pay_type == '1') {
var payInfo = JSON.stringify(data.data.info);
wechat.weChatPay(payInfo);
} else if (_this.pay_type == '2') {
ali.aliPay(data.data.info);
}

哇塞!这么清心寡欲,连支付结果都不需要我传给你么?为了防止被坑,我还是先把回调结果写好,免得到时候又找我要,又改代码……

import Foundation
import WeexSDK

/// weex回调
fileprivate var mcallback: WXModuleCallback?


public extension WXAlipayModule {
/// 调起支付宝
@objc func aliPay(_ orderStr: String) {
invokeAlipay(orderStr: orderStr)
}

/// 调起支付宝
@objc func aliPay(_ orderStr: String, callback: WXModuleCallback?) {
mcallback = callback
invokeAlipay(orderStr: orderStr)
}

/// 调起支付宝
fileprivate func invokeAlipay(orderStr: String) {
// 监听AppDelegate方式回调过来的结果
NotificationCenter.default.addObserver(forName: DKNotification.alipayResultCallback.name,
object: nil, queue: nil)
{ [weak self] noti in
Log("alipay result:", noti.object)
guard let dict = noti.object as? [AnyHashable: Any] else { return }
self?.processResult(dict)
}
AlipaySDK.defaultService().payOrder(orderStr, fromScheme: AppScheme) { [weak self] dict in
Log("alipay result:", dict)
self?.processResult(dict)
}
}

/// 处理支付宝返回的结果 - https://docs.open.alipay.com/204/105302
fileprivate func processResult(_ dict: [AnyHashable: Any]?) {
guard let resultStatus = dict?["resultStatus"] as? String else {
Log("结果未知")
mcallback?(["code": "-1", "msg": "支付失败,结果未知"])
return
}
let result = AlipayResult(rawValue: resultStatus) ?? .unknown
if result.isSuccess {
mcallback?(["code": "0", "msg": result.description])
} else {
mcallback?(["code": "-2", "msg": result.description])
}
}
}


// MARK: - 辅助类型定义
extension WXAlipayModule {
/// 支付宝支付结果码
enum AlipayResult: String, CustomStringConvertible{
/// 支付成功
case success = "9000"
/// 用户中途取消
case userCancel = "6001"
/// 支付失败
case fail = "4000"
/// 网络连接出错
case networkError = "6002"
/// 未知错误
case unknown

/// 支付是否成功
var isSuccess: Bool {
return self == .success
}

/// 文字描述
var description: String {
switch self {
case .success:
return "支付成功"
case .userCancel:
return "用户取消"
case .fail:
return "支付失败"
case .networkError:
return "网络连接错误"
case .unknown:
return "未知错误"
}
}
}
}

然后是暴露方法:

@implementation WXAlipayModule
@synthesize weexInstance;

#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
WX_EXPORT_METHOD(@selector(aliPay:))
WX_EXPORT_METHOD(@selector(aliPay:callback:))
#pragma clang diagnostic pop

- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

@end

注册module

WXSDKEngine.registerModule("ali", with: WXAlipayModule.self)

模拟器没有Alipay,妥妥的调起来了H5页面:

微信支付

Weex Market这次有微信支付的module了,而且不是无字天书,就是排版有点不走心而已,不过我既然都写了支付宝的了,微信也要给予重视呀,自己写吧!

微信SDK在pod上面有,我就直接用pod来集成了:pod 'WechatOpenSDK', '~> 1.8.1',然后Info.plist添加wexinURLType,然后老一套新建OC类,然后Swift扩展,桥接头文件加入#import <WXApi.h>。但是注意有个地方很坑,就是Weex和Wechat同时声明了一个类型WXLogLevel,这就尴尬了:

// weex 定义的
typedef NS_ENUM(NSUInteger, WXLogLevel){
...
...

// 微信定义的
typedef NS_ENUM(NSInteger,WXLogLevel){
WXLogLevelNormal = 0, // 打印日常的日志
WXLogLevelDetail = 1, // 打印详细的日志
};

只能把微信SDK头文件里面用到的所有都改名了(Weex的太多了):

typedef NS_ENUM(NSInteger,WeiXinLogLevel){
WeiXinLogLevelNormal = 0, // 打印日常的日志
WeiXinLogLevelDetail = 1, // 打印详细的日志
};

改完就可以编译通过了,写上功能代码:

import Foundation
import WeexSDK


/// weex回调
fileprivate var mcallback: WXModuleCallback?


public extension WXWechatModule {
/// 检查微信是否已安装
@objc func isWXInstalled() -> Bool {
return WXApi.isWXAppInstalled()
}

/// 调起微信
@objc func weChatPay(_ json: String) {
invokeWechat(json: json)
}

/// 调起微信
@objc func weChatPay(_ json: String, callback: WXModuleCallback?) {
mcallback = callback
invokeWechat(json: json)
}

/// 调起微信
fileprivate func invokeWechat(json: String) {
guard isWXInstalled() else {
Log("未安装微信")
mcallback?(["code": "-1", "msg": WechatResult.unknown.description])
return
}
// 注册appID
WXApi.registerApp(WXAppID)
// 监听AppDelegate方式回调过来的结果
NotificationCenter.default.addObserver(forName: DKNotification.wechatResultCallback.name,
object: nil, queue: nil)
{ [weak self] noti in
guard let url = noti.object as? URL else { return }
// app被微信回调时处理结果(用在appdelegate的handleOpenURL这一类回调方法里)
WXApi.handleOpen(url, delegate: self)
}

guard let data = json.data(using: .utf8),
let params = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
Log("支付参数有误")
mcallback?(["code": "-4", "msg": "支付参数有误"])
return
}

guard let appid = params["appid"] as? String, let partnetID = params["partnerid"] as? String,
let prepayID = params["prepayid"] as? String, let sign = params["sign"] as? String,
let nonceStr = params["noncestr"] as? String, let package = params["package"] as? String,
let timestamp = params["timestamp"] as? Double else {
Log("订单参数不完整")
mcallback?(["code": "-3", "msg": "订单参数不完整"])
return
}
WXApi.registerApp(appid)
let req = PayReq()
req.partnerId = partnetID
req.prepayId = prepayID
req.nonceStr = nonceStr
req.timeStamp = UInt32(timestamp)
req.package = package
req.sign = sign
WXApi.send(req)
Log("准备叫微信干活", appid, req.partnerId, req.prepayId, req.nonceStr, req.timeStamp, req.package, req.sign)
}

}



// MARK: - 微信SDK处理结果回调
extension WXWechatModule : WXApiDelegate {
// MARK: - WXApi的代理方法
/// 收到微信发来的要求,要求本app提供内容,具体看官方demo,我用不上,懒得实现 --- darkhandz
public func onReq(_ req: BaseReq!) {

}

/// 收到微信发来的响应(响应我们用WXApi发出的请求)
public func onResp(_ resp: BaseResp!) {
Log("wechat result:", resp)
// 支付返回结果,实际支付结果需要去服务器端查询
if let payResp = resp as? PayResp {
let result = WechatResult(rawValue: Int(payResp.errCode)) ?? .unknown
if result.isSuccess {
mcallback?(["code": "0", "msg": "支付成功"])
} else {
mcallback?(["code": "-2", "msg": result.description])
}
}
}
}



extension WXWechatModule {
/// 微信SDK返回结果类型
enum WechatResult : Int, CustomStringConvertible {

case success = 0 /**< 成功 */
case common = -1 /**< 普通错误类型 */
case userCancel = -2 /**< 用户点击取消并返回 */
case sentFail = -3 /**< 发送失败 */
case authDeny = -4 /**< 授权失败 */
case unsupport = -5 /**< 微信不支持 */
case unknown = -99 /**< 返回未知的错误 */
case unInstalled = -8888 // 未安装微信

/// 是否成功(成功包括支付成功,登录授权成功)
var isSuccess: Bool {
return self == .success
}

/// 错误描述
var description: String {
switch self {
case .success:
return "成功"
case .common:
return "通用错误"
case .userCancel:
return "用户取消"
case .sentFail:
return "发送请求失败"
case .authDeny:
return "拒绝授权"
case .unsupport:
return "不支持的请求"
case .unknown:
return "未知错误"
case .unInstalled:
return "未安装微信"
}
}
}
}

OC类还是老样子:

// .h
#import <Foundation/Foundation.h>
#import <WeexSDK/WeexSDK.h>

@interface WXWechatModule : NSObject <WXModuleProtocol>
@end


// .m
#import "WXWechatModule.h"

@implementation WXWechatModule
@synthesize weexInstance;

#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
WX_EXPORT_METHOD(@selector(weChatPay:))
WX_EXPORT_METHOD(@selector(weChatPay:callback:))
#pragma clang diagnostic pop

- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

@end

在AppDelegate注册module

WXSDKEngine.registerModule("wechat", with: WXWechatModule.self)

这次就要真机跑了,模拟器哪来的微信嘛……

地图POI搜索

最后一个需要的native功能就是这个了,做完这个就回头搞搞定位ReGeo的事吧。首先看看前端weex如何调用:

Amap.poiSearch(this.keywords,this.curCity,true,(result)=>{
this.searchList = result.filter((val)=>{
return val.address.length > 0
});
this.searchResultShow = true
})

pod安装需要的库:

pod 'AMapLocation-NO-IDFA', '~> 2.4.0'
pod 'AMapSearch-NO-IDFA', '~> 5.4.0'

导入相应头文件:

#import <AMapSearchKit/AMapSearchKit.h>
#import <AMapLocationKit/AMapLocationKit.h>

接着扩展WXMapViewModule类,添加两个全局变量,然后写功能:

/// 高德地图搜索组件
fileprivate let mapSearch = AMapSearchAPI()
/// weex回调
fileprivate var mcallback: WXModuleCallback?

public extension WXMapViewModule {
...
...(略)

/// 在指定城市里搜索包含keyword的POI
@objc func poiSearch(_ keyword: String, _ city: String, _ unuse: Bool, callback: WXModuleCallback?) {
Log("keyword:", keyword, "city:", city)
mapSearch?.delegate = self
mcallback = callback
// 构建搜索
let req = AMapPOIKeywordsSearchRequest()
req.keywords = keyword
req.city = city
req.requireExtension = true
req.cityLimit = true
req.requireSubPOIs = true
// 发起搜索,等待回调
mapSearch?.aMapPOIKeywordsSearch(req)
}


// MARK: - POI搜索回调
extension WXMapViewModule: AMapSearchDelegate {
public func onPOISearchDone(_ request: AMapPOISearchBaseRequest!, response: AMapPOISearchResponse!) {
if response.pois.isEmpty {
Log("无POI结果")
mcallback?([])
return
}
// weex不识别这里的对象,自己手段转成字典数组
let pois = response.pois.map{ poi in
return ["uid": poi.uid, "name": poi.name, "type": poi.type, "typecode": poi.typecode,
"location":["latitude": poi.location.latitude, "longitude": poi.location.longitude],
"address": poi.address, "tel": poi.tel, "distance": poi.distance, "parkingType": poi.parkingType,
"postcode": poi.postcode, "province": poi.province, "city": poi.city, "citycode": poi.citycode,
"district": poi.district, "adcode": poi.adcode
]
}
mcallback?(pois)
}

public func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
Log("搜索POI失败")
}
}

是不是很奇怪看到一大堆丑陋的代码?这是因为直接返回[AMapPOI]给web前端,他们JS不识别我们native的对象,所以我只好看看Android返回的东西,他也是直接返回搜索的POI结果数组,而我就必须手动转字典数组咯。

暴露方法签名:WX_EXPORT_METHOD(@selector(poiSearch:::callback:)),就完成了。

小结

本篇写来写去就那么一点点内容,扩展module,没什么技术含量,就到这里吧,下一篇的话,可能就说说BUG了。

github代码在此