likes
comments
collection
share

移动端小白,30天掌握Flutter双端插件开发-下(iOS篇)

作者站长头像
站长
· 阅读数 6

距离上篇文章过去了一个半月了,主要双端都开发完毕,不能继续带薪学习了,需要干点正事,让这么久的辛勤成果发挥它应有的价值-上架。Android的需要上架各个应用商店,iOS的上架App Store,第一次干这活可比攻坚技术还让人心力交瘁,一把心酸泪在心里流淌,光吐槽都能再水2000个字。这么长时间也不能光忙活个上架的事,协助同事完善了一下公司的后台管理系统,还还开发了另一款相机插件,那是后话了。

经过了Android端的开发,我们已经把插件的基本功能全部摸清楚,在项目中也正常的跑通了,按道理来讲,现在只需要了解一下双端开发差异,将kotlin的代码转换为swift端的代码,还有这些功能涉及到的权限申请重新在iOS端复现一遍即可。那岂不是看一遍文档,了解一下开发差异,再给我三天就搞定?想到这就笑出了声。

但当实际上手的时候,还是发现自己Too young, too simple。开发者账号要准备,必须使用的xcode编辑器要下载,xcode的文件目录完全看不懂,官方文档搜索功能也不会用,想打包发给同事测试还要先交100刀乐,以及其他种种问题。上一次这么难受的时候还是第一次用iPhone,用了一个月,还是回归安卓,作为用户还可以选择不用,现在但作为开发者,不用也得用。

最让人想吐槽的就是,其他编程语言的保留字和报错描述都用人们熟悉的单词,只有苹果非要发明一套,不反逻辑,但就是要反常识。

移动端小白,30天掌握Flutter双端插件开发-下(iOS篇)

还有更蛋疼的,就是感觉iOS的文章特别的少,免费课都是落后的知识,付费的系统课时间太长了,没时间去学习,而且冗余的知识也用不上。

吐槽完毕,进入正题。

一、xcode文件目录

还是右键项目目录,鼠标放在Flutter选项上,再点击子选项open iOS module in Xcode,即可看到ios的代码文件。

目录 文件夹 包含功能描述
Runner Flutter xcconfig项目配置文件
Runner 主程序代码编写文件,权限配置文件
Products app运行程序,不能直接运行
Pods 第三方插件配置文件
Frameworks Pods的库
Pods Podfile(文件) Pods依赖关系说明文件
Development Pods 插件开发写代码的地方
Frameworks 保存官方提供的插件
Products 插件自身也是一个freamwork
Targets Support Files Pods的一些配置文件

(上面的目录只有几个我们用的到,而且描述都非常不准确,都是按照自己理解写的)

这其中Runner的部分是在根目录/example/ios中,一些项目的配置需要在这里修改,真正跑起来的也是这个,Pods是在根目录/ios中的,这里才是我们主要编写方法的地方。

二、面向未来swift

知道了在哪写,该学习怎么写了。

面向相同用途的语言都会有不少类似的地方,学了Android的开发语言,再学习iOS的,就会发现有很多概念相同的地方,这也能减轻我们的心智负担。

众所周知,如同另一平台一样,iOS平台也有2种语言可以选择,老牌的object-c和新生代的swift。任何语言都是越老越稳定,资料多,资源多,但缺少现代化语言的功能;而新的语言总是有不稳定,版本更新变化大,资料少的问题,也有语法精炼,有不少好用的现代化功能。而在今天看来,swift已经到了第7个年头,也到了第五个大版本,不稳定的问题也微乎其微了,最关键相对于前者同样的功能代码量少,易于理解,毕竟代码主要是给人看的。

那么,再根据买手机配电脑的经验,出来吧,SWIFT!!!

1、数据类型

当然还是先看数据类型,上篇文章中讲到,在dart中会使用nullboolintStringMap以及Uint8List这几种类型,还是在看这个表:

移动端小白,30天掌握Flutter双端插件开发-下(iOS篇)

其他的数据都行想必都很熟悉了,在这里也是一样,了解不同数据类型的方法。主要注意一下TypedData,需要传递这种数据类型到flutter时,需要用FlutterStandrdTypedData进行包装一下。

2、基础语法

我们这个项目用的语法也不多,关键学习一下我们用到的。顺便说一句,为何不选OC,主要因为dart、kotlin、swift这三种语言有个最大的共通处,都是带有null salfty机制的语言,更符合我们的思维习惯。

变量和常量

任何语言的第一步就是声明变量。

  • 常量和变量必须在使用前声明。
  • 使用 var 来声明变量,值可读可写。
  • let 来声明常量,只能在声明的时候确定.
  • 声明的时候可以标明值的类型,也可以自动判断并不可更改类型。
//声明一个常量
let TAG:String = "plugin"
//变量,可重新赋值
var status: NSNumber = 0
//可为空的变量,稍后赋值
var img:UIImage?

流程控制

swift有for in循环, while循环,if条件判断,switch 判断,三元运算。

// for in 循环,同样可以循环字典获得键和值
let names = ["Anna", "Alex", "Brian", "Jack"] 
for name in names { 
    print("Hello, \(name)!") 
} 

// while 循环
var index = 10
while index < 20 {
   print( "index 的值为 (index)")
   index = index + 1
}

//if判断
if index > 20 {
   print( "index 的值大于20")
} else {
   print( "index 的值小于20")
}

// switch判断
switch index {
   case 100  :
      print( "index 的值为 100")
   case 10,15  :
      print( "index 的值为 10 或 15")
   case 5  :
      print( "index 的值为 5")
   default :
      print( "默认 case")
}

//三元运算
index > 20 ? print( "index 的值大于20") : print( "index 的值小于20")

函数和表达式

使用func来声明一个函数,对传递的参数及返回的值可以声明类型。

//无返回值,返回值类型可以省略
func getInfo(name: String, site: String) -> String {
    return name + site
}

闭包

闭包的作用和其他的语言的 Lambda 表达式有点相似,但在这里属实有点抽象,可以查看这篇文章swift中的闭包

闭包理解起来比较困难,但我们只需要知道他是如何声明和实现的就好,通过案例,可以简单的理解为使用{}in关键字,可以使用闭包的参数。

// 闭包的声明 
var closure: ((String, Int) -> String)! 

// 闭包的实现 
closure = { (name, age) in 
    return "\(name)\(age)岁" 
}

class类

所有编程语言的类都大同小异,我们可以为类定义属性和方法,在类的内部可以通过self关键字调用自身的方法和熟悉,而且swift会自动生成面向其它代码的外部接口。

class Marks {
    var mark: Int
    init(mark: Int) {
        selfmark = mark
    }

    func suffix(mark: Int) -> String{
        return "\(mark)分"
    }

    func getSuffix() -> String {
        return selfsuffix(mark: selfmark)
    }
}

let m1 = Marks(mark: 100)
print(m1.getSuffix())
//打印:100分
print(m1.suffix(50))
// 打印:50分

虽然和kt的语法大同小异,但很多概念还是很抽象的。当然开始学习的时候,不需要了解那些概念,只需要把他当成一个黑盒,写法不变的情况下,输入什么参数就能输出预期的结果就够了。

三、功能实现

这里的流程依然和安卓端的非常类似,无外乎导入插件-执行插件的方法。但不同平台最大的差异不在代码编写上,反而在项目配置,目录结构,插件导入等开发支出工作上。

1、pods了解

在上面的文件目录介绍中,会发现里面有2个跟目录,一个Runner,一个是pods,前者是主项目文件,项目运行起来全是依靠他,而后者就是所谓的Cocoapods,iOS项目的依赖管理工具,类似于Android的gradle,flutter的pubspec,还有前端开发的npm。

安装远程依赖

在ios中安装第三方依赖可以像flutter一样,直接在文件中配置,这里就可以直接在Pods/Podfile文件中进行配置。

target 'QSAppDemo' do
  pod 'AFNetworking'
  pod 'YYModel', '~> 1.0.4'
  pod 'OOMDetector', '1.3'
  
  # Debug模式下生效
  pod 'FLEX', '~> 2.0', :configurations => ['Debug']
  pod'WebViewJavascriptBridge',:git=>'https://github.com/marcuswestin/WebViewJavascriptBridge.git'
end

安装本地依赖

这里直接在Development Pods/{项目名}/Pod/{项目名}.podspec文件中添加如下配置

// 引用框架库
s.vendored_frameworks = "{路径}/{文件名}.framework"
// 引用静态库
s.vendored_libraries = "{路径}/{文件名}.a"
// 引用头文件
s.source_files = "{路径}/**/*"

