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 | このブログの読者になる | 更新情報をチェックする

2016年09月29日

[iOS] 印刷

iOS担当のエモトです.先日「聲の形」を見ました.京アニさん流石ですのクオリティの高さに加え,人間の無知で無垢な強暴性から始まる己自身やお互いの葛藤,そして最終局面への展開からは目が離せなませんでした.円盤買います.ぼくは植野推しです.今晩は「君の名は」を見に行きます.是が非でも定時に帰ります.

iOS端末からプリンターで印刷するには,iOS4.2で実装された UIPrintInteractionController を用いれば,印刷設定を含め印刷を行うことができます(いわゆるAirPrint).さらにiOS8から UIPrinterPickerController が追加され,プリンター選択を別にできるようになりました.

1. 始める前に

テストのたびに実際に印刷が行われていると大変,そもそもAirPrint対応のプリンターが手元にない場合,Printer Simulatorを入れておくと仮想プリンターとして印刷結果も確認できるので便利です.Printer Simulatorはアップルの ディベロッパーサイト からHardware IO Tools for Xcode (またはAdditional Tools for Xcode)を選び,ダウンロードすることができます.

2. UIPrintInteractionControllerで印刷する

UIPrintInteractionControllerの使い方は非常に簡単で,以下の通りのコードを書けば印刷することができます.例では画像を印刷しています.
let pic = UIPrintInteractionController.sharedPrintController()
pic.printingItem = UIImage(named: "hogehoge")
let completion:UIPrintInteractionCompletionHandler = { (controller, completed, error) in
    print("completed = \(completed), error = \(error)")
}
pic.presentAnimated(true, completionHandler: completion)
とはいえこれは色々と支障がでるので,端末やprintingItemが印刷可能かどうか判定する処理を追加します.ここで,canPrintの初期値をtrueにしているので,印刷可能か判定するメソッドがNSDataとNSURLを対象にしているのに対して,前述のように直接的画像データを指定できるためです.判定を網羅できなていないです.
let obj = printingItem
var canPrint:Bool = true
if obj.isKindOfClass(NSData) {
    canPrint = UIPrintInteractionController.canPrintData(obj as! NSData)
}else if obj.isKindOfClass(NSURL){
    canPrint = UIPrintInteractionController.canPrintURL(obj as! NSURL)
}
if UIPrintInteractionController.isPrintingAvailable() && canPrint {
    // ...略
}
印刷可能のものは,画像のほかに viewPrintFormatter メソッドをもつUIViewを継承したインスタンスも可能となっています
pic.printingItem = view.viewPrintFormatter()
しかしながら,viewPrintFormatterがうまく生成されない場合が多く,viewを対象とする場合は手を加えないといけません.

(a) 画像もしくはPDFに変換する
stack overflowなどを覗くとみなさん同じ悩みのようでした.ちょっと乱暴な処理ですが,画像化などで切り抜けましょう.しかし,スクロールやページングがあったらどうすればいいんだろ・・・
UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, 0.0);
view.drawViewHierarchyInRect(view.bounds, afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
pic.printingItem = image
(b) printPageRendererを設定する
これはアップルの サンプルコード PrintWebView を拝借しました.コード内のAPLPrintPageRendererを使って,以下のようにprintPageRendererを設定します.これにより,viewの印刷ができますが,上手くいくのはUIWebViewなどのようです.
let viewPrintFormatter = view.viewPrintFormatter();
let renderer = APLPrintPageRenderer();
renderer.jobTitle = info.jobName
renderer.addPrintFormatter(viewPrintFormatter, startingAtPageAtIndex: 0)
pic.printPageRenderer = renderer;

3. UIPrinterPickerControllerでプリンター選択

UIPrinterPickerControllerを使えば,前述のUIPrintInteractionControllerからプリンター選択を独立させることができます.機能差は少ないですが,プリント設定画面のスキップや自作が可能になります.
var printer:UIPrinter? = nil

func selectPrinter(){
    let ppc = UIPrinterPickerController(initiallySelectedPrinter: nil)
    ppc.presentAnimated(true, completionHandler: {
        (printerPickerController, userDidSelect, error) in
            if error == nil {
                if let printer = printerPickerController.selectedPrinter{
                    self.printer = printer
                }
            }else{
                print("error = \(error)")
            }
        })
}

func printObject(){
    // その他設定は省略
    pic.printToPrinter(self.printer, completionHandler: completion)
}
posted by Seesaa京都スタッフ at 12:00| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする

2016年04月26日

[iOS] リモコン付きイヤフォン(ヘッドフォン)のイベントを監視する

iOS担当のエモトです.先日,大洗に行ってきました.この場所はあのシーンで見た!と街を見て回ってきました.その旅行のあと,映画館にいったのですが,このシーンはあの場所で見た!と,不思議な円環の理に陥ってしまいました.

音声や音楽系アプリを取り扱う場合,イヤフォン制御が大切になります.

(a) イヤフォンを確認する
端末にイヤフォンがつながっているかは以下のようにすれば調べることができます.
#import < AVFoundation/AVFoundation.h >

- (BOOL)hasHeadphone{
    BOOL hasHeadphones = false;
    AVAudioSessionRouteDescription* route = [[AVAudioSession sharedInstance] currentRoute];
    for (AVAudioSessionPortDescription* desc in [route outputs]){
        NSString *portType = desc.portType;
        if ([portType isEqualToString:AVAudioSessionPortHeadphones]){
            || [portType isEqualToString:AVAudioSessionPortBluetoothA2DP]){
            hasHeadphones = true;
        }
    }
    return hasHeadphones;
}
しかしながら,その時だけの状態が取得できても機能不足です.イヤフォンが端末から差し抜きされたときのイベントは以下のようにすれば監視することができます.
// 監視を始める
- (void)startObservation{
    [[AVAudioSession sharedInstance] setActive:true error:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(audioSessionRouteChanged:)
                                                 name:AVAudioSessionRouteChangeNotification
                                               object:nil];
}

// 監視を終える
- (void)removeObservation{
    [[AVAudioSession sharedInstance] setActive:false error:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:AVAudioSessionRouteChangeNotification
                                                  object:nil];
}

// ヘッドフォンが差し向きされた
- (void)audioSessionRouteChanged:(NSNotification*)notification
{
}

(b) リモコンを確認する
リモコン付きイヤフォンの再生や停止などのイベントは MPRemoteCommandCenter を使えば監視することができます.
#import < MediaPlayer/MediaPlayer.h >

// 監視を始める
-(void)addRemoteCommandCenter{
    MPRemoteCommandCenter *rcc = [MPRemoteCommandCenter sharedCommandCenter];
    [rcc.togglePlayPauseCommand addTarget:self action:@selector(toggle:)];
    [rcc.playCommand addTarget:self action:@selector(play:)];
    [rcc.pauseCommand addTarget:self action:@selector(pause:)];
    [rcc.nextTrackCommand addTarget:self action:@selector(nextTrack:)];
    [rcc.previousTrackCommand addTarget:self action:@selector(prevTrack:)];	
}

// 監視を終える
-(void)removeRemoteCommandCenter{
    MPRemoteCommandCenter *rcc = [MPRemoteCommandCenter sharedCommandCenter];
    [rcc.togglePlayPauseCommand removeTarget:self];
    [rcc.playCommand removeTarget:self];
    [rcc.pauseCommand removeTarget:self];
    [rcc.nextTrackCommand removeTarget:self];
    [rcc.previousTrackCommand removeTarget:self];
}

// 再生・停止のトグルボタンが押された(他のメソッドは省略)
- (void)toggle:(MPRemoteCommandEvent*)event{
    // 何かしらの処理
}
ただし,MPRemoteCommandCenterはiOS7.1から実装されました.iOS7.0も対象する場合はremoteControlReceivedWithEventを使います(ドキュメントではMPRemoteCommandCenterの使用を推奨しています).個人的に,後者はcanBecomeFirstResponderを上書きしないとならないので,実装箇所を限定してしまい,ちょっと使いにくいなと思ってます.
// 監視を始める
- (void)hogehoge{
    id obj = NSClassFromString(@"MPRemoteCommandCenter");
    if (obj) {
        // MPRemoteCommandCenterが使える
        [self addRemoteCommandCenter];
    }else{
        [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
        [self becomeFirstResponder];

        // 監視を終える時は
        // [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
        // [self resignFirstResponder];
    }
}

// trueに上書きする
- (BOOL)canBecomeFirstResponder{
    return true;
}

// イベントが起こった
- (void)remoteControlReceivedWithEvent:(UIEvent*)receivedEvent{
    if (receivedEvent.type == UIEventTypeRemoteControl) {
        UIEventSubtype subtype = receivedEvent.subtype;
        if (subtype == UIEventSubtypeRemoteControlPlay){
        }else if (subtype == UIEventSubtypeRemoteControlPause){
        }else if (subtype == UIEventSubtypeRemoteControlTogglePlayPause) {
        }else if (subtype == UIEventSubtypeRemoteControlPreviousTrack){
        }else if (subtype == UIEventSubtypeRemoteControlNextTrack){
        }
    }
}
posted by Seesaa京都スタッフ at 12:52| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする