UnityでiPadのプリンター選択を呼び出す話

キーワード

Unity、ネイティブプラグイン、iPad、UIPrinterPickerController、UnityGetGLViewController

やりたいこと

iPadのアプリで画面のスクリーンショットを印刷するとき、印刷時にプリンターを決めるのではなく、事前にプリンターを設定しておいて、印刷ボタンを押すとメニューとか出さずにダイレクトに印刷されるようにしたい。

なのでログイン時や、アプリの設定画面でプリンターを選択しておけるようにする。

やったこと

プリンター周りの機能はiOS依存っぽいのでネイティブプラグインを書く。

今回はUnityからObjC/ObjC++を経由してSwiftの関数を呼び出す形で実装する。(参考:https://qiita.com/mao_/items/5b33c90e533a538570b8

ObjC/ObjC++を経由しなくてもSwiftのみで実装できるみたいだが、プロジェクトで既に利用されていたネイティブプラグインが上記の形だったので、ネイティブ初心者としてそれに倣う。

まずはフツーにプリントメニューを呼び出して印刷

// iOSネイティブプラグインのお作法
#if UNITY_IOS
        [DllImport("__Internal")]
        private static extern void _optionalPrint(string[] datas, int datasSize);

#endif        

// 複数画面印刷したいのでその分カメラを用意
[SerializedField] Camera[] _pageCams

public void PrintOut()
        {        
            Texture2D[] pages = new Texture2D[_pageCams.Length];
            List<string> enc = new List<string>();
            
            for (int i = 0; i < _pageCams.Length; i++)
            {
                // カメラごとにスクリーンショットをTexture2Dとして撮る            
                pages[i] = CaptureAsTexture(_pageCams[i]);
                // そのままだとswiftに持っていけないのでjpgに変換                
                enc.Add(Convert.ToBase64String(pages[i].EncodeToJPG()));
            } 
            string[] encArr = enc.ToArray();
            
            // Swiftに画像を送ってプリントメニューを出して印刷
            OptionalPrint(encArr, encArr.Length);
        }
        
Texture2D CaptureAsTexture(Camera cam)
        {
            RenderTexture rentex = new RenderTexture(1024, 768, 0);
            Texture2D tex = new Texture2D(rentex.width, rentex.height, TextureFormat.ARGB32, false);
            cam.targetTexture = rentex;
            cam.Render();
            RenderTexture.active = rentex;
            tex.ReadPixels(new Rect(0, 0, rentex.width, rentex.height), 0, 0);
            tex.Apply();
            cam.targetTexture = null;
            RenderTexture.active = null;
            return tex;
        }

void OptionalPrint(string[] datas, int datasSize)
        {
#if UNITY_IOS && !UNITY_EDITOR
            _optionalPrint(datas, datasSize);
#endif
#if UNITY_EDITOR
            Debug.Log("Print system is IOS only");
#endif
        }        

// 引数で受け取った画像の文字列を右から左に受け流すだけのコード
void _optionalPrint(const char **c, int cSize){
        NSMutableArray *marr = [NSMutableArray array];
        for (int i = 0; i < cSize; i++) {
            NSString *str = [NSString stringWithCString:c[i] encoding:NSUTF8StringEncoding];
            [marr addObject:str];
                }
        [printInstance optionalPrintWithDatas:marr ];
    } 

@objc public func optionalPrint(datas:[String]){
        // プリンターを選択していたらそれをデフォルトに置いておく
        let address: String = self.ud.string(forKey: "PRINTER_URL")!
        let url: URL = URL(string: address)!
        let printer: UIPrinter = UIPrinter(url: url)
        // 受け取った文字列を画像に戻す        
        var imgs: [UIImage] = [];
        for val in datas{
            imgs.append(String2Image(imageString: val)!);
        }
        
        let printController = UIPrintInteractionController.shared
        let printInfo = UIPrintInfo(dictionary:nil)
        // 各種設定はコードでもできる
        printInfo.outputType = UIPrintInfo.OutputType.general//カラー
        // printInfo.duplex = UIPrintInfo.Duplex.longEdge//両面印刷
        printInfo.jobName = "Print Job"
        printInfo.orientation = .portrait
        printInfo.printerID = printer.displayName
        // 印刷ページの設定
        printController.printInfo = printInfo
        // 印刷対象の設定
        printController.printingItems = imgs
        
        // 印刷メニューの呼び出し        
        printController.present(animated: true, completionHandler: nil)
    }

// StringをUIImageに変換する
// 参考 https://qiita.com/silent0321/items/4253c20e43afdbed8638
func String2Image(imageString:String) -> UIImage?{

        //空白を+に変換する
    var base64String = imageString.replacingOccurrences(of: " ", with: "+");
    
        //BASE64の文字列をデコードしてNSDataを生成
        let decodeBase64:NSData? =
    NSData(base64Encoded:base64String, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters)

        //NSDataの生成が成功していたら
        if let decodeSuccess = decodeBase64 {

            //NSDataからUIImageを生成
            let img = UIImage(data: decodeSuccess as Data)

            //結果を返却
            return img
        }
        return nil
    }    

いい感じ

本題、プリンター選択とダイレクトな印刷に分ける

 @objc public func directPrint(datas:[String]){
        let address: String = self.ud.string(forKey: "PRINTER_URL")!
        
        var imgs: [UIImage] = [];
        for val in datas{
            imgs.append(String2Image(imageString: val)!);
        }
        
        let printController = UIPrintInteractionController.shared
        let url: URL = URL(string: address)!
        let printer: UIPrinter = UIPrinter(url: url)
        let printInfo = UIPrintInfo(dictionary:nil)
             
        printController.print(to: printer, completionHandler: nil)
    }

プリンターを識別するURLでUIPrinterとかいうやつを設定できれば、さっき使ったクラスのprint関数で問題なくいけそう。

URLを取得するプリンター選択メニューはUIPrinterPickerController.presentなる関数で表示できるらしい。(参考:https://qiita.com/SatoTakeshiX/items/264dd293efeae2fdef11

いかがでしたか?

ネイティブプラグインという時点でとっつきにくい印象が強かったんですが、要所を抑えれば結構簡単に実装することができました!

これであなたもネイティブエンジニア!

とはいきませんでした

なんと上記の記事で紹介されているこの関数、UIPrinterPicker.present( animated: Bool, completionHandler completion: UIPrinterPickerController.CompletionHandler? = nil))だが、実はiPadでは使用できない。

ではどうしたらいいかというと別のオーバーロードを使用すればいいらしい。(参考:https://conocode.com/troubleshooting/ipad-print-warning-calling-uiprinterpickercontroller-presentanimatedcompletionhandler-on-ipad/)

present(
    from rect: CGRect,
    in view: UIView,
    animated: Bool,
    completionHandler completion: UIPrinterPickerController.CompletionHandler? = nil
) 

引数が増えているがrectはおそらく矩形を新たに作ってぶち込めば動きそう。

問題はviewだ、UIViewって何?とりあえず参考サイトの通りself.viewをぶち込んでみる。

はい

viewって何?

Swiftを一から勉強してみる

ぼんやり調べてみた感じviewはSwiftの基本概念らしいので、ViewがわかるレベルまでSwiftのチュートリアルをやってみた。

結果、Swift(UIKit)におけるViewとはButtonやImage、Text等のUIのオブジェクトを指し、UIViewはその中でもViewを管理するためのViewらしい。

UIを配置するベースとしてUIViewを置き、その上に他のUIやまた別のUIViewを置いて画面を構成する、アプリ画面の基礎の基礎みたいなことっぽい。

UnityからUIViewを取得

iOSアプリを構成する基礎がUIViewらしいが、UnityでビルドしたアプリのUIViewはSwiftから直接呼び出すことはできないので、ObjC/ObjC++の段階でUnityのViewControllerからUIViewを取得。(参考:https://qiita.com/mao_/items/88aeac26adb17194ebdc#unity%E3%81%8B%E3%82%89%E3%81%AE%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97

調べてみたらObjC/ObjC++でも今回使用したいクラスと関数は存在するようなので、Swiftまで到達させずにここで実行することにした。

        // プリンター選択画面を出す
        public void PrinterSelect()
        {
#if UNITY_IOS&&!UNITY_EDITOR
            _printerSelect(this.gameObject.name);
#endif
#if UNITY_EDITOR
            _printerName.Value = "SAMPLE-000ABC123";
#endif
        }

// ここでUnityのViewControllerを取得
extern UIViewController* UnityGetGLViewController();
extern "C"
{
  PrintPlugin *printInstance = [[PrintPlugin alloc]init];
  NSString *_key_url = @"PRINTER_URL";
  NSString *_key_name = @"PRINTER_NAME";
  NSString *_str_dummy = @"DUMMY";
  void _printerSelect(const char *name){
          _str_gameObj = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
          // UnityのViewを取得
          UIViewController* parent = UnityGetGLViewController();
          // UnityのViewに直にpresentはできないので新しくViewを生成してSubViewに追加
          UIView *uv = [[UIView alloc] init];
          uv.frame = CGRectMake(0, 0, 0, 0);
          [parent.view addSubview:uv];
          // プリンター選択画面を表示
          UIPrinterPickerController *ppc = [[UIPrinterPickerController alloc]init];
          [ppc presentFromRect:uv.frame inView:uv animated:true completionHandler:^(UIPrinterPickerController * _Nonnull printerPickerController, BOOL userDidSelect, NSError * _Nullable error) {
              NSString *str;
              const char *decodeName = (char *)[_str_gameObj UTF8String];
              const char *decodeFunc = (char *)[_str_func UTF8String];
              if (error != nil) {
                  // エラー
                  NSLog(@"Error : %@",error);
              }else{
                  if(userDidSelect){
                      // プリンターを選択したら
                      // udに接続確認用のurlと名前を設定
                      if(printerPickerController.selectedPrinter!=nil){
                          str = printerPickerController.selectedPrinter.URL.absoluteString;
                          [ud setObject:str forKey:_key_url];
                          str = printerPickerController.selectedPrinter.displayName;
                          [ud setObject:str forKey:_key_name];
                      }
                      const char *c = [str UTF8String];
                      UnitySendMessage(decodeName, decodeFunc, c);
                      [uv removeFromSuperview];
                  }else{
                      // 何も選択しなかったら
                      // udにプリンターが設定されているか確認->なかったらDUMMYを格納
                      NSString *urlstr = [ud stringForKey:_key_url];
                      NSString *namestr = [ud stringForKey:_key_name];
                      if(urlstr==nil||[urlstr isEqualToString:_str_dummy]||namestr==nil||[namestr isEqualToString:_str_dummy]){
                          [ud setObject:_str_dummy forKey:_key_url];
                          [ud setObject:_str_dummy forKey:_key_name];
                          const char *c = [_str_dummy UTF8String];
                          UnitySendMessage(decodeName, decodeFunc, c);
                      }else{
                          // 設定されているプリンターに接続できるか確認-> できなかったらDUMMYを格納
                          NSURL *url = [NSURL URLWithString:urlstr];
                          UIPrinter *printer = [UIPrinter printerWithURL:url];
                          [printer contactPrinter:^(BOOL available) {
                              const char *decodeName = (char *)[_str_gameObj UTF8String];
                              const char *decodeFunc = (char *)[_str_func UTF8String];
                              if(available){
                                  const char *c = (char *)[namestr UTF8String];
                                  UnitySendMessage(decodeName, decodeFunc, c);
                              }else{
                                  [ud setObject:_str_dummy forKey:_key_url];
                                  [ud setObject:_str_dummy forKey:_key_name];
                                  const char *c = (char *)[_str_dummy UTF8String];
                                  UnitySendMessage(decodeName, decodeFunc, c);
                              }
                          }];
                      }
                  }
              }
          }];
      }
}

できた

今回は選択したプリンターを別シーンで使いたかったのでUnityに返さずにUserDefaultに保存。

ダイレクトに印刷するタイミングでUserDefaultから選択したプリンターを取得して、メニューを出さずに印刷。

プリンターに繋がらない、プリンターを選択し忘れている場合はプリントメニューを出すようにして完成!!!

まとめ

  • メニューを出す印刷はUnity→ObjC→Swiftで実行
  • 事前にプリンターのみ選択する場合はUnity→ObjCでUnityGetGLViewControllerを取得してiPadのプリンター選択メニューを出す
  • その後選択されたプリンターに直で印刷するときはUnity→ObjC→Swiftで実行

こんな感じの実装になりました。

所感

要所を抑えるだけの学習だったので詰まった時の解決にとても時間がかかった。

最初から少しでもSwiftについて体系的な学習をしていれば、もっと工数を短縮できた気がする。

近道をしようとして結局遠回りになったというオチでした。


Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA