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

2016年04月11日

ArrayAdapterのコンストラクタのResourceIDの役割

こんにちは、Androidエンジニアの外山です。

今回はAndroidのListViewなどにセットするArrayAdapterのコンストラクタで指定するResourceIDの役割について調べてみました。

ArrayAdapterのコンストラクタ

ArrayAdapterのコンストラクタは以下のようになっています。
public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId,
            @NonNull List<T> objects) {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mResource = mDropDownResource = resource;
        mObjects = objects;
        mFieldId = textViewResourceId;
}

ResourceIDの使用箇所

mResourceとmFieldIdはgetViewでViewインスタンスを作成する時に使用されています。

mResourceはViewのレイアウトファイルを参照する際に、mFieldIdはレイアウトファイル内のTextViewを指定する際に使用されているようです(指定しなかった場合は生成したView自身をTextViewとして扱う)。

public View getView(int position, View convertView, ViewGroup parent) {
    return createViewFromResource(mInflater, position, convertView, parent, mResource);
}

private View createViewFromResource(LayoutInflater inflater, int position, View convertView,
        ViewGroup parent, int resource) {
    View view;
    TextView text;
    if (convertView == null) {
        view = inflater.inflate(resource, parent, false);
    } else {
        view = convertView;
    }
    try {
        if (mFieldId == 0) {
            //  If no custom field is assigned, assume the whole resource is a TextView
            text = (TextView) view;
        } else {
            //  Otherwise, find the TextView field within the layout
            text = (TextView) view.findViewById(mFieldId);
        }
    } catch (ClassCastException e) {
        Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
        throw new IllegalStateException(
                "ArrayAdapter requires the resource ID to be a TextView", e);
    }
    T item = getItem(position);
    if (item instanceof CharSequence) {
        text.setText((CharSequence)item);
    } else {
        text.setText(item.toString());
    }
    return view;
}

mDropDownResourceも同じような感じになっています。



まとめ

getViewをオーバーライドしてsuperメソッドを呼ばない場合は、引数で渡したResourceIDが使われることはないので`-1`など適当な値を渡しておけば大丈夫である。

リスト内のアイテムで一つのTextViewにのみ値をセットする場合はResourceIdを指定することでサブクラスを作成せずにAdapterを作成できるようだ。


RecyclerViewの登場によって使う機会が減ってきたListViewやGridViewですが簡単なリストを作成する際には手軽に使えていいですね。

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