上面都配置完成后,必须使用pod install命令来下载第三方库,或者使用pod update命令来更新配置文件,使用依赖生效。

原生依赖

除了第三方依赖,当然还需要添加官方的原生依赖,依然是在.podspec后缀的文件中添加如下配置。

// 引用框架库
s.frameworks = "NetworkExtension", "CoreLocation"
// 引用动态库 .lib、tbd ,去掉头尾的lib、tbd
s.libraries = "bz2.1.0.5", "iconv.2.4.0", "z", "c++"

获得这里的文件名称,和查看是否配置成功,可以直接点击pods打开Pods.xcodeproj的可视化编辑文件。

再点击左侧target-项目名打开此项目的配置,在点击Build Phases-Link Binary With Librarys查看依赖。

移动端小白,30天掌握Flutter双端插件开发-下(iOS篇)

2、导入本地依赖

由于我们的项目中使用的是本地依赖,而且是一个framework,所以我们只需要将此依赖导入到项目中,并配置podspace文件就好。

文件导入

在flutter项目中,打开ios目录,新建Freamwork文件夹,并将本地的的依赖复制粘贴进来。

移动端小白,30天掌握Flutter双端插件开发-下(iOS篇)

pods更新

打开xcode编辑器,再到pods中的.podspec文件中添加如下配置

// 引用框架库
s.vendored_frameworks = "Framework/*.framework"

再打开命令行工具,执行pod update

本地依赖不会再Link Binary With Librarys中显示,可以直接在编码文件尝试导入,有提示则依赖导入成功。

3、数据交互初始化

开始正式编写业务代码,这里就开始看出kotlin和swift的相似之处

import Flutter
import UIKit
import CoreLocation
import CoreTelephony
import HZCameraSDK
import Foundation

public class SwiftHzCameraPlugin: NSObject, FlutterPlugin, HZCameraSocketDelegate{
    
var cllocationManager: CLLocationManager!
var eventSink:FlutterEventSink?
var mPrevivew: HZDisplayView = HZDisplayView()
var img:UIImage?
public static let instance: SwiftHzCameraPlugin = SwiftHzCameraPlugin()

// 数据流监听
class SwiftStreamHandler: NSObject, FlutterStreamHandler {
 func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
  instance.eventSink = events
  return nil
 }

 func onCancel(withArguments arguments: Any?) -> FlutterError? {
  instance.eventSink = nil
  return nil
 }
}

public static func register(with registrar: FlutterPluginRegistrar) {
// 绑定MethodChannel
  let channel = FlutterMethodChannel(name: "hz_camera", binaryMessenger: registrar.messenger())
  // 绑定EventChannel
  let eventChannel = FlutterEventChannel(name: "HzCamera_event", binaryMessenger: registrar.messenger())
  eventChannel.setStreamHandler(SwiftStreamHandler())

  registrar.addMethodCallDelegate(instance, channel: channel)
}

}

4、执行方法

安卓和iOS对权限申请和连接硬件有一点差别,但整体是差不多的。

申请权限

func checkPermission() {
    CTCellularData().cellularDataRestrictionDidUpdateNotifier = { (state) in
     if state == CTCellularDataRestrictedState.restrictedStateUnknown {
      DispatchQueue.main.async {
       self.requestWLANAuth()
      }
     }
     else if state == CTCellularDataRestrictedState.restricted {
       DispatchQueue.main.async {
         self.requestWLANAuth()
       }
     }
    }
}

连接相机

// 2、连接相机
func connectCamera() {
  do {
    try HZCameraConnector.default().connectToCameraError()
  } catch let error {
   self.eventSink!([
    "code": 0,
    "data": "连接失败",
   ] as [String: Any])
  }
  // 连接相机成功后直接初始化
  setUpCamera()
}

相机连接状态改变的时候执行

// 3、相机连接状态改变的时候执行
public func cameraConnectionStateChange(_ state: E_SOCKET_STATE) {
 if E_SOCKET_STATE_CONNECTED != state {
  NSLog("相机已断开")
 } else {
  NSLog("相机已连接")
  setUpCamera()
 }
}

相机初始化,获取相关权限

安卓连接相机只需要进行一次初始化,所以把这个方法放在前面,而iOS每次连接相机都需要初始化。

