気楽なソフト工房

プログラミングについていろいろな記事を書いています。



mykonos2008

Author:mykonos2008
システムエンジニアとして働いている30代の会社員です。
仕事や趣味でプログラムを書いている方の役に立つ記事を書いていきたいと思っています。
ご意見、ご感想はこちらまで
If you are an english speaker,Please visit my english blog.

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
iOSでURLエンコーディングをする際、CoreFoundationのCFURLCreateStringByAddingPercentEscapes()を
使われている方は多いと思います。

しかし、ARC環境で、これを使う場合、注意しないとメモリリークを引き起こしてしまいます。
メモリリークを発生させない使い方は以下です。

NSString *encodedText = (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(
                                      NULL,
                                      (__bridge CFStringRef)sourceText,	//元の文字列
                                      NULL,
                                      CFSTR("!*'();:@&=+$,/?%#[]"),
                                      CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));

ポイントは戻り値を(__bridge_transfer NSString *)でキャストしていることです。
(__bridge NSString *)でキャストするとメモリリークが発生します。
Createという単語を含むCoreFoundationの関数を使用する場合、戻り値のオブジェクトの所有権を取得します。つまり、
CFRelease()を後でコールする必要があるということです。

__bridge_transferは戻り値のオブジェクトの所有権をARCに移譲するためのキーワードです。
これにより、CFRelease()を自分でコールしなくても、ARCが面倒を見てくれるようになります。

__bridgeでキャストすると、ARCへの所有権の移譲が行われないため、自分でオブジェクトを解放する必要があります。

スポンサーサイト
iOSで文字列の数値チェックを行う方法はいろいろ有りますが、一番手っ取り早いのが
NSStringの「rangeOfString:options:」を使う方法ではないかと思います。

    NSString *target = _textView.text; //対象の文字列
    NSRange match = [target rangeOfString:@"^[0-9]+$" options:NSRegularExpressionSearch];
    //数値の場合
    if(match.location != NSNotFound) {
        ・・・・・・・・
    }
    //数値でない場合
    else {
        ・・・・・・・・
    }

rangeOfStringの引数には正規表現が使用できます。
正規表現を変えることで半角英数などいろいろなチェックに応用できます。

前回の記事でCore BlueToothを使用して、iOSデバイスをBLE機器として動作させるサンプルコードを紹介しました。
そこで今回は、そのサンプル機器にiPhoneから接続するセントラル(Central)を作成するサンプルコードを紹介します。



前回の記事では接続されるとHello!と返すだけの単純なペリファレル(BLE機器)を作成しました。
ペリファレルに1つのサービスを追加し、さらにそのサービスにキャラクタリスティックを1つ持たせる形になっていました。

ペリファレルを作成する際は、CBPeripheralManagerが中心的な役割を担っていましたが、セントラルを作成する場合は
CBCentralManagerがその役割を担います。
処理を行うViewControllerにはCBCentralManagerDelegateとCBPeripheralDelegateの2つのプロトコルを採用します。

#import "ViewController.h"

#define SERVICE_UUID @"9EA08FC0-FFCF-46D1-ABE3-A1A726699681"
#define CHARACTERISTIC_UUID @"1E222259-9612-4139-B3CB-19BA1DFB9C0E"

@interface ViewController ()

@end

@implementation ViewController{
    CBCentralManager *_centralManager;
    CBPeripheral *_peripheral;
    CBService *_service;
    IBOutlet UIButton *_startConnect;
    IBOutlet UILabel *_resultLabel;
}

-(id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if(self) {
        _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
    }
    return self;
}

//CBCentralManagerのステータスが更新されるとコールされる
-(void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    NSLog(@"State Updated");
    if(central.state == CBCentralManagerStatePoweredOn) {
        NSLog(@"ready");
        //接続開始ボタンを有効にする
        _startConnect.enabled = YES;
    }
}


CBPeripheralManagerと同じく、CBCentralManagerの場合も、Bluetoothが使用可能な状態になったことを
確認してから、後続の処理を行う必要があります。CBCentralManagerのインスタンスを生成すると、
centralManagerDidUpdateState:がコールされるので、そこで状態を確認するようにしています。

//接続開始ボタンのハンドラ
-(IBAction)startConnectTouched:(id)sender
{
    CBUUID *serviceUUID = [CBUUID UUIDWithString:SERVICE_UUID];
    [_centralManager scanForPeripheralsWithServices:@[serviceUUID] options:nil];
}

接続開始ボタンがおされると、scanForPeripheralsWithServices:options:で、アドバタイズをしているペリファレルを
探し始めます。第一引数に特定のサービスのUUIDを指定することで、そのUUIDに該当するサービスを提供するペリファレルのみを
取得出来ます。

//ペリファレルを発見するとコールされる
-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral
     advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
    NSLog(@"Peripheral discoverd :%@",advertisementData);
    //スキャンを停止し、ペリファレルに接続する
    _peripheral = peripheral;
    [_centralManager stopScan];
    [_centralManager connectPeripheral:peripheral options:nil];
}

ペリファレルが発見されると、centralManager:didDiscoverPeripheral:advertisementData:RSSI:がコールされます。
目的のペリファレルが発見されたので、これ以上のスキャンは必要無いので、まずスキャンを停止します。
そして、発見されたペリファレルにconnectPeripheral:options:で接続を試みます。

//ペリファレルに接続すると、コールされる
-(void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    NSLog(@"Connected to peripherel");
    //サービスを検索する    
    _peripheral.delegate = self;
    [_peripheral discoverServices:@[[CBUUID UUIDWithString:SERVICE_UUID]]];
}

ペリファレルへ接続すると、centralManager:didConnectPeripheral:がコールされます。
CBPeripheralのdelegateに自身を設定し、次にCBPeripheralのdiscoverServices:を使用して
ペリファレルが持つ、サービスをもう一度検索します。
scanする際にサービスを指定してscanしているので、冗長な処理に思えますが、CBService
のインスタンスを取得するためには、この作業が必要になります。

//ペリファレルのサービスを発見するとコールされる
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
    NSLog(@"Services discovered");
    for (CBService *service in peripheral.services) {
        NSLog(@"Service :%@",service);
        [_peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:CHARACTERISTIC_UUID]]
                                  forService:service];
    }
}

//サービスのキャラクタリスティックを発見するとコールされる
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
    for (CBCharacteristic *characteristic in service.characteristics) {
        NSLog(@"Discovered characteristic:%@",characteristic.UUID);
        [_peripheral readValueForCharacteristic:characteristic];
    }
}

サービスが発見されるとperipheral:didDiscoverServices:がコールされますので、今度は
CBPeripheralのdiscoverCharacteristics:forService:でキャラクタリスティックの検索を開始します。

キャラクタリスティックが発見されると、peripheral:didDiscoverCharacteristicsForService:error:が
コールされますので、そこでキャラクタリスティックから値の取得を行います。

//キャラクタリスティックの値を取得するとコールされる
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    NSData *data = characteristic.value;
    NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"data = %@",result);
    _resultLabel.text = result;
    
    [_centralManager cancelPeripheralConnection:_peripheral];
}

キャラクタリスティックの値が取得されると、peripheral:didUpdateValueForCharacteristic:error:がコールされますので、
そこで画面のラベルに結果の値を設定しています。


iOS7で「iBeacon」が発表されたことで、ますます注目が集まっているBluetooth Low Energyですが、
iOSでは、iOS5から「Core Blutooth Framework」が提供され、BLE機器と接続するアプリを開発することが
出来るようになっています。
また、iOS6からはiOSのデバイスをBluetooth対応機器として、動作させるためのAPIも提供されています。
そこで本日は、iOSのデバイスをBluetooth対応機器として、動作させるサンプルコードを紹介します。

ペリファレルとセントラルについて


コードの紹介に入る前にBLEの基本について少しお話します。BLEは、クライアント-サーバ型の形式になっていて、
主にデータを発信するサーバの役割を担う端末をペリファレル(Peripherel)、ペリファレルからデータを提供される側を
セントラル(Central)と言います。

iPhoneアプリはは体温計などのペリファレルからデータを受信するセントラルとして機能するケースが多いと
思いますが、今回はデータを提供する側のペリファレルとして動作するアプリを開発してみます。

サービスとキャラクタリスティック


ペリファレルは、セントラルに対して、サービスと呼ばれる一連のデータと機能をまとめたものを提供します。
例えば、心拍系であれば心拍を計測し、そのデータを提供するサービスを提供します。一つのペリファレルが、複数のサービスを提供することも可能です。

サービスはキャラクタリスティックと呼ばれるサービスの詳細情報を持つ属性を持っています。例えば心拍系の場合、心拍や、心拍系の場所などを
キャラクタリスティックとして、持つことになると思います。

つまり、ペリファレルは、ペリファレル→サービス→キャラクタリスティックという階層構造を持っていることになります。

Core Bluetoothを利用したペリファレルの実装


最初に、「Link Binary with Libraries」で「CoreBluetooth.framework」を追加します。
そして、ペリファレルの機能を提供するクラスのヘッダで、CoreBluetooth/CoreBluetooth.hをインポートします。

今回は、ViewControllerに必要な機能を実装してみます。

#define SERVICE_UUID @"9EA08FC0-FFCF-46D1-ABE3-A1A726699681"
#define CHARACTERISTIC_UUID @"1E222259-9612-4139-B3CB-19BA1DFB9C0E"

@implementation ViewController
{
    CBPeripheralManager *_peripheralManager;
    CBMutableService *_service;
    CBCharacteristic *_characteristic;
}

-(id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if(self){
        _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
        
        //サービスとキャラクタリスティックを構築する
        CBUUID *characteristicUUID = [CBUUID UUIDWithString:CHARACTERISTIC_UUID];
        _characteristic = [[CBMutableCharacteristic alloc] initWithType:characteristicUUID
                                                                 properties:CBCharacteristicPropertyRead
                                                                 value:nil permissions:CBAttributePermissionsReadable];
        CBUUID *serviceUUID = [CBUUID UUIDWithString:SERVICE_UUID];
        _service = [[CBMutableService alloc] initWithType:serviceUUID primary:YES];
        _service.characteristics = @[_characteristic];
    }
    return self;
}

