2017年05月31日

[iOS] macOS SierraとXcode 7.3.1でクラッシュする

iOS担当のエモトです.GPD Pocket用の液晶フィルムが届きましたが,本体はまだ発送されていません.いつ届くのでしょうかね.クラウドファンディング自体初めてなのでそわそわしています.

弊社は受託開発を行っているため,少し前の Xcode を使って開発することがあります.先日,macOS Sierra で Xcode 7.3.1 を使用中に Xcode が必ずクラッシュするという状態に陥りました.開発中にアプリをクラッシュさせることはありますが,開発環境のXcode自体がクラッシュするのはなり焦りました.

xcode-bug-0.png

クラッシュするようになってから,その直近で行った作業をいろいろ検証したところ,Run Scriptの編集に原因がありました.macOS Sierra で Xcode 7.3.1 をお使いであれば,以下の方法で再現することができます.もし同症状の方がいらしたら,参考になれば幸いです.

1. Run Scriptを追加する
TARGETS -> Build Phases -> New Run Script Phase

2. 横三角ボタン(▶︎)で一度でも展開した(▼)後に他の画面に遷移するとクラッシュ.閉じたまま(一度も展開していない)ではクラッシュしない

xcode-bug-4.png


stackoverflow で漁ってみると,Sierra Betaから 7.3.1 との相性は良くない ようです.最も有効な対応策としては,同サイトでも言われるように Xcode 8.0 以上の最新版を使うことでしょう.どうしても環境を新しくできないのであれば,クラッシュ覚悟で展開後に一気に script を書くことでしょうか.賢くないですね.

また,macOS Sierra で Xcode 7.3.1 はファイル追加のフォームの崩れが起こるので,一刻も開発環境を更新するのが望ましいです.

xcode-bug-2.png


posted by Seesaa京都スタッフ at 15:35| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする

2017年04月21日

[iOS] モーダル表示処理のバッドノウハウ

iOS担当のエモトです.何に使えばいいのか分からないですが,GPD Pocketに出資してみました.かつてネットブックが盛んだったころ,EeePCを始め数台のネットブックを買っては遊んだ思い出を繰り返すんだろうなと思います.

モーダル表示したViewControllerが閉じたときの判定は,いうまでもなくdismissしたときのcompletionブロックを使えば取得できます.
- (void)dismissViewControllerAnimated:(BOOL)flag
					   	 completion:(void (^)(void))completion
しかしながら,サードパーティーSDKなどでモーダルの表示と削除がそのSDK内部で行われている場合はどうしましょうか.一般的なものであれば,デリゲートメソッドなどが用意され,取得可能だと思われます.
- (void)hogehogeWillDismiss;
- (void)hogehogeDidDismiss;
しかしながら,
- (void)hogehogeDismiss;
という場合もあります.これだとdismissがwillなのかdidなのかよくわかりません.実際に開発している人にはよく分かると思いますが,willとdidの違いは,画面制御にて大きな問題になります.開発者はdidを期待して実装したら,実はwillだったとなれば,アプリの想定外の動作やクラッシュの原因になります.

私自身もこれに似た設計のSDKに遭遇したことがあり,提供元に確認したところ,そのSDKの仕様から判定できないと回答されました.今回は試行錯誤のもとでその問題を回避した方法を紹介します.しかしながら,根本的な良い方法ではないことであるとご認識ください.

方針として,モーダルの表示と削除はサードパーティーSDKが行なっているので,内部の現象を外部から観測することで解決を試みます.まず,モーダルで表示されたViewControllerは以下のように取得できるので,これを手掛かりとして探っています.
UIViewController *vc = self.presentedViewController;
これを監視すれば解決しそうです.以前に当ブログでも紹介したKVO(Key-Value Observing, キー値監視)を使えば良い気もしますが,残念ながらpresentedViewControllerは監視できないので使えません.やりたくはないですが,タイマーを回して,presentedViewControllerを監視します.

