objective-C

CallkitとTwilio対応でつながらないエラーや切れないエラーが出た場合の対処法

 Twilioのios10・CallKit対応してエラーが出る・・・

現在、自社アプリに電話機能を入れているのですが遅まきながらiOS10に対応するためCallKitを用いてみました。

サンプル通りだとおおよそ動くのですが、発信のUIにスピーカーやミュートなぞつけていると案外思った通りに動作しないことがあるためエラーの例と解決策を書きます。

CallKitのiOS10対応、あるいは受信 / 発信 / 応答あたりの挙動がたまにおかしくなるような悩みがある方が解決すれば幸いです。

エラー1:受信側でアプリ側のCallKitで応答を押しても発信側の発信が止まらない

アプリのプロセスを切って他のアプリをいじりながら応答するとなぜか発信側の発信音が止まらず、電話を応答したことにとになりません。

しかも、アプリのプロセスを切っているのでエラーもわかりづらい・・・。

ということで、TVOIncominCallDelegateのdidFailWithErrorIncomingCallメソッドのエラーをサーバーに飛ばすという方法でデバッグしたところ、下記のエラーが出ておりました。

Inside makeCall, failed to make Error:-560577449

このエラーでGoogleで検索すると下記のページが出てきますが、読むとどうやらTwilioの中の人か詳しい人はAVAudioSession周りで何かしてないか?というのをエラーが起きた人に質問しているのがわかります。

https://github.com/twilio/voice-quickstart-objc/issues/13

ということで、僕の方もコードを調べてAVAudioSessionをどこで使ってるかな?と調べ、routeAudioToSpeakerというメソッドがサンプルにありこちらをコメントアウトすることで起こらなくなりました。

もし、同じような現象が出ている方がいましたら、AVAudioSession系のコードを実行していないかどうか確かめられると良いかと。

エラー2:30%くらいの確率で、CXEndCallActionが失敗する

CXErrorCodeRequestTransactionErrorUnknownCallUUID = 4のエラーが出力するのもAVAudioSession周りが原因でした。

この場合に現象は2パターンあります。

パターン1:Twilioで通話を切ってもCallKitのオーディオが止まらない

通話を切ってもカウントアップが終わらず「通話に戻る」がずっと出てきて大変宜しくない現象です。

この場合には緊急回避策として

 [self.callKitProvider reportCallWithUUID:alterUUID endedAtDate:callEndDate reason:reason];

というのが有効ですが、失敗した時に「接続できませんでした」というエラーが出て微妙です。(解決策はパターン2に書いてます)

パターン2:Twilioで通話を切ると「接続できませんでした」が出る

XCodeでデバッグ中によく出るエラー、あるいは [self.callKitProvider reportCallWithUUID:alterUUID endedAtDate:callEndDate reason:reason];などで無理やり切ったりすると出る現象です。

この現象を50回くらい観察して気付いたのですがCXEndCallActionが失敗するときって、アプリがもっさり開いてステータスバーの緑色の部分が微妙な動きをするときだったんですよね。

ステータスバーがアプリを開いていると出ないで、アプリを閉じると出るとか。

そのため、AudioSession周りが怪しいと睨んで全てのAVAudioSession周りのコードを実行しないようにしました。

例えば、下記のようなコードです。

[code]
 if (speakerEnabled) {

        if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord

                                              withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker

                                                    error:&error]) {

            NSLog(@"Unable to reroute audio: %@", [error localizedDescription]);

        }

    }else{

        if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord

                                              withOptions:AVAudioSessionCategoryOptionAllowBluetooth

                                                    error:&error]) {

            NSLog(@"Unable to reroute audio: %@", [error localizedDescription]);

        }

        [[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error];

        [[AVAudioSession sharedInstance] setActive:YES error:&error];

    }
[/code]

なんとなく、スピーカー周りを自分の思った通りにしたかったので、旧バージョンで実行していたのが問題だったようです。

ちなみに、普通にボタンを押した時にこのメソッドを実行しても問題は起こることはありませんでした。

下記のデリゲートメソッド近辺でAVAudioSessionを実行すると起こるようです。

・ (void)incomingCallDidConnect:(TVOIncomingCall *)incomingCall 

