2016年8月26日金曜日

画像相互変換

まだ直接相互変換できるかわからないケースもあるが、一度他の形式に変換して間接的に変換できるよね。
uiにUIImageが入ってるとして

UIImage *ui = [UIImage imageNamed:@"cat"];

UIImage -> CGImage

struct CGImage *cg = [ui CGImage];

CGImage -> UIImage

UIImage *ui = [UIImage imageWithCGImage:cg];

UIImage -> CIImage

CIImage *ci = [ui CIImage];

CIImage -> UIImage

UIImage *ui = [UIImage imageWithCIImage:ci];

CGImage -> CIImage

CIImage *ci = [CIImage imageWithCGImage:cg];

CIImage -> CGImage

CIContext *context = [CIContext contextWithOptions:nil];
struct CGImage *cg = [context createCGImage:ci fromRect:ci.extent];

UIImage -> NSData

NSData *da = UIImageJPEGRepresentation(ui, 1.0); //JPEGとして
NSData *da = UIImagePNGRepresentation(ui); //PNGとして

NSData -> UIImage

UIImage *ui = [UIImage imageWithData:da];

2016年8月25日木曜日

画像のまとめ

画像にはいろいろ種類があるのでわかったとこだけまとめ。

CGImageってなによ?

基本的なQuartzの画像データ。
頭のCGはCore Graphicsの意味。
Core Graphicsフレームワークは、Quartz描画エンジンをベースにしたC言語ベースのAPI。
出力に忠実な低レベルで軽量な2Dレンダリングを提供する。
CGRectとかCGPointなんて頭にCGが付くのもCore Graphics関係だよね?

Core Graphicsフレームワークで取り扱うもの

  • ハンドリング・パスベースの描画(ビットマップじゃなくドロー画像ということか?)
  • 変形
  • カラーマネジメント
  • オフスクリーン描画
  • パターン
  • グラデーションとシェード
  • 画像データ管理
  • 画像生成
  • マスキング
  • PDF文書生成
  • 表示
  • 分析(パース)

CGImageは構造体?

CGImageというクラスはない。型名としてCGImageを指定することができるが、以下のようにXcodeで「頭にstruct(構造体)って付けろ」って要求された。う〜んわからん。
struct CGImage *cgImage = [context createCGImage:ciImage fromRect:ciImage.extent];

CGRect、CGPointなんてのも構造体だから、CG〜はだいたい構造体として定義されとるのかな? こっちはわざわざstructを付けるように要求されないが。

CGImageRefとは?

以下は公式の説明を訳したものだけど、結局なんだかよくわからんかった。
CGImageRef不透明タイプは、ビットマップ画像とビットマップ画像マスクを意味し、あなたの供給するサンプルデータに基づく。
ビットマップ(もしくはサンプル)画像はピクセルの長方形の配列で、ソース画像中の一つのサンプルもしくはデータポイントを表している互いのピクセルである。

CIImageってなによ?

基本的なCore Imageのデータ。
CIImageクラスは画像を意味する。Core Imageの画像は変更不能(コードによって一から画像を作ったり、変更したりができないということだと思う)
CIImageオブジェクトを他のCore Imageクラス——CIFilter、CIContext、CIVector、CIColorなど——や、画像処理する際のビルトインCore Imageフィルターとともに利用することになる。
各種のソースからのデータが付いたCIImageオブジェクト(Quartz2D画像、Core Video画像バッファ(CVImageBufferRef)、URLベースのオブジェクト、NSDataオブジェクトを含む)を作ることができる。 

Core Imageフレームワークとは

静止画像とビデオ映像のリアルタイムに近い画像処理と分析(文字認識とか顔認識とか)のためのフレームワーク。
画像にフィルターかけたりすることができる。

UIImageってなによ?

UIImageオブジェクトは、iOSアプリで画像データを管理するための標準的なオブジェクト。既存の画像データから作る必要がある。(コードによって一から画像を作ったり、変更したりができないということだと思う)

2016年8月24日水曜日

OpenWeatherMapでデータが取れない

基本無料で現在の天気情報、天気予報、過去の天気情報が取れるOpenWeatherMapというサービスがある。
無料のアカウントを取って使っていたのだが、しばらく確認してなかったらエラーが出て情報が取れなくなった。
変なエラー出た
アプリの方でエラーダイアログが出るとともに、Xcodeのログにも
App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.
と出た。

OpenWeatherMap側の問題?

