読者です 読者をやめる 読者になる 読者になる

ObjecTips

基本Objective-Cで iOS とか OS X とか

App Storeでレビューする機能を実装する

Tips SKStoreReviewController App Store

iOS 10.3から登場する SKStoreReviewController のドキュメントにApp Storeのレビュー画面を直接開く方法がしれっと記載されている。

https://developer.apple.com/reference/storekit/skstorereviewcontroller/2851536-requestreview

アプリのURLのクエリに action=write-review を追加すればいいとの事。
例えば Apple の TestFight アプリを例にすると以下のコードになる

- (IBAction)review:(id)sender
{
    // 899247664 is identifier for TestFlight
    NSURL *URL = [NSURL URLWithString:@"https://itunes.apple.com/app/id899247664?action=write-review"];
    [[UIApplication sharedApplication] openURL:URL options:@{} completionHandler:nil];
}

この時の遷移の流れは以下のようになる

メソッド実行

f:id:Koze:20170202101009p:plain

アプリを離れて App Store へ遷移

f:id:Koze:20170202101258p:plain

レビュータブが選択された状態のアプリ紹介画面が一瞬表示される

f:id:Koze:20170202101404p:plain

即座にモーダルでレビュー記入画面が表示される(ログインが必要であればログイン)

f:id:Koze:20170202101636p:plain f:id:Koze:20170202101703p:plain

キャンセルorレビューを完了するとアプリ紹介画面へ戻る

f:id:Koze:20170202101404p:plain

このように action=write-review を使うと直接レビュー画面を開く事が出来る。
クエリ無しでURLを開いた場合はアプリ紹介画面の詳細タブが選択された状態で表示されるためユーザにレビュータブを選択してもらう操作が必要になる。
その点はスマートではないけどレビュータブを選択してレビューを書く前に「App サポート」の選択肢がユーザに提示されるので、もしアプリの使い方や不具合についてネガティブなレビュー書く前にサポートに連絡して欲しいという場合はクエリを使って直接レビュー記入画面を開かずに従来の方法でApp Storeに遷移するのも手かも知れない。
もしくはアプリ内でレビューボタンと不具合報告ボタンをきちんと分けておいて、不具合報告はサポートに直接連絡してもらうようなフローを取るのがいいと思う。


ちなみに iOS 10.3 の SKStoreReviewController を使ったレビュー機能はアプリ内にアラート形式のレーティング画面が出るだけなので非常に使いやすそうだけど、ドキュメントにあるようにメソッドを実行した時に常にアラートが出るわけではなくframework側でよしなに実行タイミングを決めてくれるものなので、ボタン操作などユーザのアクションに対してこのコントローラを使うのは不適切らしい。

UIImageView の画像をフェードインアウトさせて変更する

Tips Objective-C UIImageView Core Animation CALayer

UIImageView に画像を設定する際にフワッとフェード(ディゾルブ)するようにするには、CALayerCATransition のアニメーションを加えてやる

コードは以下

    UIImageView *imageView = self.imageView;
    imageView.image = image; // 画像を変更
    [imageView.layer addAnimation:[CATransition animation] forKey:nil]; // アニメーションを追加

これでOK

CATiledLayer のフェードインエフェクトを解除する

Tips Objective-C Core Animation CATiledLayer

CATiledLayer は表示時にデフォルトでフェードインエフェクトがかかるが CATiledLayer のサブクラスを作って以下のクラスメソッドをオーバーライドして 0 秒を返してやる事でフェードインしないようにする事が出来る。

+ (CFTimeInterval)fadeDuration;

実装は以下

あとは通常通り自前の描画処理を書くだけ。

カメラで撮影した写真と動画を Photos.framework でライブラリに保存する

Tips Objective-C Photos 写真 UIImagePickerController PHPhotoLibrary PHAssetChangeRequest

撮影は UIImagePickerController にお任せして撮影後のデータを Photos で保存する実装方法。

まず UIImagePickerControllersourceType でカメラ撮影を指定。
デフォルトのままだと写真撮影しかできないので、カメラ撮影で利用可能な形式を全て設定して動画撮影もできる様に mediaTypes を設定する。
これで写真と動画の撮影はOK

    UIImagePickerControllerSourceType sourceType = UIImagePickerControllerSourceTypeCamera;
    UIImagePickerController *picker = [[UIImagePickerController alloc] init];
    picker.sourceType = sourceType;
    picker.mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:sourceType];
    picker.delegate = self;
    [self presentViewController:picker animated:YES completion:nil];

撮影したデータの取得は UIImagePickerControllerDelegate のデリゲートメソッドで行う

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info

写真撮影時は info に UIImagePickerControllerOriginalImage キーで UIImage で画像データが入ってきて、動画撮影時には UIImagePickerControllerMediaURLNSURL で動画ファイルのURLが入ってくるという違いがあるので撮影方法によって条件分岐して処理する必要がある。
どちらで撮影されたかは info から UIImagePickerControllerMediaType キーで取得して判定できる。

    NSString *mediaType = info[UIImagePickerControllerMediaType];
    if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
    }
    else if ([mediaType isEqualToString:(NSString *)kUTTypeMovie]) {
    }

なおここの kUTTypeImagekUTTypeMovie のキーを使うために MobileCoreServices.framework も import しておく必要がある。
分岐の後、画像であれば PHAssetChangeRequest

+ (instancetype)creationRequestForAssetFromImage:(UIImage *)image;

動画ファイルのURLであれば

+ (nullable instancetype)creationRequestForAssetFromVideoAtFileURL:(NSURL *)fileURL;

これらのメソッドを使ってライブラリへのデータ保存を行う。

PHAssetChangeRequest は若干変わった利用方法になっていて、以下のように PHPhotoLibrary の変更を行うブロック内でこのインスタンスを作成すると PHPhotoLibrary が勝手に保存処理を行ってくれる。
目に見えて解りやすい save, write などの直接的なメソッド呼び出しでの保存ではないのでちょっと注意。

    PHPhotoLibrary *library = [PHPhotoLibrary sharedPhotoLibrary];
    [library performChanges:^{
            [PHAssetChangeRequest creationRequestForAssetFromImage:image];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
    }];

コード全体は以下のようになる

Xcode プロジェクトの watchOS 1 から watchOS 2 への移行

Tips Xcode WatchKit

watchOS 1 から watchOS 2 への移行は、Xcode のプロジェクトを選択して
Editor > Validate Settings...
を選択して Xcode にお任せてプロジェクト更新するのが良い。
でもなぜかこの時に watchOS の移行についての項目が出てこなくてプロジェクトがうまくアップデートされない事があった。
その際 Xcode プロジェクト hoge.xcodeproj のパッケージを開いて中の project.pbxproj ファイルを直接エディタで開き、
以下のように記載されているところを

productType = "com.apple.product-type.application.watchapp";
productType = "com.apple.product-type.watchkit-extension";

それぞれ以下のように変更したら Xcode がちゃんと watchOS 2 のターゲットだと認識してうまくビルドできるようになった。
productType = "com.apple.product-type.application.watchapp2";
productType = "com.apple.product-type.watchkit2-extension";


あまり無いケースだと思うけど、もし同様の問題が起きたら上記の様な事はしないで一旦 git でプロジェクトを元の状態に戻してキャッシュを消すなり Xcode を起動し直すなりして正しく Validate Settings が完了する様リトライした方が良い(と後から思った)。
参考程度に。

自作の iOS Framework を watchOS に対応させる

Tips Objective-C WatchKit

自作Farmework内で watchOS で利用できないAPIを使っていると not available on watchOS のエラーが出て watchOS 向けにビルドできない。
Apple純正の Framework と同じようにヘッダのメソッド定義に __WATCHOS_PROHIBITED を付けて watchOS では使えません宣言をすると ifdef を使ったりせずメソッドをそのままビルド可能になる。

ビルドエラー
// .h
- (void)test:(EKEventStore *)eventStore;

// .m
- (void)test:(EKEventStore *)eventStore
{
    [eventStore commit:nil]; // 'commit:' is unavailable: not available on watchOS
}
OK
// .h
- (void)test:(EKEventStore *)eventStore __WATCHOS_PROHIBITED;

// .m
- (void)test:(EKEventStore *)eventStore
{
    [eventStore commit:nil];
}

クラス自体を watchOS で使えないように宣言する場合はクラス宣言の @interface の前にこれを付けると良い

__WATCHOS_PROHIBITED @interface MyClass : NSObject

NSString の真偽値の判定

Tips NSString Objective-C

NSString で表される文字列の真偽値を判定するには boolValue メソッドが使える。

@property (readonly) BOOL boolValue NS_AVAILABLE(10_5, 2_0);

数字、YES/NO、true/false などの文字列を boolValue メソッドで判定できる

    NSLog(@"%d", @"1".boolValue); // 1
    NSLog(@"%d", @"0".boolValue); // 0
    NSLog(@"%d", @"YES".boolValue); // 1
    NSLog(@"%d", @"NO".boolValue); // 0
    NSLog(@"%d", @"true".boolValue); // 1
    NSLog(@"%d", @"false".boolValue); // 0

ドキュメントによれば最初の文字が Y, y, T, t または1~9の数字だとYESを返すとの事。
よって以下は全てYES

    NSLog(@"%d", @"Y".boolValue); // 1
    NSLog(@"%d", @"y".boolValue); // 1
    NSLog(@"%d", @"T".boolValue); // 1
    NSLog(@"%d", @"t".boolValue); // 1
    NSLog(@"%d", @"1".boolValue); // 1
    NSLog(@"%d", @"9".boolValue); // 1

以下のような文字列も最初の1文字で評価されるので全てYES

    NSLog(@"%d", @"Yabc".boolValue); // 1
    NSLog(@"%d", @"yNO".boolValue); // 1
    NSLog(@"%d", @"tfalse".boolValue); // 1

また先頭文字列のスペースは無視される、
文字列が数字の場合、先頭文字列の0の連続は無視される、0の連続の前に置かれる+-の記号は無視されるという仕様があるので以下のケースも全てYESになる。

    // スペース無視
    NSLog(@"%d", @"  y".boolValue); // 1
    // 先頭の0の連続は無視
    NSLog(@"%d", @"001".boolValue); // 1
    // 数字の前の+と-は無視
    NSLog(@"%d", @"+1".boolValue); // 1
    NSLog(@"%d", @"-1".boolValue); // 1
    // 数字の前の+と-と0の連続は無視
    NSLog(@"%d", @"+01".boolValue); // 1
    NSLog(@"%d", @"-001".boolValue); // 1

以下のケースは全てNOになる。

    // 先頭文字列がスペースではない
    NSLog(@"%d", @"- 1".boolValue); // 0
    NSLog(@"%d", @"- t".boolValue); // 0
    // 文字列が数字ではないので0の連続は無視されない
    NSLog(@"%d", @"00y".boolValue); // 0
    NSLog(@"%d", @"00t".boolValue); // 0
    // 数字の文字列の前の+と-の記号を無視するのは1つまで
    NSLog(@"%d", @"++001".boolValue); // 0
    NSLog(@"%d", @"++1".boolValue); // 0