・(void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action

TwilioのCallKit対応に起こったエラーに関するまとめ

以上のように、初期化サイクル付近でAVAudioSessionに関していじるとTwilioSDKかCallKitの動作とバッティングして予期せぬ動作が起こったみたいでした。

同様のエラーが出ている方の助けになれば幸いです。

iOS5.x系と6.x系を両方対応させるコツ

iOS5.x系でデバッグできない、起動しない、表示が崩れる・・・など

最新のXcodeを利用して組むと、いつのまにか5.x系では動かない狭いアプリになってしまいます。5.x系に調整する方法を以下に記します。

・iOS5.x系のシミュレータをインストールする

Xcode->Preferences ->Downloadsタブを開くと、過去のバージョンのSDK等の一覧が表示されますので、必要なものをダウンロードする。

・storyboardのuse auto layoutを使うとiOS5.xでは動作しない

storyboardをクリックし、File Inspectorから use auto layoutのチェックボックスを外す。

・auto layoutで一度組んだUIは、auto layoutを外すと崩れる場合がある

崩れてStoryboard上の配置を直せば問題無い場合もありますが、

Storyboard上の座標と表示座標がずれていることがあります。

その場合には、File Inspectorを開いて、右から2番目のView->auto sizingを調整することで修正できます。

・UITableView系で新しく追加されたメソッドはresponseToSelectorで確認する

・特に、setRefreshControl:と、Xcodeが自動生成するUITableViewのtableView dequeueReusableCellWithIdentifier: forIndexPath:に注意。

iOSの証明書が切れたときの注意点

実機確認で証明書問題が起こったときの対処法

ちょうど1年間のライセンスが切れ、証明書の更新を行ったときにエラーが出力されて実機で確認できなくなってしまいました。購入しただけでは問題が解決しなかったため、ios dev centerとXcode 4.5で証明書をいろいろいじることで解決できました。

以下、僕が行った手順を記します。

・iOS devのチケットを購入
購入時点では有効期限切れが解消されないので、24時間以内に来るというメールを待つ。
・window -> organizer で証明書一覧を更新する
右下のRefreshボタンにて更新可能
・古い証明書を削除し、新しい証明書を指定
エラー:The executable was signed with invalid entitlementsが出たので、以下の手順にて対処。

・実機から古い証明書を削除
・window->organizer->Device->provisioning profileのところに古い証明書があるので削除
・window -> organizer で古い証明書を削除する
※利用する証明書は削除しないこと
・左側のプロジェクトナビゲータから、ターゲットを選択
・ターゲット選択後、build settingタブを開き、code signingの箇所に新しい証明書に変更
・クリーンする
・実機で起動する

ローカルファイルがUIWEBViewで読み込まれない場合の対処法 拡張子編

UIWEBViewは拡張子が.comだと読み込まれない

前提

様々なページをWEBから取得する場合に拡張子を得ておきたいと思うのが常だと思います。
拡張子を取るための一般的な解決策は以下のコード。

[objc]
NSURL *url = [NSURL URLWithString:@"http://b.hatena.ne.jp/entry/lab.xxxxx.com/?p=123"];
NSString *extension = [[url path] pathExtension];
[/objc]

しかし、このようなコードでは拡張子が.comになってしまいます。
これをそのままUIWebViewに以下のように読み込ませるコードは以下。
以下のコードでは、リクエストに失敗してエラーが出力されます。
(エラーを取得するデリゲートメソッド:webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error)

[objc]
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[webView setDelegate:self];
NSString *bodyPath = @"/テキトーパス/test.com";
NSFileManager *fm = [NSFileManager defaultManager];
GHAssertTrue([fm fileExistsAtPath:bodyPath],@"body path is not exited");
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:bodyPath]];
[/objc]

解決策

拡張子を.htmlに書き換えれば何も問題なくロードされます。
もし、GHTestUnitを利用している方には、以下のコードをコピってみると、ちょこっと面白いです。

■エラー内容

{NSErrorFailingURLKey=file:///*********/test.com, NSErrorFailingURLStringKey=file:///Users/pig100pork/Desktop/tmp/loadRequest/test.com, NSLocalizedDescription=Frame load interrupted}
2012-07-23 13:20:56.619 GablielTest[7624:15e03] *** WebKit discarded an uncaught exception in the webView:didFailProvisionalLoadWithError:forFrame: delegate: ‘((error) == nil)’ should be FALSE. err not nil. err:Error Domain=WebKitErrorDomain Code=102 “Frame load interrupted” UserInfo=0x99e0140 {NSErrorFailingURLKey=file:///*******/test.com, NSErrorFailingURLStringKey=file:///*****loadRequest/test.com,

■bad

[objc]

-(void)test1_loadRequestTest{

UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[webView setDelegate:self];
NSString *bodyPath = @"/テキトーパス/test.com";
NSFileManager *fm = [NSFileManager defaultManager];
GHAssertTrue([fm fileExistsAtPath:bodyPath],@"body path is not exited");
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:bodyPath]];
GHAssertTrue([[req URL] isFileURL],@"url is not fileUrl");

[self prepare];

[webView loadRequest:req];
[self waitForStatus:kGHUnitWaitStatusSuccess timeout:60];

}