調べると、以前はアカウントを特定するキーをコード中に埋め込んでも埋め込まなくても呼び出せたが、最近は埋め込まないといけなくなったということだが、これは以前から以下のように埋め込んでいた。

//openWeatherMapに緯度経度とAppIDを渡し、現在の気象情報のJSONを取る
NSString *url = [NSString stringWithFormat:@"http://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&cnt=1&APPID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",coordinate.latitude ,coordinate.longitude];
NSData *json = [self correspondenceWithURL:url];

iOS側の問題だった

さらに調べたところ、iOS9からiOSのセキュリティが厳しくなり、App Transport Security(ATS)という機能によって、これが有効な場合にはウェブサービス間でHTTPの通信ができないのだそうだ。
対応は上記のエラーメッセージにも書かれているが、アプリのInfo.plistの辞書に例外サイトを指定してやらないといけない。

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>api.openweathermap.org</key>
        <dict>
            <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>

Open as Source codeで表示した場合

うまくいっただよ

うまくいきました🎵

2016年8月22日月曜日

提出すべきスクリーンショットが減った

アプリを審査に提出する際、今までは

  • iPhone6 Plus(5.5インチ)
  • iPhone6(4.7インチ)
  • iPhone5(4インチ)
  • iPhone4(3.5インチ)

の5種類のサイズのスクリーンショットを提出しないといけなかったが、これからは最も大きなiPhone6 Plusサイズだけ提出すれば、自動的に他のサイズに変換してくれるようになった。
これは楽ちん!
どうしても個別のサイズを加えたい場合はそれもオプションで設定できる。
もっと早くこうしてくれりゃ良かったのに。

あとアイコンについても同じこと頼みたいね。

2016年8月11日木曜日

iOS9から言語コードが変わった

いまさらな話で恐縮だけど、使ってるOSの言語が何に設定されているかのコード(jaとかenとか)が、iOS9から変わったようで、チェック方法によっては思い通りの結果にならないようになった。

今までは

  • 日本語
    • ja
  • 英語
    • en
とかだったんだけど、
iOS9以降はその言語コードの後に地域コードも付くようになったのだ。
例えばja-JPとかen-JPとか。

地域コードってのが何かというと、iOSの設定画面の書式のとこのこれ。
これが日本なら「-JP」なんだけど、ノルウェーだと「-NO」になる。
「日本語を日本で使う」「日本語をノルウェーで使う」とか、そういう細かな設定の違いが生じるわけだ。

英語も言語コードがenで一緒でも、地域コードはアメリカならUS、イギリスならGR(Great Britainだな)になる。

地域コードが違うと、通貨表示なんかがアメリカなら$ドル、イギリスなら£ポンド表記とかになる。


同じ言語でも、地域コードとの組み合わせによってパターンが無数に生じてしまうので、言語判定をするには、「どの言語コードで始まるか」ということで判断させるのがいい。

2016年8月7日日曜日

画像をメタデータ付きで読み書き

アプリ内にあるDocumentsディレクトリ内の画像を、メタデータ(EXIFやGPSの位置情報など)付きで読み書きする方法。

準備

プロジェクトにメタデータの付いた画像をインストールしておく。(いきなりDocumentディレクトリに画像入れられないから)
名前は一応gazo.jpgで。
最近はAssetCatalogにインストールすることが多いけど、そこの画像にアクセスする方法がよくわからないので、以前のようにXcodeのプロジェクトナビゲータの所に突っ込むこと。

今回は以下の手順を取る。
  1. リソースとしてインストールされた画像gazo.jpgとメタデータを読み込み
  2. Documentディレクトリにメタデータとともに書き込み
  3. 画像とメタデータを読み込んで表示する
また、ImageIO.frameworkをインポートしとくこと。
#import <ImageIO/ImageIO.h>

書き編

//リソースにある画像を読み、メタデータとともにアプリ内のDocumentディレクトリに保存
- (void)writeImageWithMetadata {
    //🌟準備としてリソース画像とmetadataを読む
    //リソースディレクトリへのパスを得る
    NSBundle *bundle = [NSBundle mainBundle];
    NSString *resourcePath = [bundle resourcePath];
    NSString *imgPath = [resourcePath stringByAppendingPathComponent:@"gazo.jpg"];
    NSLog(@"リソースのpath %@",imgPath);
    //リソースの画像とメタデータを得る
    CGImageSourceRef sourceRef = CGImageSourceCreateWithURL((CFURLRef)CFBridgingRetain([NSURL fileURLWithPath:imgPath]), nil);
    NSDictionary *metadata = (NSDictionary *) CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL));
    
    //🌟画像とmetadataDocumentディレクトリに保存
    //Documentに保存するファイルのpathを作る
    NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectoryNSUserDomainMaskYES)[0];
    NSString *filePath = [documentPath stringByAppendingPathComponent:@"gazo.jpg"];
    
    //書き込むための画像データを入れるNSMutableData型変数を作る
    NSMutableData *imageData = [[NSMutableData allocinit];
    //書き込むための情報を入れる変数destRefに、書き込む画像データの変数(imageData)の情報と、データタイプ(JPEGとか?)、画像ファイル中の画像枚数(複数画像を持つフォーマットもあるため)を入れる
    //sourceRefはデータタイプを得る関数で参照しているだけなので、ここでは画像そのものは加えられていない
    CGImageDestinationRef destRef = CGImageDestinationCreateWithData((CFMutableDataRef)imageData, CGImageSourceGetType(sourceRef), 1, nil);
    //引数1destRefに、引数2の画像と引数4のメタデータを追加する。引数30は複数画像がある場合の画像のインデックスらしい
    CGImageDestinationAddImageFromSource(destRef, sourceRef, 0,(CFDictionaryRef)metadata);
    //書き込むための情報destRefをファイナライズして完成
    CGImageDestinationFinalize(destRef);
    //画像データのimageDataの書き込みをすることで、destRefを参照してメタデータともども書き込んでくれるらしい
    [imageData writeToFile:filePath atomically:YES];
    //使用後は解放
    CFRelease(sourceRef);
    CFRelease(destRef);
}

読み編

//Documentディレクトリ内の画像を読み込んで表示&メタデータ取得して表示
- (void)readImageWithMetadata {
    //Documentディレクトリにあるファイルのpath作成
    NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *filePath = [documentPath stringByAppendingPathComponent:@"gazo.jpg"];

    //画像読み込み
    NSError *err = nil;
    NSData *imageData = [NSData dataWithContentsOfFile:filePath
                                              options:NSDataReadingMappedAlways
                                                error:&err];
    //エラー処理するならこの辺で
    NSLog(@"Error -> %@",err);
    //NSDataからCIImage経由でUIImageに変換
    CIImage *ciImage = [CIImage imageWithData:imageData];
    UIImage *image = [UIImage imageWithCIImage:ciImage];
    //こっから。UIImage作り直し開始
    UIGraphicsBeginImageContext(image.size);
    [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    //ここまで。UIImage作り直し終了
    _myImageView.image = [UIImage imageWithData:imageData];

    //メタデータ取得
    //CGImageSourceCreateWithURLを使う場合
    //Documentディレクトリ内の画像とメタデータを得る
    CGImageSourceRef sourceRef = CGImageSourceCreateWithURL((CFURLRef)CFBridgingRetain([NSURL fileURLWithPath:filePath]), nil);
    //sourceRefの情報からメタデータを取り出す
    NSDictionary *metadata = (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, nil));

    //使用後は解放
    CFRelease(sourceRef);

    //確認のためmetadata表示
    NSLog(@"メタデータ %@",metadata);
    //EXIF情報取り出し
    NSDictionary *ExifDictionary = [metadata objectForKey:(NSString*)kCGImagePropertyExifDictionary];
    NSLog(@"EXIFデータ %@",ExifDictionary);
}

各メソッド、関数について

NSBundleはアプリにインストールされてるリソースにアクセスするためのクラス。
resourcePathメソッドでリソースディレクトリへのpathが得られるので、ファイル名を追加。

CGImage〜Ref

CGImageSourceRef、CGImageDestinationRefは画像ファイルに付加されたEXIFなどの情報にアクセスするためのクラスで、ImageIO Frameworkが必要。
  • 画像ファイル読み込み時 
    • CGImageSourceRef 
  • 画像ファイル書き込み時 
    • CGImageDestinationRef

メタデータの中身にアクセス

metadataの辞書からEXIF情報を、キーを使って取り出し表示している。
ちなみにEXIFは入れ子になっている辞書なので、
float ss = [metadata[(NSString *)kCGImagePropertyExifDictionary][(NSString *)kCGImagePropertyExifExposureTimefloatValue];
などとやることで下の階層の情報にアクセスする。

CGImageDestinationCreateWithData

CGImageDestinationRef CGImageDestinationCreateWithDataConsumer ( CGDataConsumerRef consumer,CFStringRef type, size_t count, CFDictionaryRef options );

引数
consumer:書き込みデータのコンシューマー(何じゃそりゃ?)だそうだ。初期化しただけのNSMutableDataを指定してる。多分ここで指定した変数に画像データのsourceを書き込み、さらに結果をdestに返り値を返してるんだと思う。
type:データのタイプ(たとえばJPEGとかPNGとか)らしいが、この場合直接指定せずにCGImageSourceGetType関数でソース画像のタイプを得ている。
count:TIFFなどのように複数の画像を含む画像の場合、何枚あるのかを指定するが、1枚だけなら1でいい。
options:将来のための予約とあるので、nilでよし。

CGImageDestinationAddImageFromSource

void CGImageDestinationAddImageFromSource ( CGImageDestinationRef idst, CGImageSourceRef isrc, size_t index, CFDictionaryRef properties );

引数
idst:画像のdestination
isrc:画像のソース
index:ソース画像で画像の位置を指定するインデックス(0オリジン)。多分複数画像を持つフォーマットの場合に、何枚目の画像かを指定するんじゃないかと思うが、よくわからん。とりあえず1枚だけのフォーマットなら0でいいかと。
properties:ソース画像のプロパティに上書きするか追加するか指定する辞書。プロパティのキーがkCFNullなら画像のdestinationは削除される。わかりづらいが、ここに辞書を指定してやればソース画像(source)+辞書をdestinationに追加してくれるらしい。

変数destは書き込む画像のデータ(imagaData)とメタデータ(metadata)の情報を参照していて、CGImageDestinationFinalizeでファイナライズした後にimagaDataの方をwriteToFileすると、メタデータ付きで書き込まれるのではないか?
最後にsourceとdestはreleaseしてやる必要があるようだ。

destに色々情報を追加しつつ、最後はimageDataを書き込んでるのがわかりづらい。imageDataを書き込むとdestに書かれた参照情報を見てmetadataも一緒に書き込まれるという理解でいいのか?

CGImageにすると向きの情報が消えることがある

読み書き時に止むを得ずCGImageに変換を要する場合があるが、UIImageでは持っている画像の向きの情報であるimageOrientationプロパティがないため、UIImageに復元しても縦長に撮った画像が90度横倒しに表示されてしまうことがあった。
その場合、
[UIImage imageWithCGImage: scale: orientation:]メソッドでorientationを指定しつつ復元してやるが、それでもうまくいかない場合は、
UIGraphicsBeginImageContext()

UIGraphicsEndImageContext()
に挟まれた間で、UIImageを作り直してやるといい。
なにやらめんどくさいがしょうがない。
ちなみに前述のコード中、直接CGImageから変換したのではうまくいかなかったため、一度CIImageを経由した。
なにやら本当にめんどくさい。

謎のメタデータ追加メソッド

メタデータを追加するメソッドとしては
CGImageDestinationAddImageAndMetadata(CGImageDestinationRef idst, CGImageRef image, CGImageMetadataRef metadata, CFDictionaryRef options)
というのもあるようなのだが、APIリファレンス見てもちゃんとした説明が書いてないし、よくわからない。
釈然としないけど、CGImageDestinationAddImageFromSourceでうまくいくんならそれでいんじゃね?

アセットカタログ内の画像にアクセスしてみる

結論から言うとうまくいきませんでした。

NSString *imgSuffix = @"-universal-1x";
NSBundle *bundle = [NSBundle mainBundle];
//Assets Catalogのディレクトリへのパスを作る

NSString *imgPath = [bundle pathForResource:[NSStringstringWithFormat:@"%@%@",@"gazo2",imgSuffix] ofType:@"jpg"];

アセットカタログのjsonファイル(Contents.json)を開くと、
{
  "images" : [
    {
      "idiom" : "universal",
      "filename" : "gazo2.jpg",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}
などとなっており、アクセスにはファイル名にidiomやscaleのサフィックスを付けるというような情報を見たのだけど、サフィックスが合ってないのか、そもそも情報自体が間違いなのか、imgPathはnilになってしまい、ダメだった。
メタデータを取るためにアクセスするようなことがなければ、単にUIImageのファイル名で楽にアクセスできるのにねぇ。

感想

CGImageSourceRefCGImageDestinationRefがどういうものかよくわからず、ひたすら悩んで調べまくった。
今もよくわかってないけどね。
引数1に引数2以降の情報を追加するという仕様や、情報を追加した変数(たとえばdestRef)じゃなく画像本体の方を書き込むとdestRefを参照してメタデータも書き込まれるとかはわかりづらい。

今回はリソースとDocumentディレクトリの間だったけど、イメージライブラリとの間だとALAssetLibraryを使って(最近はPhotosFrameworkなの?)また別のやり方になるはず。
とにかくめんどっちー。
UIImageのプロパティとして簡単に扱えるようにならんもんか…。

2016年8月4日木曜日

保存先のpathが毎回変わる?

iOSのファイル構造

アプリ内にはデベロッパーやiOSがデータを読み書きできるディレクトリがある。
例えば以下のようなもの。

  • /Documents
    • アプリ固有のファイルを保存
    • アプリを消すまでiTunesでバックアップされる
  • /Library/Preferences
    • アプリ固有の環境設定ファイルを保存
    • バックアップされるが、デベロッパーは直接いじっちゃダメらしい
  • /Library/Caches
    • アプリ固有のサポートファイルを保存
    • バックアップされないし、アプリがアクティブ時でも削除される可能性がある
  • /tmp
    • 一時ファイルを保存
    • バックアップされないが、アプリがアクティブ時は保持される
一番使うのはDocumentsディレクトリで、アクセスにはpathを作って読み書きする。

pathが毎回変わる!?

そのpathなのだが、実機で確認したところ、Xcodeから起動するごとに絶対pathの途中のディレクトリ名が変更されていることがわかった。
具体的には、以下の赤の部分が大きく変わっている。

/var/mobile/Containers/Data/Application/42F81D0D-EDC1-4BB3-AC03-BBE7D6DC8C79/Documents/ファイル名
/var/mobile/Containers/Data/Application/2E9C5905-1A89-40BA-B727-3DDD8AE13BC5/Documents/ファイル名

そのため、絶対pathでファイルを書き込み、次の起動時に読もうとすると、ファイルが存在せずに読めなくなるのだ。
なぜこうなってるのかわからないが、iOS8からの仕様らしい。

対処法

対処法だが、NSSearchPathForDirectoriesInDomains という関数があるので、それを使ってやれば起動ごとに変化するpathを得ることができる。(返り値は配列なので、index 0を参照すること)
もちろんディレクトリ内のファイルにもアクセスできる。

Swift
let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first ?? ""

Objective-C
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];

このpathの後に「/ファイル名 」を加えてやれば読み書きできるのだ。

2016年8月3日水曜日

iOSβ版をインストールする

せっかく金払ってデベロッパー登録してるんだから、リリース前のOSも使ってみたい!
ってんで、初めてiOS10βをインストールしてみたよ。

インストール方法

  1. MacのiTunesで、インストールしたいデバイス(iPhoneやiPad)のバックアップを作る
  2. iTunesの設定(⌘+,)のデバイスタブを開き、作られたバックアップをマウスで右クリック(control+クリック)し、アーカイブを選択(アーカイブを選んでも特に画面に変化はないようなのでちと不安だが)
  3. アップルデベロッパーのサイトにβテストの登録ページがあるので、そこで登録
  4. βOSをインストールするデバイスiPhoneやiPadからデベロッパーサイトのダウンロードページを開き、iOSβの構成プロファイルをダウンロード(インストールされる)
  5. デバイスの設定/一般/ソフトウェアアップデートにβ版iOSが候補として表示されるので、インストールする
1、2のバックアップとアーカイブ作業は忘れないこと。でないと元に戻せなくなる。
βiOSがインストールされてもこれまでインストールされてたアプリや設定はそのまま。つまり通常のOSのアップデートと同じ状態になる。

元のOSに復元する方法

  1. MacのiTunesを起動し、復元するデバイスをコードで接続しておく
  2. スリープボタン+ホームボタンを長押しし、マークが出てもひたすら長押しし、リカバリーモードの表示が出たら離す
  3. Macの方に復元するかアップデートするかと出るから復元を選ぶ(なお、アップデートを選ぶとβiOSの最新版が(存在してれば)インストールされる)
  4. MacのiTunesの方は変化に乏しいけど、ダウンロード中って出てると思うので、待ってるとデバイスの方が復元作業に入る
  5. iTunesでデバイスに結びついてたAppleIDの入力と、復元するアーカイブの選択なんかをする
  6. さらに待ってると今までのiOSが起動してくれる
    1. こっちでもAppleIDとパスワードの入力必要だったかな?
    2. touchIDはもう一度設定し直し
    3. インストールしてたアプリはOS起動後に一個一個自動的に再インストールするみたい
    4. 削除したメールがもう一度未読として受信フォルダに出てきちゃったり、完全に同じ状態に復元してくれるわけでもないのがめんどい