標題の通り、頓挫したのでこのまま心の中にそっとしまっておこうと思ったのですが、費やした数時間を供養するためにも経緯とか理由とかを書いておこうと思います。(基本的にCore Bluetoothの話ですが、Core Bluetoothに限った話ではない部分もあると思います)
背景
先日、ズンドコキヨシ with Bluetooth Low Energy というのをやってみた際に、GATT を Swift の enum
を使って書いてみた ところ自分の中で妙な手応えみたいなものがありまして。
たとえば「ペリフェラルでサービスを追加する」処理を、Objective-Cでやっていた書き方をそのまま踏襲していたときは以下のように書いていたところが、
func publishservice () {
// キャラクタリスティックを作成
let characteristicUUID = CBUUID(string: kCharacteristicUUID)
let characteristic = CBMutableCharacteristic(
type: characteristicUUID,
properties: CBCharacteristicProperties.Read,
value: nil,
permissions: CBAttributePermissions.Readable)
// サービスを作成
let serviceUUID = CBUUID(string: kServiceUUID)
let service = CBMutableService(type: serviceUUID, primary: true)
service.characteristics = [characteristic]
// サービスを追加
self.peripheralManager.addService(service)
}
こう書いてたのが、enum
で GATT を定義して、そっちに機能を持たせることで、
func publishservice () {
// キャラクタリスティックを作成
let characteristic = Characteristic.Bar.mutableCharacteristic()
// サービスを作成
let service = Service.Foo.mutableService()
service.characteristics = [Characteristic]
// サービスを追加
peripheralManager.addService(service)
}
これぐらいシンプルに書けるようになったわけです。
ObjCでもインスタンス生成の機能を別クラスに持たせてやればできるじゃん、とかのツッコミもあるのかもしれませんが、まぁとにかく僕の中で
enum Characteristic : Int {
case ズンドコ
case キヨシ
func UUID() -> CBUUID {
switch self {
case .ズンドコ:
return CBUUID(string: "C2F0882D-2440-40F4-A4A9-95BE43B89EAE")
case .キヨシ:
return CBUUID(string: "C2F0882E-2440-40F4-A4A9-95BE43B89EAE")
}
}
private func properties() -> CBCharacteristicProperties {
switch self {
case .ズンドコ:
return [CBCharacteristicProperties.Notify, CBCharacteristicProperties.Read]
case .キヨシ:
return [CBCharacteristicProperties.Write]
}
}
private func permissions() -> CBAttributePermissions {
switch self {
case .ズンドコ:
return [CBAttributePermissions.Readable]
case .キヨシ:
return [CBAttributePermissions.Writeable]
}
}
func mutableCharacteristic() -> CBMutableCharacteristic {
return CBMutableCharacteristic(
type: UUID(),
properties: properties(),
value: nil,
permissions: permissions())
}
}
こんな感じでGATTを書くことが、いつもMarkdownで定義している GATT仕様をそのままコードに落とし込んだよう で、とても気持ちよかったのです。
で、それに加えて、キャラクタリスティックのvalueというのはNSData型で、中身に何をどんな長さで入れるかは勝手に決めていい わけですが、たとえばズンドコの例でいうと、1バイトの2種類の値で「ズン」と「ドコ」を表現する 1 と決めて、
enum ズンドコ: UInt8 {
case ズン
case ドコ
private func data() -> NSData {
return NSData(bytes: [self], length: 1)
}
init?(data: NSData) {
var bytes = [UInt8](count: data.length, repeatedValue: 0)
data.getBytes(&bytes, length: data.length)
guard let value = bytes.first, zundoko = ズンドコ(rawValue: value) else { return nil }
self = zundoko
}
}
こんな感じでenumで 取りうる値を定義しつつNSDataへのエンコードとデコード機能も持たせておく ことでセントラル/ペリフェラル間でやり取りする値の取り決めがスッキリ定義できた感がありまして。
で、このあたりに、なんかAPIKitの思想を導入できそうだな、と。
今回書いてて、Core BluetoothにもAPIKit的にタイプセーフな縛りを導入できないもんかなと。まだ具体的にはノーアイデアだけど。Swiftの勉強がてら考えてみよう。 https://t.co/upZc9um7kc
— Tsutsumi Shuichi (@shu223) 2016年4月1日
ここまで書いてて、既に「何をつくろうとしているのかよくわからないままつくりはじめた」ということがわかりましたが。。まぁつくりながら何ができるか検討してみた感じです。
頓挫した理由
厚いラッパーになりそうだった
Core Bluetoothにおいて通信をAPIKit的に書ける、というゴールを具体的に考えてみると、たとえばセントラルでとあるキャラクタリスティックの値をsubscribeするというケースを例にとると、
Central.subscribe(characteristic) { decodedValue in
// some process
}
こんな感じで、 characteristic
がどのキャラクタリスティックかが決まっていれば、その型から decodedValue
の型が推論される、ということになるのかなと。
で、こういうことをやろうとすると、ライブラリ側で CBCentralManager の peripheral:didUpdateValueForCharacteristic:error:
もラップしないといけなくなる。このメソッドをそのまま実装されると、結局この引数に渡されてくるCBCharacteristicオブジェクトをどうにでも扱えることになって、それだとこの思想を体現できてない(十分に縛れてない)ので。
こういう標準フレームワークのプロトコルをまるっとサードパーティ側ライブラリでラップするというのは、個人的にはかなりやりたくなくて使いたくもないので、まずこの点はアウト。
書く量が減らない
GATTを定義するためのプロトコルとして、
public protocol ServiceType {
func UUID() -> CBUUID
func mutableService() -> CBMutableService
}
public extension ServiceType {
func mutableService() -> CBMutableService {
return CBMutableService(type: UUID(), primary: true)
}
}
public protocol CharacteristicType {
associatedtype DecodedValue
func UUID() -> CBUUID
func properties() -> CBCharacteristicProperties
func permissions() -> CBAttributePermissions
func mutableCharacteristic() -> CBMutableCharacteristic
}
public extension CharacteristicType {
func mutableCharacteristic() -> CBMutableCharacteristic
{
return CBMutableCharacteristic(
type: UUID(),
properties: properties(),
value: nil,
permissions: permissions())
}
}
こんなものを考えてみたんですが、このプロトコルに従って例えばキャラクタリスティックを定義してみると、
- enum版
enum FooBarCharacteristicType : CharacteristicType {
case Foo
case Bar
init(characteristic: CBCharacteristic) {
if characteristic.UUID == FooBarCharacteristicType.Foo.UUID() {
self = FooBarCharacteristicType.Foo
}
else {
self = FooBarCharacteristicType.Bar
}
}
func UUID() -> CBUUID {
switch self {
case .Foo:
return CBUUID(string: "C2F0882D-2440-40F4-A4A9-95BE43B89EAE")
case .Bar:
return CBUUID(string: "C2F0882E-2440-40F4-A4A9-95BE43B89EAE")
}
}
func properties() -> CBCharacteristicProperties {
switch self {
case .Foo:
return [CBCharacteristicProperties.Notify, CBCharacteristicProperties.Read]
case .Bar:
return [CBCharacteristicProperties.Write]
}
}
func permissions() -> CBAttributePermissions {
switch self {
case .Foo:
return [CBAttributePermissions.Readable]
case .Bar:
return [CBAttributePermissions.Writeable]
}
}
}
- struct版
struct FooCharacteristic : CharacteristicType {
func UUID() -> CBUUID {
return CBUUID(string: "C2F0882D-2440-40F4-A4A9-95BE43B89EAE")
}
func properties() -> CBCharacteristicProperties {
return [CBCharacteristicProperties.Notify, CBCharacteristicProperties.Read]
}
func permissions() -> CBAttributePermissions {
return [CBAttributePermissions.Readable]
}
}
struct BarCharacteristic : CharacteristicType {
func UUID() -> CBUUID {
return CBUUID(string: "C2F0882E-2440-40F4-A4A9-95BE43B89EAE")
}
func properties() -> CBCharacteristicProperties {
return [CBCharacteristicProperties.Write]
}
func permissions() -> CBAttributePermissions {
return [CBAttributePermissions.Writeable]
}
}
なんというかすごい冗長感がある。。
そしてこのプロトコルに縛られるメリットがあんまりない。書くコード量としては CBMutableService/Characteristic生成分ぐらいしか減ってないし。。
それなら変にプロトコルを外からライブラリを導入してくるよりも、コードスニペットとかファイルテンプレートにしといた方が便利そうだなとか。
コミュニティへの貢献期待値が低い
プロトコルに縛られるメリットが少ない、と上述しましたが、「キャラクタリスティックの型が決まればやり取りするvalueの型も決まる」ようになっていればそれがメリットなのでは?とも考えました。
が、そうするためにはライブラリ利用側で「プロトコルに従ってGATTを書く」という新しい「作法」を覚える必要が生じるわけで、そしてそもそもそういうことをやろうとするキッカケとしてその「思想」を理解ししてくれないといけないわけで、どれほどの人数がそれをやってくれるんだろうかと。
iOSエンジニアが10万人いれば10万人がWeb APIをたたくとは思いますが、自分でGATTを定義して実装する機会のある人は1万人・・・もたぶんいなくて、1000人とか?とにかくそんなにはいないと思われます。
そして、GATTというのは基本そんなに頻繁に追加・変更するものじゃない(プロダクトを世に出したあとは特に)ので、このへんのコードに触る機会というのはAPIクライアントのコードに触る機会よりもグッと少ないわけです。
まとめると、ごく少ないエンジニアが、ごく少ない期間だけ意識する部分のコードを、新しい作法で書く・・・と掛け算していくとすごい使ってくれる人少そうだなと。
おわりに
自分の設計力・Swift力・見通し力のなさを露呈しているようで、書きながら何度もアップするのをやめようと思ったのですが、書いてるうちに、逆にまた光明も見えてきました。
まず、ライブラリ側で過度に標準フレームワークのAPIをラップすることはやりたくないので、そういうことが必要になるようなfeatureはスッパリ諦めて、
- GATT仕様をそのまま落とし込んだようなコードが「楽に」書ける
- それによって、セントラル・ペリフェラルの実装もスッキリする
この辺に特化したものにすれば、(母数が少ないながらも)使ってくれる人がそれなりにいるんじゃないかなと。
型の縛りが入る、というコンセプトを維持しようとすると、たぶん使い方が難しくなって使ってくれる人が減ってしまいそう(自分も使わなくなりそう)なのであきらめるとして、そうするともうSwiftyという名前はふさわしくなさそうですが。
その形がライブラリなのか、ファイルテンプレートなのか、スニペット集なのかまだわかりませんが、引き続き検討していきたいと思います。
-
BLEでは無駄な情報量をやり取りしないのが基本的思想なので、こういうケースで文字列をそのままバイナリで送るということはしない ↩