assaulter's diary

主にバイクについて

Rubyで大文字小文字を無視した検索

配列に対して、特定の文字列が含まれるかどうかを調べる際には、こんな感じにしますよね?

%w(test sample code).include?('test')

この場合、testに完全に一致しないとtrueを返してくれないので、大文字小文字を無視して検索したい場合にはこんな感じですかね。

%w(test sample code).find{|item| item =~ /^test$/i}.nil?.!

ちなみに、railsで使うんだったら、nil?.!の部分がpresent?でいいと思います。

日本で一番簡単にビットコインが買える取引所 coincheck bitcoin

Rubyで簡易ストラテジー

いい感じに慣れてきたので、Rubyに関するメモを残していこうと思います。

簡易ストラテジーの実装

異なるkeyを渡した場合に、一部のメソッドの動作を変更したい場合を想定。

class SearchClass
  module IdSearchRequest
    def self.request(value)
      # keyがIDの場合のリクエストを返却
    end
  end

  module NameSearchRequest
    def self.request(value)
      # keyがNAMEの場合のリクエストを返却
    end
  end

  MODULE_BY_KEY = {
    id: IdSearchRequest,
    name: NameSearchRequest
  }
  
  def search(key, value)
    request = MODULE_BY_KEY[key.to_sym]::request(value)
    # リクエストオブジェクトを使って検索を実行
  end
end

JavaとかだとEnumを使って実装するんでしょうが、Rubyで実装する場合はこんな感じですかね。

ちなみに、モジュールにインスタンス変数を持たせたい場合は、モジュールを取ってきてclass.eval等でincludeする方法も結構使ってます。

CoreDataを使ってみる

最近はRailsばかりいじってて、久しぶりにXCode開いたら色々忘れていたので、まずはCoreDataについてメモしていきます。

参考にしたのは下記の本です

iOS開発におけるパターンによるオートマティズム

iOS開発におけるパターンによるオートマティズム

今日のゴール

CoreDataを扱うクラスを作成してみる。

1.DataModelを作成する

newfile→Core Data→Data Modelから作成 シンプルにリレーションはなし。

f:id:assaulter:20131027141350p:plain

この状態でnewfile→NSManagedObjectSubClassを選択。すると設定したEntityと等価のクラスが作成される。

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>


@interface GuidePageEntity : NSManagedObject

@property (nonatomic, retain) NSString * identifyer;
@property (nonatomic, retain) NSString * imageUrl;
@property (nonatomic, retain) NSNumber * isLastPage;
@property (nonatomic, retain) NSNumber * pageNumber;

@end

2.CoreDataを扱うクラスを作成する

登場人物の紹介

ここで一旦CoreDataを使う場合に出てくるオブジェクト達を紹介します。

ManagedObjectModel

生成法の一例

// Returns the managed object model for the application.
// If the model doesn't already exist, it is created from the application's model.
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel != nil) {
        return _managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"GuidePageModel" withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}

newfile→Core Data→Data Modelで作成した名前を指定して作成しています。なので、あくまでデータモデルを定義しているだけ(という風に理解している・・・)

Persistent Store Cordinator

// Returns the persistent store coordinator for the application.
// If the coordinator doesn't already exist, it is created and the application's store added to it.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }
    
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"GuidePageModel.sqlite"];
    
    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    
    return _persistentStoreCoordinator;
}

データ永続化先を指定、データモデルを指定、で生成される。これら両者のブリッジを行っている・・・

Managed Object Context

_managedObjectContext = [[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];

return _managedObjectContext

基本的にRead/Writeな操作はこの人が提供するので、自分が管理するモデルとストア先を指定してあげる。

よって、これらの初期化諸々を行い、外部に必要なAPIを公開するようなクラスを作成すればいいと思われる。

AmazonS3とiOS間でファイルをやりとりする(その2)

前回 - AmazonS3とiOS間でファイルをやりとりする(その1) - assaulter's diary
の続きになります。僕はこういう感じで実装しました。

ダウンロード

仕様 : ユーザーにはDLの結果のみを見せる(今回はStringを返す)。データはよしなに扱う。

Clientを作成

メンバで保持。初期化時に鍵が必要。また、リージョンを設定しておく(やらない場合はデフォルトが入ったはず)。

- (id)init {
    if (self = [super init]) {
        _client = [[AmazonS3Client alloc] initWithAccessKey:ACCESS_KEY_ID withSecretKey:SECRET_KEY];
        _client.endpoint = [AmazonEndpoints s3Endpoint:AP_NORTHEAST_1];
    }
    return self;
}
ダウンロードapi
/// 公開する関数
- (void)downloadDataWithCompletion:(void(^)(NSString* message))resultBlock {
    [self getDataWithCompletion:^(S3GetObjectResponse *response) {
        /// response.bodyに格納されているので、よしなにやる
        if (response.bodyを使ってよしなにやった結果) {
            resultBlock(@"DL成功");
        } else {
            resultBlock(NSLocalizedString(@"保存失敗");
        }
    } errorBlock:^(NSError *error) {
        resultBlock(error.userInfo[@"NSLocalizedDescription"]);
    }];
}

/// ヘルパ関数
- (void)getDataWithCompletion:(void(^)(S3GetObjectResponse* response))responseBlock errorBlock:(void(^)(NSError* error))errorBlock {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        S3GetObjectRequest* getRequest = [[S3GetObjectRequest alloc] initWithKey:@"オブジェクトの名前" withBucket:@"バケット名"];
        getRequest.contentType = CONTENT_TYPE;
        
        S3GetObjectResponse* getResponse = [self.client getObject:getRequest];
        /// ここからmain threadで処理される(非同期)
        dispatch_async(dispatch_get_main_queue(), ^{
            if (getResponse.error) {
                errorBlock(getResponse.error);
            } else {
                responseBlock(getResponse);
            }
        });
    });
}

通信してレスポンスを取ってくるところの非同期処理はGCDを使いました。
参考 - 8.2 Grand Central Dispatch · mixi-inc/iOSTraining Wiki · GitHub
ネストはちょい深くなるけど、mainスレッドに非同期で返す部分をわざわざperformSelectorOnMainThreadでやると関数書かないといけないので、block返すだけとかそういう場合はこっちの方がスッキリするかな。

アップロード

あんま変わりませんね。clientは既に用意されている前提で、ユーザーには結果しか返しません。

/// 公開する関数
- (void)uploadDataWithCompletion:(void(^)(NSString* message))resultBlock {
    [self putDataWithResponseBlock:^(S3PutObjectResponse *response) {
        resultBlock(@"uploadに成功した");
    } errorBlock:^(NSError *error) {
        resultBlock(error.userInfo[@"NSLocalizedDescription"]);
    }];
}

/// ヘルパー
- (void)putDataWithResponseBlock:(void(^)(S3PutObjectResponse* response))responseBlock errorBlock:(void(^)(NSError* error))errorBlock {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        S3PutObjectRequest* putRequest = [[S3PutObjectRequest alloc] initWithKey:@"オブジェクト名" inBucket:@"バケット名"];
        putRequest.contentType = CONTENT_TYPE;
        putRequest.data = 適当なメソッドでデータを取ってくる;
        
        S3PutObjectResponse* putResponse = [self.client putObject:putRequest];
        dispatch_async(dispatch_get_main_queue(), ^{
            // ここからはmainThreadで実行される
            if (putResponse.error) {
                errorBlock(putResponse.error);
            } else {
                responseBlock(putResponse);
            }
        });
    });
}

iOSにてfullResoultionImageを扱う際の問題点

前回とは別の話題になります。

iOSでALAssetから画像を取得する際には、メソッドとして

・fullScreenImage

・fullResolutionImage

の2つがありますが、後者のfullResolutionImageを扱う際にハマりポイントがあったので書きます。

ハマりポイント???

iOS5から、iPhone標準のカメラアプリにおいて画像編集ができるようになりました。

普通にサムネとか取得する際には問題ないのですが、fullResolutionImageを取得する際には、この編集結果が適用されないという問題があります。

で、この編集情報が、ALAssetRepresentationのmetadata(辞書形式)の中に、AdjustmentXMPとして格納されています。

XMPの中身

<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 4.4.0">
   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about=""
            xmlns:aas="http://ns.apple.com/adjustment-settings/1.0/">
         <aas:AffineA>1</aas:AffineA>
         <aas:AffineB>0</aas:AffineB>
         <aas:AffineC>0</aas:AffineC>
         <aas:AffineD>1</aas:AffineD>
         <aas:AffineX>-331</aas:AffineX>
         <aas:AffineY>-161</aas:AffineY>
         <aas:CropX>0</aas:CropX>
         <aas:CropY>0</aas:CropY>
         <aas:CropW>1938</aas:CropW>
         <aas:CropH>1420</aas:CropH>
      </rdf:Description>
   </rdf:RDF>
</x:xmpmeta>

・AffineA~AffineYまでがアフィン変換用情報

・CropX~CropHがクロップされた領域情報

なので、自力でパースしてCGAffineなんちゃらしてClipすればいいのですが、iOS6からこのXMLを食わせると良い感じの画像を出力するメソッドがあるらしいのでメモしておきます。

XMP変換君

CIFilterにfilterArrayFromSerializedXMPって関数があるので、コレを使います。以下実装例

- (CGImageRef)getFullResolutionImage {
    ALAssetRepresentation *representation = [self.asset defaultRepresentation];
    
    NSString *XMPString = representation.metadata[@"AdjustmentXMP"];
    NSData *XMP = [XMPString dataUsingEncoding:NSUTF8StringEncoding];
    
    if (XMP) {
        /// iOS5にはないので
        if ([[CIFilter class] respondsToSelector:@selector(filterArrayFromSerializedXMP:inputImageExtent:error:)]) {
            CIImage *image = [CIImage imageWithCGImage:[representation fullResolutionImage]];
            NSArray *filterArray = [CIFilter filterArrayFromSerializedXMP:XMP
                                                         inputImageExtent:image.extent
                                                                    error:nil];
            CIContext *context = [CIContext contextWithOptions:nil];
            
            // add the filters to an image
            for (CIFilter *filter in filterArray) {
                [filter setValue:image forKey:kCIInputImageKey];
                image = [filter outputImage];
            }
            
            // create a CGImageRef which has been edited
            return [context createCGImage:image fromRect:image.extent];
        }
        // iOS5の場合でもXMPが取れるので、頑張るひとはここから頑張る感じ。
        // 今回はfullScreenImageでお茶を濁す。そろそろiOS5切っていいんじゃね・・・?
        return [representation fullScreenImage];
    }
    
    return [representation fullResolutionImage];
}

ちなみにXMPってAdobeの規格らしいっすよ。ただここらへんのカメラ寄りの規格ってちゃんとしたドキュメント無いような気がするのですが、僕の調べ方が悪いんですかね。

追記

わかってるとおもいますが、XMPに入ってる情報は回転とクロップだけなので、画像補正系には対応できません。画像補正すると写真そのものを弄るのかな・・・?

AmazonS3とiOS間でファイルをやりとりする(その1)

お久しぶりです。

AmazonにS3っていうストレージサービスがあるんですが、それをiOSで使う機会があったのでメモしておきます。

準備

・AWSアカウント

・AWS iOS SDK

・AWSアカウントのアクセスキー

参考文献:http://www.atmarkit.co.jp/ait/articles/1203/09/news120_2.html

Exceptionを発生させないようにする

How Not to Throw Exceptions with the AWS SDK for iOS - https://mobile.awsblog.com/post/Tx2PZV371MJJHUG/How-Not-to-Throw-Exceptions-with-the-AWS-SDK-for-iOS

で、どうやらiOSにおいては、Exceptionはユーザー体験に影響をおよぼすところでは使われない模様です。(Facebook SDKの場合も、NSErrorのポインタ渡してごにょるって操作してた)

iPhoneアプリの通信エラー処理を考える - iOS Advent Calendar 2011 - ninjinkun's diary

というわけで、AWSのブログのように、Exception投げないでね設定をしておきましょう。

Amazon S3の簡単な説明

S3 -ほぼ週刊AWSマイスターシリーズ第2回-

こちらの7ページ目を参考にすると、バケットを作ってそこにオブジェクトを置いていく感じになります。

S3 iOS SDKにはバケットを作るAPIもあるのですが、ユーザーにそれはさせなくて良いという判断で、バケットだけは先にコチラで作って置きます。

なので、次に説明するアップロード、ダウンロードのフローチャートは、バケットが既に用意されていることを前提に書いてます。

ファイルアップロード、ダウンロードのフローチャート

f:id:assaulter:20130725092326p:plain

 

 

 

 

f:id:assaulter:20130725092317p:plain

 

 

 

シンプル!

次回はサンプルコードを載せます。

Facebook iOS SDK3.5で投稿処理を行う際の流れ(その2)

Facebook iOS SDKはコロコロ仕様が変わる(メソッドがいきなり非推奨になったり)ので、お使いのSDKのバージョンには気をつけましょう。

フローチャート

f:id:assaulter:20130516091915p:plain

この図の各部分の処理を書いていきます

セッションが開いているか

FBSession.activeSession.isOpen

コレがBOOL返すので、それで判断

投稿する権限があるか

if([FBSession.activeSession.permissions indexOfObject:@"publish_actions"] == NSNotFound)

セッションが持ってる権限は、FBSession.activeSession.permissionsにコレクションとして格納されているので、それを見て判断する。

iOS Versionを判断する

NSArray *aOsVersions = [[[UIDevice currentDevice]systemVersion] componentsSeparatedByString:@"."];
iOsVersion = [aOsVersions[0] intValue];

これで取得できる(らしい)

iOS6以上で、端末にFacebookアカウントを設定しているかどうか

#import <Accounts/Accounts.h>

    ACAccountStore* accountStore = [ACAccountStore new];
    ACAccountType* accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook];
    
    NSDictionary *options = @{@"ACFacebookAppIdKey": APP_ID_KEY,
                              @"ACFacebookPermissionsKey" : @[@"email"],
                              @"ACFacebookAudienceKey" : ACFACEBOOK_AUDIENCE_KEY};
    
    [accountStore requestAccessToAccountsWithType:accountType options:options completion:^(BOOL granted, NSError *error) {
        if (granted) {
            NSArray* accountArray = [accountStore accountsWithAccountType:accountType];
            if (accountArray > 0) {
                /// iOS6かつ、Facebookアカウントが設定されている場合の処理を開始する
            } else {
                /// iOS5と同じ処理に移動する
            }
        } else {
            DLog(@"error");
            /// iOS5と同じ処理に移動する
        }
    }];

わざわざAccountStore使って判断するのはめんどくさいのですが、とりあえずはコレで判断できるみたい・・・ここ自信ないです。

activeなセッションに対して、投稿権限を要求する

[FBSession.activeSession
     requestNewPublishPermissions:@[@"publish_actions"]
     defaultAudience:PUBLISH_AUDIENCE_KEY
     completionHandler:^(FBSession *session, NSError *error) {
         if (error) {
             /// Handle new permissions request errors
         } else {
             /// 権限が取得できたので、投稿する処理に以降する
         }
     }];

publishPermissionと一緒に、セッションをオープンする

    [FBSession openActiveSessionWithPublishPermissions:[NSArray arrayWithObject:@"publish_actions"] defaultAudience:PUBLISH_AUDIENCE_KEY allowLoginUI:YES completionHandler:^(FBSession *session, FBSessionState status, NSError *error) {
        if (!error && status == FBSessionStateOpen) {
            [self performPublishAction];
        } else {
            [self performSelectorOnMainThread:@selector(setFBError:messages:) withObject:error withObject:[self createAuthErrorMessage:error] waitUntilDone:YES];
        }
    }];

このメソッドが、Facebookアカウントを設定済みのiOS6と、Facebookアカウントを設定していないiOS6,iOS5で挙動が変わります。
OSにアカウントを設定している場合には、そちらから取得するので、アプリ内で解決します。 そうでない場合には、safariが起動して、権限を要求するページが開きます。その後、アプリに戻ってきます。

セッションをオープンする

[FBSession openActiveSessionWithAllowLoginUI:YES];

Facebookはちょっと前の仕様変更で、publish権限を取得剃る前にまずread権限を取得しないといけないという制限があります。iOS5とIDを設定してないiOS6は上記のrequestNewPublishPermissionsを呼べば、一度safariがオープンして、read権限とpublish権限の承認を両方得ることができます。
逆にIDを設定したiOS6では正式にreadを要求してから、publishを要求するというプロセスを踏む必要があるので、上記のメソッドをコールしたあとにpublish権限の取得にうつります。

Facebookに投稿する

  FBRequest* request = [FBRequest requestForUploadPhoto:self.photo];
    [request.parameters addEntriesFromDictionary:@{@"message": self.message}];

    FBRequestConnection* connection = [[FBRequestConnection alloc] initWithTimeout:TIMEOUT_INTERVAL];
    [connection addRequest:request completionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
        if (error) {
            /// 投稿に失敗した
            [self performSelectorOnMainThread:@selector(setFBError:messages:) withObject:error withObject:[self createAPICallErrorMessage:error] waitUntilDone:YES];
        } else {
            /// 投稿に成功した
            [self performSelectorOnMainThread:@selector(setFBError:messages:) withObject:error withObject:[self createSuccessMessages] waitUntilDone:YES];
        }
    }];
    [connection start];

単純に写真を上げるだけなら、uploadPhoto的なメソッドをコールするだけですが、写真にコメントも付けたかったので、requestにメッセージも付加しています。

まとめ

コールバック地獄ですね。エラー処理は各Handlerに帰ってくるのでそれをインスタンス変数で持っておいて使う側で判断する形が一般的でしょうか?おそらくもっとスマートなやりかたがあると思うので、詳しい方コメントお願いします・・・