// 4、相机初始化
func setUpCamera() {
 requestWLANAuth()
 HZCameraMedia.default().setupCamera(
  completion: {},
  fail: {(err) in
   
  },
  progress: {(process) in
   
  }
 )
}

开始&结束预览

调用预览数据方法,会获取到RGB图像数据,可以将此数据传递到flutter层进行渲染。

// 5、开始预览
func startPreview() {
 mPrevivew.startGetRgbData({ (data: Data, w: Int32, h: Int32) in
  self.eventSink!([
   "code": 1,
   "data": [
    "frameData": FlutterStandardTypedData(bytes: data),
    "width": w,
    "height": h
   ],
  ] as [String: Any])
 }, fail: { (err) in
  NSLog(err.localizedDescription)
 })
}

func stopPreview() {
  mPrevivew.stop()
}

拍摄照片

// 6、拍摄照片
func takePhoto() {
 // 获取相机地址
 HZCameraMedia.default().onlyTakePhoto() { (picAddress) in
  // 拍摄成功去拼接照片
  self.genPanoramaPhoto(path: picAddress)
 } cameraStatus: { (status: E_TURN_STATUS) in
  // 相机状态
 } fail: { (err) in
  // 拍摄失败
 }
}

拼接照片

// 7、拼接照片
func genPanoramaPhoto(path: String) {
 let homedDirectory = NSHomeDirectory()+"/Documents/"
 
 HZCameraFileManager.default().setAlbumLocalAddress(homedDirectory)
 HZCameraFileManager.default().getPhotoWithName(path) { (img) in
  // 拼接完成的照片路径
 } fail: { (err) in
  // 拼接失败
 } progress: { (process) in
  // 拼接过程
 }
}

获取相机参数

// 8、获取相机参数
func getSystemInfo(result: @escaping  FlutterResult) {
 HZCameraSettings.default().getCameraMemoryInfo {(info) in
  result([
   "mBatteryPercent": info.chargeInfo,
   "mChargingState": info.batteryStatus == E_BATTERY_CHARGING ? "充电中" : "未充电",
   "freeMemorySpaceWithUnitG": String(format: "%.2f", Float(info.memoryFreeStorage)/1000),
  ] as [String: Any])
 } fail: { (err) in
  print(err)
 }
}

5、将数据返回到flutter层

数据通信依然是那2种基本方式,result及时返回数据和event实时监听数据。

及时返回数据

比如获取相机参数

result([
   "mBatteryPercent": info.chargeInfo,
   "mChargingState": info.batteryStatus == E_BATTERY_CHARGING ? "充电中" : "未充电",
]as [String: Any])

在安卓中,我们使用的是hashMap类型来传递参数,而在iOS中对应的则是Dictionary类型,所以可以直接使用[] as [String:Any]来声明一个字典类型。

监听实时数据

相机的图像是动态的,需要监听图像数据并实时刷新。

// 在项目初始化中声明event方法。
var eventSink:FlutterEventSink?
self.eventSink!([
 "code": 1,
 "data": [
  "frameData": FlutterStandardTypedData(bytes: data),
  "width": w,
  "height": h
 ],
] as [String: Any])

图片数据是Unit8List,所以传递到flutter中还需要一层包装。

四、总结

一边学swift一边实现功能,可以说是现学现卖了,但其实也只花费了8个工作日左右的时间,学会了dart,了解了kotlin,再来学swift可以说过一遍文档,写写小例子就可以开工了。这些功能在安卓端已经跑通了一遍,只是重写一遍,根据双端差异调整一下接口执行的流程就好。

说起来好像很简单,但iOS开发的第一步,熟悉xcode的目录加导入插件就花费了5天时间,还是花了几次学费请教了一下ios开发大佬,加起来也花费了13天时间。

这些功能完善只算是入了原生开发的门,只学会了kotlin和swift的皮毛,以及了解双端项目开发流程。对于原生布局,其他各种原生功能都还没有涉及到,需要更多的实战来掌握更多的知识点。

这些功能完成过了一个月才有空写下这篇短文。而这段时间开发另一个插件,不得不用原生布局,再配合上PlatformView,掌握了编程语言和布局方法,就像前端学会了html5+js,能做的都可以做了,算是更加入门了原生开发,又可以水2篇文章了,当然那都是后话了。

转载自:https://juejin.cn/post/6989906910971428900
评论
请登录