例として,HogeHogeSDKというサードパーティーSDKを仮定します.このSDKは,``show:``メソッドで指定したViewControllerを基底としてモダール表示するとします.また,デリゲートメソッドとして,``hogehogeDismiss``が用意され,SDKで表示したViewControllerがdismissされたときに呼ばれるとします.しかしながら,dismissが完了されたかどうかは用意されてません.
@interface HogeHogeSDK : NSObject
- (void)show:(UIViewController*)viewController;
@end

@protocol HogeHogeSDKDelegate <NSObject>
- (void)hogehogeDismiss;
@end
手順ですが,
1. モーダル表示されたら,そのpresentedViewControllerを別の変数に代入しておく
2. デリゲートを受け取ったら,presentedViewControllerに対して以下の判定を行う
- nilなら,完了メソッドを行う
- nilでなければ,nilになるまで待ったのち完了メソッドを行う
注意点として,presentedViewControllerで取得されるUIViewControllerは非公開のUIViewControllerを継承したクラスである場合が多く,何が表示されているかわからない(逆にクラス名を保持しておけば判定材料になる).モーダル表示はSDKだけはなく,自身のアプリで表示されたり,OSが表示するものもあるので,完了条件はそれぞれの環境に合わせて設定しなければなりません.

@interface ViewController ()

// サードパーティSDK
@property (nonatomic, retain) HogeHogeSDK *hogeHogeSDK;

// モーダルで表示されるViewController
@property (nonatomic, weak) UIViewController *tempViewController;

// モーダルで表示されるViewControllerのクラス
@property (nonatomic, retain) Class tempClass;

// タイマー関数の制御
@property (nonatomic, assign) NSTimeInterval progress;
@property (nonatomic, retain) NSTimer *timer;

@end

@implementation ViewController

#pragma mark - HogeHogeSDK

// サードパーティSDKで何かしらを表示させる
- (void)showHogeHogeSDK
{
    [self.hogeHogeSDK show:self];
    
    // モーダル表示がアニメーション遷移の場合に直後だと取得できないので,ディレイ処理を入れる
    dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    dispatch_after(delay, dispatch_get_main_queue(), ^(void){
        self.tempViewController = self.presentedViewController;
        self.tempClass = self.tempViewController.class;
    });

}

// サードパーティSDKの表示が終わったときに行う処理
- (void)compleHogeHogeSDK{
    // 別の画面に遷移など
}

#pragma mark - HogeHogeSDKDelegate

- (void)hogeHogeSDKDismiss:(NSString *)appID
{
	// モーダル表示がまだ残っているなら,タイマー処理を始まる.そうでなければ完了処理へ
    if (self.tempViewController) {
        [self startTimer];
    }else{
        [self compleHogeHogeSDK];
    }
}


#pragma mark モーダルを監視する

- (void)startTimer
{
    [self endTimer];
    self.progress = 0;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1
                                                  target:self
                                                selector:@selector(handleTimer:)
                                                userInfo:nil
                                                 repeats:true];
}

- (void)endTimer
{
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
        self.progress = 0;
    }
}


- (void)handleTimer:(NSTimer*)timer
{
    self.progress += timer.timeInterval;
    
    UIViewController *vc = self.presentedViewController;
    
    if (self.progress > 3) {
    // dismiss後にある一定以上たっても状態が変わらなかったら,強制的に終了させる
        [self endTimer];
        if (self.tempViewController) {
            [self.tempViewController dismissViewControllerAnimated:false
                                                             completion:^{
                                                                 [self compleHogeHogeSDK];
                                                             }];
        }else{
            [self compleHogeHogeSDK];
        }
    }else if (vc == nil
              || self.tempViewController == nil
              || (vc && [vc isKindOfClass:self.tempClass] == false)
              ) {
              /*
              以下のルールのどれかに当てはまれば完了メソッドを呼ぶ
              - 現在のモダールがnil
              - モーダル表示したときに代入したvcがnil
              - モーダルがあるが,SDKで設定したクラスではない
              */
        [self endTimer];
        [self compleHogeHogeSDK];
    }
    
}

