まいまいワークス

主にiPhoneアプリの開発で考えた事、調べた事、感じた事などを記していきます。

Twitter/Facebookのアカウントで認証して、アプリからタイムライン/フィードに投稿する[#1]

アプリの機能としてはポピュラーなもので、需要はあるはずなのになぜか日本語の解説記事やブログネタとしては、ほとんど目につかないこの課題。
実装する機会があったのでいろいろ調べて、なんとかログインから投稿までたどり着けたので手順をここに記していきたいと思います。

前提

  1. Twitter/Facebookのアカウントを持っている
  2. AFNetworkingを利用する
  3. MBaaSのひとつであるParseを利用する

目次

  1. イントロダクション
  2. Twitterのアプリ登録
  3. Facebookのアプリ登録
  4. Parseの登録と設定
  5. Twitterアカウントでの認証
  6. Twitterのタイムライン投稿
  7. Facebookアカウントでの認証
  8. Facebookのフィード投稿  

投稿以外の部分はクラスメソッド株式会社のDevelopers.IOでも詳細に解説されております。
次回以降、順を追って説明して行きます。

NSDateの罠

自社サービスでも複数のサービスで数回NSDateの罠にはまっているので、あらためて要点をまとめたいと思います。

NSDateが登場する場面

・端末から現在の時刻を取得
・UIDatePickerから任意の時刻を取得
・サーバーからyyyy-MM-dd HH:mm:ssなどの形式で取得

過去に問題が起こったケース

1.日付の書式が和暦など西暦以外に設定されている
2.24時間表示をOFFにしてAM/PM表記になっている
3.NSDateが標準時(GMT)を返すことに気づかず9時間の時差を生んでしまう

1.のケースは例えば、西暦で取得する前提で設計しているにも関わらず平成の数字が返ってくるので西暦26年3月12日といった扱いになってしまうケース

2.のケースはyyyy-MM-dd HH:mm:ssで値を取得するところに、2014-03-12 午前03:34:56 +0000というフォーマットで値が返ってくるので値が取れなくなるケース

3.は設定した時刻にコンテンツが表示されず、9時間後に表示されるといったケース

どんな環境でも同じフォーマットで値を取得したい

サーバーから値を取得して現在の時刻と比較するケース

    //例えば、この値をサーバーから取得したとする
    NSString* timeStr = @"2014-03-12 12:34:56";
    
    //フォーマットの定義
    NSDateFormatter* format = [NSDateFormatter new];
    [format setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    [format setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];

    //フォーマットの適用
    NSDate* date = [format dateFromString:timeStr];
    
    //NSDateは標準時間&端末に設定されたフォーマットで取得
    NSLog(@"GMT=%@",date);
    

    //ローカル時間に補正してNSString出力
    //(サーバーから取得した値と同じなので実用上は意味がありませんが、フォーマット適用したNSDateからNSstringへの変換の確認)
    NSString* str = [format stringFromDate:date];
    NSLog(@"localTime=%@",str);

    
    //現在時刻からの差を計算
    //マイナスだと過去の日付、プラスで未来
    NSTimeInterval interval = [date timeIntervalSinceNow];
    NSLog(@"interval=%f",interval);

    //これで端末の状態に関わらずtimeIntervalが取得できるはず

端末でNSDateを取得するケース

上記とほとんど一緒です。

  
    //UIDatePickerで値を取得
    //picker部分の記述は割愛
    NSDateFormatter* format = [NSDateFormatter new];
    [format setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    [format setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];

    NSString* dateStr = [format stringFromDate:[picker date]];
    NSLog(@"dateStr=%@",dateStr);

 

その他のケース 

これで、ほとんど問題なく時刻が扱えると思うのですが、世界中で一斉にキャンペーンを行いたいとか、時差が出ると困る場合は標準時でかつ端末のフォーマットに左右されずに値を取得したい場合

    NSDate* date = [NSDate date];

    //標準時との秒差を取得
    float diff = [[NSTimeZone localTimeZone] secondsFromGMT];
    NSLog(@"sec=%f",diff);
    
    
    //統一フォーマットで標準時を取得したい(キャンペーンの開始など...)
    NSDateFormatter* format = [NSDateFormatter new];
    [format setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    [format setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
    
    NSString* NSdateStr = [format stringFromDate:[NSDate dateWithTimeIntervalSinceNow:-diff]];
    NSLog(@"date=%@",NSdateStr);

結論

これが重要!!!
[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]

3番目はもうちょっとスマートなやり方ないかなー

メンテナンスを考慮した設計

設計時は画面単位ではなく、機能単位で考えればいい設計が出来るかな。
そのためには開発着手以降は仕様を変えないでほしいですね。
って言っても、そんな事は現実的ではないのでとりあえず、画面表示に関わるモノ以外はすべてviewControllerから切り離した方がいいかもしれませんね。
画面単位の方が直感的で設計しやすいというメリットはありますが。

Xcodeを使い倒して効率化

タイトル若干釣り気味ですw
objective-Cの構文って長いものが多かったり、普段よく使うものでもプロパティの種類がたくさんあったりで、完全に覚えているものはほとんどありません。

で、これまでどうやってきたかと言うと、過去のソースからコピペしたり、evernoteやQiitaの限定共有に書き溜めたり、その都度ググったりやってきたわけですが、Xcodeコードスニペットを追加できると知って若干興奮しています!

これだと、Xcode内でドラッグ&ドロップやショートカットで呼び出せて、かつデフォルトのコード補完のようにtabで引数のところにジャンプしたりできちゃう訳なんです!

使い方

登録

snippet.jpg

基本はコード書いて、右下のスニペットのところにドラッグ。 このとき、引数の部分を<#title#>と書くと、デフォルトの入力補完でおなじみのtabでジャンプするやつが出てきます。 この例だとUIImage* image = [UIImage imageNamed:@"<#imageName#>"]; こんな感じにしています。

呼び出し

snippet_input.png

呼び出す時は右下からドラッグ&ドロップでも構わないのですが、こんな感じでショートカットで呼び出しができてしまいます。

便利ですね♪

UITableView・UICollectionView攻略

iOSアプリの要となるtableviewとcollectionviewですが、設計の要点をおさえておかないとちょっとした事でスクロール時のパフォーマンスが落ちてしまい、アプリそのものの完成度の印象も格段に落ちてしまいます。 特にcollectionviewは1行に複数のアイテムが表示されるのでパフォーマンスがシビアになりがちです。

逆に、少しの心がけでスクロールにヌルヌル感が出てアプリのメジャー感も出るところなので設計には注意したいものです。
試行錯誤の末、かなりパフォーマンス改善が実感できるところまで来たので、
認識違いの点もあるかと思いますが留意点を列挙したいと思います。

   

注意すべき項目

  1. サーバーとの通信は非同期で行う
  2. collectionviewのcellForItemAtIndexPath、tableviewのcellForRowAtIndexPathの処理を高速にする
  3. layerによるシャドウ、角丸の効果は使わない
  4. 表示するUIImageViewのframeサイズとUIImageの画像サイズを合わせておく
  5. 透過画像、透過ビューを使わない

 

1.サーバーとの通信は非同期で行う

これは問題ないかと。 たまに初心者がハマる程度でしょうか。

 

2.collectionviewのcellForItemAtIndexPath、tableviewのcellForRowAtIndexPathの処理を高速にする

これらの処理はメインスレッドで行われるためここで時間がかかると処理がロックされてしまいます。
個人的な印象では0.01秒を越えると重たくなる印象があります。
複雑な処理はここでは行わずcellに値を渡すだけにするのが良さそうです。

//実験方法
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    
    NSLog(@"start");
    
    static NSString *cellID = @"testCell";
    _cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellID forIndexPath:indexPath];
    
【中略】
    //ここの処理が0.01秒を越えるとカクカク感が出てくる
    [NSThread sleepForTimeInterval:0.005];
【中略】    
    
    NSLog(@"end");

    return _cell;
}

 

3.layerによるシャドウ、角丸の効果は使わない

self.layer.cornerRadius = 10.f;
こんなやつです。
角丸もシャドウも数行のソース追加で見栄えのする効果が得られるのでついつい使いたくなりますが、(iPhone5sでも)目に見えて格段にパフォーマンスが落ちます。
別スレッドで画像そのものを加工するのが良さそうです

//UIImageを加工するクラスを別スレッドで叩く
-(void)round:(UIImage*)image{
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        __block UIImage *roundedImage = image;
        
        //ここでUIImageを加工するクラスを叩く
        roundedImage = [maskImageGenerate generateRoundedImage:image radius:10.f];
        
        dispatch_async(dispatch_get_main_queue(), ^{
            //結果が返ってきたらmain_queueで値を返す(UIKitの決まり事)
            
            self.image = roundedImage;
            
        });
    });

}

 

4.表示するUIImageViewのframeサイズとUIImageの画像サイズを合わせておく

意外と盲点だったのがこれ。
例えば、サーバーから160x160pxで画像を取得して
pict.frame = CGRectMake(0, 0, 64, 64);

などとやってしまうとスクロールが重たくなってしまいます。
表示サイズの画像を取得するか(これがベスト)角丸処理と同様に別スレッドでUIImageの加工を行います。

    CGSize size = CGSizeMake(64.f, 64.f);
    UIGraphicsBeginImageContextWithOptions(size, NO, 2.0);
    [image drawInRect:CGRectMake(0, 0, 64.f, 64.f)];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

 

5.透過画像、透過ビューを使わない

一般論として透過画像は重いとよく言われていますが、今回試した中では一番差異を感じませんでした。
とは言え、多少なりともパフォーマンスに影響があると思われるので、背景色をベースカラーと合わせて透過画像を使わないようにしたり、drawRectで直接描画すればパフォーマンスが上がるようです。

 

大いに参考になったサイト

なめらかに動作するUITableViewのつくりかた
基本的なチューニングはこのサイトを参考にしました

[iOS] TableView スクロールパフォーマンスの改善
よりパフォーマンスを上げるためにはこちら

UICollectionViewCellをプログラム側から選択状態にする

[self.collectionview reloadData];

上記の命令でUICollectionViewをリロードした場合、 すでに選択されていたセルの選択状態が解除されてしまいます。

そこで、あらかじめ選択・未選択の属性を配列などに保存しておき、

-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{

【中略】

    cell.selected = YES;

【中略】
}

と指定してしまうと その後、当該セルをタップしても反応しなくなるという問題が出てきます。

その場合は、以下のようにするとセルが反応してくれます。

cell.selected = YES;
[self.collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];

   

【参考】 http://stackoverflow.com/a/15414208

アプリ企画の打ち合わせをしていて、ふと感じた事

情報設計やUIの仕様決めをする際、
いろいろな人からいろいろな意見が出てきて話がまとまらない事がよくあるのですが
(なので、あまり大人数ではやりたくない)
自分の意見を推すために、他社の既存アプリを引き合いに出して正当性を主張する人がいます。

引き合いに出すアプリのUIが、いま企画しているものと完全に同じ目的で設計されており
かつ、考え抜かれたものならまだしも、
他所がやってるから…というのは思考停止状態とあまり変わらないですね。

参考にする程度にとどめておいた方がいいですね!