-(void)webViewDidStartLoad:(UIWebView *)webView{

NSLog(@"start");
GHAssertNotNil([[webView request] URL],@"webView req is nil");
}

-(void)webViewDidFinishLoad:(UIWebView *)webView{

NSLog(@"finish");
GHAssertNotNil([webView request],@"webView req is nil");
[self notify:kGHUnitWaitStatusSuccess forSelector:@selector(test1_loadRequestTest)];
}

-(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{

NSLog(@"failure");
NSLog(@"error is %@",[error description]);
GHAssertNil(error,@"err not nil. err:%@",[error description]);
[self notify:kGHUnitWaitStatusFailure];
}
[/objc]

■Good

[objc]
-(void)test1_loadRequestTest{

UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[webView setDelegate:self];
NSString *bodyPath = @"/テキトーパス/test.html";
NSFileManager *fm = [NSFileManager defaultManager];
GHAssertTrue([fm fileExistsAtPath:bodyPath],@"body path is not exited");
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:bodyPath]];
GHAssertTrue([[req URL] isFileURL],@"url is not fileUrl");

[self prepare];

[webView loadRequest:req];
[self waitForStatus:kGHUnitWaitStatusSuccess timeout:60];

}

-(void)webViewDidStartLoad:(UIWebView *)webView{

NSLog(@"start");
GHAssertNotNil([[webView request] URL],@"webView req is nil");
}

-(void)webViewDidFinishLoad:(UIWebView *)webView{

NSLog(@"finish");
GHAssertNotNil([webView request],@"webView req is nil");
[self notify:kGHUnitWaitStatusSuccess forSelector:@selector(test1_loadRequestTest)];
}

-(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{

NSLog(@"failure");
NSLog(@"error is %@",[error description]);
GHAssertNil(error,@"err not nil. err:%@",[error description]);
[self notify:kGHUnitWaitStatusFailure];
}

[/objc]

Objective-C CGColorRef CGColorSpaceCreateDeviceRGB()が見つからないときの対処法

Objective-C CGColorRef CGColorSpaceCreateDeviceRGB()と言うコードをよく見ますが、iosではdeprecatedと言われます。
その場合は、以下のように書く事で解決されます。
前回のUIWebViewでスクリーンショットを取るで紹介したコードの中で、
UIWEBViewのピクセル色を取得するために使っています。

■Good for ios

[objc]
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(CGColorSpaceCreateDeviceRGB());
[/objc]

■Bad for ios

[objc]
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
[/objc]

iOSでUIWebViewのスクリーンショットを取る方法

■問題

iOSでisLoadingが常にYESを返す場合があり、完全にロードしたかどうかを判別できない場合があります。

■解決策

webViewDidFinishLoadが呼ばれた時点で、スクリーンショットを取り、白・黒の割合が90%未満だった場合にはロードされていると認識する。
※この方法では完全にとれない場合もあります。
※100%ロードしたものを表示したい場合には、timer等で以下の関数を処理を実行させて下さい。

■予備知識

UIViewからサムネイルを取得する方法
http://araking56.blog134.fc2.com/blog-entry-184.html

画像のピクセルから色を判別する方法
http://www.markj.net/iphone-uiimage-pixel-color/

■good

[objc]
– (void)webViewDidFinishLoad:(UIWebView *)webView {

BOOL isLoaded = [self isLoaded:webView];
if(isLoaded){
[self.imageView setHidden:NO];
}
}

-(BOOL)isLoaded:(UIWebView *)webView{

//画像を送る
CGRect screenRect = webView.frame;
UIGraphicsBeginImageContext(webView.frame.size);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextFillRect(ctx, screenRect);
[webView.layer renderInContext:ctx];

UIImage *screenImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

//画像の内容を解析する
float percentage = [self getPercentageOfWhiteInImage:screenImage];

if (percentage > 0.9) {

NSLog(@"ほぼ真っ白 or 真っ黒なのでロードされていない");

}else{

NSLog(@"色がついているのでロード済み");

}
}

-(float)getPercentageOfWhiteInImage:(UIImage *)image{

CGImageRef inImage = image.CGImage;
CGContextRef cgctx = [self createARGBBitmapContextFromImage:inImage];
if (cgctx == NULL) { return 1.0; /* error */ }

size_t w = CGImageGetWidth(inImage);
size_t h = CGImageGetHeight(inImage);
CGRect rect = {{0,0},{w,h}};
CGContextDrawImage(cgctx, rect, inImage);

int startW = 0;
int startH = 0;
unsigned char* data = CGBitmapContextGetData (cgctx);

float result1 = [self getPercentageOfWhiteInPix:startW startH:startH data:data w:w h:h];
float result2 = [self getPercentageOfWhiteInPix:w/2-_estimateLengthstartH:h/2-_estimateLengthdata:data w:w h:h];
float result3 = [self getPercentageOfWhiteInPix:w-_estimateLengthstartH:h-_estimateLengthdata:data w:w h:h];

// When finished, release the context
CGContextRelease(cgctx);
// Free image data memory for the context
if (data) { free(data); }

return (result1+result2+result3)/(float)3;

}

-(float)getPercentageOfWhiteInPix:(int)startW startH:(int)startH data:(unsigned char *)data w:(size_t)w h:(size_t)h{

// Now we can get a pointer to the image data associated with the bitmap
// context.
int whiteCounter =0;
int blackCounter = 0;
int notWhiteCounter = 0;
//unsigned char* data = CGBitmapContextGetData (cgctx);
for (int width =0; width < _estimateLength ; width++) {

for (int height=0;height < _estimateLength; height++) {

if (data != NULL) {

//offset locates the pixel in the data from x,y.
//4 for 4 bytes of data per pixel, w is width of one row of data.
int offset = 4*((w*round(startH+height))+round(startW+width));
int alpha = data[offset];
int red = data[offset+1];
int green = data[offset+2];
int blue = data[offset+3];

if (red == 255 && green == 255 && blue == 255) {
whiteCounter++;
}else if(red ==0 && green==0 && blue==0){
blackCounter++;
}else {
notWhiteCounter++;
}
NSLog(@"offset: %i colors: RGB A %i %i %i %i",offset,red,green,blue,alpha);
//color = [UIColor colorWithRed:(red/255.0f) green:(green/255.0f) blue:(blue/255.0f) alpha:(alpha/255.0f)];

}

}

}

//大きい方を返す

if (whiteCounter > blackCounter) {

return (float)whiteCounter/(float)(notWhiteCounter+whiteCounter+blackCounter);

}else {

return (float)blackCounter/(float)(notWhiteCounter+whiteCounter+blackCounter);

}

}

– (CGContextRef) createARGBBitmapContextFromImage:(CGImageRef) inImage {

CGContextRef context = NULL;
CGColorSpaceRef colorSpace;
void * bitmapData;
int bitmapByteCount;
int bitmapBytesPerRow;

size_t pixelsWide = CGImageGetWidth(inImage);
size_t pixelsHigh = CGImageGetHeight(inImage);

bitmapBytesPerRow = (pixelsWide * 4);
bitmapByteCount = (bitmapBytesPerRow * pixelsHigh);

colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL)
{
fprintf(stderr, "Error allocating color space\n");
return NULL;
}

bitmapData = malloc( bitmapByteCount );
if (bitmapData == NULL)
{
fprintf (stderr, "Memory not allocated!");
CGColorSpaceRelease( colorSpace );
return NULL;
}

context = CGBitmapContextCreate (bitmapData,
pixelsWide,
pixelsHigh,
8, // bits per component
bitmapBytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedFirst);
if (context == NULL)
{
free (bitmapData);
fprintf (stderr, "Context not created!");
}

// Make sure and release colorspace before returning
CGColorSpaceRelease( colorSpace );

return context;
}
[/objc]

Error: parser error, 18 of 135 bytes parsed (node.js Objective-C)の対処方法

Error: parser error, 18 of 135 bytes parsed という、expressのエラーが出た場合の対処法です。(Postのみ)
原因は、BodyParserの仕様にクライアントがそっていない可能性があります。

■解決策

プロパティを追加する際には、form-data/nameの後のコロンやダブルクオーテーションのチェック、改行コードに\r\nを必ず使う。

[objc]
//リクエスト名の追加
[body appendData:[[NSString stringWithFormat:@”Content-Disposition: form-data; name=\”%@\” \r\n”,keyName] dataUsingEncoding:NSUTF8StringEncoding]];
[/objc]

■確認方法

node.js 側のexpressのBodyParserを外し、req.bodyを文字列に変換して確認することができます。

■Good

[objc]
//結果用のデータを作成する。
NSMutableData *body = [NSMutableData data];
NSString *boundary = @”—–123456—–“;
//boundaryの追加
[body appendData:[[NSString stringWithFormat:@”–%@\r\n”, boundary] dataUsingEncoding:NSUTF8StringEncoding]];
//リクエスト名の追加
[body appendData:[[NSString stringWithFormat:@”Content-Disposition: form-data; name=\”%@\” \r\n”,keyName] dataUsingEncoding:NSUTF8StringEncoding]];
//改行
[body appendData:[@”\r\n” dataUsingEncoding:NSUTF8StringEncoding]];
//データの追加
[body appendData:bodyData];
//改行
[body appendData:[@”\r\n” dataUsingEncoding:NSUTF8StringEncoding]];
//閉じる
[body appendData:[[NSString stringWithFormat:@”–%@–\r\n”, boudary] dataUsingEncoding:NSUTF8StringEncoding]];

[/objc]

■Bad

改行コードに\nを使ってしまう。

[objc]

//結果用のデータを作成する。
NSMutableData *body = [NSMutableData data];
NSString *boundary = @”—–123456—–“;
//boundaryの追加
[body appendData:[[NSString stringWithFormat:@”–%@\r\n”, boundary] dataUsingEncoding:NSUTF8StringEncoding]];
//リクエスト名の追加
[body appendData:[[NSString stringWithFormat:@”Content-Disposition: form-data; name=\”%@\” \n”,keyName] dataUsingEncoding:NSUTF8StringEncoding]];
//改行
[body appendData:[@”\n” dataUsingEncoding:NSUTF8StringEncoding]];
//データの追加
[body appendData:bodyData];
//改行
[body appendData:[@”\n” dataUsingEncoding:NSUTF8StringEncoding]];
//閉じる
[body appendData:[[NSString stringWithFormat:@”–%@–\r\n”, boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[/objc]

■bad2

nameにクオーテーションをつけない。

[objc]
//結果用のデータを作成する。
NSMutableData *body = [NSMutableData data];
NSString *boundary = @”—–123456—–“;
//boundaryの追加
[body appendData:[[NSString stringWithFormat:@”–%@\r\n”, boundary] dataUsingEncoding:NSUTF8StringEncoding]];
//リクエスト名の追加
[body appendData:[[NSString stringWithFormat:@”Content-Disposition: form-data; name=%@ \r\n”,keyName] dataUsingEncoding:NSUTF8StringEncoding]];
//改行
[body appendData:[@”\r\n” dataUsingEncoding:NSUTF8StringEncoding]];
//データの追加
[body appendData:bodyData];
//改行
[body appendData:[@”\r\n” dataUsingEncoding:NSUTF8StringEncoding]];
//閉じる
[body appendData:[[NSString stringWithFormat:@”–%@–\r\n”, boudary] dataUsingEncoding:NSUTF8StringEncoding]];
[/objc]

NSURL stringWithUrl relativeToUrl return nilの対処法

[NSURL stringWithUrl: relativeToUrl ] return nilの対処法を紹介します。
■解決策

相対パスにスペースが入っている場合、エスケープすると解決します。

[objc]
NSString *relativeString = [@”/logtopickeywords.cms?query=test matches” stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[/objc]

上記のコードのようにNSStringのstringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncodingを相対パスに追加して下さい。

■Good

[objc]
NSString *relativeString = [@”/logtopickeywords.cms?query=test matches” stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

NSLog(@”relativeString is %@”,relativeString);
NSString *originalPath = @”http://localhost/original/?original”;

NSURL *newUrl = [NSURL URLWithString:relativeString relativeToURL:[NSURL URLWithString:originalPath]];

NSLog(@”new URL is %@”,[newUrl absoluteString]);

STAssertTrue([[newUrl absoluteString] length] > 0,@”newUrl is %@”,[newUrl absoluteString]);
[/objc]

■Bad

[objc]
NSString *relativeString = @”/logtopickeywords.cms?query=test matches”;
NSString *originalPath = @”http://localhost/original/?original”;
NSURL *newUrl = [NSURLURLWithString:relativeString relativeToURL:[NSURLURLWithString:originalPath]];
STAssertTrue([[newUrl absoluteString] length] > 0,@”newUrl is %@”,[newUrl absoluteString]);
[/objc]