@end
今回はこのような方法で問題を回避できましたが,決して良い方法ではありません.内部の現象をある仮定のもとで外部から観測しているだけなので,今回はたまたま上手くいっただけで,SDKの内部処理によっては失敗します.ぜひ,完了メソッドを実装されてないSDK提供者様は実装をお願いします.
posted by Seesaa京都スタッフ at 12:00| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする

2016年11月02日

[iOS] Live Photos を読み込んで,再生して保存したい

iOS担当のエモトです.前回投稿後に「君の名は」を見に行きました.過去に秒速5センチメートルで心を折れた経験から不安でしたが杞憂でした.本当に素晴らしい.鑑賞後に本編小説と外伝小説を読み,再び映画に行きました.このシーンの裏にあのストーリーがあったのかと異なる視点から見ることができ,非常に楽しめました,円盤買います.さて先日,半年ぶり二回目の大洗に訪れました.今回は,戦車が二回突っ込まれた旅館に泊まりました.その旅館内で大洗戦を見て,今いる旅館が全壊と,何が何だかよく分からない体験でした.今週末は復活のガルパン4DXに行きます.

Live PhotosはiOS9から登場した動く写真です.3D Touchを搭載した機種ではLive Photosを壁紙に設定でき,ロック画面で押し込むことで写真が動きます.また3D Touchを搭載してない機種でも長押しで再生されます.

Live Photosは特別なファイルフォーマットがあるわけではなく,画像(jpgファイル)と3秒間の動画(movファイル)の組み合わせ(それぞれのファイル名を一致させ,メタデータを書き足す)で実現されます.今回は,Live Photosの読み込み,再生,保存の方法を紹介します.以下の例では,エラー処理や写真ライブラリへのアクセス許可を省略しています.適宜エラー処理やアクセス許可を追加してください.

0. Live Photosを用意する

まずは素材がないと話にならないので,Live Photosを用意しましょう.

0.1 iOS9以上のiPhoneを使って作成する

カメラアプリで写真を撮影するときに,上部にあるアイコンからLIVEをオンにすれば,撮影することができます.

0.2 動画ファイルからLive Photosを作成する

動画ファイルからの作成は,今回は このサイト にて提供されるデスクトップアプリ LoveLiver を使用しました.このサイトはLive Photosの仕組みを丁寧に説明しているので,この工程が不要でも一度読むことをおすすめします.

1. Live Photosを読み込む

用意した方法により2通りの方法あります

1.1 写真ライブラリ

通常の写真選択と同様に選択することができます
#import < Photos/Photos.h >
#import < PhotosUI/PhotosUI.h >
#import < MobileCoreServices/MobileCoreServices.h >

- (void)showImagePickerController
{
    UIImagePickerController *ipc = [[UIImagePickerController alloc] init];
    ipc.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
    ipc.delegate = self;
    ipc.allowsEditing = false;
    ipc.mediaTypes = @[(NSString*)kUTTypeImage, (NSString*)kUTTypeLivePhoto];
            
    [self presentViewController:ipc
                               animated:true
                             completion:nil];
}

- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
{
    [self dismissViewControllerAnimated:true
                             completion:nil];
    PHLivePhoto *lp = info[UIImagePickerControllerLivePhoto];
    if (lp) {
        // 取得成功
    }
}

1.2 アプリ内のバンドルまたはドキュメント

カメラアプリ以外で,アプリ内のバンドルファイルやドキュメントに配置した場合です.下記例では,Live Photos を sample.JPG と sample.MOV の組み合わせとして,バンドル内に埋め込んだ例になります.
@property (nonatomic, retain) NSURL *imageUrl;
@property (nonatomic, retain) NSURL *videoUrl;

// ファイルを読み込む
- (void)loadLivePhotoFromBundle
{    
    // bundleから読み込みを想定
    self.imageUrl = [[NSBundle mainBundle] URLForResource:@"sample" withExtension:@"JPG"];
    self.videoUrl = [[NSBundle mainBundle] URLForResource:@"sample" withExtension:@"MOV"];
    UIImage *placeholderImage = [UIImage imageNamed:@"sample.JPG"];
    
    [self makeLivePhotoFromImageUrl:self.imageUrl
                           videoUrl:self.videoUrl
                   placeholderImage:placeholderImage
                         completion:^(PHLivePhoto *livePhoto)
     {
         if (livePhoto) {
             // 取得成功
         }
     }];
}