//Bluetoothの状態が変更されるとコールされる
-(void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
{
    NSLog(@"State Updated");
    
    if(peripheral.state == CBPeripheralManagerStatePoweredOn){
        NSLog(@"ready");        

        [_peripheralManager addService:_service];
    }
}

//サービスがペリファレルに追加されるとコールされる。
-(void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error
{
    if(error) {
        NSLog(@"Error adding Service:%@",[error localizedDescription]);
    }
    else{
        NSLog(@"Adding Service Successful");
    }
}

ペリファレルを実装するにあたり、中心的な役割を担うのが、CBPeripheralManagerです。initでこのクラスのインスタンスを作成しています。
ペリファレルに対して発生する様々なイベントを処理するCBPeripheralManagerDelegateプロトコルをViewControllerに採用して、
initのdelegateに指定しています。

続いて、サービスとキャラクタリスティックを構築します。サービスとキャラクタリスティックにはそれぞれUUIDを持たせる必要があります。
MACのターミナルでuuidgenを実行し、それを割り当てています。キャラクタリスティックのvalueにはnilを指定していますが、
もし、値を指定した場合、それがキャッシュされ、自動的にその値をセントラルに返すような挙動をします。
状況に応じて動的に値を返したい場合、必ずnilを指定しておく必要があります。

CBPeripheralManagerのインスタンスを生成すると、Bluetoothが利用可能な状態になり、 peripheralManagerDidUpdateState:が
コールされます。もし、端末がBLEをサポートしていないケースなどを考慮し、ここで、状態をチェックしてから、後続の処理を
継続する必要があります。
ここでは、Bluetoothが利用可能な状態になっていることを確認し、サービスをCBPeripheralManagerに追加しています。

サービスとキャラクタリスティックが準備できたところで、次に「アドバタイズ」を開始します。
アドバタイズとは、セントラルに対して、ペリファレルの存在と公開しているサービスの情報を送信する行為のことを言います。

このサンプルアプリでは、2つのボタンを設置して、アドバタイズの開始と終了をボタンが押されたタイミングで
行うようにしました。



-(IBAction)startAdvertisingTouched:(id)sender
{  
    //アドバタイズを開始する
    [_peripheralManager startAdvertising:@{CBAdvertisementDataServiceUUIDsKey:@[_service.UUID],
         CBAdvertisementDataLocalNameKey:@"HelloPeripheral"}];
}

-(IBAction)stopAdvertisingTouched:(id)sender
{
    //アドバタイズを停止する
    [_peripheralManager stopAdvertising]; 
}

//アドバタイズが開始されるとコールされる
-(void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error
{
    if(error){
        NSLog(@"Error Advertising:%@",[error localizedDescription]);
    }
    else{
        NSLog(@"Starting Advertising Successful");
    }
}

そして、最後にセントラルからの接続と要求を受けて、結果をセントラルに返す処理です。
セントラルから要求を受けると、peripheralManager:didReceiveReadRequest:がコールされます。

-(void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request
{
    NSLog(@"Request received: %@ request = %@",request.characteristic.UUID.description,request.central.UUID);
    //キャラクタリスティックのUUIDが一致する場合
    if([request.characteristic.UUID isEqual:_characteristic.UUID]) {
        request.value = [@"Hello!" dataUsingEncoding:NSUTF8StringEncoding];
        [_peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
        return;
    }
    
    [_peripheralManager respondToRequest:request withResult:CBATTErrorAttributeNotFound];
}

今回は単純に「Hello!」という文字列をセントラルに返すだけの処理になっています。
要求されたキャラクタリスティックが自身が提供するものと一致していることを確認してから処理を行っています。
前述しましたがCBCharacteristicのインスタンスを生成する際、valueを指定しているとこのメソッドは
コールされず、自動的にそこで指定した値が返されることになります。

peripheralManager:didReceiveReadRequest:がコールされると必ずCBPeripheralManagerの
respondToRequest:withResult:をコールして、応答をセントラルに返す必要があります。

今回はセントラルに対してキャラクタリスティックの値を返却するサンプルでしたが、逆にセントラルからキャラクタリスティックの値を
更新する要求を処理することも可能です。また、セントラルの要求に応じて、継続的にキャラクタリスティックの値の変化をセントラルに
通知する処理を書くことも可能です。


UITableViewはUIScrollViewを継承しているので、UITableViewのdelegateにUIScrollViewDelegateプロトコルを
採用したクラスを設定しておけば、そのクラスで、スクロールの開始、終了イベントの通知を受けることが出来ます。
(おそらく、UITableViewがUIScrollViewのdelegateをオーバーライドしているからだと思います。)

スクロールの開始時は、(厳密にはドラッグの開始)「scrollViewWillBeginDragging:」がコールされます。
その後、「scrollViewDidScroll:」がスクロールしている間、何回もコールされます。
ですので、スクロール開始時に何か処理をするとしたら、「scrollViewWillBeginDragging:」が適しています。

その後、ドラッグが終わると、「scrollViewDidEndDragging:willDecelerate:」がコールされ、その後、
しばらくスクロールが続いた後(スクロールは急停止せず、徐々にとまります)、完全停止した後に「scrollViewDidEndDecelerating:」がコールされます。
スクロール終了時の処理は、場合に応じてこの2つから選択すればよいと思います。

今回、これを利用した以下のようなサンプルアプリを作ってみました。



ViewControllerのViewにUITableViewとキャラクター画像のUIImageViewを貼付け、スクロールが開始すると、キャラクター画像を
アニメーションで徐々に透過し、スクロールが終了すると、今度はアニメーションで徐々に表示されるようにしました。

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController
{
    NSArray *_fruitsArray;
    IBOutlet UITableView *_tableView;
    IBOutlet UIImageView *_charactor;
}

-(id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if(self){
        _fruitsArray = @[@"apple",@"banana",@"grape",@"melon",@"strawberry",@"persimmon",
                         @"kiwi fruit",@"cherry",@"plum",@"pear",@"pineapple",@"loquat"];
    }
    
    return self;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [_fruitsArray count];
}

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *identifer = @"cell";
    
    UITableViewCell *cell = [_tableView dequeueReusableCellWithIdentifier:identifer];
    if(!cell){
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifer];
    }
    
    cell.textLabel.text = _fruitsArray[indexPath.row];
    return cell;
}

-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    [UIView animateWithDuration:0.5f animations:^{
        _charactor.alpha = 0.0f;
    }];
}

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{   
    [UIView animateWithDuration:0.5f animations:^{
        _charactor.alpha = 1.0f;
    }];
}


UITableViewのdelegateの設定は、storyboardで行っています。

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。