美文网首页iOS开发你需要知道的Uniapp
UNIAPP----IOS端原生插件开发实战(一)

UNIAPP----IOS端原生插件开发实战(一)

作者: 扶不起的蝌蚪 | 来源:发表于2021-07-13 13:32 被阅读0次

    1.前言

    UNIAPP----Android端原生插件开发实战二进行了Android端的原生插件的开发,由于本业务同样需要IOS端的代理传输,所以下面介绍一下IOS端的原生插件开发过程。

    2.工具材料清单

    工具/材料 版本/版本名
    HBuilder X 3.1.18
    Xcode Version 12.1 (12A7403)
    UNI-SDK iOSSDK@3.1.18.80433_20210609

    3.原生环境配置

    3.1创建插件工程

    根据iOS平台uni原生插件开发教程,我们打开 Xcode,创建一个新的工程(Project),template 选择 Framework ,然后点击 Next。

    根据文档描述,工程存放路径,建议直接存放在 iOSSDK目录中的 HBuilder-uniPluginDemo 插件开发主工程目录下,如下图所示,然后点击 Create

    Project创建完成之后,删除掉自动创建的头文件,删除后的样子如下

    然后选中工程名,在TARGETS->Build Settings中,将 Mach-O Type 设置为 Static Library如下图所示

    然后将插件工程关闭,接下来需要将插件工程导入到插件开发主工程中。

    3.2导入插件工程

    打开 iOSSDK/HBuilder-uniPluginDemo工程目录,双击目录中的HBuilder-uniPlugin.xcodeproj文件运行插件开发主工程

    在 Xcode 项目左侧目录选中主工程名,然后点击右键选择Add Files to “HBuilder-uniPlugin”

    然后选择您刚刚创建的插件工程路径中,选中插件工程文件,勾选Create folder referencesAdd to targets两项,然后点击Add

    这时在 Xcode 左侧目录中可以看到插件工程已经添加到了主工程中,如下图所示

    3.3 工程配置

    在 Xcode 项目左侧目录选中主工程名,在TARGETS->Build Phases->Dependencies中点击+

    在弹窗中选中插件工程,如图所示,然后点击Add,将插件工程添加到Dependencies

    然后在Link Binary With Libraries中点击+,同样在弹窗中选中插件工程,点击Add

    此时可以看到 DependenciesLink Binary With Libraries都添加了插件工程,如下图所示

    接下来需要在插件工程的Header Search Paths中添加开发插件所需的头文件引用,头文件存放在主工程的HBuilder-Hello/inc中,添加方法如下图所示,在 Xcode 项目左侧目录选中插件工程名,找到TARGETS->Build Settings->Header Search Paths双击右侧区域打开添加窗口,然后将inc目录拖入会自动填充相对路径,然后将模式改成recursive

    4.SDK配置

    SDK的库文件SecurePortal.framework是必须的,其包括了用户NBA的登录认证以及代理业务功能。
    SecurePortal.framework包含两个头文件一个是SPNBAClient.hLibSecIDLite.h
    SPNBAClient.h包含NBA的登录认证,代理和相关的接口。
    LibSecIDLite.h是获取360ID的动态口令以及二维码授权等接口。

    4.1.导入SDK文件

    SDK文档中描述到:

    App工程添加SecurePortal.framework的工程配置如下:
    需要将动态库添加到Embeded BinariesLinked Frameworks and Libraries这两个选项里面。

    再结合官网的描述

    如果您的插件需要依赖第三方的SDK,开发阶段引入三方SDK的时候要引入到主工程,然后将三方SDK提供的 .h 头文件直接添加到插件工程中这样就可以正常调用三方SDK的Api了,功能开发完毕后在构建插件包的时候,需要将依赖的三方SDK库文件放到ios路径下,然后按照规范编辑 package.json;

    因此,先进行第一步,将SecurePortal.framework添加到iosTunnel.xcodeproj
    为方便引用,先在目录下新建一个Frameworks目录

    然后将SecurePortal.framework拷贝到这个目录下面,如下图所示

    拷贝成功后可以同时看到Build Phases里面也有了这个库文件的link

    framework引入完毕后,接下来在主工程对SDK进行引用
    选中工程,如图所示,然后在Link Binary With Libraries点击+

    会发现没有看到这个SecurePortal.framework,这时候选择下面的Add Files

    选择我们放到iosTunnel目录下面的SecurePortal.framework

    image.png

    然后在Embed Frameworks处同样添加SecurePortal.framework

    4.2业务代码实现

    新建头文件和.m文件,如下图所示(直接选择新建cocopods class可以同时生成.h和.m文件)

    存放到iosTunnel目录下

    开始业务代码的实现,头文件中先把官方文档的部分抄过来,在引入SDK的头文件

    //tunnel.h
    #import <Foundation/Foundation.h>
    // 引入 DCUniModule.h 头文件
    #import "DCUniModule.h"
    #import <SecurePortal/SPNBAClient.h>
    @interface tunnel : DCUniModule
    @property (nonatomic, strong) UniModuleKeepAliveCallback callback;
    @end
    

    这几行代码引入过后,等待IDE编译小半分钟(此时Xcode不会提示你它正在编译),如果对着SPNBAClient等类右键能够跳转到定义的话就说明头文件引入成功了。

    下面进行方法的实现

    //tunnel.m
    #import "tunnel.h"
    
    @interface tunnel()<SPNBAClientDelegate,NSURLSessionDelegate>
    
    typedef void (^CompletioBlock)(NSDictionary *dic, NSURLResponse *response, NSError *error);
    typedef void (^SuccessBlock)(NSDictionary *data);
    typedef void (^FailureBlock)(NSError *error);
    
    @end
    @implementation tunnel
    
    UNI_EXPORT_METHOD(@selector(connectNBA:callback:))
    // 通过宏 UNI_EXPORT_METHOD 将异步方法暴露给 js 端
    //UNI_EXPORT_METHOD(@selector(testAsyncFunc:callback:))
    /// 异步方法(注:异步方法会在主线程(UI线程)执行)
    /// @param options js 端调用方法时传递的参数
    /// @param callback 回调方法,回传参数给 js 端
    - (void)connectNBA:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
        //    NSLog(@"传递过来的参数是%@",options);
        self.callback = callback;
        NSString* NBA_host = @"xxx.xxx.xxx.xx";
        NSString* NBA_port = @"xxx";
        NSString* auth_username = options[@"NBAUsername"];
        NSString* auth_password = options[@"NBAPassword"];
        NSDictionary *loginDic = @{
            @"NBA_host"      : NBA_host,
            @"NBA_port"      : NBA_port,
            //        @"auth_server"   : @"认证服务器名,默认选取第一个",
            @"auth_username" : auth_username,
            @"auth_password" : auth_password,
            @"auth_mode"     : @0, //0用户名密码登录,1,证书登录, 2 动态口令, 3 二维码
            @"auth_autologin": @1, //1自动登录,0手动登录,需要实现登录界面
            //        @"extra_xxxxxx" :  extra_ 开头的额外的参数
        };
        NSLog(@"loginDic%@",loginDic);
    }
    
    - (void)didLoginSuccess {
        NSLog(@"登录成功");
        self.callback(@"隧道登录成功", NO);
    }
    - (void)onLoginErrorID:(NSInteger)errid msg:(NSString*)errmsg {
        if (self.callback)
        {
            NSLog(@"失败信息---%@",errmsg);
            self.callback(errmsg, NO);
        }
    }
    
    // 通过宏 UNI_EXPORT_METHOD_SYNC 将同步方法暴露给 js 端
    UNI_EXPORT_METHOD_SYNC(@selector(get:callback:))
    - (void)get:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
        NSString *urlString = options[@"url"];
        NSURL *url = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        [request setHTTPMethod:@"GET"];
        NSDictionary *proxyConfigDic = nil;
        NSInteger proxyPort = [SPNBAClient queryProxyPort];
        //NSLog(@"端口=%ld",proxyPort);
        if(proxyPort)
        {
            NSString *host = @"xxx.x.x.x";
            proxyConfigDic = @{(NSString*)kCFStreamPropertyHTTPProxyHost: host,
                               (NSString*)kCFStreamPropertyHTTPProxyPort: @(proxyPort),
                               (NSString*)kCFNetworkProxiesHTTPEnable:@YES,
                               (NSString*)kCFStreamPropertyHTTPSProxyHost: host,
                               (NSString*)kCFStreamPropertyHTTPSProxyPort:@(proxyPort)
            };
        }
        NSURLSessionDataTask* sessionTask = [self   createSessionWithRequest:request                                          withProxyConfig:proxyConfigDic
                       options:options
                        callback:callback];
        [sessionTask resume];
    }
    
    // 通过宏 UNI_EXPORT_METHOD_SYNC 将同步方法暴露给 js 端
    UNI_EXPORT_METHOD_SYNC(@selector(post:callback:))
    - (void)post:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
        NSString *urlString = options[@"url"];
        NSURL *url = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        request.HTTPMethod = @"POST";
        NSDictionary *proxyConfigDic = nil;
        NSInteger proxyPort = [SPNBAClient queryProxyPort];
        NSLog(@"url=%@",url);
        NSLog(@"端口=%ld",proxyPort);
        if(proxyPort)
        {
            NSString *host = @"xxx.x.x.x";
            proxyConfigDic = @{(NSString*)kCFStreamPropertyHTTPProxyHost: host,
                               (NSString*)kCFStreamPropertyHTTPProxyPort: @(proxyPort),
                               (NSString*)kCFNetworkProxiesHTTPEnable:@YES,
                               (NSString*)kCFStreamPropertyHTTPSProxyHost: host,
                               (NSString*)kCFStreamPropertyHTTPSProxyPort:@(proxyPort)
            };
        }
        NSURLSessionDataTask* sessionTask = [self createSessionWithRequest:request                                                    withProxyConfig:proxyConfigDic
                                             options:options
                                             callback:callback];
        [sessionTask resume];
    }
    
    - (NSURLSessionDataTask*)createSessionWithRequest:(NSURLRequest*)aRequest
                                      withProxyConfig:(NSDictionary*)proxyConfigDic
                                              options: (NSDictionary *)options
                                             callback:(UniModuleKeepAliveCallback)callback
    {
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        config.connectionProxyDictionary = proxyConfigDic;
        if([options objectForKey:@"token"])
        {
            NSLog(@"token=%@",options[@"token"]);
            config.HTTPAdditionalHeaders = @{@"token":options[@"token"]};
        }
        NSURLSession *session = [NSURLSession sessionWithConfiguration:config
                                                              delegate:self
                                                         delegateQueue:nil];
        NSURLSessionDataTask *sessionTask = [session dataTaskWithRequest:aRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            if(error)
            {
                NSLog(@"请求错误:%@",[error localizedDescription]);
                callback([error localizedDescription], NO);
            }
            else
            {
                NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
                NSLog(@"请求成功:%@",dataStr);
                callback(dataStr, NO);
            }
        }];
        return sessionTask;
    }
    @end
    

    5.原生应用测试

    5.1 编写UNI端业务代码

    <template>
        <view>
            <!--状态栏 -->
            <view class="status_bar"></view>
            <view class="login">
                <view class="content">
                    <!-- 头部logo -->
                    <view class="header">
                        <image src="@/static/images/logo.png"></image>
                    </view>
                    <text class="title"></text>
                    <!-- 主体表单 -->
                    <view class="main">
                        <wInput v-model="account" type="text" placeholder="账号" :focus="isFocus" :disabled="disabled">
                        </wInput>
                        <wInput v-model="password" type="password" placeholder="密码" :disabled="disabled"></wInput>
                    </view>
                    <wButton class="wbutton" text="登 录" :rotate="isRotate" @click="startLogin"></wButton>
                </view>
                <yomol-upgrade :type="upgradeType" :url="upgradeUrl" title="发现新版本" :content="upgradeContent"
                    ref="yomolUpgrade"></yomol-upgrade>
                <!-- 隧道modal -->
                <tui-modal :show="modal" :custom="true" fadeIn >
                    <view class="tui-modal-custom">
                        <view class="tui-prompt-title">NBA账号</view>
                        <input class="tui-modal-input"   v-model="NBAUsername" />
                        <view class="tui-prompt-title">NBA密码</view>
                        <input class="tui-modal-input password"   v-model="NBAPassword" />
                        <tui-button height="72rpx" :size="28" shape="circle" @click="requestNBA">提交</tui-button>
                    </view>
                </tui-modal>
            </view>
        </view>
    </template>
    
    <script>
        let tunnel
        if (uni.getSystemInfoSync().platform == "android")
            tunnel = uni.requireNativePlugin('NBATunnel')
        if (uni.getSystemInfoSync().platform == "ios")
            tunnel = uni.requireNativePlugin("NBATunnel-NBATunnel")
        import wInput from "@/components/watch-login/watch-input.vue"; 
        import wButton from "@/components/watch-login/watch-button.vue"; 
        import tuiModal from '@/components/tui-modal/tui-modal.vue';
        import { DbHelper } from "@/js/db.js";
        export default {
            data() {
                return {
                    account: "",
                    password: "",
                    isRotate: false, //是否加载旋转
                    isFocus: false, // 是否聚焦
                    disabled: false,
                    upgradeType: "pkg", //pkg 整包 wgt 升级包
                    upgradeContent: "", //更新内容
                    upgradeUrl: "", //更新地址
                    NBAUsername:'',
                    NBAPassword:'',
                    modal:false,
                };
            },
            components: {
                wInput,
                wButton,
            },
            async mounted() {
                await DbHelper.init();
                this.isLogin();
            },
            methods: {
                async isLogin() {
                    if (uni.getSystemInfoSync().platform == "android")
                    {
                        uni.showLoading({mask:true})
                        //无本地NBA数据
                        if(!uni.getStorageSync('NBAInfo'))
                        {
                            uni.hideLoading()
                            let [,res] = await uni.showModal({
                                content: '即将发起NBA权限请求,请点击确认,若在此过程中退出应用或者拒绝了权限,需要重装本应用才能重新发起NBA权限请求!',
                                showCancel:false,
                            });
                            this.modal = true
                        }
                        else//有本地NBA数据,说明之前已经建立了网卡
                        {
                            let NBAInfo =  uni.getStorageSync('NBAInfo')
                            this.NBAUsername = NBAInfo.NBAUsername
                            this.NBAPassword = NBAInfo.NBAPassword
                            uni.hideLoading()
                            await this.requestNBA()
                        }
                    }
                    if (uni.getSystemInfoSync().platform == "ios") 
                    {
                        uni.showLoading({mask:true})
                        //无本地NBA数据
                        if(!uni.getStorageSync('NBAInfo'))
                        {
                            uni.hideLoading()
                            let [,res] = await uni.showModal({
                                content: '请输入正确的NBA账号密码才能后续登录!',
                                showCancel:false,
                            });
                            this.modal = true
                        }
                        else//有本地NBA数据,说明之前已经建立了网卡
                        {
                            let NBAInfo =  uni.getStorageSync('NBAInfo')
                            this.NBAUsername = NBAInfo.NBAUsername
                            this.NBAPassword = NBAInfo.NBAPassword
                            uni.hideLoading()
                            await this.requestNBA()
                        }
                        
                    }
                },
                /**
                 * @description 连接NBA服务器
                 */
                async requestNBA(){
                    return new Promise((resolve,rejcet) => {
                        uni.showLoading({
                            title: 'NBA连接中...',
                            mask: true
                        });
                        if (!this.NBAUsername)
                            return uni.showToast({
                                title: "NBA账号不能为空!",
                                icon: "none"
                            }); //  显示提示框
                        if (!this.NBAPassword)
                            return uni.showToast({
                                title: "NBA密码不能为空!",
                                icon: "none"
                            });
                        if (uni.getSystemInfoSync().platform == "android") 
                        {
                            tunnel.connectNBA({
                                NBAUsername:this.NBAUsername,
                                NBAPassword:this.NBAPassword
                            },async res=>{
                                this.modal = false
                                uni.hideLoading()
                                if(res == '隧道登录成功' || res == '请求权限')
                                {
                                    let NBAInfo = {
                                        NBAUsername:this.NBAUsername,
                                        NBAPassword:this.NBAPassword
                                    }
                                    uni.setStorageSync('NBAInfo',NBAInfo);
                                    let { account,password } = uni.getStorageSync("userInfo"); // 从本地缓存中同步获取指定 key 对应的内容。
                                    if (!account) return; // 本地没有用户信息 直接返回(停留在登录页面)
                                    this.isFocus = false;
                                    this.isRotate = true;
                                    this.disabled = true;
                                    this.account = account;
                                    this.password = password;
                                    setTimeout(()=>{this.getUpdate()},1000)
                                }
                                else 
                                {
                                    if(/02000405/.test(res))
                                    {
                                        await uni.showModal({
                                            content:`NBA账号或者密码错误,请重新输入` ,
                                            showCancel:false,
                                        });
                                        this.NBAUsername = ''
                                        this.NBAPassword = ''
                                        uni.removeStorageSync('NBAInfo');
                                        this.modal = true
                                    }
                                    else
                                    {
                                        uni.showModal({
                                            content:res,
                                            showCancel:false
                                        }); 
                                    }
                                    rejcet(res)
                                }
                            })
                        }
                        if (uni.getSystemInfoSync().platform == "ios") 
                        {
                            let NBAInfo = {
                                NBAUsername:this.NBAUsername,
                                NBAPassword:this.NBAPassword
                            }
                            tunnel.connectNBA(NBAInfo,async res=>{
                                console.log(res); 
                                this.modal = false
                                uni.hideLoading()
                                if(res == '隧道登录成功' || res == '请求权限')
                                {
                                    uni.setStorageSync('NBAInfo',NBAInfo);
                                    let { account,password } = uni.getStorageSync("userInfo"); // 从本地缓存中同步获取指定 key 对应的内容。
                                    if (!account) return; // 本地没有用户信息 直接返回(停留在登录页面)
                                    this.isFocus = false;
                                    this.isRotate = true;
                                    this.disabled = true;
                                    this.account = account;
                                    this.password = password;
                                    setTimeout(()=>{uni.reLaunch({url: "/pages/home/home"})},1000)
                                }
                                else 
                                {
                                    if(/用户名或密码错误/.test(res))
                                    {
                                        await uni.showModal({
                                            content:`NBA账号或者密码错误,请重新输入` ,
                                            showCancel:false,
                                        });
                                        this.NBAUsername = ''
                                        this.NBAPassword = ''
                                        uni.removeStorageSync('NBAInfo');
                                        this.modal = true
                                    }
                                    else
                                    {
                                        uni.showModal({
                                            title:"NBA登录失败",
                                            content:res,
                                            showCancel:false
                                        }); 
                                    }
                                    rejcet(res)
                                }
                            })
                        }
                    })
                    
                    
                },
                // 检查网络状态,并进一步检查APP更新情况(有网条件)
                async getUpdate() {
                    let [, netWork] = await uni.getNetworkType()
                    if (netWork.networkType == "2g" || netWork.networkType == "none") 
                    {
                        if (uni.getStorageSync("userInfo"))
                            uni.reLaunch({url: "/pages/home/home"}); 
                    }   
                    else
                    {
                        plus.runtime.getProperty(plus.runtime.appid, async widgetInfo => {
                            let {data: res} = await this.$http.get('/api/basedata/GetAppUpdateMsg',{
                                params:{
                                    appid: plus.runtime.appid,
                                    version: plus.runtime.version,
                                    imei: plus.device.imei,
                                }
                            })
                            if (res.data) 
                            {
                                this.upgradeUrl = res.data.DownLoadURL;
                                this.upgradeContent = res.data.Describe || "1.性能优化\n2.修复部分错误"
                                this.$refs.yomolUpgrade.show();
                            } else 
                                uni.reLaunch({url: "/pages/home/home"})
                        });
                    }
                },
                async startLogin(e) {
                    if (this.isRotate) return;
                    if (!this.account)
                        return uni.showToast({
                            title: "账号不能为空!",
                            icon: "none"
                        }); //  显示提示框
                    if (!this.password)
                        return uni.showToast({
                            title: "密码不能为空!",
                            icon: "none"
                        });
                    this.isRotate = true; 
                    this.disabled = true; 
                    let res;
                    if (uni.getSystemInfoSync().platform == "android")
                    {
                        try {
                            let data = await this.$http.post("/api/security/token", {
                                username: this.account,
                                password: this.password,
                            });
                            res = data.data;
                        } catch (e) {
                            this.isRotate = false;
                            this.disabled = false;
                            return;
                        }
                        let {data: res2} = await this.$http.get("/api/account/GetUserInfo",{
                            custom: { auth: false },
                            header: { token: res.token }
                        });
                        let userInfo = {
                            account: this.account,
                            password: this.password,
                            token: res.token
                        };
                        for (let key in res2.data) {
                            userInfo[key] = res2.data[key];
                        }
                        uni.setStorageSync("userInfo", userInfo); 
                        await this.getUpdate()
                        this.isRotate = false;
                    }
                    if (uni.getSystemInfoSync().platform == "ios") 
                    {
                        tunnel.post({
                            url:`${this.$http.config.baseURL}/api/security/token?username=${this.account}&password=${this.password}`,
                        },callBack=>{
                            callBack = JSON.parse(callBack)
                            console.log(callBack);
                            //存储token
                            if(callBack.status != 0)
                            {
                                uni.showToast({
                                    title: callBack.msg,
                                    icon: 'none'
                                });
                                this.isRotate = false;
                                this.disabled = false;
                                return
                            }
                            tunnel.get({
                                url:`${this.$http.config.baseURL}/api/account/GetUserInfo`,
                                token:callBack.token
                            },callBack2=>{
                                callBack2 = JSON.parse(callBack2)
                                console.log(callBack2);
                                let userInfo = {
                                    account: this.account,
                                    password: this.password,
                                    token: callBack.token
                                };
                                for (let key in callBack2.data) 
                                {
                                    userInfo[key] = callBack2.data[key];
                                }
                                console.log(userInfo);
                                uni.setStorageSync("userInfo", userInfo); 
                                uni.reLaunch({url: "/pages/home/home"})
                            })  
                        })
                    }
                },
            },
        };
    </script>
    

    编写完成后,右键UNI项目: 发行-原生APP本地打包-生成本地打包APP资源

    打开APP资源路径,然后删除掉HBuilder-uniPlugin/HBuilder-Hello/Pandora/apps/_UNI_33C5A38/www这个WWW文件夹,然后把生成的离线WWW文件拷贝过去。

    资源文件拷贝完毕后,选中工程中的HBuilder-uniPlugin-Info.plist文件右键->Open As->Source Code找到dcloud_uniplugins节点,copy下面的内容添加到dcloud_uniplugins节点下,按您插件的实际信息填写对应的项

    编写plist文件完毕后,选择HBuilder这个target后,插上真机,command+B后等待一段时间即可进行测试。

    6.插件打包

    测试完毕之后,首先生成iosTunnel.Framework文件,

    再选中iosTunnel,command+B进行framework文件编译生成

    生成完成后,将第三方SDK库文件与我们自己写生成的库文件放置为如下目录

    //package.json
    {
        "name": "原生插件",
        "id": "NBATunnel",
        "version": "1.0",
        "description": "原生插件",
        "_dp_type":"nativeplugin",
        "_dp_nativeplugin":{
            "ios": {
                "plugins": [{
                    "type": "module",
                    "name": "NBATunnel-NBATunnel",
                    "class": "NBATunnel"
                }],
                "frameworks": ["SecurePortal.framework"],
                "integrateType": "framework",
                "deploymentTarget": "9.0"
            }
        }
    }
    

    然后再manifest.json选择本地插件,提交云端打包即可。

    相关文章

      网友评论

        本文标题:UNIAPP----IOS端原生插件开发实战(一)

        本文链接:https://www.haomeiwen.com/subject/wrvjpltx.html