// PHLivePhotoを生成する
- (void)makeLivePhotoFromImageUrl:(NSURL*)imageUrl
                         videoUrl:(NSURL*)videoUrl
                 placeholderImage:(UIImage*)placeholderImage
                       completion:(void (^)(PHLivePhoto *livePhoto))block
{
    NSArray *urls = @[imageUrl, videoUrl];
    [PHLivePhoto requestLivePhotoWithResourceFileURLs:urls
                                     placeholderImage:placeholderImage
                                           targetSize:CGSizeZero
                                          contentMode:PHImageContentModeAspectFit
                                        resultHandler:^(PHLivePhoto * _Nullable livePhoto,
                                                        NSDictionary * _Nonnull info)
    {
        NSError *error = info[@"PHLivePhotoInfoErrorKey"];
        NSNumber * degradedKey = info[@"PHLivePhotoInfoIsDegradedKey"];
        NSNumber *cancelledKey = info[@"PHLivePhotoInfoCancelledKey"];
        NSLog(@"%s, error %@, degradedKey %@, cancelledKey %@",
              __func__, error, degradedKey, cancelledKey);

        if (block && livePhoto && (degradedKey == false)) {
            block(livePhoto);
        }
    }];
}

2. Live Photosを再生する

Live Photos は PHLivePhotoView を使って再生します.なお,PHLivePhotoViewはStoryboardのUIパーツ一覧にはなかったので,UIViewを選択したのち,クラスをPHLivePhotoViewに書き換えて使用しました.
@property (weak, nonatomic) IBOutlet PHLivePhotoView *livePhotoView;

- (void)viewDidLoad 
{
    [super viewDidLoad];
    self.livePhotoView.contentMode = UIViewContentModeScaleAspectFit;
    self.livePhotoView.delegate = self;
}

- (void)playLivePhoto:(PHLivePhoto*)livePhoto
{
    if (livePhoto) {
        self.livePhotoView.livePhoto = livePhoto;
        [self.livePhotoView startPlaybackWithStyle:PHLivePhotoViewPlaybackStyleFull];
    }
}

// 再生開始
- (void)livePhotoView:(PHLivePhotoView *)livePhotoView
willBeginPlaybackWithStyle:(PHLivePhotoViewPlaybackStyle)playbackStyle
{
    NSLog(@"%s, playbackStyle %@", __func__, @(playbackStyle));
}

// 再生終了
- (void)livePhotoView:(PHLivePhotoView *)livePhotoView
didEndPlaybackWithStyle:(PHLivePhotoViewPlaybackStyle)playbackStyle
{
    NSLog(@"%s, playbackStyle %@", __func__, @(playbackStyle));
}

3. Live Photosを保存する

1.2にてバンドルなどから読み込んだ Live Photos を写真ライブラリに保存する場合は以下のようにします(1.1は既に写真ライブラリに入っているのでなし).なお.保存は iOS 9.1 以上となっており,注意が必要です.
- (void)saveLivePhotoFromImageUrl:(NSURL*)imageUrl
                         videoUrl:(NSURL*)videoUrl
                       completion:(void (^)(BOOL success))completion
{
     [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
          PHAssetCreationRequest *request = [PHAssetCreationRequest creationRequestForAsset];
          [request addResourceWithType:PHAssetResourceTypePhoto
                                                fileURL:imageUrl
                                                options:nil];
          [request addResourceWithType:PHAssetResourceTypePairedVideo
                                                 fileURL:videoUrl
                                                 options:nil];
          } completionHandler:^(BOOL success, NSError * _Nullable error) {
               dispatch_async(dispatch_get_main_queue(), ^{
                   if (completion){
                       completion(success);
                   }
               });
          }];
}
posted by Seesaa京都スタッフ at 18:00| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする